Skip to content

Commit 387f39d

Browse files
authored
Merge pull request #5339 from cloudflare/jasnell/jsg-struct-template
2 parents 1872787 + eaad4cb commit 387f39d

File tree

10 files changed

+112
-41
lines changed

10 files changed

+112
-41
lines changed

src/workerd/api/streams/standard.c++

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2252,7 +2252,7 @@ jsg::Promise<void> ReadableStreamJsController::cancel(
22522252

22532253
const auto doCancel = [&](auto& consumer) {
22542254
auto reason = js.v8Ref(maybeReason.orDefault([&] { return js.v8Undefined(); }));
2255-
KJ_DEFER(state.init<StreamStates::Closed>());
2255+
KJ_DEFER(doClose(js));
22562256
return consumer->cancel(js, reason.getHandle(js));
22572257
};
22582258

src/workerd/io/compatibility-date.capnp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,4 +1192,13 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
11921192
$compatDisableFlag("disable_python_external_sdk")
11931193
$experimental;
11941194
# Don't include the Python sdk from the runtime, use a vendored copy.
1195+
1196+
fastJsgStruct @141 :Bool
1197+
$compatEnableFlag("enable_fast_jsg_struct")
1198+
$compatDisableFlag("disable_fast_jsg_struct");
1199+
# Enables the fast jsg::Struct optimization. With this enabled, JSG_STRUCTS
1200+
# will use a more efficient creation pattern that reduces construction time.
1201+
# However, optional fields will be explicitly set to undefined rather than
1202+
# being omitted, which is an observable behavior change.
1203+
# TODO(soon): Once proven in production, add a default on date
11951204
}

src/workerd/io/worker.c++

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,9 @@ Worker::Isolate::Isolate(kj::Own<Api> apiParam,
10651065
if (features.getEnhancedErrorSerialization()) {
10661066
lock->setUsingEnhancedErrorSerialization();
10671067
}
1068+
if (features.getFastJsgStruct()) {
1069+
lock->setUsingFastJsgStruct();
1070+
}
10681071

10691072
if (impl->inspector != kj::none || ::kj::_::Debug::shouldLog(::kj::LogSeverity::INFO)) {
10701073
lock->setLoggerCallback([this](jsg::Lock& js, kj::StringPtr message) {

src/workerd/jsg/jsg.c++

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ void Lock::setUsingEnhancedErrorSerialization() {
191191
IsolateBase::from(v8Isolate).setUsingEnhancedErrorSerialization();
192192
}
193193

194+
void Lock::setUsingFastJsgStruct() {
195+
IsolateBase::from(v8Isolate).setUsingFastJsgStruct();
196+
}
197+
198+
bool Lock::isUsingFastJsgStruct() const {
199+
return IsolateBase::from(v8Isolate).getUsingFastJsgStruct();
200+
}
201+
194202
bool Lock::isUsingEnhancedErrorSerialization() const {
195203
return IsolateBase::from(v8Isolate).getUsingEnhancedErrorSerialization();
196204
}

src/workerd/jsg/jsg.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,14 @@ struct HasStructTypeScriptDefine<T, decltype(T::_JSG_STRUCT_TS_DEFINE_DO_NOT_USE
717717
template <typename TypeWrapper, typename Self> \
718718
using JsgFieldWrappers = \
719719
::workerd::jsg::TypeTuple<JSG_FOR_EACH(JSG_STRUCT_FIELD, , __VA_ARGS__)>; \
720+
template <typename Self> \
721+
static v8::Local<v8::DictionaryTemplate> jsgGetTemplate(v8::Isolate* isolate) { \
722+
kj::Vector<std::string_view> names; \
723+
JSG_FOR_EACH(JSG_STRUCT_FIELD_COL, , __VA_ARGS__); \
724+
auto namesPtr = names.asPtr().asConst(); \
725+
return v8::DictionaryTemplate::New( \
726+
isolate, v8::MemorySpan<const std::string_view>(namesPtr.begin(), namesPtr.size())); \
727+
} \
720728
template <typename Registry, typename Self, typename Config> \
721729
static void registerMembersInternal(Registry& registry, Config arg) { \
722730
JSG_FOR_EACH(JSG_STRUCT_REGISTER_MEMBER, , __VA_ARGS__); \
@@ -761,6 +769,10 @@ consteval size_t prefixLengthToStrip(const char (&s)[N]) {
761769
// it stripped.
762770
#define JSG_STRUCT_FIELD_NAME(_, name) name##_JSG_NAME_DO_NOT_USE_DIRECTLY[] = #name
763771

772+
#define JSG_STRUCT_FIELD_COL(_, name) \
773+
::workerd::jsg::jsgAddToStructNames<decltype(::kj::instance<Self>().name), \
774+
name##_JSG_NAME_DO_NOT_USE_DIRECTLY, ::workerd::jsg::prefixLengthToStrip(#name)>(names)
775+
764776
// (Internal implementation details for JSG_STRUCT.)
765777
#define JSG_STRUCT_FIELD(_, name) \
766778
::workerd::jsg::FieldWrapper<TypeWrapper, Self, decltype(::kj::instance<Self>().name), \
@@ -1061,6 +1073,16 @@ class SelfRef: public V8Ref<v8::Object> {
10611073
inline Value asValue(Lock& js) const;
10621074
};
10631075

1076+
template <typename U>
1077+
static constexpr bool isUsableStructField = !kj::isSameType<U, SelfRef>() &&
1078+
!kj::isSameType<U, Unimplemented>() && !kj::isSameType<U, WontImplement>();
1079+
1080+
template <typename T, const char* name, size_t prefix>
1081+
void jsgAddToStructNames(auto& names) {
1082+
constexpr const char* exportedName = name + prefix;
1083+
if constexpr (isUsableStructField<T>) names.add(exportedName);
1084+
}
1085+
10641086
// TODO(cleanup): This class was meant to be a ByteString (characters in the range [0,255]), but
10651087
// its only use so far is in api::Headers. But making the Headers class use ByteStrings turned
10661088
// out to be unwise. Nevertheless, it is still useful to keep around in order to provide
@@ -2626,6 +2648,8 @@ class Lock {
26262648

26272649
void setCaptureThrowsAsRejections(bool capture);
26282650
void setUsingEnhancedErrorSerialization();
2651+
void setUsingFastJsgStruct();
2652+
bool isUsingFastJsgStruct() const;
26292653
bool isUsingEnhancedErrorSerialization() const;
26302654

26312655
void setNodeJsCompatEnabled();

src/workerd/jsg/setup.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ class IsolateBase {
255255
return usingEnhancedErrorSerialization;
256256
}
257257

258+
void setUsingFastJsgStruct() {
259+
usingFastJsgStruct = true;
260+
}
261+
262+
bool getUsingFastJsgStruct() const {
263+
return usingFastJsgStruct;
264+
}
265+
258266
bool pumpMsgLoop() {
259267
return v8System.pumpMsgLoop(ptr);
260268
}
@@ -319,6 +327,7 @@ class IsolateBase {
319327
bool allowTopLevelAwait = true;
320328
bool usingNewModuleRegistry = false;
321329
bool usingEnhancedErrorSerialization = false;
330+
bool usingFastJsgStruct = false;
322331

323332
// Only used when the original module registry is used.
324333
bool throwOnUnrecognizedImportAssertion = false;

src/workerd/jsg/struct.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class FieldWrapper {
9191
explicit FieldWrapper(v8::Isolate* isolate)
9292
: nameHandle(isolate, v8StrIntern(isolate, exportedName)) {}
9393

94+
// The is the original, slow-path wrap implementation that uses Set(). Prefer the other overload
95+
// for better performance. It is, however, a breaking change to remove this overload so we
96+
// need to keep it with a compatibility flag.
9497
void wrap(Lock& js,
9598
TypeWrapper& wrapper,
9699
v8::Isolate* isolate,
@@ -112,6 +115,24 @@ class FieldWrapper {
112115
}
113116
}
114117

118+
void wrap(Lock& js,
119+
TypeWrapper& wrapper,
120+
v8::Isolate* isolate,
121+
v8::Local<v8::Context> context,
122+
kj::Maybe<v8::Local<v8::Object>> creator,
123+
Struct& in,
124+
v8::MaybeLocal<v8::Value>& out,
125+
size_t& idx) {
126+
if constexpr (kj::isSameType<T, SelfRef>()) {
127+
// Ignore SelfRef when converting to JS.
128+
} else if constexpr (kj::isSameType<T, Unimplemented>() || kj::isSameType<T, WontImplement>()) {
129+
// Fields with these types are required NOT to be present, so don't try to convert them.
130+
} else {
131+
idx++;
132+
out = wrapper.wrap(js, context, creator, kj::mv(in.*field));
133+
}
134+
}
135+
115136
Type unwrap(TypeWrapper& wrapper,
116137
v8::Isolate* isolate,
117138
v8::Local<v8::Context> context,
@@ -149,10 +170,42 @@ class StructWrapper<Self, T, TypeTuple<FieldWrappers...>, kj::_::Indexes<indices
149170
return typeid(T);
150171
}
151172

173+
// A count of the JSG_STRUCT fields that are usable for the v8::DictionaryTemplate
174+
// version of wrap (i.e. not SelfRef, Unimplemented, or WontImplement).
175+
static constexpr size_t kCountOfUsableFields =
176+
((isUsableStructField<typename FieldWrappers::Type> ? 1 : 0) + ...);
177+
152178
v8::Local<v8::Object> wrap(
153179
Lock& js, v8::Local<v8::Context> context, kj::Maybe<v8::Local<v8::Object>> creator, T&& in) {
154180
auto isolate = js.v8Isolate;
155181
auto& fields = getFields(isolate);
182+
183+
// Fast path using a cached dictionary template.
184+
if (js.isUsingFastJsgStruct()) {
185+
v8::MaybeLocal<v8::Value> values[kCountOfUsableFields]{};
186+
187+
size_t idx = 0;
188+
(kj::get<indices>(fields).wrap(
189+
js, static_cast<Self&>(*this), isolate, context, creator, in, values[idx], idx),
190+
...);
191+
192+
// We use a cached dictionary template to improve performance on repeated struct wraps.
193+
194+
v8::Local<v8::DictionaryTemplate> tmpl;
195+
if (templateHandle.IsEmpty()) {
196+
tmpl = T::template jsgGetTemplate<T>(isolate);
197+
templateHandle.Reset(isolate, tmpl);
198+
} else {
199+
tmpl = templateHandle.Get(isolate);
200+
}
201+
202+
// Make sure we filled in the expected number of fields.
203+
KJ_ASSERT(idx == kCountOfUsableFields);
204+
205+
return tmpl->NewInstance(context, values);
206+
}
207+
208+
// Original slow path.
156209
v8::Local<v8::Object> out = v8::Object::New(isolate);
157210
(kj::get<indices>(fields).wrap(
158211
js, static_cast<Self&>(*this), isolate, context, creator, in, out),
@@ -217,6 +270,7 @@ class StructWrapper<Self, T, TypeTuple<FieldWrappers...>, kj::_::Indexes<indices
217270
void getTemplate() = delete;
218271

219272
private:
273+
v8::Global<v8::DictionaryTemplate> templateHandle;
220274
kj::Maybe<kj::Tuple<FieldWrappers...>> lazyFields;
221275

222276
kj::Tuple<FieldWrappers...>& getFields(v8::Isolate* isolate) {

src/workerd/jsg/value-test.c++

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ JSG_DECLARE_ISOLATE_TYPE(OptionalIsolate,
115115

116116
KJ_TEST("optionals and maybes") {
117117
Evaluator<OptionalContext, OptionalIsolate> e(v8System);
118+
e.getIsolate().setUsingFastJsgStruct();
118119
e.expectEval("takeOptional(new NumberBox(123))", "number", "123");
119120
e.expectEval("takeOptional()", "number", "321");
120121
e.expectEval("takeOptional(undefined)", "number", "321");
@@ -161,14 +162,14 @@ KJ_TEST("optionals and maybes") {
161162

162163
e.expectEval(
163164
"var object = makeTestOptionalFields(undefined, undefined, null);\n" ENUMERATE_OBJECT,
164-
"string", "nullable: null");
165+
"string", "optional: undefined, lenient: undefined, nullable: null");
165166
e.expectEval("var object = makeTestOptionalFields('foo', 'bar', null);\n" ENUMERATE_OBJECT,
166167
"string", "optional: foo, lenient: bar, nullable: null");
167168
e.expectEval("var object = makeTestOptionalFields('foo', 'bar', 'baz');\n" ENUMERATE_OBJECT,
168169
"string", "optional: foo, lenient: bar, nullable: baz");
169170
e.expectEval(
170171
"var object = makeTestOptionalFields(undefined, undefined, 'bar');\n" ENUMERATE_OBJECT,
171-
"string", "nullable: bar");
172+
"string", "optional: undefined, lenient: undefined, nullable: bar");
172173
#undef ENUMERATE_OBJECT
173174

174175
e.expectEval("readTestAllOptionalFields({})", "string", "(absent), 321");

src/wpt/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ wpt_test(
108108
wpt_test(
109109
name = "streams",
110110
size = "large",
111+
compat_flags = ["enable_fast_jsg_struct"],
111112
config = "streams-test.ts",
112113
wpt_directory = "@wpt//:streams@module",
113114
)

src/wpt/streams-test.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ export default {
329329
comment: 'To be investigated',
330330
expectedFailures: [
331331
'ReadableStream with byte source (empty): calling getReader with invalid arguments should throw appropriate errors',
332-
'ReadableStream with byte source (empty) default reader: canceling via the reader should cause the reader to act closed',
333332
'ReadableStream with byte source (empty) BYOB reader: canceling via the reader should cause the reader to act closed',
334333
],
335334
},
@@ -338,7 +337,6 @@ export default {
338337
comment: 'To be investigated',
339338
expectedFailures: [
340339
'Async iterator instances should have the correct list of properties',
341-
'return(); next()',
342340
'return(); next() [no awaiting]',
343341
'return(); return() [no awaiting]',
344342
'return(); next() with delayed cancel() [no awaiting]',
@@ -348,8 +346,6 @@ export default {
348346
'next() that succeeds; return() [no awaiting]',
349347
'next() that succeeds; next() that reports an error; next()',
350348
'next() that succeeds; next() that reports an error(); return()',
351-
'Async-iterating a pull source manually',
352-
'return(); next() with delayed cancel()',
353349
],
354350
},
355351
'readable-streams/bad-strategies.any.js': {
@@ -444,11 +440,6 @@ export default {
444440
'ReadableStreamDefaultReader closed promise should be rejected with undefined if that is the error',
445441
// TODO(conform): The spec expects this to be a TypeError, not a RangeError
446442
'getReader() should call ToString() on mode',
447-
// In the actual WPT, the result is expected to explicitly contain `value: undefined`,
448-
// but we omit that.
449-
'Reading twice on a stream that gets closed',
450-
// TODO: Investigate why this test is failing
451-
'Reading twice on a closed stream',
452443
],
453444
},
454445
'readable-streams/floating-point-total-queue-size.any.js': {
@@ -478,20 +469,7 @@ export default {
478469
'ReadableStream.from: cancel() rejects when return() throws synchronously',
479470
'ReadableStream.from accepts an array of promises',
480471
'ReadableStream.from accepts a sync iterable of promises',
481-
'ReadableStream.from accepts an empty iterable',
482-
'ReadableStream.from accepts an array of values',
483-
'ReadableStream.from accepts an array iterator',
484-
'ReadableStream.from accepts a Set',
485-
'ReadableStream.from accepts a Set iterator',
486-
'ReadableStream.from accepts a sync generator',
487-
'ReadableStream.from accepts a sync iterable of values',
488-
'ReadableStream.from accepts an async iterable',
489-
'ReadableStream.from accepts an async generator',
490-
'ReadableStream.from accepts a ReadableStream',
491-
'ReadableStream.from accepts a ReadableStream async iterator',
492472
'ReadableStream.from ignores a null @@asyncIterator',
493-
'ReadableStream.from: return() is not called when iterator completes normally',
494-
'ReadableStream.from(array), push() to array while reading',
495473
'ReadableStream.from: cancelling the returned stream calls and awaits return()',
496474
],
497475
},
@@ -593,14 +571,11 @@ export default {
593571
'readable-streams/tee.any.js': {
594572
comment: 'To be investigated',
595573
expectedFailures: [
596-
'ReadableStream teeing: should be able to read one branch to the end without affecting the other',
597574
'ReadableStream teeing: errors in the source should propagate to both branches',
598575
'ReadableStreamTee should only pull enough to fill the emptiest queue',
599576
'ReadableStreamTee stops pulling when original stream errors while branch 1 is reading',
600577
'ReadableStreamTee stops pulling when original stream errors while branch 2 is reading',
601578
'ReadableStreamTee stops pulling when original stream errors while both branches are reading',
602-
'ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream',
603-
'ReadableStream teeing: enqueue() and close() while both branches are pulling',
604579
'ReadableStream teeing: canceling both branches should aggregate the cancel reasons into an array',
605580
'ReadableStream teeing: canceling both branches in reverse order should aggregate the cancel reasons into an array',
606581
'ReadableStream teeing: failing to cancel the original stream should cause cancel() to reject on branches',
@@ -616,25 +591,12 @@ export default {
616591
],
617592
expectedFailures: [
618593
'ReadableStream (empty): calling getReader with invalid arguments should throw appropriate errors',
619-
'ReadableStream (empty) reader: canceling via the reader should cause the reader to act closed',
620-
'ReadableStream reader (closed before getting reader): read() should fulfill with { value: undefined, done: true }',
621-
'ReadableStream reader (closed before getting reader): read() multiple times should fulfill with { value: undefined, done: true }',
622-
'ReadableStream reader (closed before getting reader): read() should work when used within another read() fulfill callback',
623594
'ReadableStream reader (closed before getting reader): releasing the lock should cause closed to reject and change identity',
624-
'ReadableStream reader (closed after getting reader): read() should fulfill with { value: undefined, done: true }',
625-
'ReadableStream reader (closed after getting reader): read() multiple times should fulfill with { value: undefined, done: true }',
626-
'ReadableStream reader (closed after getting reader): read() should work when used within another read() fulfill callback',
627595
'ReadableStream reader (closed after getting reader): releasing the lock should cause closed to reject and change identity',
628-
'ReadableStream reader (closed via cancel after getting reader): read() should fulfill with { value: undefined, done: true }',
629-
'ReadableStream reader (closed via cancel after getting reader): read() multiple times should fulfill with { value: undefined, done: true }',
630-
'ReadableStream reader (closed via cancel after getting reader): read() should work when used within another read() fulfill callback',
631596
'ReadableStream reader (closed via cancel after getting reader): releasing the lock should cause closed to reject and change identity',
632597
'ReadableStream (errored via returning a rejected promise in start) reader: releasing the lock should cause closed to reject and change identity',
633598
'ReadableStream reader (errored before getting reader): releasing the lock should cause closed to reject and change identity',
634599
'ReadableStream reader (errored after getting reader): releasing the lock should cause closed to reject and change identity',
635-
'ReadableStream (two chunks enqueued, still open) reader: cancel() after a read() should still give that single read result',
636-
'ReadableStream (two chunks enqueued, then closed) reader: third read(), without waiting, should give { value: undefined, done: true } (sequential)',
637-
'ReadableStream (two chunks enqueued, then closed) reader: third read(), without waiting, should give { value: undefined, done: true } (nested)',
638600
],
639601
},
640602

0 commit comments

Comments
 (0)