Skip to content

Fix CursorWindow crash on large Steam libraries - #961

Merged
utkarshdalal merged 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-cursor-window-crash
Mar 24, 2026
Merged

Fix CursorWindow crash on large Steam libraries#961
utkarshdalal merged 1 commit into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-cursor-window-crash

Conversation

@jeremybernstein

@jeremybernstein jeremybernstein commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Try loading all owned apps in one shot (zero overhead for normal libraries)
  • On SQLiteBlobTooBigException, retry with progressively smaller pages (50 → 25 → ... → 1)
  • Rethrows if a single row exceeds the window (nothing we can do at the DB layer)
  • Flow reactivity preserved via COUNT(*) observer with distinctUntilChanged + flatMapLatest
  • Property-only updates (name, icon) won't re-emit until the next count change

Test plan

  • Normal library (<100 games) loads without regression
  • Large library (450+ games with big depot blobs) no longer crashes
  • Library updates correctly when PICS data arrives (Flow still reactive)

Fixes #960


Summary by cubic

Prevents crashes on large Steam libraries by paging the owned-apps query and handling SQLiteBlobTooBigException. Small libraries still load in one shot; the app list reloads only when the owned-app count changes. Fixes #960.

  • Bug Fixes
    • Try full load; on overflow, fall back to smaller pages (50 → 25 → ... → 1).
    • Rethrow if a single row still exceeds the CursorWindow.
    • Reload only on COUNT(*) changes with distinctUntilChanged + flatMapLatest to cancel stale reloads.
    • Load pages in a Room @Transaction and sort by LOWER(name), id with LIMIT/OFFSET for stable, consistent results.

Written for commit af8849b. Summary will update on new commits.

Summary by CodeRabbit

  • Performance Improvements

    • App list now reloads based on owned-app count to avoid unnecessary refreshes and cancels in-flight reloads during rapid changes.
    • Pagination added for loading large libraries to reduce memory and CPU usage.
    • App list ordering refined to be more deterministic for consistent browsing.
  • Stability

    • Adaptive page sizing handles oversized datasets to prevent load failures.
    • Full-list loads run transactionally to avoid partial or inconsistent results.

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaced the single-query Room-backed getAllOwnedApps Flow with a count-driven reactive pipeline and transactional paged loading; added adaptive page-size retry on SQLiteBlobTooBigException and centralized the owned-app WHERE clause into OWNED_APPS_WHERE. (≤50 words)

Changes

Cohort / File(s) Summary
SteamAppDao Reactive Pipeline & Paging
app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
Replaced direct Room Flow<List<SteamApp>> with _observeOwnedAppCount(...).distinctUntilChanged().flatMapLatest{...}; added _observeOwnedAppCount, suspend _getOwnedAppsPage(limit, offset, ...), @Transaction suspend _getAllOwnedAppsPaged(...); introduced paged SELECT (LIMIT/OFFSET), adaptive page-size halving on SQLiteBlobTooBigException, shared OWNED_APPS_WHERE filter, and changed ordering to ORDER BY LOWER(app.name), app.id.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant DAO as DAO:getAllOwnedApps
    participant Flow as FlowPipeline
    participant DB as Database

    Caller->>DAO: subscribe/getAllOwnedApps()
    DAO->>Flow: start observing (_observeOwnedAppCount)
    Flow->>DB: SELECT COUNT(*) WHERE OWNED_APPS_WHERE
    DB-->>Flow: emit count
    Flow->>Flow: distinctUntilChanged()
    alt count changed
        Flow->>DB: fetch page 0 (LIMIT/OFFSET)
        DB-->>Flow: return page data
        loop fetch remaining pages
            Flow->>DB: fetch next page (LIMIT/OFFSET)
            DB-->>Flow: return page data
        end
    else SQLiteBlobTooBigException during page fetch
        Flow->>Flow: halve pageSize (>=1) and retry
        Flow->>DB: fetch page with smaller LIMIT
        DB-->>Flow: return page data or error
    end
    Flow-->>Caller: emit accumulated List<SteamApp>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #945: Modifies SteamAppDao.getAllOwnedApps filtering and ORDER BY; closely related to the owned-app WHERE and ordering changes here.

Poem

🐇 I hopped through tables, counted with care,
Pages sprung up to lighten the fare.
When blobs grew bulky I split the load,
Halved the hops down a safer road.
A nimble hop — no more crashed lair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly references the main problem addressed in the PR—the CursorWindow crash on large Steam libraries—which is the central objective of the changeset.
Linked Issues check ✅ Passed The implementation aligns with all core objectives: prevents CursorWindow overflow via adaptive paging, preserves Flow reactivity through COUNT(*) observation with distinctUntilChanged, uses transactional queries with stable LIMIT/OFFSET paging, and handles SQLiteBlobTooBigException with progressive page-size reduction [#960].
Out of Scope Changes check ✅ Passed All changes are scoped to SteamAppDao and directly address the crash issue: new count observer, paged loader with exception handling, and transactional query refactoring are all necessary to fix issue #960.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt">

<violation number="1" location="app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt:53">
P1: Offset pagination with non-unique sort key causes unstable ordering</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt Outdated

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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt (1)

82-90: Correct implementation with documented trade-off.

The reactive pipeline correctly uses flatMapLatest to cancel stale reloads during rapid PICS inserts. The distinctUntilChanged() on count is a documented trade-off that aligns with LibraryViewModel's existing behavior (which already only re-filters on size changes per the relevant snippet at lines 116-128).

One minor observation: the inner flow construction could alternatively use flow { emit(...) } from the existing import scope rather than the fully qualified kotlinx.coroutines.flow.flow:

🔧 Optional simplification
-        .flatMapLatest { // cancel stale reloads during rapid PICS inserts
-            kotlinx.coroutines.flow.flow { emit(_getAllOwnedAppsPaged(invalidPkgId)) }
-        }
+        .flatMapLatest { // cancel stale reloads during rapid PICS inserts
+            flow { emit(_getAllOwnedAppsPaged(invalidPkgId)) }
+        }

Add to imports:

import kotlinx.coroutines.flow.flow
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt` around lines 82 - 90,
The code uses a fully qualified call kotlinx.coroutines.flow.flow inside
getAllOwnedApps to emit the paged results; replace it with the unqualified flow
by adding import kotlinx.coroutines.flow.flow and changing the call to flow {
emit(_getAllOwnedAppsPaged(invalidPkgId)) } in the getAllOwnedApps pipeline
(referencing getAllOwnedApps and _getAllOwnedAppsPaged) to simplify the code and
use the existing import scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt`:
- Around line 82-90: The code uses a fully qualified call
kotlinx.coroutines.flow.flow inside getAllOwnedApps to emit the paged results;
replace it with the unqualified flow by adding import
kotlinx.coroutines.flow.flow and changing the call to flow {
emit(_getAllOwnedAppsPaged(invalidPkgId)) } in the getAllOwnedApps pipeline
(referencing getAllOwnedApps and _getAllOwnedAppsPaged) to simplify the code and
use the existing import scope.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adf51f8f-33f1-46ba-b56c-7218e018d940

📥 Commits

Reviewing files that changed from the base of the PR and between 36b670b and 6f01948.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt

@jeremybernstein

jeremybernstein commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

The query change in #945 (adding EXISTS subquery on steam_license) triggered this. Room now observes both tables, so license inserts (3272 on login) cause repeated re-queries of the full SELECT *.

@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from 6f01948 to 7c2937a Compare March 21, 2026 13:22
@utkarshdalal

Copy link
Copy Markdown
Owner

Nice, need to review

@jeremybernstein

Copy link
Copy Markdown
Contributor Author

@phobos665 phobos665 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.

Makes sense, LGTM

@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from 7c2937a to 2d598d0 Compare March 22, 2026 08:19

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt`:
- Around line 12-14: The use of flatMapLatest in getAllOwnedApps is experimental
and requires opt-in; add the `@OptIn`(ExperimentalCoroutinesApi::class) annotation
either at the top of the file (file-level) or directly on the getAllOwnedApps
function to satisfy the compiler, and ensure
kotlinx.coroutines.ExperimentalCoroutinesApi is imported if you add the
annotation; update SteamAppDao.kt accordingly so flatMapLatest usage is properly
opted-in.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b7f275f-73d3-4861-bb1d-d53248153eac

📥 Commits

Reviewing files that changed from the base of the PR and between 7c2937a and 2d598d0.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt

Comment thread app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from 2d598d0 to bb3a7a8 Compare March 22, 2026 10:31

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt`:
- Around line 44-50: _replace the COUNT(*) invalidation key with a lightweight
mutation-sensitive projection:_ change the query used by _observeOwnedAppCount
(and the similar query at lines 84-93) to select a small sentinel projection of
the library rows (e.g. app primary key and/or a last_modified/updated_at or
rowid) instead of COUNT(*), and update the return type from Flow<Int> to a Flow
of that projection (e.g. Flow<List<Int>> or Flow<List<IdAndTs>>), so
getAllOwnedApps() sees ordering/content/name/icon/PICS changes and will
invalidate correctly via distinctUntilChanged().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26457d6e-d952-438a-ae7d-7783bcfb4b4d

📥 Commits

Reviewing files that changed from the base of the PR and between 2d598d0 and bb3a7a8.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt

Comment thread app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from bb3a7a8 to a90a579 Compare March 23, 2026 08:13
@jeremybernstein

Copy link
Copy Markdown
Contributor Author

Re-pushed with the query back to the pre-#945 state (it was reverted). With the revert, this PR might no longer be strictly necessary, but it's a good defensive measure. I'm going to submit a new PR with an alternative strategy for the problem #945 was trying to solve, though.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt`:
- Around line 60-70: The current loop lowers the shared pageSize when catching
SQLiteBlobTooBigException, which then throttles all subsequent pages; instead
scope the reduced size to the current fetch only by introducing a
temporary/currentPageSize passed to _getOwnedAppsPage (or reset pageSize back to
Int.MAX_VALUE after a successful fetch), so that when a page overflow occurs you
retry the same offset with smaller sizes (PAGE_SIZE, pageSize/2,...), but once a
page succeeds you restore the default/full pageSize for the next offset; update
usages of pageSize and the catch path around _getOwnedAppsPage, referencing
variables pageSize, offset, PAGE_SIZE and the exception
SQLiteBlobTooBigException to locate and implement the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f24c11b4-96df-4620-ba53-d666f47c9950

📥 Commits

Reviewing files that changed from the base of the PR and between bb3a7a8 and a90a579.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt

Comment thread app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt Outdated
@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from a90a579 to b6a2b62 Compare March 23, 2026 08:37
getAllOwnedApps loaded all rows in one cursor, which can overflow the
CursorWindow when rows contain large blobs. Try loading all at once;
on SQLiteBlobTooBigException, retry with progressively smaller pages
(50 → 25 → ... → 1). Rethrows if a single row exceeds the window.

Flow reactivity preserved via COUNT(*) observer that triggers a
reload on every table change.
@jeremybernstein
jeremybernstein force-pushed the jb/fix-cursor-window-crash branch from b6a2b62 to af8849b Compare March 23, 2026 15:03
@utkarshdalal
utkarshdalal merged commit d7e8e7b into utkarshdalal:master Mar 24, 2026
2 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.

Crash: SQLiteBlobTooBigException in getAllOwnedApps on large libraries

3 participants