Skip to content

Commit

Permalink
Improve instantiation speed by not using a megamorphic dispatch in th…
Browse files Browse the repository at this point in the history
…e instance constructor
  • Loading branch information
airhorns committed Nov 7, 2023
1 parent c89df41 commit ed8b219
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 73 deletions.
3 changes: 2 additions & 1 deletion Benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ node --prof-process isolate-*.log
You can postprocess the generated `isolate-0x<something>.log` file into the data `flamegraph` expects, and then feed it to `flamegraph` to see a visual breakdown of performance. You can do that in one command like so:

```shell
node --prof-process --preprocess -j isolate-*.log | pnpm flamebearer
node --prof-process --preprocess -j isolate-*.log > test.perf
pnpm speedscope test.perf
```

#### CPU profiling with VSCode
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"lodash": "^4.17.21",
"microtime": "^3.1.1",
"prettier": "^2.8.8",
"speedscope": "^1.16.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
Expand Down
35 changes: 35 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 12 additions & 34 deletions src/class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type { IModelType as MSTIModelType, ModelActions } from "mobx-state-tree"
import { types as mstTypes } from "mobx-state-tree";
import "reflect-metadata";
import { RegistrationError } from "./errors";
import { $fastInstantiator, buildFastInstantiator } from "./fast-instantiator";
import { buildFastInstantiator } from "./fast-instantiator";
import { defaultThrowAction, mstPropsFromQuickProps, propsFromModelPropsDeclaration } from "./model";
import {
$env,
$identifier,
$memoizedKeys,
$memos,
$originalDescriptor,
$parent,
$quickType,
Expand All @@ -23,8 +25,6 @@ import type {
IAnyType,
IClassModelType,
IStateTreeNode,
InputTypesForModelProps,
InputsForModel,
InstantiateContext,
ModelPropertiesDeclaration,
ModelViews,
Expand Down Expand Up @@ -59,8 +59,6 @@ const metadataPrefix = "mqt:properties";
const viewKeyPrefix = `${metadataPrefix}:view`;
const actionKeyPrefix = `${metadataPrefix}:action`;
const volatileKeyPrefix = `${metadataPrefix}:volatile`;
const $memos = Symbol.for("mqt:class-model-memos");
const $memoizedKeys = Symbol.for("mqt:class-model-memoized-keys");

/**
* A map of property keys to indicators for how that property should behave on the registered class
Expand All @@ -82,40 +80,19 @@ class BaseClassModel {
return extend(this, props);
}

/** Properties set in the fast instantiator compiled constructor, included here for type information */
[$readOnly]!: true;
[$type]!: IClassModelType<TypesForModelPropsDeclaration<any>>;
/** @hidden */
readonly [$env]?: any;
/** @hidden */
readonly [$parent]?: IStateTreeNode | null;
/** @hidden */
[$memos] = null;
[$identifier]?: any;
/** @hidden */
[$memoizedKeys] = null;
[$memos]!: Record<string, any> | null;
/** @hidden */
[$identifier]?: any;

constructor(
snapshot: InputsForModel<InputTypesForModelProps<TypesForModelPropsDeclaration<any>>> | undefined,
context: InstantiateContext,
parent: IStateTreeNode | null,
/** @hidden */ hackyPreventInitialization = false
) {
if (hackyPreventInitialization) {
return;
}

this[$env] = context.env;
this[$parent] = parent;

(this.constructor as IClassModelType<any>)[$fastInstantiator](this as any, snapshot, context);
}

get [$readOnly]() {
return true;
}

get [$type]() {
return this.constructor as IClassModelType<TypesForModelPropsDeclaration<any>>;
}
[$memoizedKeys]!: Record<string, boolean> | null;
}

/**
Expand Down Expand Up @@ -162,7 +139,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
tags?: RegistrationTags<Instance>,
name?: string
) {
const klass = object as any as IClassModelType<any>;
let klass = object as any as IClassModelType<any>;
const mstActions: ModelActions = {};
const mstViews: ModelViews = {};
const mstVolatiles: Record<string, VolatileMetadata> = {};
Expand Down Expand Up @@ -263,6 +240,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
}
case "volatile": {
mstVolatiles[metadata.property] = metadata;
break;
}
}
}
Expand Down Expand Up @@ -310,7 +288,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
(klass as any).mstType = (klass as any).mstType.volatile((self: any) => initializeVolatiles({}, self, mstVolatiles));
}

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

return klass as any;
Expand Down
92 changes: 56 additions & 36 deletions src/fast-instantiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,13 @@ import { MapType, QuickMap } from "./map";
import { OptionalType } from "./optional";
import { ReferenceType, SafeReferenceType } from "./reference";
import { DateType, IntegerType, LiteralType, SimpleType } from "./simple";
import { $identifier } from "./symbols";
import type { IAnyClassModelType, IAnyType, IClassModelType, Instance, InstantiateContext, SnapshotIn, ValidOptionalValue } from "./types";

export const $fastInstantiator = Symbol.for("mqt:class-model-instantiator");

export type CompiledInstantiator<T extends IAnyClassModelType = IAnyClassModelType> = (
instance: Instance<T>,
snapshot: SnapshotIn<T>,
context: InstantiateContext
) => void;
import { $env, $identifier, $memoizedKeys, $memos, $parent, $readOnly, $type } from "./symbols";
import type { IAnyType, IClassModelType, ValidOptionalValue } from "./types";

/**
* Compiles a fast function for taking snapshots and turning them into instances of a class model.
**/
export const buildFastInstantiator = <T extends IClassModelType<Record<string, IAnyType>, any, any>>(model: T): CompiledInstantiator<T> => {
export const buildFastInstantiator = <T extends IClassModelType<Record<string, IAnyType>, any, any>>(model: T): T => {
return new InstantiatorBuilder(model).build();
};

Expand All @@ -38,15 +30,15 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an

constructor(readonly model: T) {}

build(): CompiledInstantiator<T> {
build(): T {
const segments: string[] = [];

for (const [key, type] of Object.entries(this.model.properties)) {
if (isDirectlyAssignableType(type)) {
segments.push(`
// simple type for ${key}
instance["${key}"] = ${this.expressionForDirectlyAssignableType(key, type)};
`);
this["${key}"] = ${this.expressionForDirectlyAssignableType(key, type)};
`);
} else if (type instanceof OptionalType) {
segments.push(this.assignmentExpressionForOptionalType(key, type));
} else if (type instanceof ReferenceType || type instanceof SafeReferenceType) {
Expand All @@ -58,43 +50,71 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
} else {
segments.push(`
// instantiate fallback for ${key} of type ${type.name}
instance["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
this["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
snapshot?.["${key}"],
context,
instance
this
);
`);
}
}

for (const [key, _metadata] of Object.entries(this.model.volatiles)) {
segments.push(`
instance["${key}"] = ${this.alias(`model.volatiles["${key}"]`)}.initializer(instance);
this["${key}"] = ${this.alias(`model.volatiles["${key}"]`)}.initializer(this);
`);
}

const identifierProp = this.model.mstType.identifierAttribute;
if (identifierProp) {
segments.push(`
const id = instance["${identifierProp}"];
instance[$identifier] = id;
context.referenceCache.set(id, instance);
const id = this["${identifierProp}"];
this[$identifier] = id;
context.referenceCache.set(id, this);
`);
}

const innerFunc = `
return function Instantiate${this.model.name}(instance, snapshot, context) {
${segments.join("\n")}
const defineClassStatement = `
return class ${this.model.name} extends model {
[$memos] = null;
[$memoizedKeys] = null;
constructor(
snapshot,
context,
parent,
/** @hidden */ hackyPreventInitialization = false
) {
super(null, null, null, true);
if (hackyPreventInitialization) {
return;
}
this[$env] = context.env;
this[$parent] = parent;
${segments.join("\n")}
}
get [$readOnly]() {
return true;
}
get [$type]() {
return this.constructor;
}
}
`;

const aliasFuncBody = `
const { QuickMap, QuickArray, $identifier } = imports;
const { QuickMap, QuickArray, $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type } = imports;
${Array.from(this.aliases.entries())
.map(([expression, alias]) => `const ${alias} = ${expression};`)
.join("\n")}
${innerFunc}
${defineClassStatement}
`;

// console.log(`function for ${this.model.name}`, "\n\n\n", aliasFuncBody, "\n\n\n");
Expand All @@ -105,7 +125,7 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
const aliasFunc = new Function("model", "imports", aliasFuncBody);

// evaluate aliases and get created inner function
return aliasFunc(this.model, { $identifier, QuickMap, QuickArray }) as CompiledInstantiator<T>;
return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T;
}

private expressionForDirectlyAssignableType(key: string, type: DirectlyAssignableType) {
Expand All @@ -132,7 +152,7 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
if (${varName}) {
const referencedInstance = context.referenceCache.get(${varName});
if (referencedInstance) {
instance["${key}"] = referencedInstance;
this["${key}"] = referencedInstance;
return;
}
}
Expand Down Expand Up @@ -162,14 +182,14 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
let createExpression;
if (isDirectlyAssignableType(type.type)) {
createExpression = `
instance["${key}"] = ${varName}
this["${key}"] = ${varName}
`;
} else {
createExpression = `
instance["${key}"] = ${this.alias(`model.properties["${key}"].type`)}.instantiate(
this["${key}"] = ${this.alias(`model.properties["${key}"].type`)}.instantiate(
${varName},
context,
instance
this
);
`;
}
Expand All @@ -188,20 +208,20 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
if (!isDirectlyAssignableType(type.childrenType) || type.childrenType instanceof DateType) {
return `
// instantiate fallback for ${key} of type ${type.name}
instance["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
this["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
snapshot?.["${key}"],
context,
instance
this
);
`;
}

// Directly assignable types are primitives so we don't need to worry about setting parent/env/etc. Hence, we just
// pass the snapshot straight through to the constructor.
return `
instance["${key}"] = new QuickArray(
this["${key}"] = new QuickArray(
${this.alias(`model.properties["${key}"]`)},
instance,
this,
context.env,
...(snapshot?.["${key}"] ?? [])
);
Expand All @@ -212,8 +232,8 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
const mapVarName = `map${key}`;
const snapshotVarName = `snapshotValue${key}`;
return `
const ${mapVarName} = new QuickMap(${this.alias(`model.properties["${key}"]`)}, instance, context.env);
instance["${key}"] = ${mapVarName};
const ${mapVarName} = new QuickMap(${this.alias(`model.properties["${key}"]`)}, this, context.env);
this["${key}"] = ${mapVarName};
const ${snapshotVarName} = snapshot?.["${key}"];
if (${snapshotVarName}) {
for (const key in ${snapshotVarName}) {
Expand Down
Loading

0 comments on commit ed8b219

Please sign in to comment.