Optimize cross-sheet formula recalculation#30
Conversation
Three improvements to reduce unnecessary computation during multi-sheet collaboration: 1. Cache cross-sheet formula srefs so recalculateCrossSheetFormulas() skips the full scan on repeated calls. The cache is invalidated when local formulas change (setData, removeData, paste, etc.). 2. Filter remote-change events by path — only trigger cross-sheet recalc when cell data actually changes ($.sheets.<id>.cells), skipping style, dimension, and presence updates. 3. Replace the hardcoded 1000x100 range in recalculateAllFormulaCells() with getFormulaGrid(), which covers all formula cells regardless of sheet size. Co-Authored-By: Claude Opus 4.6 <[email protected]>
📝 WalkthroughWalkthroughAdds documentation and tests for cross-sheet recalculation; frontend now filters Yorkie remote-change events to only trigger cross-sheet recalcs for cell-related paths; Sheet model caches cross-sheet formula srefs with invalidation on data and structural mutations to avoid repeated scanning. Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend as Frontend (sheet-view)
participant Yorkie as Yorkie (remote-change)
participant Sheet as Sheet Model
participant Calc as Calculator
Frontend->>Yorkie: subscribe to remote-change events
Yorkie-->>Frontend: remote-change(event)
alt event.value.operations missing
Frontend->>Sheet: schedule cross-sheet recalc()
else operations present
Frontend->>Frontend: inspect operation paths
alt path matches $.sheets.<tabId>.cells... or $.tabs.<tabId>.name
Frontend->>Sheet: schedule cross-sheet recalc()
else
Frontend-->>Frontend: ignore (non-cell change)
end
end
Sheet->>Sheet: ensure crossSheetFormulaSrefs cache (build if null)
Sheet->>Calc: compute cross-sheet formula updates using cached srefs
Calc-->>Sheet: updated values
Sheet-->>Frontend: publish updated cell state
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Verification: verify:selfResult: ✅ PASS in 94.3s
Verification: verify:integrationResult: ✅ PASS |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/src/app/spreadsheet/sheet-view.tsx`:
- Around line 718-737: The current doc.subscribe handler only triggers
scheduleCrossSheetRecalc when ops touch $.sheets.<id>.cells, which misses
formula-relevant remote changes; update the ops filter inside the subscription
(the code around doc.subscribe, ops extraction, and hasCellChange) to also
detect operations whose path matches $.tabs[*].name and $.sheets[*].merges (e.g.
add tests for /^\$\.tabs\.[^.]+\.name/ and /^\$\.sheets\.[^.]+\.merges/
alongside the existing cells regex) so that tab renames and merge/unmerge ops
call scheduleCrossSheetRecalc as well.
In `@packages/sheet/src/model/worksheet/sheet.ts`:
- Around line 2663-2669: The cross-sheet cache invalidation is only called in
some callers but not when formulas change via autofill(), undo(), or redo(),
causing recalculateCrossSheetFormulas() to operate on stale data; update the
shared mutation entry points so any path that mutates formulas/history calls
invalidateCrossSheetCache() centrally (e.g., inside the shared formula/history
mutation helper functions used by autofill, undo, redo) rather than adding
callers—modify the mutation helpers that both apply changes and update history
to invoke invalidateCrossSheetCache(), ensuring methods like autofill(), undo(),
and redo() no longer need bespoke invalidation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 061ae50f-7502-47bc-8571-53c1107e188e
📒 Files selected for processing (4)
docs/tasks/active/20260314-cross-sheet-calc-lessons.mddocs/tasks/active/20260314-cross-sheet-calc-todo.mdpackages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/sheet/src/model/worksheet/sheet.ts
The remote-change filter only checked cell operations, missing merge and tab-rename changes that affect cross-sheet formula resolution. Also, autofill/undo/redo could mutate formulas without invalidating the cross-sheet formula cache, leaving stale values. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/sheet/src/model/worksheet/sheet.ts (1)
2664-2670:⚠️ Potential issue | 🟠 MajorCross-sheet cache can still go stale through public mutation APIs.
Line 2666 says to invalidate when formulas change, but
setCell()(Line 797) andsetGrid()(Line 804) can mutate formulas without callinginvalidateCrossSheetCache(). If cache was already built, laterrecalculateCrossSheetFormulas()can run against stale membership.💡 Suggested fix
async setCell(ref: Ref, cell: Cell): Promise<void> { + this.invalidateCrossSheetCache(); await this.store.set(this.normalizeRefToAnchor(ref), cell); } async setGrid(grid: Grid): Promise<void> { + this.invalidateCrossSheetCache(); await this.store.setGrid(grid); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/worksheet/sheet.ts` around lines 2664 - 2670, setCell() and setGrid() can mutate formulas without invalidating the cross-sheet formula cache, so calls to recalculateCrossSheetFormulas() may run against stale data; update the public mutation APIs (setCell and setGrid) to call invalidateCrossSheetCache() whenever they change cell contents (or conservatively on any successful mutation) so the cache is cleared when formulas are added/removed/modified; reference the methods setCell, setGrid, invalidateCrossSheetCache, and recalculateCrossSheetFormulas when making the change.
🧹 Nitpick comments (1)
packages/sheet/src/model/worksheet/sheet.ts (1)
1169-1173: Defer cache invalidation until after no-op guards.Line 1169 and Line 3051 invalidate even when the operation exits early (no-op move / blocked move / already-sorted filter). This causes unnecessary cache rebuilds.
♻️ Suggested refactor
private async moveCells( axis: Axis, src: number, count: number, dst: number, options?: { skipPostRecalculate?: boolean }, ): Promise<void> { - this.invalidateCrossSheetCache(); // No-op if source and destination are the same if (dst >= src && dst <= src + count) { return; } for (const [anchorSref, span] of this.merges) { const anchor = parseRef(anchorSref); if (isMergeSplitByMove(anchor, span, axis, src, count)) { return; } } + this.invalidateCrossSheetCache(); await this.store.moveCells(axis, src, count, dst);async sortFilterByColumn( col: number, direction: 'asc' | 'desc', ): Promise<boolean> { - this.invalidateCrossSheetCache(); if (!this.filterRange || !this.isColumnInFilter(col)) { return false; } ... const alreadySorted = desired.every((row, i) => row === dataStart + i); if (alreadySorted) { return true; } + this.invalidateCrossSheetCache(); // Build row mapping: oldRow → newRow. const rowMap = new Map<number, number>();Also applies to: 1175-1179, 3051-3054, 3104-3107
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/worksheet/sheet.ts` around lines 1169 - 1173, The call to this.invalidateCrossSheetCache is happening before early-return/no-op guards, causing unnecessary cache rebuilds; move the invalidateCrossSheetCache invocation so it runs only after the method determines the operation will proceed (i.e., after no-op checks/blocked-move checks/already-sorted checks). Update each location that currently calls this.invalidateCrossSheetCache early (the occurrences around the move/filtered/sort handlers—e.g., the moveRows, filterRows, and sortRows code paths) to defer the invalidation until after the guard that returns on no-op, so the method returns without touching the cache when no change occurs. Ensure behavior remains identical when an actual mutation occurs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/sheet/src/model/worksheet/sheet.ts`:
- Around line 2664-2670: setCell() and setGrid() can mutate formulas without
invalidating the cross-sheet formula cache, so calls to
recalculateCrossSheetFormulas() may run against stale data; update the public
mutation APIs (setCell and setGrid) to call invalidateCrossSheetCache() whenever
they change cell contents (or conservatively on any successful mutation) so the
cache is cleared when formulas are added/removed/modified; reference the methods
setCell, setGrid, invalidateCrossSheetCache, and recalculateCrossSheetFormulas
when making the change.
---
Nitpick comments:
In `@packages/sheet/src/model/worksheet/sheet.ts`:
- Around line 1169-1173: The call to this.invalidateCrossSheetCache is happening
before early-return/no-op guards, causing unnecessary cache rebuilds; move the
invalidateCrossSheetCache invocation so it runs only after the method determines
the operation will proceed (i.e., after no-op checks/blocked-move
checks/already-sorted checks). Update each location that currently calls
this.invalidateCrossSheetCache early (the occurrences around the
move/filtered/sort handlers—e.g., the moveRows, filterRows, and sortRows code
paths) to defer the invalidation until after the guard that returns on no-op, so
the method returns without touching the cache when no change occurs. Ensure
behavior remains identical when an actual mutation occurs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 33a9891d-66c5-4e8b-8260-50af8ecc802e
📒 Files selected for processing (2)
packages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/sheet/src/model/worksheet/sheet.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/frontend/src/app/spreadsheet/sheet-view.tsx
Five test cases verify cross-sheet formula recalculation works correctly through Yorkie sync between two clients: - Formula resolution after sync - Value updates via recalc - SUM with cross-sheet range - Local dependent chain propagation - Remote-change event path filtering Uses MemStore for Sheet (avoiding ANTLR bundle/Node.js incompatibility) with GridResolver reading from the synced Yorkie document. Tests run via `pnpm frontend test:integration` with YORKIE_RPC_ADDR set. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.ts (1)
6-6: Consider adding a descriptive skip message for better test output.When tests are skipped due to missing
YORKIE_RPC_ADDR, the test output doesn't explain why. Adding context helps developers understand the skip reason.-const shouldRun = Boolean(process.env.YORKIE_RPC_ADDR); +const shouldRun = Boolean(process.env.YORKIE_RPC_ADDR); +const skipReason = shouldRun ? false : "YORKIE_RPC_ADDR not set - skipping Yorkie integration tests";Then use
{ skip: skipReason }in test options.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.ts` at line 6, The test currently uses shouldRun = Boolean(process.env.YORKIE_RPC_ADDR) without explaining skips; define a descriptive skipReason (e.g., "YORKIE_RPC_ADDR not set - integration tests requiring Yorkie RPC are skipped"), set shouldRun = Boolean(process.env.YORKIE_RPC_ADDR) as before, and pass the skip option into the test(s) or suite (use { skip: skipReason } where tests are registered) so skipped tests show the reason; update references to shouldRun and the test registration to use the new skipReason when skipping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.ts`:
- Line 6: The test currently uses shouldRun =
Boolean(process.env.YORKIE_RPC_ADDR) without explaining skips; define a
descriptive skipReason (e.g., "YORKIE_RPC_ADDR not set - integration tests
requiring Yorkie RPC are skipped"), set shouldRun =
Boolean(process.env.YORKIE_RPC_ADDR) as before, and pass the skip option into
the test(s) or suite (use { skip: skipReason } where tests are registered) so
skipped tests show the reason; update references to shouldRun and the test
registration to use the new skipReason when skipping.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: dcabe39f-8b4c-4938-9402-6aad46006d12
📒 Files selected for processing (4)
docs/tasks/active/20260314-cross-sheet-yorkie-integration-todo.mdpackages/frontend/package.jsonpackages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.tspackages/frontend/tests/helpers/cross-sheet-yorkie.ts
Summary
recalculateCrossSheetFormulas()now caches which cells contain cross-sheet references, skipping the full formula scan on repeated calls. Cache is invalidated when local formulas change (setData, removeData, paste, shift, move, sort).$.sheets.<id>.cellspath pattern), skipping style, dimension, and presence updates.recalculateAllFormulaCells()now usesgetFormulaGrid()instead of a fixed range, correctly covering all formula cells regardless of sheet size.Test plan
pnpm verify:fastpasses (lint + all unit tests including cross-sheet-calculation tests)=Sheet2!A1— verify value updates🤖 Generated with Claude Code
Summary by CodeRabbit
Performance Improvements
Bug Fixes
Tests
Documentation