Skip to content

Commit 0e243e6

Browse files
committed
perf(runtime): claim host objects only once a DOMException exists
HasCustomHostObject makes V8 consult IsHostObject for every plain JS object in a serialized graph — measured ~25ns each, ~+12% on an object-heavy structuredClone. An isolate that never constructed a DOMException cannot be holding one, so the claim is gated on a per-isolate flag markCloneable flips with the first instance; until then serialization runs the pre-claim path untouched. Accepted edge, documented at the sample site: a getter running during the very clone could construct the isolate's first DOMException after a false sample — that one instance degrades to a plain object, the pre-feature behavior, and every later serialization sees the flag.
1 parent 3f49755 commit 0e243e6

1 file changed

Lines changed: 30 additions & 7 deletions

File tree

‎NativeScript/runtime/StructuredSerialization.cpp‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ namespace {
1515
// The private symbol markCloneable stamps on every DOMException instance.
1616
// Private, so app code can neither forge the brand onto an impostor nor strip
1717
// it; per isolate because a worker's instances are branded and checked on its
18-
// own isolate, and only bytes cross between them.
18+
// own isolate, and only bytes cross between them. `anyInstances` flips when
19+
// the first instance is branded and gates HasCustomHostObject: claiming host
20+
// objects makes V8 consult IsHostObject for every plain JS object in a
21+
// serialized graph (~25ns each, ~+12% on an object-heavy clone), and an
22+
// isolate that never constructed a DOMException cannot be holding one, so it
23+
// keeps serializing on the exact pre-claim path. Every instance passes
24+
// through markCloneable — deserialization rebuilds via the constructor — so
25+
// the flag cannot miss one.
1926
struct DomExceptionBrandState {
2027
Persistent<Private> brand;
28+
bool anyInstances = false;
2129
};
2230

2331
// Empty once teardown has begun — callers bail to their fallback.
@@ -34,6 +42,11 @@ Local<Private> DomExceptionBrand(Isolate* isolate) {
3442
return state->brand.Get(isolate);
3543
}
3644

45+
bool AnyDomExceptionInstances(Isolate* isolate) {
46+
auto* state = Caches::StateFor<DomExceptionBrandState>(isolate);
47+
return state != nullptr && state->anyInstances;
48+
}
49+
3750
void MarkCloneableCallback(const FunctionCallbackInfo<Value>& info) {
3851
Isolate* isolate = info.GetIsolate();
3952
if (info.Length() < 1 || !info[0]->IsObject()) {
@@ -43,10 +56,12 @@ void MarkCloneableCallback(const FunctionCallbackInfo<Value>& info) {
4356
if (brand.IsEmpty()) {
4457
return;
4558
}
46-
info[0]
47-
.As<Object>()
48-
->SetPrivate(isolate->GetCurrentContext(), brand, v8::True(isolate))
49-
.FromMaybe(false);
59+
if (info[0]
60+
.As<Object>()
61+
->SetPrivate(isolate->GetCurrentContext(), brand, v8::True(isolate))
62+
.FromMaybe(false)) {
63+
Caches::StateFor<DomExceptionBrandState>(isolate)->anyInstances = true;
64+
}
5065
}
5166

5267
} // namespace
@@ -137,8 +152,16 @@ class SerializerDelegate : public ValueSerializer::Delegate {
137152
// With this returning true, V8 asks IsHostObject about every plain JS
138153
// object in the graph — the cost of claiming a plain-JS class as a host
139154
// object is one private-symbol lookup per object (Node pays the same for
140-
// its JSTransferable protocol).
141-
bool HasCustomHostObject(Isolate* isolate) override { return true; }
155+
// its JSTransferable protocol). Claimed only once this isolate has actually
156+
// constructed a DOMException; until then serialization runs the pre-claim
157+
// path untouched. V8 samples this once per ValueSerializer. Accepted edge:
158+
// a getter invoked during this very clone could construct the isolate's
159+
// FIRST DOMException and return it into the graph after a false sample —
160+
// that one instance degrades to a plain object (the pre-feature behavior)
161+
// instead of cloning; every later serialization sees the flag.
162+
bool HasCustomHostObject(Isolate* isolate) override {
163+
return AnyDomExceptionInstances(isolate);
164+
}
142165

143166
Maybe<bool> IsHostObject(Isolate* isolate, Local<Object> object) override {
144167
// Only branded DOMException instances are claimed; native-backed wrappers

0 commit comments

Comments
 (0)