Skip to content

perf(usage): reduce memory and stalls in cold Antigravity reads - #18262

Closed
only21mil wants to merge 3 commits into
pingdotgg:mainfrom
only21mil:perf/13852-usage-keyset-paging
Closed

only21mil wants to merge 3 commits into
pingdotgg:mainfrom
only21mil:perf/13852-usage-keyset-paging

Conversation

@only21mil

@only21mil only21mil commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Cold or changed Antigravity databases load whole blob tables synchronously before yielding. #17615 switched each query to statement.all(); decoded usage maps also retain the original row blobs. #17139 caches unchanged databases, but cannot avoid this first read.

On a synthetic 318 MiB history, peak arrayBuffers fall from 308.3 to 76.8 MiB and the longest event-loop stall from 93.8 to 47.2 ms, medians of 6 reads per reader.

Change

Before #17615, the metadata reader iterated rows and yielded every 256 rows. This restores that granularity on the shared SqlClient by paging.

readDatabase reads gen_metadata, steps and trajectory_metadata_blob in 256-row keyset pages only for ordinary rowid tables with no index and no column named rowid. It uses WHERE rowid > ? ORDER BY rowid LIMIT ? and yields between full pages inside the same read-only transaction. The cursor starts below every rowid, then continues from the last rowid read as text and bound as a bigint, so rowids above 2^53, zero and negative rowids all page correctly. Other schemas keep main's exact single query.

A stable idx sort restores metadata order. Without an idx rowid alias, main sorts a rowid-order scan; its default single-threaded sorter preserves rowid order for equal keys, checked on SQLite 3.53.1 for in-memory and spilled sorts. SQLite does not guarantee tie order in general. On an idx INTEGER PRIMARY KEY alias, main's ORDER BY idx is a rowid scan with no sort step; indexed tables keep main's query and order.

metadata() decodes fields(usage.slice()) and fields(retryUsage.slice()), so retained fields hold small usage-message copies instead of whole row blobs. Paging alone reduced stalls but left retained blob memory unchanged; both changes address the same cold-read cost.

Valid blob results, merge/filter rules, the read-only SQLite layer, single transaction, busy_timeout = 100, database/WAL cache invalidation and exports stay the same. Main's ??= already skipped protobuf decoding after the first trajectory timestamp. A paged read stops after the page holding that timestamp, so a value node:sqlite cannot convert on a later page no longer fails the database; one on that same page still does.

Scope and approval

Issue #13852 is labelled accepted, and the maintainer's triage of 2026-09-26 confirms the cold-read stall on the server thread. #17615 later made each table read a single statement.all() on 2026-10-09. This establishes the problem, not approval of every implementation. Paging bounds row count, not blob bytes; worker isolation and a single very large blob remain follow-ups.

Disk persistence of decoded results was proposed in #17391, now closed, and stays out of scope here; nothing was copied or carried forward from it. Open #17400 fixes a different problem, double counting, and rewrites the same two fields(...) calls in metadata(), so whichever lands second must rebase and keep both .slice() copies.

Verification

Linux x64, Node 24.21.0. Focused checks ran from the repository root at this branch's head e146a97964:

vp test run --reporter=verbose apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
vp lint --report-unused-disable-directives apps/server/src/provider/Drivers/antigravityUsageReader.ts apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
vp fmt --check apps/server/src/provider/Drivers/antigravityUsageReader.ts apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
(cd apps/server && npx tsc --noEmit)
  • Main's reader at ed0f7b908b, with this branch's tests: exit 1, 4 failed and 15 passed.
  • This branch: exit 0, 19 passed. Both paged and fallback paths return main's results for these fixtures.
  • Lint, format and server typecheck: exit 0; no lint diagnostics or TypeScript errors.
Test details

Main's four failures:

  • "reads large Antigravity databases a bounded page of blobs at a time": AssertionError: SELECT idx, data FROM gen_metadata ORDER BY idx: expected 600 to be at most 256.
  • "pages Antigravity tables whose rowids exceed Number.MAX_SAFE_INTEGER": AssertionError: SELECT idx, data FROM gen_metadata ORDER BY idx: expected 300 to be at most 256.
  • "stops reading Antigravity trajectory pages at the first timestamp": AssertionError: SELECT data FROM trajectory_metadata_blob: expected 600 to be at most 256.
  • "reads Antigravity tables whose idx is an INTEGER PRIMARY KEY": AssertionError: expected gen_metadata to be read in rowid pages, got: SELECT idx, data FROM gen_metadata ORDER BY idx: expected false to be true.

The large fixture checks 600 generation and 600 step rows, 1,200 records, all 1,200 blob rows and at most 256 per read. The large-rowid fixture checks the minimum rowid, zero and values starting at 2^53 + 2, all 900 blob rows and ordered results. Its first full-page cursor is 2^53 + 255, which Number conversion would round. The trajectory fixture checks a timestamp in page two followed by malformed protobufs, exactly two 256-row reads and the expected timestamp. All assert no errors.

Other fixtures cover equal-idx rowid order with reversed blob byte order, parameterized INTEGER PRIMARY KEY/WITHOUT ROWID cases, a one-row-per-table rowid-column fallback, and indexed metadata/trajectory order. The INTEGER PRIMARY KEY case asserts paged SQL for both metadata tables; WITHOUT ROWID asserts main's exact query once for each, with 3 generation and 2 step blobs. The rowid-column and indexed tests also assert main's exact SQL runs once, unpaged; indexed order assertions record SQLite's current plan.

Mutations that remove the index check or the rowid-column check fail the matching fallback tests. Forcing paged off fails the four tests above, and converting the cursor to Number fails the large-rowid test.

This branch's tests failing on main's reader and passing on this branch

The focused test file on main's reader has 4 failures, exit 1; on this branch all 19 tests pass, exit 0.

The synthetic 318 MiB benchmark used Node 24.18.1 and ran on the first pushed head 39769cd575. The later commits change only tests, one code comment and how the probe result is read, and the benchmark was not rerun. All 12 runs returned 5 files, 9,856 records and no errors, with identical full-result SHA-256 after making database paths relative.

metric main this branch
wall time 390.4 ms 402.8 ms
longest event-loop stall (monitorEventLoopDelay max) 93.8 ms 47.2 ms
event-loop delay p99 87.6 ms 16.3 ms
max RSS 593.0 MiB 356.2 MiB
RSS added by the read (max RSS minus RSS before the call) 483.3 MiB 246.9 MiB
peak arrayBuffers 308.3 MiB 76.8 MiB
records 9,856 9,856
result sha256 309d8a23572b 309d8a23572b

Median of 6 interleaved cold reads per reader, fresh process each, warm OS page cache, shared host under load. Stall ranges: main 85.7 to 119.3 ms, branch 35.7 to 63.8 ms. Wall-time ranges: main 364.8 to 517.8 ms, branch 360.1 to 529.5 ms. Every branch stall was below the best main stall, but overlapping wall-time ranges establish no wall-time improvement or maximum stall guarantee.

Verbatim benchmark summary for main and this branch

Verbatim summary stdout for the six interleaved cold reads per reader.

Unpublished benchmark harness, commands and measurement method

The harness is not published or part of this PR. It generates one 309.5 MiB database with 9,600 rows per metadata table and four 2.1 MiB databases with 64 rows per table. Blobs contain 16 KiB of protobuf metadata with deterministic unused padding; generation/step response IDs match. Each database has four trajectory rows, uses WAL and closes cleanly before reading.

gen_metadata and steps declare idx INTEGER PRIMARY KEY, so main's ORDER BY idx is a rowid scan without a sort. With an unindexed non-key idx, main also sorts every blob; this benchmark does not measure that cost. The trajectory table is an ordinary unindexed rowid table.

Scratch files live in $TMPDIR/antigravity-bench, with reader copies in before/ and after/ and dependencies from the server:

cd "$TMPDIR/antigravity-bench"
node generate.mjs --out data --big-mb 300 --small-count 4 --small-mb 2 \
  --row-kb 16 --step-ratio 0.5 --trajectory-rows 4
mkdir -p results
node measure.mjs --reader before/antigravityUsageReader.ts \
  --data data --label main | tee -a results/r1-interleaved-main.jsonl
node measure.mjs --reader after/antigravityUsageReader.ts \
  --data data --label branch | tee -a results/r1-interleaved-branch.jsonl
node summarize.mjs results/r1-interleaved-main.jsonl results/r1-interleaved-branch.jsonl

Collect six pairs in ABBA order: main/branch on odd pairs, branch/main on even pairs. Each calls readAntigravityUsage(dir, 0) once in a fresh process without a reader cache. The OS page cache stays warm.

Stall uses monitorEventLoopDelay at 1 ms resolution, checked by a 1 ms interval watchdog. RSS uses the kernel high-water mark; 1 ms memory sampling may miss peaks during stalls. Added RSS takes each run's maximum minus baseline before the median. Full result hash: 309d8a23572b67f06fe50fb612a4b880032ac5dd64fb0f42430322c62eb74f51.

Not checked:

  • macOS and Windows.
  • Full suite was not run locally; CI runs it on each push.
  • A real large Antigravity history end to end or actual Antigravity schemas. No real Antigravity database was inspected.
  • A cold OS page cache or SQLite in the Node 26 server binary.

Model and harness: Claude Opus 5.5 in Claude Code and GPT-6.1 Sol in Codex, run from T3 Code.

🤖 Generated with Claude Code

Cold or changed Antigravity databases were read with one statement.all()
per blob table, so every row was in memory and the event loop was blocked
until the whole table returned. Decoded usage maps also held subarray views
of each row blob, which kept every blob alive until the read finished.

Page gen_metadata, steps and trajectory_metadata_blob by rowid in groups
of 256 only for ordinary rowid tables with no index and no column named
rowid. Yield between pages inside the same read-only transaction. For
these tables, main's ORDER BY idx sorts a rowid-order scan, so a stable
sort by idx after paging returns the same order. Every other table runs
main's exact query as one read, preserving its results and order.

Read the rowid cursor as text and bind it as a bigint so rowids above
2^53 do not fail. Tests cover those rowids, WITHOUT ROWID tables, a column
named rowid, covering indexes that retain main's tie order, and trajectory
paging. Copy each usage message before decoding it so the row blob can
be freed.

Refs pingdotgg#13852

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 11, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 11, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This changes a production Antigravity usage reader’s database access, ordering, memory retention, scheduling, and trajectory-decoding behavior, with schema-dependent paging and early termination. Extensive tests cover the intended cases, but the implementation is complex and usage/metering-sensitive rather than a purely mechanical performance tweak.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 11, 2026 — with ChatGPT Codex Connector
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 11, 2026 16:12

Dismissing prior approval to re-evaluate 39769cd

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3a1e057c-e9a2-4377-a6e1-ade05df5db35

📥 Commits

Reviewing files that changed from the base of the PR and between 39769cd and e146a97.


📒 Files selected for processing (2)
  • apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
  • apps/server/src/provider/Drivers/antigravityUsageReader.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.



📝 Walkthrough
📝 Walkthrough

Walkthrough

The Antigravity usage reader now pages eligible SQLite tables, preserves compatibility with other table forms, sorts metadata by idx, and stops trajectory scanning at the first non-null timestamp in query order.

Changes

Antigravity usage reads

Layer / File(s) Summary
Paged row access and table compatibility
apps/server/src/provider/Drivers/antigravityUsageReader.ts, apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
Eligible ordinary rowid tables are read in batches of up to 256 rows using text-cast rowid cursors. Other tables use single-query reads. Usage blobs are copied before parsing. Tests cover paging, large rowids, table compatibility, and query limits.
Metadata ordering and trajectory stopping
apps/server/src/provider/Drivers/antigravityUsageReader.ts, apps/server/src/provider/Drivers/antigravityUsageReader.test.ts
Metadata rows are sorted by idx. Trajectory scanning stops at the first non-null timestamp in query order. Tests cover repeated idx values and SQLite ordering for indexed queries.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: juliusmarminge


Merge Risk: ⚪ Minimal · up to e146a

This change reads Antigravity usage data in bounded pages, which lowers memory use and event-loop stalls on large databases. Results for valid data are unchanged. No actionable merge-blocking risk remains. The real Antigravity table schemas were not checked.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e146a

The change preserves existing read-only access and adds no new external entrypoint. Remaining uncertainty concerns ordering compatibility and interruption cleanup, rather than an observed security weakness.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The compared change does not expand database reachability or query authority: access remains through the existing provider-selected stores and read-only connections. No new caller, privileged operation, or cross-service path was identified in the inspected scope.

Trust Boundaries and Controls

  • observed — Database contents influence decoded usage records, but do not supply executable SQL. Table choices and query strings are fixed by the reader, cursor values are bound parameters, and the connection remains read-only with extension loading disabled by default.

Resilience and Maintainability Implications

  • observed — Only successful complete database reads populate the cache; read failures are recorded without appending partial candidates. Database and WAL fingerprints govern reuse, unvisited entries are evicted, and existing scoped finalizers close connections and release transaction permits. Interruption-specific transaction behavior remains a coverage limitation.

Pre-merge checks | Passed 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the main change: reducing memory use and event-loop stalls during cold Antigravity reads.
Description check Passed The description includes all required sections, explains the problem and implementation, links the related issue and maintainer triage, documents focused verification results, and identifies untested …

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

only21mil and others added 2 commits October 11, 2026 22:00
Qualify the reader comment on equal-idx order: it rests on SQLite's
default single-threaded sorter, and read the pageable probe with
Number() so it also holds under safe-integer mode.

Tests now guard the large-rowid read against an empty spy and cover
rowids 0 and the minimum, use an equal-idx fixture where rowid order
differs from blob byte order, check that unpageable tables run main's
single query, and fold the two keyed-table cases into one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The INTEGER PRIMARY KEY case now checks that both tables are read in
rowid pages, and the WITHOUT ROWID case that each keeps main's single
query. The large-rowid fixture starts at 2^53 + 2, so the first page's
cursor is a rowid that a Number would round. The reader comment notes
that the equal-key sort order is observed, not documented by SQLite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@only21mil only21mil changed the title perf(server): read Antigravity usage in bounded pages perf(usage): reduce memory and stalls in cold Antigravity reads Oct 12, 2026
@t3dotgg

t3dotgg commented Oct 14, 2026

Copy link
Copy Markdown
Member

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Taken over in #19446. Your three commits are cherry-picked unchanged and credited there.

The no-index check skips the real schema. Antigravity declares idx as the primary key and indexes steps (schema fingerprint 1ca98426f561), so steps was never paged. Each gen_metadata row holds the whole context so far, so a 256-row page was the whole table. On 211 synthetic databases with that schema, the longest cold-read stall was about 320 ms on both main and this branch.

#19446 pages both tables by rowid (one generation, or up to 64 steps, per statement), keeps your .slice() copies, and also bounds OpenCode's first page. The longest cold stall there is 6 ms.

@t3dotgg

t3dotgg commented Oct 14, 2026

Copy link
Copy Markdown
Member

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Closing: this work landed in #19446 as 52ffe0c, with your commits included and you credited as co-author.

@t3dotgg t3dotgg closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants