The zero-allocation contract requirement is violated by the only API the crate exposes
Источник:
dev/fera-research/docs/oc2/zero-allocation-requirement-violated.md(read-only; сгенерировано scripts/sync-docs.mjs)
The zero-allocation contract requirement is violated by the only API the crate exposes
Section titled “The zero-allocation contract requirement is violated by the only API the crate exposes”- kind: problem
- area: perf
- severity: mid
- evidence:
~/dev/fera/chroix-project/CHROIX-DISPLAY-LIST.md:27-28— “Zero per-frame allocation on the hot path. Transport is an arena / ring buffer; the list is rewritten in place frame-to-frame.”~/dev/fera/chroix-project/CHROIX-DISPLAY-LIST.md:49— “consumed once, then either reused (frame N+1 rewrites the same arena) or diffed against the previous frame”~/dev/fera/chroix-project/UI-FRAMEWORK-IDEAS-V2.md:183— “Arena transport (§5): display lists move in arenas / ring buffers, no per-frame allocation.”~/dev/fera/chroix-project/UI-FRAMEWORK-IDEAS-V2.md:193— “Rule: optimization is a contract requirement, not an emergent property. A design that regresses a frame budget or adds per-frame allocation fails review.”~/dev/fera/chroix-contract/src/encode.rs:20—pub fn encode(&self) -> Vec<u8>~/dev/fera/chroix-contract/src/encode.rs:29—let mut op_stream = Vec::new();~/dev/fera/chroix-contract/src/list.rs:20,28,34,40— public API is exactlynew,push_op,frame_id,ops— noclear,reset,retain, or arena entry point~/dev/fera/chroix-contract/src/damage.rs:21,26,37,45,50— public API is exactlynew,push,count,is_empty,iter
Sum (Суть)
Section titled “Sum (Суть)”Three places in the corpus state the same requirement, and one of them makes it a review gate
rather than a goal: V2 §19:193 — “A design that regresses a frame budget or adds per-frame
allocation fails review.”
The implementation currently adds two fresh allocations per frame and offers no way not to:
// chroix-contract/src/encode.rs:20#[must_use]pub fn encode(&self) -> Vec<u8> { // <-- new Vec<u8> every call ... // encode.rs:29 let mut op_stream = Vec::new(); // <-- second Vec, every call OpStream::write(&mut op_stream, self.ops());And the type being encoded exposes no reuse entry point at all:
// chroix-contract/src/list.rs — the complete public APIpub fn new(frame_id: u64) -> Selfpub fn push_op(&mut self, op: Op)pub fn frame_id(&self) -> u64pub fn ops(&self) -> &[Op]There is no clear, no reset, no encode_into(&mut [u8]), no arena, no ring buffer. So
“frame N+1 rewrites the same arena” is not merely unimplemented — the API shape makes it
impossible without a breaking change, because encode() hands ownership of a new buffer to
the caller each time.
This is worth flagging precisely because of when it will hurt. Today the crate is 60% real and
its backends decode buffers that nothing yet produces at volume, so two allocations per frame
cost nothing measurable. The trap is that this is an API-level decision, and the cost of
fixing it later scales with everything built on top: once pipelines, conformance fixtures
(TODO.md 2g) and a reference scene exist, changing encode() -> Vec<u8> into
encode_into(&mut DisplayListBuf) is a breaking change across every crate.
Adjacent, same file cluster: damage has no merge logic either
Section titled “Adjacent, same file cluster: damage has no merge logic either”While measuring I checked the other half of the same requirement. damage.rs’s complete public
API is new, push, count, is_empty, iter — there is no union, no overlap test, no
coalescing, no area/box helper, and no merge threshold, and none is marked TODO.
CHROIX-DISPLAY-LIST.md:334-336 §12 lists “Damage merge threshold” as the one remaining open
question, and :232-233 states the contract carries a merged list. So the merge step is
specified, listed as open, and simply absent — which is fine while damage is inert (it is
hardcoded 0 at encode.rs:47-48, already recorded as TODO.md G7), but it means TODO.md’s
Stage 2e (“Damage list on the wire”) will ship a raw, unmerged rect list and look done.
Options (with tradeoffs)
Section titled “Options (with tradeoffs)”- A. Change the API now, while it is cheap. Add
encode_into(&mut Vec<u8>)(or a smallDisplayListBufwith an internalVec<u8>+ length cursor that iscleared and refilled), keepencode() -> Vec<u8>as a thin convenience wrapper for tests. Cost: a small additive change now; a breaking change across four crates later. - B. Defer to the same stage as the side tables. TODO.md Stage 2 already rewrites the encoder to emit brushes/transforms/strings/damage; fold the buffer-reuse change in there. Cost: zero extra work if Stage 2 happens; but Stage 2’s stated purpose is the wire, not allocation, so the requirement stays invisible until someone remembers it.
- C. Do nothing. Cost: the V2 §19 review gate is violated silently, and the violation becomes structural.
Proposed solution
Section titled “Proposed solution”A, folded into TODO.md Stage 2 (i.e. B + A). The two are the same edit region, so:
- Add
encode_into(&mut DisplayListBuf)whereDisplayListBufowns the bytes and exposesclear()+len(). Keepencode() -> Vec<u8>as a wrapper so existing tests and theDisplayList::decode(&[u8])round-trip path keep working unchanged. - Do it inside Stage 2 rather than as separate work — Stage 2 rewrites
encode()anyway to stop hardcoding the six side-table offsets to0. Adding reuse at the same time is marginal cost, and it gets the change in while the API has one consumer per backend. - Add the damage merge as an explicit item before Stage 2e’s “damage on the wire” is called done — otherwise Stage 2 ships a raw rect list that looks like the spec’s merged list.
- Whichever way it goes, the requirement should be written into the task list next to the
spec quote, because
V2 §19:193makes it a review gate and nothing in the current plan mentions allocation.
Honest sizing: this is severity: mid, not high. Nothing regresses today because nothing
produces display lists at volume yet. It is listed mid because the fix is an API change and
the cost of deferral grows with every crate added — and because the corpus explicitly promises
this will fail review, so shipping it silently contradicts a stated gate.