Skip to content

[do not merge] perf(dsl): skip placeholder pattern in materializeClassInstance#37

Draft
spiral-ladder wants to merge 1 commit into
mainfrom
bing/skip-materialize-placeholder
Draft

[do not merge] perf(dsl): skip placeholder pattern in materializeClassInstance#37
spiral-ladder wants to merge 1 commit into
mainfrom
bing/skip-materialize-placeholder

Conversation

@spiral-ladder
Copy link
Copy Markdown
Contributor

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)

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
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