Skip to content

Refactor/perf memory display - #1

Merged
gethash merged 11 commits into
mainfrom
refactor/perf-memory-display
Oct 14, 2026
Merged

gethash merged 11 commits into
mainfrom
refactor/perf-memory-display

Conversation

@gethash

@gethash gethash commented Oct 14, 2026

Copy link
Copy Markdown
Owner

No description provided.

gethash and others added 10 commits October 14, 2026 13:29
RenderPage now copies the bitmap out of WASM memory into a pooled Go
buffer and destroys the PDFium bitmap while still holding the document
lock. Previously the cleanup ran on the main thread without the lock
while the prefetcher or thumbnail loader could be rendering on the same
module, racing on PDFium's allocator. It also bounds WASM memory to one
live bitmap and avoids views that detach when the module's memory grows.

go-pdfium rejects Windows drive-letter paths inside WebAssembly, so the
PDF's directory is now mounted read-only at /doc and opened by a POSIX
path. This also stops PDFium from mounting the whole filesystem.

Add internal/pdf/pdftest, a tiny PDF writer, so render tests run in CI
on every OS (colour/size, unicode path, concurrent vs serial renders),
and generate examples/sample.pdf from it with a golden test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pdf: cache the compiled PDFium module on disk (wazero compilation
  cache in the user cache dir); Open drops from ~1.1s to ~40ms warm.
  Page sizes are read once at Open, so PageSize never waits on a render.
- pdf: new priority Scheduler (urgent > visible > prefetch > background)
  replaces the FIFO prefetcher and the ad-hoc thumbnail goroutines. It
  coalesces urgent requests, cancels replaced queues and never starts a
  job that is in flight or awaiting upload.
- app: shared pipeline (slides.go) for both windows; navigation never
  renders on the Ebiten goroutine. Stand-ins (same page, other size) are
  shown while exact renders land, and transitions wait for the incoming
  slide. The presenter now prefetches what the next advance needs.
- app: single overview implementation for audience and presenter, with a
  debounced preview-size render and per-tile grid-layer updates.
- ipc: Hello (static data) once on connect, then state only on change;
  the presenter extrapolates clock/progress. Socket in a private 0700
  dir, single connection.
- Skip unchanged frames (SetScreenClearedEveryFrame(false) + signature),
  true crossfade, rune-aware notes wrapping with typographic fallbacks,
  --no-fullscreen honoured by the presenter, hidden --debug-stats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cache entries are charged at real texture size (Ebitengine rounds
  textures to powers of two), so --cache-mb and the auto budget cap
  actual GPU memory. The auto budget keeps at least three slides, and
  prefetch depth is derived from it so prefetched slides aren't evicted
  before use. The presenter budget follows its pane sizes.
- Slides are uploaded as unmanaged images: dedicated textures that are
  freed on eviction instead of atlas pages that linger until empty.
- Cache.Put keeps an existing (possibly pinned) image; the inverted
  PurgeNotMatching is replaced by PurgeExcept, and a resize drops every
  unpinned render.
- Thumbnail store is budgeted from the grid and purges stale sizes; the
  disk cache moves to <user cache>/boozle/thumbs (0700) with age/size
  pruning; memory is returned to the OS when the overview closes.
- The overview preview prefers an exact preview-size render and draws
  without mipmaps.

300-page deck, 1280x720: steady RSS ~155 MB (was ~355 MB on main),
GPU 48 MB (was ~76-88 MB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Heap profiling full-screen on a Retina display showed most of the Go
heap in render results waiting for upload and in Ebitengine's staging
buffers, which only return to its reuse pool after a GC:

- The scheduler caps pixels of finished-but-undrained renders at 32 MB,
  so at most one large slide waits while thumbnails still stream.
- The pipeline nudges a background GC after every 32 MB uploaded, so
  staging buffers are recycled instead of piling up.

300-page deck full-screen at 3600x2338: peak RSS 369 MB (main: 1086 MB),
steady ~340 MB (main: ~850 MB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ebitengine goes fullscreen on the monitor it believes the window is on.
On Linux that is derived from the window position, cached for ~60 ticks
and lags behind asynchronous X11 moves (and window managers may ignore
pre-map positions), so SetMonitor + SetFullscreen in one tick often
landed on the primary display.

- window.go: a placer state machine waits for the window to map, moves
  it if needed, waits until the target monitor is reported, goes
  fullscreen, verifies after the cache refresh and retries once; F uses
  the same path. --debug-stats traces each step.
- display: resolve monitors by index, exact name or unique substring;
  --list-monitors prints name/size/scale/primary; the initial window is
  sized to fit inside the target so it opens centred there.
- The presenter process is spawned once the audience window is placed
  (2 s fallback) and receives the resolved index; both windows set
  WM_CLASS boozle/audience|presenter.
- config: explicit flags win over the sidecar via cobra's Changed set
  (fixes --monitor 0, -P -1, --start 1, --bg '#000000', --loop=false);
  monitor specs are strings (sidecar accepts ints or names); invalid
  --transition and absurd --pages ranges are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Navigation goes through one goTo primitive: Home/End/page jumps/l
  record the previous position, and jumping to the current slide is a
  no-op (no fade into itself). Numpad digits work in the audience window.
- One SidecarPath helper in config replaces the copy in pptxnotes.
- Version test checks main.version against the newest CHANGELOG release
  instead of a hard-coded string; CLI help and the no-args hint describe
  monitor names, overview keys and flag precedence, without the
  duplicated sidecar section.
- CI: gofmt, go mod tidy -diff and staticcheck; gofmt the remaining test
  files; socket-permission test skipped on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- cache: a Put never evicts its own entry. With a budget smaller than
  the pinned on-screen slide plus one more (small --cache-mb, 4K), the
  fresh render was evicted on insert and never requested again, so the
  screen stuck on the old slide (and the presenter re-rendered its next
  pane forever). Put now returns the image it keeps.
- scheduler: clear a job's outstanding mark only after its result is in
  the cache, so it can't be rendered twice; drop the test-only
  Outstanding accessor. Thumbnail delivery passes on the image the store
  kept.
- ipc: serialise the connect-time replay with Send so a newer state
  can't be overwritten by the replayed one; the presenter waits for its
  first state, not just the Hello.
- Opening the overview finishes a running slide transition; a resize
  during the overview's exit no longer re-queues every thumbnail.
- Stale comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README: monitor selection by name, a Linux multi-monitor section
  (XWayland, XRandR/Xinerama runtime libs, placement log, WM_CLASS,
  global X11 scale), a "Performance and memory" section describing the
  real-GPU-memory budget, cache directories and --debug-stats, overview
  keys, flag precedence, --autoquit semantics, platform list, and tests
  that no longer need BOOZLE_TEST_PDF.
- CLAUDE.md: architecture covers every package and the new pipeline;
  gotchas updated (render scheduler, Go-owned pixels, /doc mount,
  texture-size budgeting, redraw signatures, fullscreen placer, IPC
  hello/state, Changed-based precedence); stale items removed.
- release.yml: add darwin-amd64 (macos-15-intel) and linux-arm64
  (ubuntu-24.04-arm) builds and a --version smoke test per target.
- NOTICE: attribute all linked third-party modules.
- examples/sample.boozle.toml matches the sample deck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut the 1.3.0 changelog section and bump the default version string.
Document that the overview opens in the presenter window when presenter
view is on.

install.sh no longer prints a quarantine workaround that never applies
to curl downloads (and lacked sudo for /usr/local/bin); it clears the
attribute itself instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a cache budget below a few slides (e.g. --cache-mb 1, or a small
budget on a 4K display), a slow machine could finish the urgent render
and a prefetch between two frames. drain then uploaded both, and the
prefetch's Put evicted the urgent slide before the window pinned it.
refreshDisplay requests each slide only once, so that page never showed
at exact size. TestTinyCacheStillShowsEveryPage failed on the amd64 CI
runners this way.

drain now pins every slide it uploads until it returns.
TestDrainKeepsEverySlideItUploads reproduces the batch deterministically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gethash gethash closed this Oct 14, 2026
@gethash gethash reopened this Oct 14, 2026
examples/sample.pdf is generated as plain ASCII, so git treated it as
text and the Windows runner (core.autocrlf=true) checked it out with
CRLF line endings. That fails TestSamplePDFUpToDate and corrupts the
PDF's xref byte offsets for anyone cloning on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gethash
gethash merged commit a4ebf45 into main Oct 14, 2026
3 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.

1 participant