Skip to content

[M1-ALLOC-01 fix] Attribute the alloc window to the owner thread - #78

Merged
offdev merged 2 commits into
masterfrom
fix/m1-alloc-01-owner-thread
Oct 6, 2026
Merged

offdev merged 2 commits into
masterfrom
fix/m1-alloc-01-owner-thread

Conversation

@offdev

@offdev offdev commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Master run 37519998162: macOS arm64 still failed TileMapZeroAlloc after the settle-window fix of PR #77 — 48 blocks, first site resolved (via the dladdr diagnostic) to:

/System/Library/PrivateFrameworks/SkyLight.framework/... + CGSSnarfAndDispatchDatagrams

Root cause

The macOS graphics framework chain (loaded by the GL/GLFW suites earlier in the same test binary) keeps doing background heap work on its own threads — PR #77's run showed QuartzCore's one-time init, this run shows the ongoing WindowServer datagram dispatch. The settle-until-clean workaround is a coin flip against ongoing background activity — and it was never the right fix, because the watch was measuring the wrong thing:

the contract (G-R1 / FR-2.2) is that the LOOP'S THREAD allocates nothing — it never said every thread in the process must. A process-wide counter trips the loop's invariant on any background thread's allocation.

Fix: owner-thread attribution

allocWatchArm() records the calling thread as the window's owner; watchRecord() counts only the owner thread's allocations (in addition to the existing logging-emit exclusion). Strictly more correct for the M1-ALLOC-01 per-tick assertion too — it could previously false-fire on any background allocation during a tick. The disarmed hot path is unchanged; the armed non-owner path costs two atomic loads + a branch.

  • New regression test ZeroAlloc.WatchExcludesNonOwnerThreads (a non-owner thread's allocation inside an armed window is NOT counted; fails on the old process-wide semantics).
  • tilemap zero-alloc test: the settle workaround is removed — one honest window, structurally immune to framework background work; the failure message keeps the dladdr site resolution (module + symbol) for future flakes.
  • Contract updated in alloc_watch.h, docs/api/alloc_watch.md, game_loop.h/.cpp, related test/doc comments; laige-api.json regenerated (symbol set unchanged — line shifts + one summary text).

Verification

  • All six local trees: build 112/112, build-clang 112/112, build-release 101/101, build-shared 112/112, build-asan 109/109, build-tsan 109/109 (the new thread handoff is race-clean under TSan); include + determinism lints OK; api-* suite green after manifest regeneration.
  • macOS cannot be verified locally (Linux-only machine); the PR lane runs both macOS lanes (ci:macos label) — they are the execution evidence.

Master run 37519998162: macOS arm64 STILL failed TileMapZeroAlloc
after the settle-window fix — 48 blocks, first site now resolved to
SkyLight +CGSSnarfAndDispatchDatagrams (the WindowServer client's
datagram dispatch). The macOS graphics framework chain (loaded by
the GL/GLFW suites earlier in the same binary) keeps doing
background heap work on its own threads — first QuartzCore's
one-time init, now ongoing WindowServer datagram dispatch — and the
process-wide window was counting it. 'Settle until clean' is a coin
flip against an ongoing background activity; it is not the fix.

Root cause: the watch's contract (G-R1, FR-2.2) is that the LOOP'S
THREAD — the tick path — allocates nothing; it never said every
thread in the process must. The process-wide counter measured the
wrong thing: any background thread (framework, render, OS) that
allocates during a window was tripping the loop's invariant.

Fix: owner-thread attribution. allocWatchArm() records the calling
thread as the window's owner; watchRecord() counts only the owner
thread's allocations (plus the existing logging-emit exclusion).
This is strictly more correct for the per-tick assertion too — it
could previously false-fire on any background allocation during a
tick. The disarmed hot path is unchanged; the armed non-owner path
costs two loads + a branch.

- New regression test ZeroAlloc.WatchExcludesNonOwnerThreads (a
  non-owner thread's allocation inside an armed window is not
  counted; fails on the old process-wide semantics).
- tilemap zero-alloc test: the settle workaround is removed — a
  single honest window, now structurally immune to framework
  background work; the failure message keeps the dladdr site
  resolution (module + symbol).
- Contract updated in alloc_watch.h, docs/api/alloc_watch.md,
  game_loop.h/.cpp, and the related test/doc comments; laige-api.json
  regenerated (symbol set unchanged — line shifts + one summary
  text).
@offdev offdev added the ci:macos label Oct 6, 2026
@offdev
offdev merged commit e4a7b7c into master Oct 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant