Skip to content

Optimize cross-sheet formula recalculation#30

Merged
hackerwins merged 3 commits into
mainfrom
optimize-cross-sheet-calc
Mar 14, 2026
Merged

Optimize cross-sheet formula recalculation#30
hackerwins merged 3 commits into
mainfrom
optimize-cross-sheet-calc

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Cache cross-sheet formula srefs: 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).
  • Filter remote-change events by path: Only trigger cross-sheet recalc when cell data actually changes ($.sheets.<id>.cells path pattern), skipping style, dimension, and presence updates.
  • Replace hardcoded 1000×100 range: recalculateAllFormulaCells() now uses getFormulaGrid() instead of a fixed range, correctly covering all formula cells regardless of sheet size.

Test plan

  • pnpm verify:fast passes (lint + all unit tests including cross-sheet-calculation tests)
  • Manual: edit cells in Sheet2, switch to Sheet1 with =Sheet2!A1 — verify value updates
  • Manual: remote user edits style/dimension only — verify no unnecessary recalc triggered

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance Improvements

    • Faster cross-sheet formula recalculation via caching and targeted invalidation.
  • Bug Fixes

    • Reduced spurious recalculations by only triggering on actual cell or tab-name changes (ignores style/dimension edits and other non-cell events).
  • Tests

    • Added cross-sheet integration tests and test helpers validating remote-sync, recalculation, and event payloads.
  • Documentation

    • Added lessons, phased roadmap, and guidance on cross-sheet calculation and event handling.

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]>
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Documentation
docs/tasks/active/20260314-cross-sheet-calc-lessons.md, docs/tasks/active/20260314-cross-sheet-calc-todo.md, docs/tasks/active/20260314-cross-sheet-yorkie-integration-todo.md
New docs: Phase 1 lessons, Yorkie event shape notes, multi-phase plan/todo, and integration-test plan.
Frontend — Event filtering & tests
packages/frontend/src/app/spreadsheet/sheet-view.tsx, packages/frontend/package.json, packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.ts, packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.test.ts
Subscription now inspects event.value.operations; triggers cross-sheet recalcs only for cell-related paths ($.sheets.<tabId>.cells...) or tab-name changes; added integration tests and test script for Yorkie-backed cross-sheet scenarios.
Sheet model — Cache & invalidation
packages/sheet/src/model/worksheet/sheet.ts
Added private `crossSheetFormulaSrefs: Set
Test helpers
packages/frontend/tests/helpers/cross-sheet-yorkie.ts
New Yorkie-based test helper exposing CrossSheetEnv and setupCrossSheetEnv to bootstrap dual-client cross-sheet test environments and subscriptions.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through tabs to spy the trails,
Found event paths and tiny rails,
A cached set beneath my paw,
Skips the noise and keeps the law,
Recalc when cells sing — hop, hooray! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly describes the main optimization: caching cross-sheet formula srefs to reduce unnecessary recalculation. This accurately captures the primary technical focus of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch optimize-cross-sheet-calc
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 94.3s

Lane Status Duration
sheet:build ✅ pass 13.0s
verify:fast ✅ pass 46.2s
frontend:build ✅ pass 13.5s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.4s
cli:build ✅ pass 1.6s
verify:entropy ✅ pass 14.8s
verify:browser ✅ pass 0.4s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 347ba26 and 9ca426d.

📒 Files selected for processing (4)
  • docs/tasks/active/20260314-cross-sheet-calc-lessons.md
  • docs/tasks/active/20260314-cross-sheet-calc-todo.md
  • packages/frontend/src/app/spreadsheet/sheet-view.tsx
  • packages/sheet/src/model/worksheet/sheet.ts

Comment thread packages/frontend/src/app/spreadsheet/sheet-view.tsx
Comment thread packages/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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/sheet/src/model/worksheet/sheet.ts (1)

2664-2670: ⚠️ Potential issue | 🟠 Major

Cross-sheet cache can still go stale through public mutation APIs.

Line 2666 says to invalidate when formulas change, but setCell() (Line 797) and setGrid() (Line 804) can mutate formulas without calling invalidateCrossSheetCache(). If cache was already built, later recalculateCrossSheetFormulas() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca426d and 583c32b.

📒 Files selected for processing (2)
  • packages/frontend/src/app/spreadsheet/sheet-view.tsx
  • packages/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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between 583c32b and 635d104.

📒 Files selected for processing (4)
  • docs/tasks/active/20260314-cross-sheet-yorkie-integration-todo.md
  • packages/frontend/package.json
  • packages/frontend/tests/app/spreadsheet/yorkie-cross-sheet.integration.ts
  • packages/frontend/tests/helpers/cross-sheet-yorkie.ts

@hackerwins
hackerwins merged commit 482c749 into main Mar 14, 2026
3 checks passed
@hackerwins
hackerwins deleted the optimize-cross-sheet-calc branch March 14, 2026 11:28
@coderabbitai coderabbitai Bot mentioned this pull request Apr 18, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant