Skip to content

perf(dsl): skip placeholder pattern in materializeClassInstance#37

Merged
nazarhussain merged 4 commits into
mainfrom
bing/skip-materialize-placeholder
May 28, 2026
Merged

perf(dsl): skip placeholder pattern in materializeClassInstance#37
nazarhussain merged 4 commits into
mainfrom
bing/skip-materialize-placeholder

Conversation

@spiral-ladder
Copy link
Copy Markdown
Contributor

@spiral-ladder spiral-ladder commented May 22, 2026

Summary

Optimize DSL class-return materialization by removing the internal placeholder instance path.

Previously, returning a DSL class from a function/method created a JS instance with an internal external-marker argument, allocated and wrapped a temporary placeholder native object, removed that wrap, destroyed the placeholder, then wrapped the real native object.

This PR changes materialization to call the class constructor directly in an internal materialization mode, skip normal init, and wrap the real native object after napi_new_instance returns.

Details

  • Avoids placeholder native allocation/wrap/remove/destroy during class returns
  • Removes now-unused placeholder finalizer hint handling
  • Preserves subclass constructors for same-class returns
  • Rejects cross-class or non-Zapi constructors during materialization
  • Rejects subclass constructors that return a replacement object
  • Ensures returned native resources are deinitialized if materialization fails
  • Keeps static class field support from current main

Analysis from @spiral-ladder

Per-call cost of every factory method that returns a DSL class (PublicKey.fromBytes, Signature.aggregate, SecretKey.sign, etc.) was:

1 napi_external alloc (V8 young-gen)
1 napi_new_instance -> constructor allocs placeholder native + napi_wrap
1 removeWrapChecked -> tears down placeholder finalizer entry
1 destroyInternalPlaceholder
1 native alloc for the real object
1 napi_wrap + typeTagObject

That's 2 native allocs + 1 free, 2 napi_wraps + 1 unwrap, 2 typeTagObject writes, and an extra V8 young-gen object per call. In lodestar's attestation/signature hot path this measurably increases Scavenge time.

Replace with a threadlocal marker: materializeClassInstance sets materialize_target before calling napi_new_instance(argc=0, args=null). The generated constructor early-returns when isMaterializing(T) is true, skipping the placeholder alloc/wrap entirely. materialize then wraps the real object directly.

After patch: 1 native alloc + 1 napi_wrap + 1 typeTagObject per call.

Bench (lodestar-z bench/napi/materialize.bench.mjs, 200k iters):
PublicKey.fromBytes 3.28% -> 2.10% Scavenge (-1.18 pp, -69% count)
Signature.fromBytes 0.78% -> 0.47% Scavenge (-38% count)

@spiral-ladder spiral-ladder self-assigned this May 22, 2026
@nazarhussain nazarhussain changed the title [do not merge] perf(dsl): skip placeholder pattern in materializeClassInstance perf(dsl): skip placeholder pattern in materializeClassInstance May 26, 2026
@nazarhussain nazarhussain marked this pull request as ready for review May 26, 2026 16:04
spiral-ladder and others added 4 commits May 28, 2026 12:00
Per-call cost of every factory method that returns a DSL class
(PublicKey.fromBytes, Signature.aggregate, SecretKey.sign, etc.) was:

  1 napi_external alloc (V8 young-gen)
  1 napi_new_instance       -> constructor allocs placeholder native + napi_wrap
  1 removeWrapChecked       -> tears down placeholder finalizer entry
  1 destroyInternalPlaceholder
  1 native alloc for the real object
  1 napi_wrap + typeTagObject

That's 2 native allocs + 1 free, 2 napi_wraps + 1 unwrap, 2 typeTagObject
writes, and an extra V8 young-gen object per call. In lodestar's
attestation/signature hot path this measurably increases Scavenge time.

Replace with a threadlocal marker: materializeClassInstance sets
`materialize_target` before calling napi_new_instance(argc=0, args=null).
The generated constructor early-returns when isMaterializing(T) is true,
skipping the placeholder alloc/wrap entirely. materialize then wraps the
real object directly.

After patch: 1 native alloc + 1 napi_wrap + 1 typeTagObject per call.

Bench (lodestar-z bench/napi/materialize.bench.mjs, 200k iters):
  PublicKey.fromBytes  3.28% -> 2.10% Scavenge (-1.18 pp, -69% count)
  Signature.fromBytes  0.78% -> 0.47% Scavenge (-38% count)
  Consume the marker only in the intended constructor so nested
  normal construction cannot skip native wrapping. Clean up returned
  native resources when materialization fails.
@nazarhussain nazarhussain force-pushed the bing/skip-materialize-placeholder branch from ea4b430 to ba7c8c7 Compare May 28, 2026 10:00
@nazarhussain nazarhussain merged commit 35016d1 into main May 28, 2026
6 checks passed
@nazarhussain nazarhussain deleted the bing/skip-materialize-placeholder branch May 28, 2026 10:07
@spiral-ladder
Copy link
Copy Markdown
Contributor Author

Analysis from @spiral-ladder

Analysis from claude, to be precise ;)

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.

2 participants