Skip to content

github.com beachballs ~30s on load: CPU Gaussian blur runs per tile on the UI thread (+ hydration, shutdown-abort follow-ups) #81

Description

@codymullins

Loading https://github.com in the live GUI shows the macOS spinning wheel for ~30s+. Root-caused on 2026-06-10 (branch feat/js-stack-trampoline) with a UI-thread CPU trace during the stall: the navigation itself completes (first paint at ~4.8s), but show_page(+firstPresent) took 31s and the deferred-phase re-show another 23s. After load, the live loop keeps the process at ~200% CPU.

The trace shows the UI thread spending 20.9s of a 26.3s sample inside SixLabors GaussianBlur, reached via:

WebviewPanel.PresentSurface
-> Compositor.RenderToSurface -> EmitGpuTextureTile
-> ImageSharpBackend.RenderTexture -> ApplyFilterGroup -> ApplyFilterChain
-> GaussianBlur

GitHub's homepage CSS has large blurs on hero glow elements: blur(180px) x2, blur(150px), blur(100px) x2, blur(40-60px) several (site-*.css, home-*.css). The cost is new with the Tier-5 filters/backdrop work — before it, filter: was ignored.

Three multipliers stack:

  1. ImageSharpBackend.ApplyFilterGroup (src/Starling.Paint/Backend/ImageSharpBackend.cs:2803) rasterizes the whole padded filter-group bounds into an offscreen Image<Rgba32> (up to 64M px) and blurs it on the CPU. Sigma scales with device scale, so retina doubles the kernel and quadruples the pixels.
  2. Compositor.EmitGpuTextureTile (src/Starling.Paint/Compositor/Compositor.cs:422) hands every tile miss the layer's full display list, so the complete group blur re-runs once per 2048x512 tile the blurred layer touches.
  3. It all runs synchronously inside ShowPage -> PresentSurface on the UI thread.

Work items

  • Cache the filtered-group raster keyed by (group content hash, scale) and slice tiles from it, instead of re-running the full blur per tile. Biggest payoff.
  • Clip the filter-group raster to the tile rect inflated by the blur halo (~3 sigma) so per-tile cost is proportional to the tile, not the group.
  • Large-sigma blur strategy: downsample -> blur -> upsample for sigma above ~16, and/or move blur to the WebGPU backend so the CPU leaves the filter pipeline.
  • GitHub hydration failures (separate from the stall; body content mostly absent): TypeError: Class extends value is not a constructor across bundles (custom-elements path), JS stack overflow in element-registry-*.js, Index was outside the bounds of the array in marketing-essentials-*.js.
  • Shutdown abort: quitting the busy GUI dies with SIGABRT (exit 134) in a CoreCLR EbrCollector::EnterCriticalRegion assert during exit finalizers (-[NSApplication terminate:] -> __cxa_finalize_ranges -> managed VSD resolve). .NET 11 preview shutdown path; needs a repro/workaround (e.g. clean managed teardown before exit).

Evidence

  • UI-thread dotnet-trace during the stall: 20.9s GaussianBlur inclusive, thread-pool workers pegged ~20s each in ImageSharp parallel convolution; OnScrollChanged 14.8s inclusive (scroll re-blurs).
  • Aspire structured logs: first-paint: 4800 ms, show_page ... showPage(+firstPresent)=30999ms, second pass =23038ms.
  • Memory note: github-blur-present-stall (Claude Code project memory).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions