Fix 50k-row edit performance: replace deep-watch undo/persistence with explicit dirty-marking
The debounced undo-history and localStorage persistence were driven by
watch(() => doc, ..., { deep: true }) -- Vue's deep watch re-traverses
the entire reactive object graph on every single mutation just to
re-establish tracking, an O(document size) cost per edit regardless of
what actually changed. Measured on a 50k-row table: one setCellInput
call went from ~450ms to <1ms with the deep watch disabled.
Both watches are replaced by scheduleSave()/markDirty(), called
explicitly by every content-mutating store function (mirroring the
existing bump()/recalcTick convention), with pure selection/navigation
functions calling scheduleSave() only so cursor movement still never
becomes an undo step. applyDocumentState (undo/redo/replaceDocument)
schedules persistence but never marks history dirty, since those
callers already reconcile history state themselves -- this also makes
the old applyingHistory guard flag dead code, now removed.
setCellInput gets an O(1) same-value no-op guard (the highest-frequency
mutator) so retyping an unchanged value creates no history noise; the
remaining no-op safety net (a real fingerprint diff) still lives in
commitHistoryStep, now running once per debounced burst instead of per
keystroke.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2241,6 +2241,34 @@ describe('undo / redo', () => {
|
||||
expect(s.cellFormat(id, 0, 0)).toEqual({});
|
||||
});
|
||||
|
||||
it('re-committing a cell\'s unchanged value is a true no-op -- no step, redo survives', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
vi.advanceTimersByTime(600); // table creation is its own step
|
||||
setCell(s, name, 'A1', '1');
|
||||
vi.advanceTimersByTime(600);
|
||||
setCell(s, name, 'A1', '2');
|
||||
vi.advanceTimersByTime(600);
|
||||
|
||||
s.undo(); // undoes '2' -> back to '1'
|
||||
expect(s.cellInput(id, 0, 0)).toBe('1');
|
||||
expect(s.canRedo.value).toBe(true);
|
||||
|
||||
// F2, Enter without changing anything -- a true no-op, must not clear redo
|
||||
s.setCellInput(id, 0, 0, '1');
|
||||
expect(s.canRedo.value).toBe(true);
|
||||
s.redo();
|
||||
expect(s.cellInput(id, 0, 0)).toBe('2');
|
||||
|
||||
// clearing an already-empty cell is likewise a no-op -- creates no step
|
||||
s.setCellInput(id, 5, 5, '');
|
||||
vi.advanceTimersByTime(600);
|
||||
s.undo(); // must undo the '1' -> '2' edit directly, not some no-op clear step
|
||||
expect(s.cellInput(id, 0, 0)).toBe('1');
|
||||
expect(s.cellInput(id, 5, 5)).toBe('');
|
||||
});
|
||||
|
||||
it('resetToSeed starts a fresh history -- the prior document is not undoable back', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
|
||||
Reference in New Issue
Block a user