Repository navigation
A5 (ceilings): each engine's code size and stack per call, gated - #87
Merged
Merged
Conversation
The second half of the roadmap's A5 (row P5). scripts/ceilings.py
measures one probe per engine and format and gates each figure one-sided
against <engine>/bench/ceilings.json, in the ratchet job on every target:
- The probes (<engine>/bench/icount/probe_main.cpp, tools/probe/
probes.cmake): async's stereo converter at the default profile through
push() and pull(); bridge's duplex pair and rational's by-2 pair through
process(). They stream 32-frame blocks and are built Release in the
ratchet's tree, so the ceilings describe the code the counts do.
- Sizes on M33, M55 and Hexagon: the engine's own .text and .rodata (the
probe's translation unit, from its link map; the Arm linker scripts fold
.rodata into .text, so `size` cannot split them) and the whole image's
.text + .rodata.
- Stack on M33, per call, as two figures:
- the static worst path: GCC's frames under -fstack-usage
-fcallgraph-info=su for the compiled code, plus the frames of the
precompiled runtime routines it reaches, read from the linked
probe's disassembly;
- the high-water painted and read under QEMU over a second of blocks
(tools/probe/stack_meter.h).
The script fails if the painted figure exceeds the bound. The first
measurement without the runtime frames showed why both are needed:
async pull painted 256 B against a 160 B GCC-only path, the difference
being newlib's exp and libgcc's soft-double helpers.
Each ceiling is the recorded figure + 3 %, rounded up to 64 B (sizes, the
DspTap bench.yml rule) or 8 B (stack). The READMEs' tables are generated
from the files (scripts/ceilings.py docs, checked by the icount-docs job).
Recorded locally with the CI toolchains (arm-none-eabi-gcc 13.2.1
15:13.2.rel1-2, clang 19.1.5 for Hexagon, qemu-system-arm on mps2-an505).
Largest M33 stack per call: async pull 276 B static / 256 B painted, bridge
328 / 268, rational 128 / 128. Engine .text on M33: async 15.2-16.1 KB,
bridge 31.1-32.7 KB, rational 9.0-11.0 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
This is the second half of roadmap item A5, and it closes row P5 for all three engines. A new script,
scripts/ceilings.py, measures one small probe program per engine and sample format, and checks every figure against a ceiling recorded in<engine>/bench/ceilings.json. The ceilings are one-sided: a figure fails only if it rises above its ceiling. The check runs as new steps in the existing instruction-count ratchet job.The probes (
<engine>/bench/icount/probe_main.cpp, built bytools/probe/probes.cmake) each stream 32-frame stereo blocks at the default profile:push()andpull();process();up_2stage and adown_2stage, each callingprocess().The probes are built Release in the ratchet's own build tree, so the ceilings describe the same code the instruction counts do. Their names stay outside the ratchet's
*_icount_*pattern.Code size, on M33, M55 and Hexagon:
.textand.rodata: the input sections of the probe's own source file, read from its link map, which means the engine's headers as compiled into the probe. The map is needed because the Arm linker scripts put.rodatainside.text, sosizecan't separate them..textplus.rodata, including the C/C++ runtime and startup code.Stack, on M33, per call, as two figures:
-fstack-usage -fcallgraph-info=su. Precompiled runtime routines (newlib, libgcc's soft-double helpers) are absent from GCC's graph, so their frames are read from the linked probe's disassembly. The result bounds every path, not just the one the probe takes.tools/probe/stack_meter.h).The script fails if the measured figure ever exceeds the static bound, since that would mean the analysis missed a frame.
Ceiling rule: recorded figure + 3 %, rounded up to 64 bytes for sizes (DspTap's
bench.ymlrule) or 8 bytes for stack. Each engine README's Footprint section gains a "Code size and stack" subsection. Its tables are generated from the ceilings file (scripts/ceilings.py docs), and the existing docs-freshness job checks they're current.Why
Row P5 asks that each engine's memory footprint and per-call stack use be stated and gated. #86 covered the heap. This PR covers the code a deployment links and the stack each real-time call needs. On a Pico 2 class part, those are budgeted the same way the heap is.
The two stack figures are both needed. My first run used GCC's frames alone, and async's
pullcame out at 160 B static while the measured high-water was 256 B. The gap was newlib'sexpand libgcc's soft-double helpers, which are precompiled and have no frames in GCC's call graph. Once their frames come from the disassembly, the static figure (276 B) bounds the measured one on every call.Recorded figures, M33. Sizes are the engine's own
.text. Stack is the largest call, static / measured:.text(f32 / Q15 / Q31)pull276 / 256 B (f32)down328 / 216 B (f32); 288 / 268 B (Q15)up128 / 128 B (f32)The full tables, M55 and Hexagon included, are in each README.
The roadmap says "per-profile" ceilings. Here they are per sample format: the quality profiles are runtime values over a single compiled instantiation, so they all share the same code. The roadmap entry says so.
Verification
15:13.2.rel1-2), clang 19.1.5 for Hexagon, and qemu-system-arm onmps2-an505. All three are the ones that have reproduced the ratchet's counts exactly before. Recording meansscripts/ceilings.py measure --updatein a tree configured exactly as the ratchet job configures it.-fcallgraph-info, so the flag is now added only on the bare-metal GCC legs.scripts/check_retired_words.py, andscripts/ceilings.py docs --check.llvm-sizein the toolchain'sbindirectory, which it already puts onPATH.Notes for the reviewer
.textand.rodatafigures are the ones that move when an engine changes..rodatais 0 B for async: clang keeps those constants out of.rodatathere. A ceiling of 0 B means any constant data that appears later fails the gate and has to be re-recorded with a reason.🤖 Generated with Claude Code
https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
Generated by Claude Code