Skip to content

Commit 87b5ceb

Browse files
committed
Generate a monomorphic version of .createReadOnly, .instantiate and .is for each class model
The VScode Deopt explorer showed that `.createReadOnly` inside the class model `register` function was getting deoptimized because it closes over too-wide a variety of `klass` variable references. We're closing over it so it is correct, but to v8, the callsite is still the same I guess, so it has to deoptimize to pull the right value for the closure! Same for .is and .instantiate -- these dynamically produced functions make a huge difference if they are re-eval'd for each class instead of defined dynamically. Removing this deoptimization almost 2x'd to the large root benchmark for me!
1 parent 9e65af5 commit 87b5ceb

File tree

2 files changed

+47
-25
lines changed

2 files changed

+47
-25
lines changed

src/class-model.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -252,25 +252,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
252252
klass.volatiles = mstVolatiles;
253253

254254
// conform to the API that the other MQT types expect for creating instances
255-
klass.instantiate = (snapshot, context, parent) => new klass(snapshot, context, parent);
256-
(klass as any).is = (value: any) => value instanceof klass || klass.mstType.is(value);
257255
klass.create = (snapshot, env) => klass.mstType.create(snapshot, env);
258-
klass.createReadOnly = (snapshot, env) => {
259-
const context: InstantiateContext = {
260-
referenceCache: new Map(),
261-
referencesToResolve: [],
262-
env,
263-
};
264-
265-
const instance = new klass(snapshot, context, null) as any;
266-
267-
for (const resolver of context.referencesToResolve) {
268-
resolver();
269-
}
270-
271-
return instance;
272-
};
273-
274256
klass.schemaHash = memoize(async () => {
275257
const props = Object.entries(klass.properties as Record<string, IAnyType>).sort(([key1], [key2]) => key1.localeCompare(key2));
276258
const propHashes = await Promise.all(props.map(async ([key, prop]) => `${key}:${await prop.schemaHash()}`));
@@ -288,7 +270,12 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
288270
(klass as any).mstType = (klass as any).mstType.volatile((self: any) => initializeVolatiles({}, self, mstVolatiles));
289271
}
290272

273+
// define the class constructor and the following hot path functions dynamically
274+
// .createReadOnly
275+
// .is
276+
// .instantiate
291277
klass = buildFastInstantiator(klass);
278+
292279
(klass as any)[$registered] = true;
293280

294281
return klass as any;

src/fast-instantiator.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,40 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
7474
`);
7575
}
7676

77+
let className = this.model.name;
78+
if (!className || className.trim().length == 0) {
79+
className = "AnonymousModel";
80+
}
81+
7782
const defineClassStatement = `
78-
return class ${this.model.name} extends model {
83+
return class ${className} extends model {
7984
[$memos] = null;
8085
[$memoizedKeys] = null;
8186
87+
static createReadOnly = (snapshot, env) => {
88+
const context = {
89+
referenceCache: new Map(),
90+
referencesToResolve: [],
91+
env,
92+
};
93+
94+
const instance = new ${className}(snapshot, context, null);
95+
96+
for (const resolver of context.referencesToResolve) {
97+
resolver();
98+
}
99+
100+
return instance;
101+
};
102+
103+
static instantiate(snapshot, context, parent) {
104+
return new ${className}(snapshot, context, parent);
105+
};
106+
107+
static is(value) {
108+
return (value instanceof ${className}) || ${className}.mstType.is(value);
109+
};
110+
82111
constructor(
83112
snapshot,
84113
context,
@@ -119,13 +148,19 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
119148

120149
// console.log(`function for ${this.model.name}`, "\n\n\n", aliasFuncBody, "\n\n\n");
121150

122-
// build a function that closes over a bunch of aliased expressions
123-
// evaluate the inner function source code in this closure to return the function
124-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
125-
const aliasFunc = new Function("model", "imports", aliasFuncBody);
151+
try {
152+
// build a function that closes over a bunch of aliased expressions
153+
// evaluate the inner function source code in this closure to return the function
154+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
155+
const aliasFunc = new Function("model", "imports", aliasFuncBody);
126156

127-
// evaluate aliases and get created inner function
128-
return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T;
157+
// evaluate aliases and get created inner function
158+
return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T;
159+
} catch (e) {
160+
console.warn("failed to build fast instantiator for", this.model.name);
161+
console.warn("dynamic source code:", aliasFuncBody);
162+
throw e;
163+
}
129164
}
130165

131166
private expressionForDirectlyAssignableType(key: string, type: DirectlyAssignableType) {

0 commit comments

Comments
 (0)