Repository navigation
Count the runtime profile's changed functions by name - #3370
Conversation
The staleness test's 10% limit on changed functions counted each unit object's functions. The pack compiles some units more than once, and a function whose control flow differs between two of those builds matches the profile's record in one of them only, so a profile collected the same day already read 7.3% (382 of 5,226 on main), and no refresh could bring that down. The profile is applied when a unit object is compiled, by profile name and control-flow hash; a record has no object. The manifest now also counts by name, as the counters measure does: a function is changed when no object's build of it matches. Main reads 2 of 1,464. CI's runtime pack cache key did not name the committed profiles, the include forced into every ELF unit, or the LLVM helper's sources, so a change to one of them alone restored the pack built before it. A test holds the key to the files the pack build reads. RELEASING.md names the functions limit and says where the three training projects come from; AGENTS.md points to it. Co-Authored-By: Claude Code <noreply@anthropic.com>
CI verifies the runtime pack in every job, restored or rebuilt, so the log now says which profile the pack holds and how it matches. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CI on f7c64dd (run 42418933560), read once: 98 jobs passed, 20 failed, plus the aggregate The pack was rebuilt with the committed profile. Both caches were saved. The numbers are for the merge with main afb6825: one more changed function than at the branch point (2, and 382 per object). The three touched test files ran green in the Linux plain lane: The run is red, for reasons that are on main. This PR touches
No failure is in a file this PR touches or in the Linux x64 pack. |
Follow-up to #3369 (the runtime profile refresh), which found three defects around the runtime profile. Test, CI configuration and docs only: no runtime source, compiler source or profile changes.
1. The staleness test counted a floor that no refresh can clear
packages/runtime-pack-common/test/runtime-profile.test.tsfails when more than 10% of the functions the profile describes changed. It readmismatched / (annotated + mismatched)from the pack manifest, and those two numbers add up every unit object's functions. The pack compiles some units more than once (default, workers, young-nursery and other variants) and some names twice (a stub in one unit, the function in another). Where the control flow of two such builds differs, the profile's record matches one of them and reads as mismatched in the other, however fresh the profile is. On main, with a profile collected the same day, that is 382 of 5,226 (7.31%). The limit left room for about 40 really changed functions, and main changed 40 in the three days before #3369.The measure now counts by profile name: a function is changed when no object's build of it matches the profile. The counters limit (5%) has used that rule since it was added.
Why by name, and not by object and name: the profile is applied when a unit object is compiled (
runtime-unit --profile-use), not at link time. LLVM looks each function up by its profile name and takes the record whose control-flow hash is the function's. A record is keyed by name and hash; the format has no object, and a local function's profile name (<file>.c;<name>) is the same in every build of its unit. So a per-object key cannot be expressed, and "this name has a record that some build still uses" is the question the format can answer.One thing the by-name count does not see, stated here so nobody has to find it: the committed profile holds 1,606 records for 1,485 names, so 121 names have two records (both builds were trained). If an edit changes the control flow of only one of those builds (an edit inside code that differs between the builds), the name stays annotated through the other. The counters limit has the same rule. An exact count needs the LLVM helper to report which hash matched, which is a helper change and not part of this PR.
The manifest keeps the per-object numbers (
annotated,mismatched,missing: how much of the pack is optimized with the profile) and gainsannotated_namesandmismatched_names. The summary moved frombuild.mjsintoscripts/profile-match.mjswith unit tests that run on every host. A pack whose manifest lacks the new fields fails the test with "rebuild the runtime pack".Numbers (Linux x64 Sandbox, main f88df8d sources)
The 2 names on main are real staleness:
scr_events.c;scr_events_dispatchandscr_run_exit_listeners, changed by #2801 after the profile was collected.Mutation check
In a scratch copy, a script gave profiled runtime functions one more branch at their start (
{ volatile int m = 0; if (m) m = 0; }), which changes their control-flow hash and nothing they compute, and the pack was rebuilt. Three functions (scr_dyn_new_str,scr_dyn_to_string,scr_bytes_write_str): the count goes from 2 to 5, the three are named first instale, the test passes. 303 definitions: 291 names, and the test fails with291 of the 1464 runtime functions the profile describes changed since it was collected (most counts: scr_bytes.c;scr_bytes_convert_span, scr_arr_release, …); run node scripts/refresh-runtime-profile.mjs: expected 0.1987704918032787 to be less than or equal to 0.1.The limit stays at 10%
The number is unchanged; what it measures is now what its comment always said (the share of the described functions that changed). In practice the old measure fired at about 40 changed functions, the new one at 146. At the rate of the three days before #3369 (40 functions, 2.81% of the counters), the counters limit of 5% is reached after about five days and the functions limit after about ten, so the counters limit, which weighs hot functions, decides when to refresh, and the functions limit catches a broad change to cold code.
2. CI did not build a new profile into the pack it tested
The runtime pack cache key in
.github/actions/native-packages/action.ymldid not namepackages/runtime-pack-common/profiles/, so a change to the profile alone restored the pack built with the old one. Checking the rest of that key against what the pack build reads found two more omissions of the same kind:packages/runtime-pack-common/unwind-tail.h, forced into every ELF unit since End every packaged ELF object's unwind table with a function no executable links #3199 (only*.mjsof that directory was in the key);native/llvm-codegen/**,packages/llvm-*/package.json,packages/llvm-*/scripts/**): the helper compiles every speed unit and applies the profile, so a helper change restored a pack built by the previous helper.scripts/sandbox-test.mjsalready lists the helper among the pack's inputs.All three are in the key now.
tests/harness/native-packages-cache.test.tsholds it there: every file ofpackages/runtime-pack-commonoutside its tests must match a pattern of the runtime key, and the runtime key must hold every pattern of the helper key. The other cache keys (helper, LLVM distribution, Zig, libnode) do not depend on the profile, and the helper key already hashes all ofnative/llvm-codegen/, which holds the helper's own profile.The pack's verify step, which runs in every CI job whether the pack was restored or rebuilt, now prints the match, so a log shows which profile a pack holds:
runtime profile: 2 of the 1464 functions it describes changed since it was collected (382 of 5226 per unit object), holding 0.00% of its counts.What the first CI run does. The action file is itself part of the helper key and the runtime key, so this change misses both caches once for every target: each job that calls the action builds its LLVM helper and its runtime pack, then saves them. The same happens once on main after the merge (a pull request's caches are not visible to main). Later runs restore as before. Because the PR touches
.github/,scripts/ci-lanes.mjsturns every lane on for it.3. RELEASING.md was incomplete
The runtime profile paragraph named only the 5% counters limit. It now names both limits and how functions are counted, says where the three training projects of step 7 come from (the
typeorm,t3code-serverandvscodeentries of tsc-ts'spackages/tsc-ts/profiles/projects.json), what the tsc-ts checkout needs, that the island start-up workloads are left out without a libnode pack, and how to measure a refresh (speed against speed; the zero-cost gate builds at release).AGENTS.mdgets a paragraph that points there from the failing test.Proof
packages/runtime-pack-common/testandtests/harness/native-packages-cache.test.ts: 11 files, 47 tests pass on the Linux x64 Sandbox with the pack built from this branch.pnpm lint: 0 warnings.pnpm format:check: clean.build.mjsfrom the same sources and the pack built by this branch: every file underartifacts/is byte-identical, and the two manifests differ only in the two new fields (725,603 and 725,667 bytes).Not in this PR
🤖 Generated with Claude Code