Skip to content

perf(db): read the newest N days without sorting all of history - #611

Merged
abdulsaheel merged 7 commits into
OpenStrap:mainfrom
DropTabl:fix/served-day-scan
Oct 31, 2026
Merged

abdulsaheel merged 7 commits into
OpenStrap:mainfrom
DropTabl:fix/served-day-scan

Conversation

@DropTabl

@DropTabl DropTabl commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Why

Home gets slower the longer someone has used the app. The growth soak (#609) measured HomeData.load at 22 ms with 30 days of history and 303 ms with 730, nearly all of it in getToday.

The cause is LocalDb.recentDayResults. It applied its LIMIT after joining and sorting every day's served row, payload_json (~100 KB a day) included, so reading the last 30 days meant fetching and sorting all of history. It runs inside refreshComputeFreshness, which happens on every derive and on every getToday while a night is still building, and in _latestBundle / crossday callers.

Per call, measured with a real 103 KB bundle:

30 days 365 days 730 days
recentDayResults(30) 57–65 ms 137–163 ms 253–600 ms
refreshComputeFreshness 319–327 ms 327–538 ms 540–1031 ms
recentDayResultsMeta(30) 2 ms 3 ms 6 ms

What changed

  • The served-version subquery is factored out of _servedDayJoin into _servedVersions. _servedDayJoin expands to exactly the same SQL as before, so every other reader and the coach views are unchanged, and the ceiling (F-31) stays in one place.
  • New _servedNewestDaysJoin applies ORDER BY day_id DESC LIMIT ? inside that subquery. The newest N days are picked from the covering index first, and only those N rows are fetched.
  • recentDayResults and recentDayResultsMeta use it. The other readers read all of history by design, so they are untouched.

Tests

db_serve_version_and_reads_test, new test "the newest-N reads stay flat as history grows":

  • It builds 30 and then 600 days, each with a ~100 KB bundle at three versions (an older version, the served one, and a future one that must never be served).
  • Rows are checked against the old query verbatim as an oracle, for both recentDayResults and recentDayResultsMeta.
  • Timing: at 600 days, measured back to back on the same machine, the new read must be at least 3× faster than the old query.
    • main: 14 → 132 ms, fails.
    • This change: 14 → 11 ms.

The existing F-31 tests (a future version is never served, an older one still is) pass unchanged. flutter analyze shows no issues, and the DB / repository / coach / readiness tests pass (484).

Not in this PR

sleepWindowRows has the same shape (1 → 131 ms at 730 days) but filters on the served row before its LIMIT, so this rewrite doesn't fit it. It isn't on the Home path.

🤖 Generated with Claude Code

Summary by Sourcery

Keep recent day-result reads fast as the database history grows.

Enhancements:

  • Optimize newest-day result reads by selecting the requested days before loading and sorting their full records, keeping performance stable as history grows.

Tests:

  • Add coverage verifying newest-day reads match the previous served-version behavior and remain at least three times faster with large histories.

Summary by CodeRabbit

  • Bug Fixes
    • Recent-day results, metadata, and scalar values now consistently cover the newest requested days. Older days are less likely to appear in place of more recent ones when results are combined across day and version data, keeping these related views aligned.

recentDayResults applied its LIMIT after joining and sorting every day's
served row, payload_json (~100 KB) included, so the 30-day read behind
refreshComputeFreshness went from ~60 ms at 30 days of history to 250-600 ms
at 730. That runs on every derive and on every Home load while a night is
still building: the growth soak (OpenStrap#609) measured getToday at 22 -> 303 ms.

Pick the newest N served versions from the covering index first, then fetch
only those rows. The served-version subquery is factored out of
_servedDayJoin, which expands to the same SQL as before, so the ceiling stays
in one place. recentDayResultsMeta takes the same join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR improves Home-path database performance by selecting the newest N served day IDs through the covering index before joining large result payloads, preserving version-ceiling semantics and query results while avoiding scans and sorts over all historical rows. A regression test verifies equivalence for full and metadata reads and checks that performance remains nearly flat as history grows.

Sequence diagram for optimized recent day database read

sequenceDiagram
    participant HomePath
    participant LocalDb
    participant SQLite
    participant DayResult

    HomePath->>LocalDb: recentDayResults(limit)
    LocalDb->>SQLite: _servedNewestDaysJoin(limit)
    SQLite->>DayResult: Select MAX(algo_version) <= _servedAlgoCeiling
    DayResult-->>SQLite: Newest N day_id/version pairs
    SQLite->>DayResult: Join selected rows via _servedOn
    DayResult-->>SQLite: N result rows with payloads
    SQLite-->>LocalDb: Rows ordered by day_id DESC
    LocalDb-->>HomePath: Newest N day results
Loading

Flow diagram for newest-N served day reads

flowchart LR
    A[recentDayResults or recentDayResultsMeta] --> B[_servedNewestDaysJoin]
    B --> C[_servedVersions]
    C --> D[Covering index: newest day_id values]
    D --> E[ORDER BY day_id DESC LIMIT N]
    E --> F[_servedOn joins selected versions]
    F --> G[Fetch N result rows and payloads]
    G --> H[Return newest N days]
Loading

File-Level Changes

Change Details Files
Optimizes newest-N served-day queries by limiting candidate day IDs before joining full result rows.
  • Extracted the shared served-version aggregation and join predicate while preserving the existing algorithm-version ceiling.
  • Added a newest-days join that orders and limits the version subquery before fetching large payload rows.
  • Updated full-result and metadata readers to use the bounded join while leaving all-history readers unchanged.
lib/data/db.dart
Adds regression coverage for result equivalence and performance as history grows.
  • Creates multi-version day results with large payloads, including older and future versions, to validate serving behavior.
  • Compares returned rows with the previous query as an oracle for both result readers.
  • Measures median reads at 30 and 600 days and enforces a bounded growth threshold.
test/db_serve_version_and_reads_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: OpenStrap/edge
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: b61693e
  • Layers: Repository YAML
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 23ba97b2-d3a4-47ce-a299-411e181601f0
Commits

Reviewing files that changed from the base of the PR and between c7492ad and 626aeaa.

Files ignored due to path filters: 1
  • test/db_serve_version_and_reads_test.dart is excluded by !test/**
Files selected for processing: 1
  • lib/data/db.dart

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

Walkthrough

The served-version SQL now limits the selected day IDs to the newest N days before joining and sorting result rows. recentDayResults, recentDayResultsMeta, and recentDayScalars use this limited join.

Changes: Served day results

Layer / File(s) Summary
Limit served days before joining
lib/data/db.dart
Shared SQL fragments preserve each day’s highest algo_version at or below _servedAlgoCeiling and select the newest N day IDs. The recent result, metadata, and scalar readers use this limited join instead of limiting after joining and sorting rows.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Possibly related PRs

  • OpenStrap/edge#644: Adds recentDayScalars, which this change updates to limit selected days before joining records.
  • OpenStrap/edge#620: Changes the executor contract for recentDayResults, a reader whose SQL this change also updates.
  • OpenStrap/edge#653: Applies a newest-days-first approach to a separate served-day reader.

Suggested reviewers: abdulsaheel

Merge Risk: Minimal · up to 626ae

The recent-day readers retain their existing results while limiting selected days earlier. No merge-blocking risk was identified.

Pre-merge checks: Passed 4
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main performance change: reading the newest N days without sorting all historical data.
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 unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="test/db_serve_version_and_reads_test.dart" line_range="298" />
<code_context>
+
+      expect(key(await LocalDb.recentDayResults(30)), key(await oracle(30)));
+      expect(key(await LocalDb.recentDayResultsMeta(30)), key(await oracle(30)));
+      expect(long, lessThanOrEqualTo(short * 3 + 30),
+          reason: 'recentDayResults(30): $short ms at 30 days of history, '
+              '$long ms at 600');
</code_context>
<issue_to_address>
**Timing assertion flakes on slow runners**

When the test runs on a loaded or slow machine and query latency varies between the short- and long-history measurements, the test `the newest-N reads stay flat as history grows` compares wall-clock medians with `long <= short * 3 + 30` at line 298. On a loaded or slow runner, the 600-day measurement exceeds that threshold even when the query is correct, so the test fails for machine load rather than a regression.

Move the timing check to a controlled benchmark, or avoid failing the test based on wall-clock latency.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: test/db_serve_version_and_reads_test.dart:298


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

An absolute bound (600-day read <= 3x the 30-day read + 30 ms) can fail on a
slow or loaded runner. Time the old full-history query back to back on the
same 600 days instead, so load slows both sides alike: 3x faster or better
(measured ~10x with the change, 1x without).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
the test file was reformatted along with the new timing test, which buried
the one real addition in a few hundred lines of reflow.
same sort-everything-then-limit shape as recentDayResults: every served row
was extracted and sorted to return the last N.
@abdulsaheel

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor
Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@abdulsaheel
abdulsaheel merged commit bf404de into OpenStrap:main Oct 31, 2026
4 checks passed
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.

2 participants