Skip to content

perf(runtime): reject non-private property names without validating UTF-8 (read −10.5%) - #8970

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-private-member-fast-path
Aug 28, 2026
Merged

perf(runtime): reject non-private property names without validating UTF-8 (read −10.5%)#8970
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf-private-member-fast-path

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

private_member_storage_name runs at the top of both the generic property read (js_object_get_field_by_name) and the generic write (field_set_by_name), so every property operation in the program pays it. It reached its verdict via str_from_string_header, which UTF-8-validates the entire key before the prefix compare can reject it.

In a computed-key read loop that is ~7.5% self time (plus its share of from_utf8) — all of it spent proving that "k123" does not begin with #.

A private-member storage name always starts with # and is longer than the #<perry:private-member: prefix, so a length compare plus one byte settles it for every ordinary key, with no validation and no slice. Keys that pass the filter still take the original path, so private members and #-prefixed ordinary keys are untouched.

Measurement

Interleaved A/B pairs, min-of-15, quiet load (~1.9), 16-core Linux host:

loop main this PR
read only 38 ms 34 ms −10.5%
combined overwrite 82 ms 76 ms −7.3%
write only 44 ms 43 ms −2.3%

Means move the same direction on all three (45→43, 93→85, 47→46). An earlier 6-pair run was inconclusive — the spread exceeded the effect — so this is the tightened measurement.

Correctness

  • perry-runtime: 2779 passed / 0 failed.
  • Differential against pre-change perry on a private-member exercise — instance fields, static #instances, private methods, private getters, #x in obj, subclassing, and an ordinary property literally named #<perry:private-member:1:x> — is byte-identical, including on the cases where perry currently diverges from node.
  • The guard performs no dereference the previous path didn't already perform (null check, then byte_len, then the payload's first byte via the shared string_header_payload helper).

Note

Building that differential surfaced three pre-existing correctness bugs in private class members on main (compound assignment on a private field yields NaN; private fields are enumerable own properties; an ordinary key matching the internal prefix is silently dropped). They are not affected by this PR — same output before and after — and are filed separately as #8969.

https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP

Summary by CodeRabbit

  • Performance
    • Improved property access performance by quickly filtering keys that cannot reference private members.
    • Reduced overhead in read and overwrite operations while preserving existing behavior.
  • Bug Fixes
    • Maintained correct handling for private members and specially formatted keys.
  • Validation
    • Confirmed output remains identical to the previous implementation.

…TF-8

private_member_storage_name runs at the top of both the generic property read
and the generic write, so every property operation pays it — and it reached
its verdict by UTF-8-validating the entire key before the prefix compare could
reject it. Measured at ~7.5% self time in a computed-key read loop (plus its
share of from_utf8), all of it spent proving that "k123" does not start with
'#'. A length compare plus one byte settles it for every ordinary key.

Keys that pass the filter still take the original path, so private members and
'#'-prefixed ordinary keys behave exactly as before.

Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
@coderabbitai

coderabbitai Bot commented Aug 28, 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: Pro Plus

Run ID: 02300fea-4c50-4115-bc59-befdb3de3018

📥 Commits

Reviewing files that changed from the base of the PR and between 4236000 and 2f7e77b.

📒 Files selected for processing (2)
  • changelog.d/8970-private-member-name-fast-reject.md
  • crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs

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


📝 Walkthrough

Walkthrough

The generic property path now rejects ordinary keys with a length and first-byte check before UTF-8 validation. Private-member handling remains unchanged for keys that pass the filter. The shared prefix is now a constant, and the changelog records benchmark results.

Changes

Private member name filtering

Layer / File(s) Summary
Fast rejection and prefix reuse
crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs, changelog.d/8970-private-member-name-fast-reject.md
The runtime rejects null, short, and non-#-prefixed keys before UTF-8 validation. Passing keys use the existing path. Prefix stripping uses the shared constant. The changelog records benchmark and differential-output results.

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

Merge Risk: ⚪ Minimal · up to 2f7e7

This localized optimization avoids unnecessary UTF-8 validation for ordinary property names while preserving the existing path for private-member keys, with passing tests and equivalent differential results. No actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the runtime performance optimization and its main effect: rejecting non-private property names without UTF-8 validation.
Description check ✅ Passed The description is detailed and directly addresses the change, motivation, benchmarks, correctness validation, test results, and related pre-existing issues. It does not use the repository template he…
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.
Full details: Description check

Explanation

The description is detailed and directly addresses the change, motivation, benchmarks, correctness validation, test results, and related pre-existing issues. It does not use the repository template headings or checklist, but it provides the required substantive information.

Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ 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.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged. The reasoning holds and the implementation is careful in the two places it had to be:

  • The length guard runs before the byte read (byte_len <= PRIVATE_MEMBER_PREFIX.len() returns early), so the payload dereference only happens when at least one byte exists — an empty or short key never reads out of bounds.
  • Comparing one byte against b'#' needs no UTF-8 validation: any sequence whose first byte differs is correctly rejected regardless of what follows, and anything starting with # still goes through the validated path, so the slow path's behaviour is unchanged.
  • Using <= rather than < is right too — a storage name is strictly longer than the prefix, so a key exactly the prefix's length cannot be one.

Nice to see it reuse crate::object::string_header_payload rather than open-coding the offset; that keeps the string-payload-access ratchet at baseline, which is where a couple of recent PRs tripped.

Validation — runtime 2779/0, codegen 1341/0 (RUST_TEST_THREADS=1); scripts/run_lint_gates.sh 57 of 58 including the compile tier (-D warnings, clippy) — the exception is the pre-existing \${{ }} substitution artifact (#8929).

The −10.5% is not re-measured here.

@proggeramlug
proggeramlug merged commit 24200a0 into PerryTS:main Aug 28, 2026
29 checks passed
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