skip to content

An undo feature built on full-state mementos is consuming too much memory in production. How do you diagnose and reduce the cost while keeping correctness?

level: principalimportance: nice to knowfreq 22%

answer

  1. Cost = history length × per-snapshot size
  2. Diagnose with heap dump + dominator/retained size
  3. Fixes: bound depth, deltas, structural sharing, spill to disk
  4. Large state + small edits → switch to Command/op-log
  5. Watch shallow-copy aliasing → leak, not just cost

basics

~20 s

Each memento is a full copy of state, so an unbounded history grows memory linearly. Fix it by bounding history depth, storing deltas instead of full snapshots, sharing immutable sub-state structurally, or moving older snapshots off-heap/to disk — choosing based on how big state is and how it changes.

solid answer

~50 s

Full-state mementos cost one deep copy per saved step, so the root cause is usually an unbounded history multiplied by large per-snapshot size; a heap dump or profiler confirms it by showing the snapshot collection dominating retained heap. The remedies trade memory for other concerns. First, bound the history: a fixed-depth ring buffer drops the oldest mementos, capping memory at the cost of limited undo depth. Second, store deltas — record only what each change altered rather than the whole state — which is cheap when edits are small but adds reconstruction complexity. Third, exploit immutability and structural sharing: if state is built from persistent/immutable data structures, snapshots can share unchanged sub-trees so each memento adds only the changed portion. Fourth, spill cold history off-heap or to disk via serialization, keeping only recent snapshots in memory. Whichever you choose, set an explicit history-size policy and verify with measurement; for very large state with small invertible edits, switching to Command-based (operation) undo may beat snapshots entirely.

go deeper

for a junior

Understands that each saved state uses memory and that keeping unlimited history is a problem.

for a middle

Suggests capping the undo history depth and recognizes that big state means big snapshots.

for a senior

Diagnoses with a heap dump, distinguishes too-many vs too-large snapshots, and applies deltas or bounding with attention to restore correctness.

for a principal

Designs the undo subsystem holistically: chooses snapshot vs delta vs op-log vs structural sharing per workload, sets explicit memory/history policy, plans off-heap/persistence and coordinates undo/redo lifetimes and regression guards.

## Why full-state mementos blow up memory A memento is, by definition, an **independent (usually deep) copy** of the state needed to restore the Originator. If you snapshot before every change and never discard old snapshots, memory grows as **(history length) × (per-snapshot size)**. For a large document or object graph, this is the classic 'undo eats all the RAM' problem. ## Step 1 — Diagnose Confirm the cause before changing anything: - Take a **heap dump** (e.g. `jmap`, or `-XX:+HeapDumpOnOutOfMemoryError`) and open it in a tool like **Eclipse MAT** or **VisualVM**. Look at the **dominator tree** and **retained size**: an undo stack holding many mementos will show up as the dominant retained set. - Check whether the problem is **too many** snapshots (history unbounded) or **too large** snapshots (each copy is huge), or both — the fix differs. - Watch for a subtle bug: a memento that **accidentally retains references** to live, growing objects (a shallow copy aliasing the Originator's mutable graph) can pin far more than intended — that's a memory leak, not just cost. ## Step 2 — Reduce, by strategy ### A. Bound the history (cap depth) Keep only the last N mementos in a fixed-size structure (ring buffer / bounded deque); evict the oldest when full. - **Pro:** simplest, hard memory ceiling. - **Con:** undo depth is limited to N. ### B. Store deltas instead of full snapshots Record only **what changed** per step (e.g. 'replaced chars 10–14 with "abc"') rather than the entire state. - **Pro:** tiny per step when edits are small. - **Con:** restoring an old state may require replaying/inverting deltas; more complex; this shades into the **Command** pattern. ### C. Immutability + structural sharing If the state is built from **immutable / persistent data structures**, two snapshots can **share** the parts that didn't change; each new memento only adds the modified sub-structure. (This is how persistent collections work — unchanged sub-trees are reused.) - **Pro:** snapshots become cheap and naturally safe (no defensive copies, no aliasing bugs). - **Con:** requires designing state around immutable structures up front. ### D. Spill cold history off-heap / to disk Keep only recent mementos on the heap; **serialize older ones** to off-heap memory or disk, reloading on demand. - **Pro:** large/long histories without heap pressure; serialized mementos persist across runs. - **Con:** serialization cost, I/O latency, and the usual serialization caveats (Serializable, versioning, security of untrusted bytes). ### E. Switch patterns for the right workload For **large state with small, invertible edits**, an **operation log (Command)** undo is fundamentally cheaper than snapshots — store the small operation, not the big state. Consider migrating rather than optimizing snapshots. ## Step 3 — Set policy and verify - Make the **history-size policy explicit** (max depth, max bytes, or time-based eviction) instead of letting it grow unbounded. - **Re-measure** after the change with the same heap-dump/profiler workflow to confirm retained heap dropped and undo still works. - Add a regression guard (e.g. a test asserting the history is bounded) so the fix doesn't regress. ## Cross-cutting correctness notes - Evicting old mementos must not break a memento still referenced by a redo stack — coordinate undo/redo lifetimes. - If you adopt deltas or structural sharing, ensure restore still yields a **byte-for-byte equivalent** state; add tests that save → mutate → restore → assert equality. - Beware the leak variant above: a *shallow* memento that aliases live mutable state both corrupts snapshots and inflates retained memory — fix with proper immutable snapshots.

  • How does using immutable/persistent data structures for state make mementos cheaper?
    Because unchanged parts of the structure can be shared between snapshots instead of copied. Each new memento only needs to reference the modified sub-structure, so memory grows with the size of changes rather than the size of the whole state — and there are no defensive copies or aliasing bugs.
  • What's a subtle memory bug specific to mementos that a profiler reveals as a leak rather than mere cost?
    A shallow memento that stores references into the Originator's live mutable graph instead of an independent copy. It both corrupts the snapshot and pins (retains) the live objects, so the undo history holds far more memory than its visible snapshots suggest.

saying these in an interview costs you the question

  • Just raising the heap size instead of bounding the history
  • Assuming snapshots are cheap without measuring per-snapshot size
  • Storing deltas/structural sharing but not verifying restore correctness
  • Evicting a memento still needed by the redo stack

context