Skip to content

perf(runtime): reuse the marshalled UTF-8 buffer for repeated C string arguments - #450

Draft
edusperoni wants to merge 1 commit into
mainfrom
feat/cstring-external-fastpath
Draft

perf(runtime): reuse the marshalled UTF-8 buffer for repeated C string arguments#450
edusperoni wants to merge 1 commit into
mainfrom
feat/cstring-external-fastpath

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

What

Marshalling a JS string to a native char* argument previously copied it with strdup on every call, parking each copy on a throwaway external string for GC to reclaim. The IsExternalOneByte fast path above that code could never fire for runtime-created strings, because the argument string itself was never externalized — the reuse design was half-wired.

This PR completes it:

  • Interop::WriteValue's CStringEncoding path now externalizes the argument string in place via v8::String::MakeExternal when the string is pure ASCII and V8 permits it. Later marshals of the same string hit the fast path and reuse the same buffer — zero copies, zero allocations.
  • The ASCII gate is exact: a one-byte external string's content is Latin-1, which coincides with the UTF-8 buffer precisely when every character is ASCII, i.e. when the UTF-8 byte count equals the UTF-16 length.
  • Non-ASCII, young-generation, internalized, and otherwise non-externalizable strings keep the previous copy-per-call behavior (the anchor external string that ties the buffer's lifetime to GC).

Hardening

The pre-existing fast path blindly trusted resource->data() of any external one-byte string as NUL-terminated UTF-8 — but V8's resource contract promises Latin-1 content with no terminator. OneByteStringResource now keeps a thread-safe ownership registry, and the fast path only reuses buffers this runtime created (OneByteStringResource::Owns).

Safety notes

  • The echoed-pointer return path (Reference::FromPointerPointer::NewInstance) takes no ownership of the buffer, so there is no double-free against the externalized string's resource. The ~ReferenceWrapper disposeData_ fix from addf287 is load-bearing for this interaction.
  • MakeExternal failure is handled by falling back to the anchor path, which then owns the resource; ownership is unambiguous on every branch.
  • Buffer lifetime changes from "until the next GC" to "as long as the JS string lives" for externalized (ASCII) strings.

Tests

Two new specs in ReferenceTests.js, built on functionWithCharPtr echoing its argument pointer:

  • Reuse: repeated marshals of the same old-space ASCII string yield the identical buffer address (fails on any runtime without reuse, since both copies stay alive simultaneously). The spec promotes the string with __collect() first — V8 refuses to externalize young-generation strings, mirroring the real-world shape where long-lived strings are the ones marshalled repeatedly.
  • Fallback: non-ASCII strings round-trip as UTF-8 on every call.

Full suite: 1378 tests, 0 failures (Debug, iOS simulator).

Open question for review

Whether the perf win justifies the added state (the resource registry) without a motivating benchmark — this PR exists to make that discussion concrete. If a real workload shows C-string marshalling is never hot, closing unmerged is a fine outcome.

…g arguments

Marshalling a JS string to char* copied it with strdup on every call and
parked each copy on a throwaway external string for GC to reclaim; the
IsExternalOneByte fast path above it could never fire for runtime-created
strings because the argument itself was never externalized.

WriteValue now externalizes the argument in place via MakeExternal when the
string is pure ASCII (UTF-8 byte count == UTF-16 length, the exact condition
under which the buffer equals the string's one-byte content) and V8 permits
it, so later marshals of the same string reuse the buffer with no copy.
Non-ASCII, young-generation, and otherwise non-externalizable strings keep
the previous copy-per-call behavior.

The fast path is also now gated on an ownership registry in
OneByteStringResource: only buffers this runtime created are known to be
NUL-terminated UTF-8, while a foreign resource's data is Latin-1 with no
terminator guarantee and can no longer be handed to native code blindly.
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

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