Skip to content

Commit

Permalink
Generate a monomorphic version of .createReadOnly, .instantiate and .…
Browse files Browse the repository at this point in the history
…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!
  • Loading branch information
airhorns committed Nov 13, 2023
1 parent 9e65af5 commit 1c75b10
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 7 deletions.
5 changes: 5 additions & 0 deletions src/class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,12 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
(klass as any).mstType = (klass as any).mstType.volatile((self: any) => initializeVolatiles({}, self, mstVolatiles));
}

// define the class constructor and the following hot path functions dynamically
// .createReadOnly
// .is
// .instantiate
klass = buildFastInstantiator(klass);

(klass as any)[$registered] = true;

return klass as any;
Expand Down
49 changes: 42 additions & 7 deletions src/fast-instantiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,40 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
`);
}

let className = this.model.name;
if (!className || className.trim().length == 0) {
className = "AnonymousModel";
}

const defineClassStatement = `
return class ${this.model.name} extends model {
return class ${className} extends model {
[$memos] = null;
[$memoizedKeys] = null;
static createReadOnly = (snapshot, env) => {
const context = {
referenceCache: new Map(),
referencesToResolve: [],
env,
};
const instance = new ${className}(snapshot, context, null);
for (const resolver of context.referencesToResolve) {
resolver();
}
return instance;
};
static instantiate(snapshot, context, parent) {
return new ${className}(snapshot, context, parent);
};
static is(value) {
return (value instanceof ${className}) || ${className}.mstType.is(value);
};
constructor(
snapshot,
context,
Expand Down Expand Up @@ -119,13 +148,19 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an

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

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

// evaluate aliases and get created inner function
return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T;
// evaluate aliases and get created inner function
return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T;
} catch (e) {
console.warn("failed to build fast instantiator for", this.model.name);
console.warn("dynamic source code:", aliasFuncBody);
throw e;
}
}

private expressionForDirectlyAssignableType(key: string, type: DirectlyAssignableType) {
Expand Down

0 comments on commit 1c75b10

Please sign in to comment.