Tao: reclaim the GPU resource cache on the macOS and Linux hosts - #641
Conversation
9309f13 to
266d58f
Compare
CI: both remaining failures are pre-existing on
|
| tree | result |
|---|---|
this PR (266d58fa) |
FAIL 3/3, times out at 30.6 s |
base nucleus-2.6 (1b86ec95) |
FAIL 3/3, times out at 30.6 s |
pre-merge parent (bcb469ce, merge of #634) |
FAIL 2/2, times out at 30.6 s |
The case is red on the branch that introduced it, and stays red with or without this PR. It is not #635 either — it predates that merge. Worth its own issue: timed out waiting for: final bounds applied after the toggling storm, WindowApiV2HeadfulCases.rapidPlacementTogglingThenBoundsConverges.
tao-headful (ubuntu-latest) — "satellite placement opened over a mapped parent, never seen away from its anchor"
This PR has no behavioural effect off macOS — GpuResourceCache.kt is constants only, and the Windows host changed by a constant rename and a comment. The same assertion (case premise: the satellite did not end up off the parent's right edge) also fired on feat/tao-satellite-window's own run 33823405675 before that branch went green, alongside several other workspace cases. Flaky under CI load, from #635.
GraalVM ×3 failed on the first run and passed unchanged on the second — flakes, ignore.
Correction to the previous force-push
The first run's macOS failure was "an embedded native view survives a resize storm", and I attributed it to the settle purge landing inside that case's frame-counting window. That attribution was wrong: the same case failed on feat/tao-satellite-window's run 33823405675, on a tree without this PR.
Removing the settle purge stands, but on its own merits, not on CI's: the footprint measurement above (68 MB → 72 MB across a storm, ~nothing returned by purge + GC) is the reason, and the commit message has been amended to say so.
Net: this PR does not break anything. The base branch is red.
Correction: the v2 maximize case is flaky, not hard-brokenMy previous comment read the 3/3 local failures as "red on
So it passed twice on CI, once on a tree containing this PR, and failed once. It does not discriminate between trees. Locally it is a different story: on my machine it fails 100% — 3/3 on this PR, 3/3 on Scope note: the case only exists on Still worth its own issue — a case that is 100% red on one machine and green on another is hiding a real ordering/timing assumption — but it is unrelated to this PR, and unrelated to #635. The |
The Metal and GL scene hosts never purged Skia's GPU resource cache; only the Windows host did, off the resize path (#347, #477). This brings macOS up to that level and pulls the shared policy into one file. Measured first: Ganesh already hands out 256 MiB by default, exactly the value the Windows host writes at attach. So that write was a no-op and the comment claiming it "forces purgeAsNeeded on every flush ... which is what keeps the steady state bounded at all" was wrong — Skia purges to fit its budget whether or not we set one. What reclaims is the purge. The comment is corrected rather than propagated. macOS gets the same mechanism, adapted to the backend: - the budget is anchored inside the same runOnRenderThread hop as makeMetal, since writing it purges to fit and so belongs on the owning thread; - purgeGpuResourceCache() submits the limit-toggle to the render executor instead of awaiting it — Metal has no current context, so none of the #514 foreign-context hazard applies here, but the context is thread-affine, and blocking the Tao main thread would park the drag behind the in-flight replay; - onResizeStreamAdvanced() purges every 250 ms while sizes stream and once more 500 ms after the last one, standing in for the WM_EXITSIZEMOVE macOS never sends. One deliberate divergence from Windows: the settle's System.gc() is gated on the burst having carried at least 8 resize events. Windows only sees WM_EXITSIZEMOVE after a real drag, but here every size change settles, including the single event a zoom, a snap or a programmatic resize produces, and a stop-the-world collection after each of those costs more than it returns. The Windows resize path is untouched beyond the constant move and the comment fix.
The settle pair (a 500 ms timer standing in for the WM_EXITSIZEMOVE macOS never sends, then a purge and a System.gc()) does not pay for itself on this backend. macOS paces its resize frames through the display link, so it never accumulates the way Windows' unpaced modal loop does: a 60-step resize storm on tao-demo moved the graphics footprint 68 MB -> 72 MB, and a purge + GC at the end of it returned essentially none of that. Against that nil benefit sits a real cost — a full cache purge re-mints the glyph atlas and layer backings, and the GC is stop-the-world, both landing half a second after every resize, including the single event a zoom, a snap or a programmatic resize produces. Keep the in-drag periodic purge, which is free (every frame of a drag is re-rastering anyway) and bounds a long drag on a large display. Drop the settle timer, the GC nudge and the burst-count gate that tried to make the nudge affordable; with them go the two constants and the host scope they needed, so startRenderLoop goes back to its local scope. The reclaim #638 actually wants is at rest, not at drag end, and belongs on the idle path.
266d58f to
888ab6c
Compare
Third and last host of the shared policy from GpuResourceCache: the EGL
host now anchors the same budget at attach and purges the per-size
scratch periodically while resize events stream, like Windows inside the
modal resize/move loop and macOS on the display-link-paced one.
The purge is armed in onResized but performed in the render pass. That
is not a detail: onResized runs on the event-loop thread with no EGL
context bound and the swap thread may be holding ours for its
eglSwapBuffers, while the render pass is the one point where this host's
context is current on this thread. It also lands right after
applyPendingNativeResize has closed the previous size's Surface and
BackendRenderTarget, which is exactly when their backing memory is
unlocked and the toggle has something to return.
Unlike macOS, the Linux resize path IS where the memory sits. On a
60-step storm, then a second storm shrinking back through the same
sizes (NVIDIA graphics memory for the process, native Wayland / GNOME):
without purge 13 MB -> 106 MB -> 114 MB
with purge 13 MB -> 71 MB -> 27 MB
Without the purge the footprint ratchets: the second storm revisits
sizes the first already paid for and still grows, because nothing ever
releases the scratch of a size no frame will ask for again. With it the
footprint tracks the current window instead of the high-water mark of
every size ever seen. Same shape on the X11 attach path (112 MB -> 23
MB). Frame throughput is unchanged in both regimes (~1050 frames per
1000 ms on Wayland, ~93 on the vsync-paced X11 path), so the reclaim
costs no frames.
No settle purge and no System.gc() nudge, for the reason macOS has
none: GTK offers no drag-end signal, and a timer standing in for one
buys a stop-the-world collection after every zoom, snap and
programmatic resize.
Refs #638
First slice of #638: bring the macOS Metal host and the Linux GL host up to the level the Windows host has had since #347 / #477, and pull the shared cache policy into one place. All three hosts now run the same policy.
Step 0: the measurement that reframes the issue
Created a Metal
DirectContextand read its inherited budget:Exactly the value
RESOURCE_CACHE_LIMIT_BYTESwrites on Windows. So that write was a no-op, and the comment on it — "an explicit budget forcespurgeAsNeededon each flush so the cache stays bounded" — was wrong: Skia purges to fit its budget whether or not we set one. What actually reclaims is the purge. The comment is corrected here rather than copied to two more platforms.Corollary for the rest of #638: proposal 1 ("cap per surface") is not "cap the uncapped", it is "run below Skia's default" — a different and more debatable change, deliberately not made here.
What's in
scene/GpuResourceCache.kt(new) — shared policy: the limit-toggle as the only primitive skiko gives us (resourceCacheLimitand nothing else — nofreeGpuResources, nopurgeUnlockedResources, not even a usage read-back), its two binding properties (frees unlocked resources only; issues backend deletes so it must run where the context is usable), and the two constants. Windows' two private constants move here — same values, same behaviour.scene/TaoComposeSceneHost.kt(macOS)runOnRenderThreadhop asmakeMetal— writing it purges to fit, so it belongs on the owning threadpurgeGpuResourceCache()submits the toggle torenderExecutorrather than awaiting it. Metal has no current context, so none of the Tao/Windows: rapid resize of secondary DecoratedWindow blanks parent window content #514 foreign-context hazard applies; the hazard here is thread affinity. Non-blocking on purpose: the caller is the Tao main thread on the resize path, and awaiting would park the drag behind the in-flight replay. FIFO puts the purge cleanly between two framesscene/TaoComposeSceneHostLinux.kt(Linux, EGL/GL)makeGLWithInterface, while the fresh context is still the onenativeAttach*left currentonResizedand performed in the render pass. Not a detail:onResizedruns on the event-loop thread with no EGL context bound — the swap thread may be holding ours for itseglSwapBuffers— while the render pass is the one point where this host's context is current on this thread. It also lands right afterapplyPendingNativeResizeclosed the previous size'sSurface+BackendRenderTarget, i.e. exactly when their backing memory is unlocked and the toggle has something to returnOne deliberate divergence from Windows, on both new hosts. No settle purge and no
System.gc()nudge. Windows only seesWM_EXITSIZEMOVEafter a real drag; macOS and GTK have no drag-end signal at all, so a timer would have to stand in for one — and it would then fire after every size change, including the single event a zoom, a snap or a programmatic resize produces. A stop-the-world collection half a second after each of those costs more than it returns; on macOS it was also measured as a visible stall (a window with a liveNativeViewembed dropped below 4 frames per 400 ms right after a storm).The Windows resize path is otherwise untouched (#477 / #514 intact).
Results, per platform
macOS — the mechanism, not the memory. The resize path is not where macOS memory sits. On
tao-demo, a 60-step resize storm moves the graphics bucket 68 MB → 72 MB, and a settle purge + GC returns essentially none of it — consistent with macOS pacing resize through the display link instead of Windows' unpaced modal loop.Linux — the resize path really is where the memory sits. A 60-step storm, then a second storm shrinking back through the same sizes (NVIDIA graphics memory for the process, GNOME / native Wayland):
Without the purge the footprint ratchets — the second storm revisits sizes the first already paid for and still grows, because nothing ever releases the scratch of a size no frame will ask for again. With it, the footprint tracks the current window instead of the high-water mark of every size ever seen. Same shape on the X11 attach path (112 MB → 23 MB).
Frame throughput is unchanged in both regimes — ~1050 frames per 1000 ms of storm on Wayland, ~93 on the vsync-paced X11 path, with and without the purge — so the reclaim costs no frames.
So the PR gives macOS parity, and gives Linux an actual reclaim. The 299 MB of #638 still lives in the at-rest triggers, which stay for the follow-ups: a purge registry +
trimGpuMemory(), then wiring it toIdleGcControlleronfeat/nucleus-optimization.Not in scope here
macOS and Linux popup-layer / standalone-popup contexts, and every at-rest trigger.
Test plan
:decorated-window-tao:compileKotlin:decorated-window-tao:detekt,:decorated-window-tao:ktlintMainSourceSetCheck:decorated-window-tao:testSEVERE/ exceptiontaoHeadfulTest, resize-storm cases: all pass, including a texture view appearing and disappearing during a resize storm (the case that would catch a purge eating live texture imports)taoHeadfulTest, full suite: 103 run, 6 failed — the same 6window v2 cloneposition-request cases fail on the base commit of this session (GDK ignores client positioning on native Wayland), so pre-existingRefs #638