Skip to content

perf: plain struct values and cached namespace lookups (classic) - #26

Merged
triniwiz merged 2 commits into
mainfrom
perf/struct-values-namespaces
Oct 17, 2026
Merged

triniwiz merged 2 commits into
mainfrom
perf/struct-values-namespaces

Conversation

@triniwiz

Copy link
Copy Markdown
Member

Summary

Two changes to the classic (V8) engine.

1. Struct values are plain JS objects

WinRT structs were interceptor objects. Each one built a new ObjectTemplate and boxed a DeclarationFFI that was never freed. Now each struct type gets one FunctionTemplate per isolate (runtime/src/struct_value.rs), and values are ordinary objects with one property per field. All values of a type share one V8 map and the constructor's prototype.

WinRT structs are value types, so a value is a snapshot. Passing it back to native serializes its fields with the existing append_struct_object_bytes. DeclarationFFI.struct_instance is removed.

Bugs fixed along the way:

  • About 550 bytes leaked for every struct returned from native.
  • Every new Rect(...) wrote into one buffer stored on the constructor, so creating a second struct changed the first.
  • Nested struct fields read as undefined (e.g. Duration.TimeSpan).
  • {...s} gave {}, JSON.stringify(new Rect(...)) gave {}, String(s) threw, instanceof was false, and properties added in JS were dropped.
  • new Rect() threw, and new Rect({ X: 1 }) produced a broken object. Missing fields are now zero, and wrong field types throw a TypeError.

A TypeName value ({ Name, Kind }) passed to a method or property is serialized, and String struct fields are now written.

2. Namespace and enum lookups are cached as real properties

The namespace/enum interceptor is now non-masking. Each resolved name is defined as a read-only, non-enumerable own property, so repeat lookups no longer go through the interceptor. Writes to a reserved name are ignored, and throw a TypeError in strict mode, same as an already-resolved name.

Gotcha: define_own_property goes through the object's setter interceptor. A thread-local guard lets the define through, and the side store keeps a copy as a backup. Without that guard, every lookup rebuilt a constructor and a V8 template, which are never freed, and memory grew without limit. namespace_members.rs checks Windows.Foundation.Uri === Windows.Foundation.Uri to catch this.

Performance

Release build, ns per op over 1M iterations:

Access Before After
Windows.UI.Colors.Red.R ~3100 ~760
read a field of a struct you already hold 55 ~1
memory held after 1M struct returns +540 MB, never freed +0 MB
Windows.Foundation.Uri 159 8
Windows.UI.Xaml.Controls.Button 355 27

Behaviour changes

  • Struct values are snapshots. A struct read from a property doesn't track later native changes, which was already true.
  • Object.keys(struct) lists fields in metadata order.
  • Assigning to a resolved namespace member throws in strict mode.

Testing

  • New integration-tests/tests/struct_values.rs and namespace_members.rs.
  • All integration test binaries pass.
  • Runtime unit tests show the same failures as main (11 known UI-thread/environment failures).
  • cargo check passes for the napi-only build (--no-default-features --features napi_engine) and for the combined build.

Not in this PR

Windows.UI.ColorHelper.ToDisplayName(color) crashes with an access violation on main as well; small structs passed by value need a separate look.

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 65960a69-8f99-4b4b-93ba-9ecbe4bdddf8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@triniwiz
triniwiz merged commit 55406ee into main Oct 17, 2026
4 checks passed
@triniwiz
triniwiz deleted the perf/struct-values-namespaces branch October 17, 2026 08:21
Structs: values are plain JS objects built from a per-type template
cached per isolate (new struct_value.rs), replacing per-value
interceptor objects.
- fixes a ~550 byte leak per struct returned from native
- fixes every `new Rect(...)` sharing one buffer
- nested struct and String fields are now read
- spread, JSON, instanceof, toString and expando properties work
- `new Rect()` zero-fills; wrong field types throw TypeError
- Colors.Red.R: ~3100 -> ~760 ns; held field read: 55 -> ~1 ns

Namespaces/enums: the interceptor is non-masking and resolved names
become read-only own properties, so repeat lookups skip it.
- Windows.UI.Xaml.Controls.Button: 355 -> 27 ns
TimedTextStyle has no Padding property, so the old test only round-tripped
a JS expando. Use TimedTextRegion.Padding and assert no own property.
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