Undo stack implementation retains unreachable `Value` objects, leaks memory
See original GitHub issueDo you want to request a feature or report a bug?
Bug
What’s the current behavior?
Open the https://www.slatejs.org/#/history slate example, open the Developer Tools and start a Memory > Allocation Instrumentation on Timeline profile. Then perform the following:
- Type 4-5 characters
- Undo the typed characters so the undo stack length is 0
- Type a character to clear the redo stack
- Type another character to remove any remnants of previous props from React’s memoziedProps.
Observe that memory allocated when the original 4-5 characters were typed is still around, even though it is now impossible to reach those operations via undo/redo:
- Note: I’ve verified that disabling undo/redo entirely and it also fixes these leaked allocations, they’re definitely undo/redo related.
What’s the expected behavior?
Slate should not retain memory related to undo/redo operations which can no longer be reached via undo/redo.
Fix
I think this is caused by the fact that the undo redo stack is stored in value.data
. Since each undo operation has a copy of value
, and value
contains an undo stack, Slate creates a recursive structure of Value’s with undo stacks that reference older values, and keeping a handle to a single undo/redo entry is enough to retain the entire stack forever. Clipping to 100 undo entries is nice, but since entry 0’s operation.value.data.get('undos')
retains 100 more entries further into the history, it’s not effective at limiting the memory footprint.
After applyOperation applies an undo or redo, the code restores the current undo/redo stack which means that it actually doesn’t need the undo/redo stacks of each saved Value
at all. I patched Commands.save
to remove these and it resolves the issue:
Commands.save = (editor, operation) => {
const { operations, value } = editor
// Remove the undo/redo history from the operation's Value
operation = operation.withMutations(op => {
const v2 = op.value.withMutations(v => {
let d2 = op.value.data
d2 = d2.remove('undos')
d2 = d2.remove('redos')
v.set('data', d2)
})
op.set('value', v2)
})
...
@ianstormtaylor I think my hack above is fairly gross, so I’ve filed this as an issue and not a PR just yet. I actually don’t know much about ImmutableJS and I’m not sure if there’s a better way to accomplish this, or if another fix would be preferable?
Issue Analytics
- State:
- Created 5 years ago
- Reactions:3
- Comments:8 (7 by maintainers)
Top GitHub Comments
Just a reminder that there is actually a PR open that fixes this from a while ago: https://github.com/ianstormtaylor/slate/pull/2225. It removes the need to keep track of
value
and makes all ops invertible (when converting from and to JSON).This is solved now. Thank you @bryanph!