Skip to content

ci: benchmark PRs against main with the incremental update suite#1650

Open
kevin-dp wants to merge 3 commits into
mainfrom
kevin/perf-benchmark-ci
Open

ci: benchmark PRs against main with the incremental update suite#1650
kevin-dp wants to merge 3 commits into
mainfrom
kevin/perf-benchmark-ci

Conversation

@kevin-dp

@kevin-dp kevin-dp commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Extracts the incremental update benchmark from #1648 into a standalone suite and adds a CI workflow that automatically measures the performance impact of PRs against main.

What's included

  • scripts/bench/incremental-update.ts — the benchmark from perf(db): explore incremental update hot paths #1648, with the opt-in perf-tracing removed (those trace hooks only exist on that branch's instrumented code paths, so the extracted benchmark relies purely on its own wall-clock timing and runs against any revision). It measures per-write latency (median/p75/p95/min/max/stddev) plus cold hydrate time for five live-query shapes — filtered+ordered list, join, aggregate subquery, nested includes, detail view — across fixture scales (100/1k/10k rows), source index modes (none/manual), and mutation modes (synced/optimistic).
  • scripts/bench/compare-incremental-update.ts — compares two benchmark reports and emits a markdown summary: per-case deltas with significance flags (default: >20% relative and >0.05ms absolute, so sub-ms jitter isn't flagged), plus geometric-mean ratios overall and per group to surface consistent shifts that are individually below the noise floor.
  • .github/workflows/benchmark.yml — runs on non-draft PRs that touch packages/db, packages/db-ivm, or the benchmark itself (and on workflow_dispatch). It benchmarks the PR merge commit and its merge-base with main back-to-back on the same runner using the same benchmark code for both sides, then publishes the comparison as a job summary, uploads the raw reports as artifacts, and posts/updates a sticky PR comment (skipped gracefully for fork PRs, whose token is read-only — the job summary always has the results).

Trigger choice

Draft PRs are skipped, so the benchmark starts when a PR is opened/marked ready for review and re-runs on pushes to it (with concurrency cancellation). Easy to tighten later (e.g. label-gated or on approval only) by changing the on:/if: conditions.

Validation

  • Benchmark runs green against current main (the suite is decoupled from perf(db): explore incremental update hot paths #1648's code changes).
  • Compared two same-code runs: overall geomean 1.01×, no significant changes flagged — the noise floor holds.
  • Compared perf(db): explore incremental update hot paths #1648's branch against main with this tooling: overall geomean 0.86× with consistent per-group improvements at the 10k scale (down to 0.5× medians), demonstrating that real perf changes are clearly visible in the report. (It also flagged a >3× median regression for the issue detail + comments case with optimistic writes at 10k scale on that branch — worth a look over there.)
  • Full local run takes ~12s (M2); the CI config uses 100 iterations + 20 warmup writes per case, so the whole job should stay in the low minutes.
  • Workflow passes zizmor with no findings.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a new Benchmark workflow that runs on pull requests and manual dispatch, generating baseline vs. candidate benchmark outputs and summaries.
    • Introduced an incremental update benchmark runner with configurable scenarios, warmups, iterations, and detailed runtime metrics.
    • Added a comparison tool that produces a Markdown report with regression/improvement detection and rich per-case breakdowns.
  • Bug Fixes
    • Improved handling for draft and forked pull requests when skipping runs and updating PR comments.
  • Documentation
    • Benchmark results are now posted to PR comments and included in job summaries for easier review.

kevin-dp and others added 2 commits July 7, 2026 15:02
Extracts the incremental update benchmark from #1648 (with the opt-in
perf tracing removed, since those hooks only exist on that branch) and
adds a workflow that runs it on non-draft PRs touching packages/db,
packages/db-ivm, or the benchmark itself. The workflow benchmarks the
PR merge commit and its merge-base with the same benchmark code, then
posts a comparison as a job summary and sticky PR comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-row significance flags miss consistent sub-floor shifts across many
cases; the geometric mean of per-case median ratios surfaces them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Incremental update benchmark

Comparing e1edb50e (this PR) against 3ec3e7ae (base). Times are per-write medians over 100 iterations (20 warmup writes).

1 regression(s), 0 improvement(s) (threshold: ±20% and >0.05ms).

Overall median write time vs base: 1.02× (geometric mean of per-case ratios; lower is faster).

Query Δ median (geomean) Best case Worst case Flags
list: newest 50 open -2.1% -37.7% +86.1%
list + author -0.4% -22.1% +64.3%
list + comment count -0.9% -14.1% +29.5%
list + 3 recent comments +3.7% -25.4% +121.6% 1 🔴
issue detail + comments +11.4% -16.5% +94.4%

Each row aggregates the 12 scale/index/write-mode configurations of that query; per-configuration tables below.

100 rows/collection | source indexes: none | synced writes — geomean 1.05×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.147ms 0.136ms -7.2% 0.299ms 0.230ms -23.0%
list + author 0.146ms 0.145ms -0.7% 0.260ms 0.260ms +0.3%
list + comment count 0.142ms 0.143ms +0.8% 0.271ms 0.241ms -10.9%
list + 3 recent comments 0.099ms 0.089ms -10.0% 0.200ms 0.184ms -8.4%
issue detail + comments 0.063ms 0.095ms +50.1% 0.161ms 0.154ms -4.7%
100 rows/collection | source indexes: none | optimistic writes — geomean 0.98×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.183ms 0.188ms +3.1% 0.378ms 0.288ms -23.8%
list + author 0.147ms 0.168ms +14.6% 0.291ms 0.290ms -0.5%
list + comment count 0.127ms 0.120ms -5.2% 0.223ms 0.208ms -6.9%
list + 3 recent comments 0.092ms 0.084ms -8.6% 0.161ms 0.179ms +10.9%
issue detail + comments 0.110ms 0.098ms -10.5% 0.163ms 0.189ms +15.9%
100 rows/collection | source indexes: manual | synced writes — geomean 1.48×, 1 change(s)
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.063ms 0.061ms -2.8% 0.119ms 0.153ms +28.4%
list + author 0.075ms 0.123ms +64.3% 0.167ms 0.175ms +4.9%
list + comment count 0.081ms 0.105ms +29.5% 0.177ms 0.169ms -4.1%
list + 3 recent comments 0.050ms 0.110ms +121.6% 0.082ms 0.160ms +96.2% 🔴
issue detail + comments 0.071ms 0.109ms +53.6% 0.094ms 0.141ms +49.9%
100 rows/collection | source indexes: manual | optimistic writes — geomean 1.06×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.122ms 0.141ms +16.4% 0.223ms 0.205ms -8.2%
list + author 0.177ms 0.138ms -22.1% 0.315ms 0.262ms -16.7%
list + comment count 0.093ms 0.089ms -3.8% 0.126ms 0.131ms +3.9%
list + 3 recent comments 0.071ms 0.115ms +60.5% 0.164ms 0.160ms -2.5%
issue detail + comments 0.066ms 0.064ms -3.0% 0.116ms 0.092ms -21.0%
1,000 rows/collection | source indexes: none | synced writes — geomean 1.08×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.040ms 0.039ms -2.9% 0.046ms 0.048ms +5.9%
list + author 0.057ms 0.056ms -1.4% 0.080ms 0.091ms +14.0%
list + comment count 0.064ms 0.060ms -7.0% 0.096ms 0.141ms +46.9%
list + 3 recent comments 0.079ms 0.094ms +19.3% 0.117ms 0.137ms +17.2%
issue detail + comments 0.061ms 0.086ms +41.2% 0.112ms 0.127ms +13.7%
1,000 rows/collection | source indexes: none | optimistic writes — geomean 0.87×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.118ms 0.080ms -32.5% 0.181ms 0.169ms -6.5%
list + author 0.155ms 0.140ms -10.2% 0.192ms 0.192ms +0.3%
list + comment count 0.079ms 0.078ms -1.2% 0.106ms 0.105ms -0.2%
list + 3 recent comments 0.065ms 0.059ms -9.9% 0.123ms 0.092ms -24.8%
issue detail + comments 0.057ms 0.053ms -6.6% 0.078ms 0.090ms +14.9%
1,000 rows/collection | source indexes: manual | synced writes — geomean 0.89×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.078ms 0.049ms -37.7% 0.124ms 0.137ms +10.0%
list + author 0.060ms 0.058ms -3.5% 0.132ms 0.121ms -7.9%
list + comment count 0.060ms 0.059ms -1.7% 0.125ms 0.118ms -5.7%
list + 3 recent comments 0.038ms 0.038ms -0.1% 0.049ms 0.056ms +13.6%
issue detail + comments 0.037ms 0.034ms -6.3% 0.086ms 0.062ms -28.0%
1,000 rows/collection | source indexes: manual | optimistic writes — geomean 1.00×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.136ms 0.146ms +7.3% 0.217ms 0.214ms -1.2%
list + author 0.103ms 0.100ms -2.5% 0.147ms 0.132ms -10.1%
list + comment count 0.082ms 0.083ms +0.2% 0.172ms 0.115ms -33.0%
list + 3 recent comments 0.065ms 0.063ms -2.3% 0.113ms 0.113ms +0.2%
issue detail + comments 0.060ms 0.058ms -2.4% 0.091ms 0.091ms -0.0%
10,000 rows/collection | source indexes: none | synced writes — geomean 0.99×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.040ms 0.074ms +86.1% 0.089ms 0.099ms +11.7%
list + author 0.077ms 0.068ms -10.8% 0.159ms 0.085ms -46.2%
list + comment count 0.059ms 0.050ms -14.1% 0.172ms 0.067ms -61.1%
list + 3 recent comments 0.128ms 0.103ms -19.9% 0.162ms 0.142ms -12.5%
issue detail + comments 0.065ms 0.054ms -16.5% 0.099ms 0.078ms -21.4%
10,000 rows/collection | source indexes: none | optimistic writes — geomean 0.92×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.083ms 0.084ms +1.2% 0.110ms 0.114ms +2.9%
list + author 0.104ms 0.106ms +1.2% 0.138ms 0.134ms -3.2%
list + comment count 0.103ms 0.097ms -6.3% 0.164ms 0.137ms -16.5%
list + 3 recent comments 0.079ms 0.059ms -25.4% 0.164ms 0.138ms -16.2%
issue detail + comments 0.059ms 0.053ms -8.8% 0.081ms 0.078ms -3.0%
10,000 rows/collection | source indexes: manual | synced writes — geomean 1.08×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.045ms 0.040ms -11.5% 0.080ms 0.059ms -26.6%
list + author 0.060ms 0.055ms -8.1% 0.082ms 0.079ms -3.7%
list + comment count 0.064ms 0.066ms +3.0% 0.094ms 0.106ms +13.4%
list + 3 recent comments 0.039ms 0.034ms -10.9% 0.078ms 0.059ms -24.9%
issue detail + comments 0.035ms 0.068ms +94.4% 0.080ms 0.145ms +80.5%
10,000 rows/collection | source indexes: manual | optimistic writes — geomean 0.98×
Query Base median PR median Δ median Base p95 PR p95 Δ p95
list: newest 50 open 0.099ms 0.097ms -1.4% 0.165ms 0.152ms -7.4%
list + author 0.108ms 0.102ms -5.3% 0.190ms 0.132ms -30.5%
list + comment count 0.108ms 0.108ms +0.2% 0.144ms 0.167ms +16.1%
list + 3 recent comments 0.070ms 0.067ms -5.1% 0.154ms 0.096ms -37.7%
issue detail + comments 0.070ms 0.072ms +2.5% 0.144ms 0.122ms -15.1%

Runner: node v24.8.0, linux 6.17.0-1018-azure, AMD EPYC 9V74 80-Core Processor. Timings on shared CI runners are noisy; treat small deltas as indicative only.

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1650

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1650

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1650

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1650

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1650

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1650

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1650

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1650

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1650

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1650

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1650

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1650

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1650

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1650

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1650

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1650

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1650

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1650

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1650

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1650

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1650

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1650

commit: a7e9584

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 125 kB

ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/collection/change-events.js 1.43 kB
packages/db/dist/esm/collection/changes.js 1.38 kB
packages/db/dist/esm/collection/cleanup-queue.js 810 B
packages/db/dist/esm/collection/events.js 434 B
packages/db/dist/esm/collection/index.js 3.62 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 1.69 kB
packages/db/dist/esm/collection/mutations.js 2.47 kB
packages/db/dist/esm/collection/state.js 5.4 kB
packages/db/dist/esm/collection/subscription.js 3.74 kB
packages/db/dist/esm/collection/sync.js 2.88 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 5.1 kB
packages/db/dist/esm/event-emitter.js 748 B
packages/db/dist/esm/index.js 3.1 kB
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 767 B
packages/db/dist/esm/indexes/basic-index.js 2.06 kB
packages/db/dist/esm/indexes/btree-index.js 2.19 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 557 B
packages/db/dist/esm/local-only.js 916 B
packages/db/dist/esm/local-storage.js 2.12 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.75 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 5.84 kB
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.89 kB
packages/db/dist/esm/query/compiler/expressions.js 430 B
packages/db/dist/esm/query/compiler/group-by.js 3.56 kB
packages/db/dist/esm/query/compiler/index.js 6.67 kB
packages/db/dist/esm/query/compiler/joins.js 2.5 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 923 B
packages/db/dist/esm/query/compiler/order-by.js 1.74 kB
packages/db/dist/esm/query/compiler/select.js 1.53 kB
packages/db/dist/esm/query/effect.js 4.77 kB
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir.js 1.25 kB
packages/db/dist/esm/query/live-query-collection.js 360 B
packages/db/dist/esm/query/live/collection-config-builder.js 9.1 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 1.93 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/utils.js 1.81 kB
packages/db/dist/esm/query/optimizer.js 2.92 kB
packages/db/dist/esm/query/predicate-utils.js 2.97 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/subset-dedupe.js 960 B
packages/db/dist/esm/scheduler.js 1.3 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 3.04 kB
packages/db/dist/esm/utils.js 927 B
packages/db/dist/esm/utils/array-utils.js 273 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 5.61 kB
packages/db/dist/esm/utils/comparison.js 1.11 kB
packages/db/dist/esm/utils/cursor.js 457 B
packages/db/dist/esm/utils/index-optimization.js 2.39 kB
packages/db/dist/esm/utils/type-guards.js 157 B
packages/db/dist/esm/utils/uuid.js 449 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6798c84c-a8d1-49c7-bfc1-db6a3df3bf0b

📥 Commits

Reviewing files that changed from the base of the PR and between 4c1de95 and a7e9584.

📒 Files selected for processing (1)
  • scripts/bench/compare-incremental-update.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/bench/compare-incremental-update.ts

📝 Walkthrough

Walkthrough

Adds a benchmark runner for incremental-update scenarios, a comparison script for base versus candidate reports, and a GitHub Actions workflow that runs both on pull requests and manual dispatches, publishes artifacts, and updates a PR comment.

Changes

Benchmark Suite

Layer / File(s) Summary
Benchmark runner: types, CLI args, and query cases
scripts/bench/incremental-update.ts
Defines fixture and result types, parses benchmark CLI options, and declares the live-query benchmark cases and write functions.
Benchmark execution and fixture management
scripts/bench/incremental-update.ts
Runs each benchmark case, builds manual-sync collections and fixtures, applies source indexes, performs mutation scenarios, and finalizes write cleanup.
Statistics and report formatting
scripts/bench/incremental-update.ts
Computes timing statistics, gathers runtime metadata, and formats the textual benchmark report output.
Base/candidate comparison script
scripts/bench/compare-incremental-update.ts
Loads benchmark reports, matches runs by composite key, classifies regressions and improvements, computes aggregate ratios, renders Markdown output, and parses comparison CLI options.
CI workflow wiring
.github/workflows/benchmark.yml
Adds the Benchmark workflow, runs the benchmark on PR head and base commits, publishes comparison artifacts, and updates a PR comment.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHub as GitHub Actions
  participant Benchmark as benchmark.yml
  participant Runner as incremental-update.ts
  participant Compare as compare-incremental-update.ts
  participant PullRequest as PR comment
  GitHub->>Benchmark: trigger on PR or dispatch
  Benchmark->>Runner: generate candidate.json and base.json
  Benchmark->>Compare: generate comparison.md
  Benchmark->>PullRequest: post or update comment
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new CI benchmark workflow comparing PRs against main.
Description check ✅ Passed It describes the benchmark extraction, workflow, triggers, and validation, though it doesn't use the repo's exact template sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kevin/perf-benchmark-ci

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.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 4.26 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/index.js 249 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
packages/react-db/dist/esm/useLiveQuery.js 1.37 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 567 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
scripts/bench/compare-incremental-update.ts (2)

97-108: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Key joining without escaping risks collisions.

resultKey concatenates fields with | without any escaping or type prefixes. If any label/query string ever contains |, boundaries between fields shift and unrelated results could be matched incorrectly.

As per coding guidelines, "When converting keys to strings, ensure no collisions by using proper encoding with type prefixes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench/compare-incremental-update.ts` around lines 97 - 108, The
resultKey helper is building a composite string key by joining raw RunResult
fields with a plain delimiter, which can collide if any value contains that
delimiter. Update resultKey to use a collision-safe encoding with explicit type
prefixes or escaped/serialized fields so each RunResult property remains
unambiguous, and keep the matching logic in compare-incremental-update.ts
aligned with the new key format.

Source: Coding guidelines


159-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

isRegression/isImprovement are near-duplicate logic.

Both functions share the same significance check and only differ in ratio direction; could be collapsed into a single classify helper returning 'regression' | 'improvement' | 'none' to avoid recomputing isSignificant twice per comparison in formatMarkdown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench/compare-incremental-update.ts` around lines 159 - 179, The
isRegression and isImprovement helpers duplicate the same significance logic and
cause isSignificant to be evaluated twice for each Comparison in formatMarkdown.
Refactor the comparison classification in compare-incremental-update.ts into a
single helper (for example, a classify-style function) that checks significance
once and returns regression, improvement, or none based on medianRatio
direction, then update formatMarkdown to use that shared result instead of
calling both helpers separately.
.github/workflows/benchmark.yml (1)

22-28: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider a job-level timeout.

No timeout-minutes is set; a hung pnpm install/build/benchmark could occupy the runner up to GitHub's default (6h) before failing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/benchmark.yml around lines 22 - 28, The benchmark job
currently has no execution limit, so a hung install/build/benchmark can hold a
runner until GitHub’s default timeout. Add a job-level timeout setting to the
benchmark job in the workflow definition alongside the existing if/runs-on
fields, using the benchmark job identifier to keep the limit explicit and
prevent runaway runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/bench/compare-incremental-update.ts`:
- Around line 15-52: The local type copies in compare-incremental-update.ts are
drifting from the shared incremental-update.ts contract, and
sourceIndexMode/mutationMode have been widened to string. Replace the duplicated
Summary, FixtureScale, RunResult, and Report definitions by importing the shared
types from incremental-update.ts, or export them there if needed, so
compare-incremental-update stays aligned with the canonical SourceIndexMode and
MutationMode unions and preserves type safety.
- Around line 204-296: The footer in formatMarkdown is incorrectly reading
runtime data from the outer candidate closure instead of the candidateReport
parameter, which breaks the function’s explicit data flow. Update the Runner
line to use candidateReport.metadata for node, platform, and cpu so
formatMarkdown depends only on its arguments and remains testable/reusable. Keep
the rest of the function consistent with the existing use of baseReport and
candidateReport.

---

Nitpick comments:
In @.github/workflows/benchmark.yml:
- Around line 22-28: The benchmark job currently has no execution limit, so a
hung install/build/benchmark can hold a runner until GitHub’s default timeout.
Add a job-level timeout setting to the benchmark job in the workflow definition
alongside the existing if/runs-on fields, using the benchmark job identifier to
keep the limit explicit and prevent runaway runs.

In `@scripts/bench/compare-incremental-update.ts`:
- Around line 97-108: The resultKey helper is building a composite string key by
joining raw RunResult fields with a plain delimiter, which can collide if any
value contains that delimiter. Update resultKey to use a collision-safe encoding
with explicit type prefixes or escaped/serialized fields so each RunResult
property remains unambiguous, and keep the matching logic in
compare-incremental-update.ts aligned with the new key format.
- Around line 159-179: The isRegression and isImprovement helpers duplicate the
same significance logic and cause isSignificant to be evaluated twice for each
Comparison in formatMarkdown. Refactor the comparison classification in
compare-incremental-update.ts into a single helper (for example, a
classify-style function) that checks significance once and returns regression,
improvement, or none based on medianRatio direction, then update formatMarkdown
to use that shared result instead of calling both helpers separately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db0f6f77-810d-4a17-a5e2-33d8c19ebc76

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec3e7a and 4c1de95.

📒 Files selected for processing (3)
  • .github/workflows/benchmark.yml
  • scripts/bench/compare-incremental-update.ts
  • scripts/bench/incremental-update.ts

Comment on lines +15 to +52
type Summary = {
iterations: number
medianMs: number
p75Ms: number
p95Ms: number
minMs: number
maxMs: number
stddevMs: number
}

type FixtureScale = {
label: string
issueCount: number
userCount: number
commentCount: number
}

type RunResult = {
query: string
scenario: string
scale: FixtureScale
sourceIndexMode: string
mutationMode: string
coldHydrateMs: number
writeSummary: Summary
}

type Report = {
metadata: {
node: string
platform: string
cpu: string
gitSha: string
iterations: number
warmup: number
}
results: Array<RunResult>
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicated types drift from the upstream contract.

Summary, FixtureScale, and RunResult are copy-pasted from scripts/bench/incremental-update.ts. Worse, sourceIndexMode/mutationMode are widened here to string instead of the SourceIndexMode/MutationMode literal unions used upstream (per the shared contract in incremental-update.ts:85-121), so this file loses type safety and both copies can silently diverge as the runner evolves.

Consider exporting the shared types from incremental-update.ts and importing them here.

As per coding guidelines, "Extract common logic into utility functions when identical or near-identical code blocks appear in multiple places."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench/compare-incremental-update.ts` around lines 15 - 52, The local
type copies in compare-incremental-update.ts are drifting from the shared
incremental-update.ts contract, and sourceIndexMode/mutationMode have been
widened to string. Replace the duplicated Summary, FixtureScale, RunResult, and
Report definitions by importing the shared types from incremental-update.ts, or
export them there if needed, so compare-incremental-update stays aligned with
the canonical SourceIndexMode and MutationMode unions and preserves type safety.

Source: Coding guidelines

Comment on lines +204 to +296
function formatMarkdown(
baseReport: Report,
candidateReport: Report,
allComparisons: Array<Comparison>,
threshold: number,
): string {
const lines: Array<string> = []
const regressed = allComparisons.filter((comparison) =>
isRegression(comparison, threshold),
)
const improved = allComparisons.filter((comparison) =>
isImprovement(comparison, threshold),
)

lines.push(`## Incremental update benchmark`)
lines.push(``)
lines.push(
`Comparing \`${candidateReport.metadata.gitSha}\` (this PR) against \`${baseReport.metadata.gitSha}\` (base). ` +
`Times are per-write medians over ${candidateReport.metadata.iterations} iterations ` +
`(${candidateReport.metadata.warmup} warmup writes).`,
)
lines.push(``)

if (regressed.length === 0 && improved.length === 0) {
lines.push(
`**No significant changes** (threshold: ±${Math.round(threshold * 100)}% and >${absoluteFloorMs}ms).`,
)
} else {
lines.push(
`**${regressed.length} regression(s), ${improved.length} improvement(s)** ` +
`(threshold: ±${Math.round(threshold * 100)}% and >${absoluteFloorMs}ms).`,
)
}
lines.push(``)
lines.push(
`Overall median write time vs base: **${formatRatio(
geomeanRatio(allComparisons),
)}** (geometric mean of per-case ratios; lower is faster).`,
)
lines.push(``)

const groups = new Map<string, Array<Comparison>>()
for (const comparison of allComparisons) {
const groupKey = `${formatScale(comparison.scale)} | source indexes: ${
comparison.sourceIndexMode
} | ${comparison.mutationMode} writes`
const group = groups.get(groupKey) ?? []
group.push(comparison)
groups.set(groupKey, group)
}

for (const [groupKey, group] of groups) {
lines.push(`<details>`)
const changed = group.filter(
(comparison) => marker(comparison, threshold) !== ``,
).length
const changedSuffix = changed > 0 ? `, ${changed} change(s)` : ``
lines.push(
`<summary><b>${groupKey}</b> — geomean ${formatRatio(
geomeanRatio(group),
)}${changedSuffix}</summary>`,
)
lines.push(``)
lines.push(
`| Query | Base median | PR median | Δ median | Base p95 | PR p95 | Δ p95 | |`,
)
lines.push(`| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |`)
for (const comparison of group) {
lines.push(
`| ${comparison.query} | ${formatMs(
comparison.base.writeSummary.medianMs,
)} | ${formatMs(comparison.candidate.writeSummary.medianMs)} | ${formatDelta(
comparison.medianRatio,
)} | ${formatMs(comparison.base.writeSummary.p95Ms)} | ${formatMs(
comparison.candidate.writeSummary.p95Ms,
)} | ${formatDelta(comparison.p95Ratio)} | ${marker(
comparison,
threshold,
)} |`,
)
}
lines.push(``)
lines.push(`</details>`)
lines.push(``)
}

lines.push(
`<sub>Runner: node ${candidate.metadata.node}, ${candidate.metadata.platform}, ${candidate.metadata.cpu}. ` +
`Timings on shared CI runners are noisy; treat small deltas as indicative only.</sub>`,
)

return lines.join(`\n`)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Uses closure variable candidate instead of the candidateReport parameter.

Lines 290-293 reference the top-level candidate variable rather than the candidateReport parameter that the rest of the function correctly uses (e.g. lines 221-224). It happens to work today because they point to the same object, but it breaks the function's parameter contract and hurts testability if this function is ever reused/tested independently.

As per coding guidelines, "Prefer explicit function parameters over closures to improve testability and clarify data flow dependencies."

♻️ Proposed fix
   lines.push(
-    `<sub>Runner: node ${candidate.metadata.node}, ${candidate.metadata.platform}, ${candidate.metadata.cpu}. ` +
+    `<sub>Runner: node ${candidateReport.metadata.node}, ${candidateReport.metadata.platform}, ${candidateReport.metadata.cpu}. ` +
       `Timings on shared CI runners are noisy; treat small deltas as indicative only.</sub>`,
   )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function formatMarkdown(
baseReport: Report,
candidateReport: Report,
allComparisons: Array<Comparison>,
threshold: number,
): string {
const lines: Array<string> = []
const regressed = allComparisons.filter((comparison) =>
isRegression(comparison, threshold),
)
const improved = allComparisons.filter((comparison) =>
isImprovement(comparison, threshold),
)
lines.push(`## Incremental update benchmark`)
lines.push(``)
lines.push(
`Comparing \`${candidateReport.metadata.gitSha}\` (this PR) against \`${baseReport.metadata.gitSha}\` (base). ` +
`Times are per-write medians over ${candidateReport.metadata.iterations} iterations ` +
`(${candidateReport.metadata.warmup} warmup writes).`,
)
lines.push(``)
if (regressed.length === 0 && improved.length === 0) {
lines.push(
`**No significant changes** (threshold: ±${Math.round(threshold * 100)}% and >${absoluteFloorMs}ms).`,
)
} else {
lines.push(
`**${regressed.length} regression(s), ${improved.length} improvement(s)** ` +
`(threshold: ±${Math.round(threshold * 100)}% and >${absoluteFloorMs}ms).`,
)
}
lines.push(``)
lines.push(
`Overall median write time vs base: **${formatRatio(
geomeanRatio(allComparisons),
)}** (geometric mean of per-case ratios; lower is faster).`,
)
lines.push(``)
const groups = new Map<string, Array<Comparison>>()
for (const comparison of allComparisons) {
const groupKey = `${formatScale(comparison.scale)} | source indexes: ${
comparison.sourceIndexMode
} | ${comparison.mutationMode} writes`
const group = groups.get(groupKey) ?? []
group.push(comparison)
groups.set(groupKey, group)
}
for (const [groupKey, group] of groups) {
lines.push(`<details>`)
const changed = group.filter(
(comparison) => marker(comparison, threshold) !== ``,
).length
const changedSuffix = changed > 0 ? `, ${changed} change(s)` : ``
lines.push(
`<summary><b>${groupKey}</b> — geomean ${formatRatio(
geomeanRatio(group),
)}${changedSuffix}</summary>`,
)
lines.push(``)
lines.push(
`| Query | Base median | PR median | Δ median | Base p95 | PR p95 | Δ p95 | |`,
)
lines.push(`| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |`)
for (const comparison of group) {
lines.push(
`| ${comparison.query} | ${formatMs(
comparison.base.writeSummary.medianMs,
)} | ${formatMs(comparison.candidate.writeSummary.medianMs)} | ${formatDelta(
comparison.medianRatio,
)} | ${formatMs(comparison.base.writeSummary.p95Ms)} | ${formatMs(
comparison.candidate.writeSummary.p95Ms,
)} | ${formatDelta(comparison.p95Ratio)} | ${marker(
comparison,
threshold,
)} |`,
)
}
lines.push(``)
lines.push(`</details>`)
lines.push(``)
}
lines.push(
`<sub>Runner: node ${candidate.metadata.node}, ${candidate.metadata.platform}, ${candidate.metadata.cpu}. ` +
`Timings on shared CI runners are noisy; treat small deltas as indicative only.</sub>`,
)
return lines.join(`\n`)
}
lines.push(
`<sub>Runner: node ${candidateReport.metadata.node}, ${candidateReport.metadata.platform}, ${candidateReport.metadata.cpu}. ` +
`Timings on shared CI runners are noisy; treat small deltas as indicative only.</sub>`,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/bench/compare-incremental-update.ts` around lines 204 - 296, The
footer in formatMarkdown is incorrectly reading runtime data from the outer
candidate closure instead of the candidateReport parameter, which breaks the
function’s explicit data flow. Update the Runner line to use
candidateReport.metadata for node, platform, and cpu so formatMarkdown depends
only on its arguments and remains testable/reusable. Keep the rest of the
function consistent with the existing use of baseReport and candidateReport.

Source: Coding guidelines

The per-configuration tables are collapsed, so the comment showed no
per-query numbers without expanding; a visible rollup (geomean, spread,
flags per query) carries the signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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