Skip to content

perf(shapes): append a freshly allocated descriptor id without the family membership scan - #9768

Closed
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:perf/keystroke-shape-family-append
Closed

perf(shapes): append a freshly allocated descriptor id without the family membership scan#9768
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:perf/keystroke-shape-family-append

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What

shape_descriptor_ensure_with_holes — the interning funnel every object shape
goes through — ends with

inner.facts_push_back(facts, id);
inner.family_push_back(keys_id, id);

and both helpers answer "is this id already in the list?" with a linear scan
(IdList::contains). The family under a keys array holds every descriptor ever
created for that keys array
(that is what it is for: the shapes a keys array has
had). So interning the n-th descriptor for a keys array cost O(n), and a
program that keeps bumping a shape's semantic generation — a TUI re-rendering the
same component tree — pays quadratic time in the history of each keys array.

The scan is dead work at both sites: id was handed out by alloc_shape_id two
statements earlier, and that allocator is a strictly increasing counter that
never reuses a value (on exhaustion it parks at SHAPE_ID_END rather than
wrapping, precisely so an id can never alias). An id allocated after a list was
built cannot be in it — in that family or any other.

The change

  • IdList::append_unchecked — append without the membership scan, documented
    with the invariant that licenses it.
  • ShapeTableInner::family_append_fresh / facts_append_fresh use it, and the
    two sites that append a just-allocated id (shape_descriptor_ensure_with_holes
    and the id move in shapes_slot_list) call those.
  • Every other caller keeps push_back: the metadata rekey that re-files
    existing ids under a moved keys address can legitimately meet an id the
    destination list already holds.

What is claimed

This is a complexity fix, not a CPU win. Interning a shape descriptor is now
O(1) in the number of descriptors the keys array has ever had, instead of O(n) —
a cost that grows with process lifetime and therefore does not show its true size
in any short measurement. That is the whole claim.

Proof that the scan is gone (macOS sample, main thread, leaf samples, 400-char
streamed reply on the claude-code TUI, cc_ks2 = the same branch without this
commit):

leaf before (cc_ks2) after (cc_ks4)
IdList::contains 681 (6.19 %) 3 (0.02 %)
of which under family_push_back 647 (95 %)

200x fewer samples. shape_descriptor_ensure itself stays at 0.36 %, i.e.
interning still happens, it just no longer walks the family. On main the same
symbol reads 95 samples (0.79 %) in that window — it grows as the shape table
does, which is exactly the point.

End to end it is flat, and I am explicitly not claiming otherwise. Same
session, same measure_lock.sh, 400-character reply
(stream_scale.py --mem --idle 12):

arm turn CPU idle-12 CPU peak RSS FP settled
cc_ks2 (without this PR) 7.09 s 3.96 s 659 MB 634 MB
cc_ks4 (with this PR + #9769) 6.98 s 3.90 s 658 MB 651 MB
node 2.1.112 0.29 s 0.02 s 375 MB 175 MB

0.11 s on a 7 s turn is inside the run-to-run spread. It is not a CPU win and
must not be counted as one. (A 3300-character arm of the same candidate came in
at 37.95 s against 51.8 s previously recorded for cc_ks2 — the direction this
predicts, longer run ⇒ longer families ⇒ more scan removed — but the two numbers
are from different sessions and I am not treating that gap as measured either.)

Correctness

New test interning_appends_each_new_descriptor_to_the_family_exactly_once pins
the observable consequence — six distinct descriptors for one keys array appear
in its family exactly once each, in birth order, and re-interning the same facts
reuses the existing id and appends nothing — so a later change that routes a
recycled id through the fresh path fails here instead of silently duplicating a
family entry. Verified running, not merely present.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 684f0be6-9b8d-4197-86db-b18bc2b1eaf0

📥 Commits

Reviewing files that changed from the base of the PR and between 12efed1 and fd03784.

📒 Files selected for processing (5)
  • changelog.d/keystroke-shape-family-append.md
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry-runtime/src/object/shapes_slot_list.rs
  • crates/perry-runtime/src/object/shapes_store.rs
  • crates/perry-runtime/src/object/shapes_tests.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The runtime adds unchecked IdList appends for freshly allocated shape ids. Shape descriptor interning and tombstone rekeying use the new path. A test verifies family ordering, uniqueness, and reuse. The changelog records profiling data for the removed scans.

Changes

Shape descriptor interning

Layer / File(s) Summary
Unchecked append contract
crates/perry-runtime/src/object/shapes_store.rs
IdList::append_unchecked contains the append logic. push_back keeps its membership check before calling it.
Fresh descriptor publication and validation
crates/perry-runtime/src/object/shapes.rs, crates/perry-runtime/src/object/shapes_slot_list.rs, crates/perry-runtime/src/object/shapes_tests.rs, changelog.d/keystroke-shape-family-append.md
Shape descriptor interning and tombstone rekeying use fresh-append helpers for ids that are not already listed. The test verifies birth order, single insertion, and reuse. The changelog records the measured scan cost.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c8b94

This change removes redundant membership scans when publishing newly allocated shape descriptors while preserving checked handling for existing IDs. The covered ordering and reuse behavior indicates no remaining merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 4 files. (1 skipped: 1…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary performance change: appending freshly allocated shape descriptor IDs without a family membership scan.
Description check ✅ Passed The description is detailed, relevant, and explains the problem, implementation, complexity impact, correctness rationale, measurements, and test coverage. It does not use the template headings and do…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…mily membership scan

`shape_descriptor_ensure_with_holes` ends with `facts_push_back` +
`family_push_back`, and both answer "is this id already in the list?" with a
linear scan. A family holds every descriptor ever created for one keys array,
so interning the n-th descriptor for a keys array cost O(n) and a render loop
that keeps bumping a shape's semantic generation paid quadratic time in that
history. On the compiled claude-code TUI `IdList::contains` was 6.2 % of
main-thread leaf samples during a streamed reply and 5.9 % in the window after
it, 95 % of it under `ShapeTableInner::family_push_back`.

The scan is dead work at those sites: `alloc_shape_id` handed the id out two
statements earlier and never reuses a value (it parks at `SHAPE_ID_END` rather
than wrapping), so an id allocated after a list was built cannot be in it.

* `IdList::append_unchecked`, with the invariant that licenses it.
* `ShapeTableInner::family_append_fresh` / `facts_append_fresh` use it; the two
  sites that append a just-allocated id call those. The metadata rekey, which
  re-files EXISTING ids under a moved keys address, keeps `push_back`.

Test: interning_appends_each_new_descriptor_to_the_family_exactly_once.

Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2
@proggeramlug
proggeramlug force-pushed the perf/keystroke-shape-family-append branch from fd03784 to c8b9449 Compare September 5, 2026 08:56
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (1d63fa91f). The previous CI run was against a base that was itself red — the main-triage lane's #9776/#9777/#9780 and the train124 gate fixes have since landed — so the failures on it were the base's, not this PR's. Re-running against a green base.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed on main via merge train #9804 (rebase-merged, so your commits keep their authorship). Thanks!

proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Sep 5, 2026
Rebasing onto main brings in PerryTS#9768's `family_append_fresh`, the append that
skips `IdList`'s membership scan for a freshly allocated id. It is the append
`shape_descriptor_intern` uses, and it did not exist when this branch added
rule-1 arming to `family_push_back` / `family_push_front`, so the rebase merges
clean and silently drops the note for every freshly interned descriptor.

`keys` is the canonical keys array's ADDRESS and the minor-scoped rekey scanner
visits only logged keys, so an unlogged family is invisible to a copying minor:
the keys array moves, the family stays filed under the old address, and the
descriptor is lost. Both intents kept — the membership scan stays gone, the note
comes back.

Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2
proggeramlug pushed a commit that referenced this pull request Sep 5, 2026
Rebasing onto main brings in #9768's `family_append_fresh`, the append that
skips `IdList`'s membership scan for a freshly allocated id. It is the append
`shape_descriptor_intern` uses, and it did not exist when this branch added
rule-1 arming to `family_push_back` / `family_push_front`, so the rebase merges
clean and silently drops the note for every freshly interned descriptor.

`keys` is the canonical keys array's ADDRESS and the minor-scoped rekey scanner
visits only logged keys, so an unlogged family is invisible to a copying minor:
the keys array moves, the family stays filed under the old address, and the
descriptor is lost. Both intents kept — the membership scan stays gone, the note
comes back.

Claude-Session: https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant