The census reads ludic.physics' jph_heap_bytes/_peak and the Vulkan runtime's lvk_ac_bytes/_peak by
name (dlsym, so nothing a package declares is declared twice) and prints jolt, vulkan and the rest
of the heap apart. An @alloc_ok function counts a scope in and out (@lp_fdecl): what it and its
callees make is marked declared in the side table, reported as 'declared' in the census and left
out of a frame's verdict and of Mem.kept().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deps_reach's graph gains the handlers and each @On body (an emit reaches its event's listeners).
Roots: a handler in a frame phase, every reducer, an @On body, and a function stored as a System's
tick. Every allocating construct in what they reach - new, a list literal, push (grow), text built
by + or a template, words/floats/buffer/bytes - is a falloc line with the shortest chain from a
root (root>..>last six), and the program's count is frame_allocs. @alloc_ok("why") on a function or
a handler takes it and what only it reaches out; the reason is required. ludic deps --allocs lists
them, and frame_allocs is a number --check ratchets. Maroon Lake starts at 2661.
Not yet: @frame on a step list's field (only 'tick' is a root field so far), statement-level
@alloc_ok, the three lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The census's native line is malloc's live bytes over every zone since judging began less what
Ludic's tracked blocks kept - the libraries' and drivers' growth, read before the census file is
opened. A tracked block counts malloc_size(), not the size asked for, so kept is what the heap pays
and the residual carries no rounding. @malloc_zone_statistics is declared once, by the fence or by
Os.heap_bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every allocation the compiler emits goes through @lp_malloc/@lp_calloc/@lp_realloc/@lp_free, and a
Ludic-level one first stores its site (function, file, line, kind) in @lp_site. Off, that is one load
and a predictable branch (30 M allocations: 0.87-0.91 s against 0.87-0.90 s on leaks2).
On (the default in a headless build, and windowed under R3D_DEV), tracking starts at the first frame
on its own and judging once R3D_ALLOC_WARM frames in a row kept nothing (600) or R3D_ALLOC_WARM_MAX
after (re)start; Mem.play()/Mem.rewarm() sends a load back to its warm-up. A judged frame that ends
holding more than it began with is reported by site with its callers (the unwinder, taken only once
judging) and fails the run with exit 86 (R3D_ALLOC_FENCE=off|count|warn|fail). R3D_ALLOC_CENSUS
writes the totals and top sites at exit. The build's defaults are --fence=, --fence-warm=,
--fence-census= or a fence line in the program's package.ludic; the environment overrides them.
The runtime is IR (emit_fence_ir.ludic, generated from a template); tracking is a side table in one
calloc'd region, so no block carries a header and pointers crossing to natives stay safe. Examples
alloc_fence, alloc_fence_leak and alloc_fence_auto with cases in ludic-dev test; reseeded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A co-op guest loading its models segfaulted in copyBufferToBuffer under vkQueueSubmit (0x68, AGX
LegacyBlitContext): a buffer primed into the frame's command buffer was released later in that
frame, and gvk_buf_release destroys a buffer no draw has marked, so the copy read a freed one. The
copies now run in their own command buffer before the frame's begins, checked against the slot's
handle as they are recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chunk's trunks and boulders put in and taken out made a new shape each time and freed none. The
table is by the body's index, made once at the world's size. tests/reuse_test: 1000 owned bodies put
and removed hold no more shapes and 32 bytes of heap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by reading every builtin (Os.platform's 8 KB per call started it). Log builds its line only at
or above the threshold and frees it; DateTime.format folds through + so its pieces go; Input.text
encodes into one buffer; Path/Mime/Fs/Os free their temporaries on every path; string results of
Text/Path/Mime/DateTime/Os dirs are fresh and Text frees a fresh argument. Reseeded.
runtime_temps.ludic: 19.8 MB -> 0 over 20,000 rounds, 64 KB -> 0 over 200 of file work; clean under
MallocScribble. string_temps still 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lp_os_platform and lp_os_arch malloc'd 8 KB per call (the uname buffer) and kept none of it:
string_temps now asks both every round, 327 MB over 20,000 before, 0 after. Reseeded. render3d:
MoltenVK made a mapped buffer's MTLBuffer at its first bind (fn_gvk_draw +4 blocks in the boat
window); gvk_buf_reserve queues it and the next frame's command buffer copies 4 bytes out of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stream_update made a Chunk and a words copy per new chunk (fn_stream_update +39 blocks / 13.2 KB a
window with the hiker in the drifting boat). The pool holds STREAM_MAX_CHUNKS records; the arena
is twice the layer's cap; eviction compacts it; a chunk that cannot be kept is gathered from the
scratch. stream_clear_all frees records, lists and streams. steady: 300 new cells, cap 256: 156 KB
before, 0 / -4.9 KB after, and every kept chunk's data checked against its cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
np_queue made a NetMsg and a buffer per message, every flush a new keep list, and every message
that came in a NetMessage and a buffer; nr_text a new string per text read. A party plays at
dozens a second, so all of it is kept now: a pool of MTU-sized records for the queue (a peer's two
lists swapped by the flush), the inbox's records in two halves swapped when a message finds the
inbox empty, net_written's one record, a relay hello's and a STUN request's bytes in one scratch
buffer, and the texts read out interned. Tests: 6000 messages with the heap flat (Os.heap_bytes),
and an inbox record that holds still across a drain and comes back two drains on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/reuse_test: 200 walkers made and removed hold one place and 0 bytes of heap (Os.heap_bytes);
twenty closes and opens keep the same shape and walker lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pools kept every command object ever recorded, so the heap grew each time a frame drew more than
any before (fn_gvk_draw under vkCmdBindVertexBuffers/BindIndexBuffer/Draw in the leakcheck; ~650 B a
draw). Off: 3.7 ms a frame either way over 520 actors. steady: 20 to 200 actors grows 5-7 KB with it
off and 120 KB with it on (bound 16 KB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
th_fact made a ThingFact per placement, removal and use, and the valley places and removes Things
all day. The records come back two drains after they were handed out, so a reader placing a Thing
while it reads the last drain's list never sees one change (the test holds exactly that).
thing_use takes ThingsState mut, since it pushes a fact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No bake and no new file: the skeleton comes from the skin's parents and rest pose (its own joints
and their ancestors - a kit holds several rigs), a clip from anim_read_doc's channels. Built by
native/build.sh from the pinned release; the Windows DLL imports KERNEL32 alone and passes there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vk.heap_bytes pulled the Vk module - and on lang/uifree the GL window path - into a plain program,
which then failed to link (_cgl_offscreen, lgl_GetError). Os.heap_bytes is malloc_zone_statistics
through a weak reference (0 where there is none, and on Windows). Reseeded. Docs for it and for
Json.free / Json.free_all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gvk_tex_storage freed none of ici, out, req and vci (8 blocks a two-texture model); tex_load_ex kept
dds_path_of's string. Found with malloc_history over 400 load/release rounds (712 bytes a round, all
of it these). steady: the model's bound is 4 KB over 200 (was 1.6 MB), and the frame is the least of
three settled windows - a valley self-test beside it read 87 KB once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s[a .. b] is always a copy, so it is freed once a +, a comparison or print has read it. Reseeded.
string_temps.ludic adds a slice compared and a slice concatenated each round (960 KB over 20,000
before, 0 after) and a kept slice read after its +.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The left half of a + chain, a template's pieces and holes, a number's text and a side made only to be
compared are marked fresh and freed after the +, ==, != or print that reads them. lp_int_str and
lp_long_str move their digits to the start of the buffer, so the pointer they return is the one
malloc gave. Reseeded. examples/lang/string_temps.ludic: kept intermediates stay good, and 20,000
rounds grow the heap 0 bytes (2.9 MB before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the suite's parallel load a frame read ~80 KB high: MoltenVK's completion handlers release a
finished command buffer on their own thread and lagged. Settled, 12 of 12 runs four at a time pass;
the frame's bound drops from 64 KB to 4 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A freed texture or buffer id goes on a spare list the next one takes; tex_note_size keeps sizes by id.
model_release frees prims, meshes, material names, the skin and leaves gltf_cached. actor_release
takes an actor off the stage and actor_new reuses its record (ECS's Things come and go all day).
steady.ludic: an actor round at 0 bytes over 2000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thing_put and thing_spawn made a new record for every Thing placed, and the world places and removes
them all day (an animal's sign and bed, a drop, a fish), so play grew by a record per placement.
Removed records wait in a queue compacted in place; once 32 wait, the oldest is set back as new
with a new uid. ludic.base's grid_reserve makes the buckets for n rows now, since a rehash in play
leaves the old bucket array behind. Tests: a record comes back only past the lag, as new, with a
new uid, the indexes agree, and the spare queue stays bounded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>