Skip to content

[build] JSG: key struct field wrappers on the field type, not on the field - #7248

Open
fhanau wants to merge 1 commit into
mainfrom
felix/081226-api-shrink-p0
Open

[build] JSG: key struct field wrappers on the field type, not on the field#7248
fhanau wants to merge 1 commit into
mainfrom
felix/081226-api-shrink-p0

Conversation

@fhanau

@fhanau fhanau commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

FieldWrapper was parameterized on the declaring struct, a pointer to the member, and a pointer to the field's exported name, so every field of a JSG_STRUCT got its own instantiation of the class. Nothing in FieldWrapper actually depends on which field it serves, only on the field's type. This results in many instances of FieldWrapper, who can have large mangled names based on the template arguments.

Pass those three things in at runtime instead. StructWrapper already knows the struct, so it does the member access itself and hands the field to FieldWrapper by reference. The exported name is stored in the wrapper and typeid(Struct) is passed to unwrap() for error messages. FieldWrapper is now instantiated once per field type, shared by every field of that type. In the downstream api translation unit that reduces the number of field wrappers by over 80%.

To make the names available at runtime, JSG_STRUCT adds a jsgFieldNames table and the field list becomes a pack of pointers to members. The table also replaces jsgGetTemplate() and jsgAddToStructNames(): building the DictionaryTemplate is now a loop in StructWrapper over that table rather than a per-field function template.

==============

This massively reduces code size by reducing excessive template instantiations. Compile time (which I think people care about more) also improves. Part of a 3-part series to debloat workerd-api. Code changes made using an LLM, fully reviewed and edited by me.

No optimization, limited debug info, before change:

181932008 bazel-bin/external/+local_repository+workerd/src/workerd/server/_objs/workerd-api/workerd-api.pic.o
499236528 bazel-bin/external/+local_repository+workerd/src/workerd/server/workerd

No optimization, limited debug info, after:

157725688 bazel-bin/external/+local_repository+workerd/src/workerd/server/_objs/workerd-api/workerd-api.pic.o
475323200 bazel-bin/external/+local_repository+workerd/src/workerd/server/workerd

When compiling server directory with -O3, before:

 98693320 bazel-bin/external/+local_repository+workerd/src/workerd/server/_objs/workerd-api/workerd-api.pic.o
422550640 bazel-bin/external/+local_repository+workerd/src/workerd/server/workerd

When compiling server directory with -O3, after:

 90612616 bazel-bin/external/+local_repository+workerd/src/workerd/server/_objs/workerd-api/workerd-api.pic.o
414583536 bazel-bin/external/+local_repository+workerd/src/workerd/server/workerd

…field

FieldWrapper was parameterized on the declaring struct, a pointer to the member,
and a pointer to the field's exported name, so every field of a JSG_STRUCT got
its own instantiation of the class. Nothing in FieldWrapper actually depends on
which field it serves, only on the field's type. This results in many instances
of FieldWrapper, who can have large mangled names based on the template
arguments.

Pass those three things in at runtime instead. StructWrapper already knows the
struct, so it does the member access itself and hands the field to FieldWrapper
by reference. The exported name is stored in the wrapper and typeid(Struct) is
passed to unwrap() for error messages. FieldWrapper is now instantiated once per
field type, shared by every field of that type. In the downstream api
translation unit that reduces the number of field wrappers by over 80%.

To make the names available at runtime, JSG_STRUCT adds a jsgFieldNames table
and the field list becomes a pack of pointers to members. The table also
replaces jsgGetTemplate() and jsgAddToStructNames(): building the
DictionaryTemplate is now a loop in StructWrapper over that table rather than a
per-field function template.
@fhanau
fhanau requested review from dcarney-cf and jasnell September 4, 2026 13:55
@fhanau
fhanau requested review from a team as code owners September 4, 2026 13:55
Comment thread src/workerd/jsg/jsg.h
template <typename TypeWrapper, typename Self> \
using JsgFieldWrappers = \
::workerd::jsg::TypeTuple<JSG_FOR_EACH(JSG_STRUCT_FIELD, , __VA_ARGS__)>; \
static constexpr ::kj::StringPtr jsgFieldNames[] KJ_UNUSED = { \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSG_STRUCT is a public macro and these two ordinary member names were not previously reserved. A struct that declares jsgFieldNames or JsgFields now fails to compile when it is annotated, which is a source-compatibility regression for embedders. Use the existing explicit internal-name convention for the generated members and update the corresponding consumers.

Suggested change
static constexpr ::kj::StringPtr jsgFieldNames[] KJ_UNUSED = { \
static constexpr ::kj::StringPtr _JSG_STRUCT_FIELD_NAMES_DO_NOT_USE_DIRECTLY[] KJ_UNUSED = { \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it's not an actual problem that breaks any actual code, I think the suggestion is probably a good idea. The DO_NOT_USE_DIRECTLY is a worthwhile hint. I wouldn't consider these suggestions to be blocking in any way tho.

Comment thread src/workerd/jsg/jsg.h
return v8::DictionaryTemplate::New( \
isolate, std::span<const std::string_view>(namesPtr.begin(), namesPtr.size())); \
} \
using JsgFields = ::workerd::jsg::StructFields<JSG_FOR_EACH(JSG_STRUCT_FIELD, , __VA_ARGS__)>; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using JsgFields = ::workerd::jsg::StructFields<JSG_FOR_EACH(JSG_STRUCT_FIELD, , __VA_ARGS__)>; \
using _JSG_STRUCT_FIELDS_DO_NOT_USE_DIRECTLY = ::workerd::jsg::StructFields<JSG_FOR_EACH(JSG_STRUCT_FIELD, , __VA_ARGS__)>; \

Comment thread src/workerd/jsg/struct.h
Comment on lines +311 to +312
return lazyFields.emplace(
kj::tuple(FieldWrapper<Self, FieldType<fields>>(isolate, T::jsgFieldNames[indices])...));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return lazyFields.emplace(
kj::tuple(FieldWrapper<Self, FieldType<fields>>(isolate, T::jsgFieldNames[indices])...));
kj::tuple(FieldWrapper<Self, FieldType<fields>>(
isolate, T::_JSG_STRUCT_FIELD_NAMES_DO_NOT_USE_DIRECTLY[indices])...));

Comment thread src/workerd/jsg/struct.h
static v8::Local<v8::DictionaryTemplate> makeTemplate(v8::Isolate* isolate) {
static constexpr bool isUsable[] = {isUsableStructField<FieldType<fields>>...};
kj::Vector<std::string_view> names(kCountOfUsableFields);
for (auto i: kj::indices(T::jsgFieldNames)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (auto i: kj::indices(T::jsgFieldNames)) {
for (auto i: kj::indices(T::_JSG_STRUCT_FIELD_NAMES_DO_NOT_USE_DIRECTLY)) {

template <typename Self, typename T>
class TypeWrapperBase<Self, T, JsgKind::STRUCT>
: public StructWrapper<Self, T, typename T::template JsgFieldWrappers<Self, T>> {
: public StructWrapper<Self, T, typename T::template JsgFields<T>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: public StructWrapper<Self, T, typename T::template JsgFields<T>> {
: public StructWrapper<Self, T, typename T::template _JSG_STRUCT_FIELDS_DO_NOT_USE_DIRECTLY<T>> {

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

I'm Bonk, and I've done a quick review of your PR.

Refactors JSG struct field wrappers to reduce template instantiations and binary size.

  1. P2: Source compatibility regression. JSG_STRUCT now reserves ordinary jsgFieldNames and JsgFields member names, breaking annotated structs that already use either name. Posted five grouped suggestion comments with the required renames and consumer updates.

github run

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