Skip to content

perf(roms): let a gallery window fetch skip the library count - #4062

Merged
gantoine merged 3 commits into
rommapp:masterfrom
Spinnich:fix/gallery-window-skip-total
Aug 2, 2026
Merged

perf(roms): let a gallery window fetch skip the library count#4062
gantoine merged 3 commits into
rommapp:masterfrom
Spinnich:fix/gallery-window-skip-total

Conversation

@Spinnich

@Spinnich Spinnich commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #4053

Loading more covers into a gallery you are already looking at makes the server re-count
the whole filtered library, every single time.

The page already knows that total. It learned it on its first request, and nothing can
change it mid-scroll. But the flags a window fetch sends to save work
(with_char_index=false, with_filter_values=false, with_rom_id_index=false) are
exactly what puts the backend on its non-index path, and that path answers total with a
separate COUNT over the filtered set. So the opt-out meant to avoid a full-library scan
quietly swapped it for a different one, and the frontend then wrote the result over the
identical number it already had.

This PR mirrors the existing with_rom_id_index opt-out with a with_total one, and has
the gallery's window fetch use it. Follow-up to #4054, where I flagged this as
deliberately out of scope.

What changed

File Why
backend/endpoints/roms/__init__.py Adds the with_total query parameter (defaults to true, so existing callers are unaffected) and skips get_rom_count when it is false. CustomLimitOffsetPage.total is redeclared as GreaterEqualZero | None so the page can carry a null.
backend/tests/endpoints/roms/test_rom.py Two tests. The first spies on get_rom_count and asserts it is never called, so it guards the query being skipped rather than merely the field being null, with a control request proving the count still runs by default. The second pins the deliberate asymmetry below.
frontend/src/__generated__/models/CustomLimitOffsetPage_SimpleRomSchema_.ts Regenerated from the backend schema. One line: total: number becomes total: (number | null).
frontend/src/services/api/rom.ts Optional withTotal on GetRomsParams, forwarded only when explicitly set. Purely additive, so no existing call site changes.
frontend/src/v2/stores/galleryRoms.ts fetchWindowAt sends withTotal: false alongside the three sidecar opt-outs it already sends, but only once the bootstrap has established the size. The existing null guard on data.total then leaves that size alone.
frontend/src/v2/stores/galleryRoms.test.ts A window after the bootstrap sends the flag and the null response does not blank the established total; a first window with no bootstrap still asks for the total; the default path still sends no flag at all.

Results

Cost of the redundant COUNT alone, per scroll batch, measured on my library and reported
in #4053:

Wasted per scroll batch
Before #4054, with "Group ROMs" on 6.13s
After #4054 landed ~1.0s
With this PR none, the query is never issued

The saving is the whole figure rather than a fraction of it, because this removes the
query instead of making it faster.

What a reviewer should look at

  • total becomes nullable in the API contract. This is the part worth scrutiny.
    Runtime behaviour is unchanged for every existing caller, since the flag defaults to
    true, but the OpenAPI type widens from number to number | null, which any
    generated client outside this repo will see. I declared it as GreaterEqualZero | None
    rather than giving it a default, so the field stays required in the schema (nullable,
    not optional) and keeps its ge=0 constraint.
  • The rom-id-index branch deliberately ignores the flag. When with_rom_id_index=true
    the total is just len(rom_id_index), already in hand, so suppressing it would save
    nothing and cost the caller information. That asymmetry is documented in the parameter
    description and pinned by a test so it does not read as an oversight later.
  • No v1 files change, so check-v1-frozen stays green. The two v1 consumers of total
    (RandomBtn.vue, ContextualRandomBtn.vue) already guard with
    if (!romsResponse.total), so they narrow the nullable type with no edit, and
    withTotal is optional on GetRomsParams.
  • The bootstrap cannot accidentally opt out. SidecarOptions, the type
    fetchInitialMetadata accepts, has no withTotal member, so the request that
    establishes the gallery's size structurally cannot skip the count.
  • No stale totals across a filter change. Both resetGallery() and
    invalidateWindows() clear total and metadataLoaded, so the first request under a
    new filter always re-bootstraps with the count.

Testing

  • Full backend suite green, plus tests/endpoints/roms/test_rom.py and the v2
    galleryRoms store suite re-run individually.
  • Full frontend suite green (613 tests, 47 files), vue-tsc clean, trunk fmt and
    trunk check clean.
  • Both new backend tests were checked against the pre-fix tree, where they fail. The
    count-skip test asserts on the query not being issued, not just on the response field,
    so it cannot pass by serialisation alone.
  • Verified in a browser against a running instance: the bootstrap request carries total,
    the follow-up window fetch sends with_total=false and comes back with total: null,
    and the gallery, its count badge and the AlphaStrip all render unchanged.
  • Limitation worth stating: my dev library is small enough that only window 0 exists, so
    the second-and-later window path is covered by the store unit tests rather than by the
    browser check. The timings above come from my large library, not from the dev instance.

Known follow-up, deliberately not in this PR

getRecentRoms and getRecentPlayedRoms still send with_rom_id_index=false and discard
the total they get back, so every home page load pays two counts for nothing. It is the
same one-line opt-out, but it lives in both services/api/rom.ts and
services/cache/api.ts, and in the latter the parameter map is duplicated into
clearRecentRomsCache / clearRecentPlayedRomsCache as a cache-key pattern. Adding the
flag to the request without adding it there too would silently break cache invalidation,
so it deserves its own PR rather than a rider on this one.

AI assistance disclosure

This PR was written primarily by Claude Code (Claude Opus 5), including the backend
change, the frontend change, the tests and this description. I directed the work, reviewed
every change, and the performance numbers come from measurements against my own library
rather than from estimates.

Checklist

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Opting out of the rom id index made the endpoint fall back to a COUNT
over the whole filtered set, so every scroll batch re-derived a total
the page already had from its bootstrap.

Add a with_total flag that skips that count, and have the v2 gallery's
window fetch pass it alongside the sidecar opt-outs it already sends.
total comes back null there, which the store's existing guard leaves
alone rather than applying over the size it knows.

Fixes rommapp#4053

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an optional ROM-count opt-out and uses it for gallery window requests after initial metadata has established the result size.

  • Makes the paginated ROM response total nullable when counting is explicitly skipped.
  • Forwards with_total through the frontend ROM API service.
  • Preserves the gallery's established total while avoiding redundant count queries.
  • Adds backend and frontend coverage for default, opt-out, and bootstrap behavior.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness or security issues identified.

The count opt-out remains default-on for existing callers, the gallery enables it only after establishing its total, nullable responses are guarded from overwriting that state, and tests cover both backend query execution and frontend lifecycle behavior.

Important Files Changed

Filename Overview
backend/endpoints/roms/init.py Adds the default-on with_total parameter, skips the standalone count only when requested, and permits a required nullable total.
backend/tests/endpoints/roms/test_rom.py Verifies the count query is skipped, default counting remains intact, and the ROM-ID-index path retains its already-computed total.
frontend/src/generated/models/CustomLimitOffsetPage_SimpleRomSchema_.ts Updates the generated response model to reflect the backend's nullable total contract.
frontend/src/services/api/rom.ts Adds optional withTotal request support while preserving existing callers' default behavior.
frontend/src/v2/stores/galleryRoms.ts Skips redundant totals only after metadata is loaded and retains the established total when subsequent responses return null.
frontend/src/v2/stores/galleryRoms.test.ts Covers post-bootstrap opt-out behavior, null-total preservation, and the first-window bootstrap path.

Reviews (1): Last reviewed commit: "perf(roms): let a gallery window fetch s..." | Re-trigger Greptile

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Spinnich
Spinnich requested a review from gantoine August 2, 2026 00:13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gantoine
gantoine merged commit 4fac604 into rommapp:master Aug 2, 2026
9 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.

[Bug] Every gallery scroll re-counts the whole library for a total the page already has

2 participants