Skip to content

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

Open
DropTabl wants to merge 2 commits into
OpenStrap:mainfrom
DropTabl:fix/served-day-scan
Open

DropTabl wants to merge 2 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 by limiting eligible days before retrieving their full records.

Enhancements:

  • Optimize newest-day result reads by selecting the limited set of days before fetching full history rows, keeping performance stable as database history grows.

Tests:

  • Add coverage verifying newest-N result reads match the previous query while remaining performant across large histories.

Summary by CodeRabbit

  • Bug Fixes
    • Recent-day results and their associated metadata now consistently cover the newest requested days, rather than limiting results after combining day and version data. This helps prevent older days from appearing in place of more recent ones.

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 →

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters: 1
  • test/db_serve_version_and_reads_test.dart is excluded by !test/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: OpenStrap/edge
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: c8e6355
  • Layers: Repository YAML
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2ca72a5c-eef7-4cb1-a63d-849ae3c89b9c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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: c8e6355
  • Layers: Repository YAML
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d2a869dc-5f18-46b3-a8ed-64043de4d246
📥 Commits

Reviewing files that changed from the base of the PR and between c8e6355 and e84537c.

⛔ 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 results. Both recent-result queries use this limited join.

Changes: Served day results

Layer / File(s) Summary
Limit served days before joining
lib/data/db.dart
The shared SQL fragments preserve each day’s highest algo_version at or below _servedAlgoCeiling and select the newest N day IDs. recentDayResults and recentDayResultsMeta use this limited join instead of limiting the joined rows.

Priority: ⬇️ Low

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

Change: Refactor

Suggested reviewers: abdulsaheel

Merge Risk: ⚪ Minimal · up to e8453

The query rewrite preserves recent and unlimited reads while reducing work for bounded history queries.

Pre-merge checks: Passed 5
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.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
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.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

✨ 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

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

@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 ✨

Comment thread test/db_serve_version_and_reads_test.dart Outdated
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>

This branch has not been deployed

No deployments
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