Skip to content

Raise the image cache size so a large zone view fits in it - #11295

Open
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:speed-up-large-zone-views
Open

Raise the image cache size so a large zone view fits in it#11295
liamiak wants to merge 1 commit into
Card-Forge:masterfrom
liamiak:speed-up-large-zone-views

Conversation

@liamiak

@liamiak liamiak commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fixes the freeze when opening a large zone view — most visibly the library during a tutor
search. Sorting, closing and reopening it froze again each time.

Cause

ImageCache is capped at 400 entries, but a single large zone view needs two per card: the
decoded original and the scaled copy the panel paints. A 500-card library therefore evicts
everything it just loaded and rebuilds the whole view on every refresh.

400 was never a considered choice for this workload — it is just what UI_IMAGE_CACHE_MAXIMUM
has always defaulted to. This treats that exact value as unset and uses 1500; any other value
is one somebody chose and is left alone. It also switches to soft values, so memory pressure
rather than entry count is what ultimately evicts, which is what this class already documented
itself as doing.

Measured

500-card library, driving ImageCache.getImage through the same entry point CardPanel uses,
on the EDT:

master this
first open 6807 7058
reopen 1803 / 1788 6 / 4
first request at a new size 5467 4946
again at that size 4493 2
back to the first size 5239 1

Everything after the first render: 17,890ms → 4,959ms. Heap on the same measurement:
375–423MB before, 433–458MB after — about +40MB.

What this does not do

The first open is unchanged; that is the card faces being rendered for the first time, and it
is a separate problem. Caching the rendered face fixes the "new size" row (4946 → 1673) but
holds one full 488x680 render per card — about +550MB of heap on this measurement — so it
needs a bounded design before it is worth proposing. Panel-bookkeeping and async-load changes
I could not show a benefit for are parked until I can.

The measurement uses an install with no downloaded card images, so every card takes the
placeholder path — the worst case. With images present the absolute numbers will be smaller;
the capacity argument is unchanged.

No test: this is a timing and memory property of a cache, and a timing assertion in CI would
be flaky.

🤖 Implemented with the assistance of Claude Code (Opus 5).

@liamiak
liamiak force-pushed the speed-up-large-zone-views branch 2 times, most recently from 056a5a7 to a7105d9 Compare July 20, 2026 18:40
@tool4ever

Copy link
Copy Markdown
Contributor

thanks, interesting ideas but rather large:
might take a while until a Dev with GUI interest has time to comb through it

@liamiak
liamiak force-pushed the speed-up-large-zone-views branch from 9efe953 to c11667f Compare July 25, 2026 19:24
@liamiak

liamiak commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed a rewrite of this branch — "rather large" was fair, and I wanted to re-review
the whole thing with Opus 5.0 and restructure it for ease of evaluation rather than leave it
sitting as one big lump. Rebased onto current master (nothing there has touched these files).

It is now five commits, each one mechanism, each building and passing the desktop test suite
on its own. The description above is rewritten around measurements rather than assertions.

Instrumenting it changed the PR rather than just documenting it. I had assumed the cost was
image decode plus cache eviction thrash. It is neither: ~77% of a 6.2s reopen was
re-rendering card faces for cards with no local image
— ~284 of them at ~16ms, redrawn on
every refresh because placeholder renders were explicitly never cached ("the rendering speed
seems to be fast enough", which does not survive being multiplied by 284). The quadratic panel
bookkeeping I had led with turned out to be ~30ms of the 6156.

That also let me delete things rather than add them. Gone since you last looked:

  • the new ZONE_CARD_IMAGE_SIZE preference and its ForgePreferences entry, along with the
    prefs.save() it did from inside a layout path — once a drawn card face is cached
    independently of display size, preloading no longer needs to know what size a view will ask
    for, so the preference had nothing left to do
  • a whole-cache scan per downloaded image, replaced by a multimap of the affected keys
  • a static font cache and a couple of other micro-optimisations that were not worth the
    questions they invited

Net effect is one fewer file touched than the version you commented on, and no new user
preference anywhere in the PR.

Two things I would rather flag than have you find:

  • Commit 3 (async loading) is not measurable on the machine I tested on. ~57% of that
    collection's images are missing locally, and placeholder rendering has to stay on the EDT
    because FCardImageRenderer has mutable statics — so moving decode off it changes little
    there. The case it helps is a complete local collection, where the same numbers imply
    seconds saved, but that is inference from measurement rather than a measurement. If you
    would rather see it land separately, or not at all, that is a reasonable call.
  • All the numbers come from one machine and one collection state, so the 77% is not a
    universal figure.

@tool4ever

Copy link
Copy Markdown
Contributor

good call, that will surely help

@tool4ever

Copy link
Copy Markdown
Contributor

Also worth noting Nr. 5 is related to a previous attempt: #7838

@liamiak

liamiak commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — that pointer led somewhere I didn't expect.

Reading #7838, the concern tehdiplomat raised there turns out not to be specific to that PR. Desktop's on-demand fetcher has never handled rate limiting at all: SwingImageFetcher downloads via ImageIO.read(URL), which discards the HTTP status, so a 429 is indistinguishable from any other failed download and it keeps asking. Mobile got proper 429 handling and a cooldown in 678aae4 back in February, and the scryfallCooldownTime field it uses is declared on the shared ImageFetcher base class — only the mobile half was ever wired up. Desktop also keeps no gap between requests, while GuiDownloadService already keeps 100ms, and fetches go onto a work-stealing pool so a deck editor grid of missing images asks for all of them at once. I've opened #11524 for that; it's independent of this PR.

On the constraint itself, I went looking and found docs/Card-Images.md. It says "you can no longer predownload card images", and three lines later "You can do small pre-caching by loading your decks in the deck editor prior to playing to download just those images." As I read it, commit 5 sits on the second line rather than the first: same deck, same volume, same fetchImage path as opening the deck editor, and gated by the same UI_ENABLE_ONLINE_IMAGE_FETCHER preference that governs passive auto-download everywhere else. What differs is that it triggers itself at match start rather than waiting for you to open the editor. With #11524 in, it also inherits the pacing and the cooldown.

That said, automatic versus user-initiated is your call, not mine — if you'd rather pre-caching stayed something the player asks for, say so and I'll drop commit 5. The measured win in this PR is the rendering work; the fetching commit is the least evidenced part of it.

@liamiak
liamiak force-pushed the speed-up-large-zone-views branch from c11667f to 98a02b5 Compare August 23, 2026 17:30
@liamiak liamiak changed the title Search window speedups, including for very large decks Raise the image cache size so a large zone view fits in it Aug 23, 2026
@liamiak

liamiak commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed this down to one commit, 13 lines. Your first note — that it was large and would need a GUI dev to comb through — turned out to be the most useful thing said about it.

Re-measuring against current master (nothing upstream has touched these files in 379 commits, and it rebased clean), I had the causation wrong. I told you ~77% of a reopen was re-rendering card faces and that caching those renders was the fix. The re-rendering was real, but it was a symptom: the 400-entry cap cannot hold even one 500-card view, so the scaled copies were evicted and the faces had to be redrawn. Raise the cap, the scaled copies survive, and the re-rendering stops on its own.

Everything after the first render goes 17,890ms → 4,959ms from the cap alone. Caching the renders as well takes it to 1,722ms — but holds one full 488x680 image per card, about +550MB of heap against +40MB for this. That is a poor trade for the one remaining case, and it needs a bounded design before I would send it.

The rest is parked rather than dropped. The render cache needs that memory question answered; commits 3 and 5 I still have no measurement for, which is also the honest answer to the pre-caching question I asked you and you never needed to answer. If any of them are worth reviving, I would rather bring each back on its own with its own number.

Opening a large zone view - most visibly the library during a tutor search -
froze the UI for seconds, and froze it again on every sort, close and reopen.

The cache is capped at 400 entries while a single large zone view needs two per
card: the decoded original and the scaled copy the panel paints. A 500-card
library therefore evicted everything it had just loaded and rebuilt the whole
view on every refresh.

400 was never a considered choice for this workload, it is just what this
preference has always defaulted to, so treat that exact value as unset and use
1500; any other value is one somebody chose and is left alone. Also switch to
soft values so memory pressure rather than entry count is what ultimately
evicts, which is what this class already documented itself as doing.

Measured on a 500-card library with no images downloaded: reopening the view
cost ~1.8s and re-requesting at a different size ~4.5s, both now ~1ms. Costs
about 40MB of heap on that measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants