Skip to content

Commit 1490d2b

Browse files
committed
Improve instantiation speed by not using a megamorphic dispatch in the instance constructor
1 parent b43d39e commit 1490d2b

File tree

4 files changed

+80
-72
lines changed

4 files changed

+80
-72
lines changed

src/class-model.ts

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import type { IModelType as MSTIModelType, ModelActions } from "mobx-state-tree"
33
import { types as mstTypes } from "mobx-state-tree";
44
import "reflect-metadata";
55
import { RegistrationError } from "./errors";
6-
import { $fastInstantiator, buildFastInstantiator } from "./fast-instantiator";
6+
import { buildFastInstantiator } from "./fast-instantiator";
77
import { defaultThrowAction, mstPropsFromQuickProps, propsFromModelPropsDeclaration } from "./model";
88
import {
99
$env,
1010
$identifier,
11+
$memoizedKeys,
12+
$memos,
1113
$originalDescriptor,
1214
$parent,
1315
$quickType,
@@ -23,8 +25,6 @@ import type {
2325
IAnyType,
2426
IClassModelType,
2527
IStateTreeNode,
26-
InputTypesForModelProps,
27-
InputsForModel,
2828
InstantiateContext,
2929
ModelPropertiesDeclaration,
3030
ModelViews,
@@ -59,8 +59,6 @@ const metadataPrefix = "mqt:properties";
5959
const viewKeyPrefix = `${metadataPrefix}:view`;
6060
const actionKeyPrefix = `${metadataPrefix}:action`;
6161
const volatileKeyPrefix = `${metadataPrefix}:volatile`;
62-
const $memos = Symbol.for("mqt:class-model-memos");
63-
const $memoizedKeys = Symbol.for("mqt:class-model-memoized-keys");
6462

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

83+
/** Properties set in the fast instantiator compiled constructor, included here for type information */
84+
[$readOnly]!: true;
85+
[$type]!: IClassModelType<TypesForModelPropsDeclaration<any>>;
8586
/** @hidden */
8687
readonly [$env]?: any;
8788
/** @hidden */
8889
readonly [$parent]?: IStateTreeNode | null;
8990
/** @hidden */
90-
[$memos] = null;
91+
[$identifier]?: any;
9192
/** @hidden */
92-
[$memoizedKeys] = null;
93+
[$memos]!: Record<string, any> | null;
9394
/** @hidden */
94-
[$identifier]?: any;
95-
96-
constructor(
97-
snapshot: InputsForModel<InputTypesForModelProps<TypesForModelPropsDeclaration<any>>> | undefined,
98-
context: InstantiateContext,
99-
parent: IStateTreeNode | null,
100-
/** @hidden */ hackyPreventInitialization = false
101-
) {
102-
if (hackyPreventInitialization) {
103-
return;
104-
}
105-
106-
this[$env] = context.env;
107-
this[$parent] = parent;
108-
109-
(this.constructor as IClassModelType<any>)[$fastInstantiator](this as any, snapshot, context);
110-
}
111-
112-
get [$readOnly]() {
113-
return true;
114-
}
115-
116-
get [$type]() {
117-
return this.constructor as IClassModelType<TypesForModelPropsDeclaration<any>>;
118-
}
95+
[$memoizedKeys]!: Record<string, boolean> | null;
11996
}
12097

12198
/**
@@ -162,7 +139,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
162139
tags?: RegistrationTags<Instance>,
163140
name?: string
164141
) {
165-
const klass = object as any as IClassModelType<any>;
142+
let klass = object as any as IClassModelType<any>;
166143
const mstActions: ModelActions = {};
167144
const mstViews: ModelViews = {};
168145
const mstVolatiles: Record<string, VolatileMetadata> = {};
@@ -263,6 +240,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
263240
}
264241
case "volatile": {
265242
mstVolatiles[metadata.property] = metadata;
243+
break;
266244
}
267245
}
268246
}
@@ -310,7 +288,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
310288
(klass as any).mstType = (klass as any).mstType.volatile((self: any) => initializeVolatiles({}, self, mstVolatiles));
311289
}
312290

313-
klass[$fastInstantiator] = buildFastInstantiator(klass);
291+
klass = buildFastInstantiator(klass);
314292
(klass as any)[$registered] = true;
315293

316294
return klass as any;

src/fast-instantiator.ts

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,13 @@ import { MapType, QuickMap } from "./map";
44
import { OptionalType } from "./optional";
55
import { ReferenceType, SafeReferenceType } from "./reference";
66
import { DateType, IntegerType, LiteralType, SimpleType } from "./simple";
7-
import { $identifier } from "./symbols";
8-
import type { IAnyClassModelType, IAnyType, IClassModelType, Instance, InstantiateContext, SnapshotIn, ValidOptionalValue } from "./types";
9-
10-
export const $fastInstantiator = Symbol.for("mqt:class-model-instantiator");
11-
12-
export type CompiledInstantiator<T extends IAnyClassModelType = IAnyClassModelType> = (
13-
instance: Instance<T>,
14-
snapshot: SnapshotIn<T>,
15-
context: InstantiateContext
16-
) => void;
7+
import { $env, $identifier, $memoizedKeys, $memos, $parent, $readOnly, $type } from "./symbols";
8+
import type { IAnyType, IClassModelType, ValidOptionalValue } from "./types";
179

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

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

3931
constructor(readonly model: T) {}
4032

41-
build(): CompiledInstantiator<T> {
33+
build(): T {
4234
const segments: string[] = [];
4335

4436
for (const [key, type] of Object.entries(this.model.properties)) {
4537
if (isDirectlyAssignableType(type)) {
4638
segments.push(`
4739
// simple type for ${key}
48-
instance["${key}"] = ${this.expressionForDirectlyAssignableType(key, type)};
49-
`);
40+
this["${key}"] = ${this.expressionForDirectlyAssignableType(key, type)};
41+
`);
5042
} else if (type instanceof OptionalType) {
5143
segments.push(this.assignmentExpressionForOptionalType(key, type));
5244
} else if (type instanceof ReferenceType || type instanceof SafeReferenceType) {
@@ -58,43 +50,71 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
5850
} else {
5951
segments.push(`
6052
// instantiate fallback for ${key} of type ${type.name}
61-
instance["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
53+
this["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
6254
snapshot?.["${key}"],
6355
context,
64-
instance
56+
this
6557
);
6658
`);
6759
}
6860
}
6961

7062
for (const [key, _metadata] of Object.entries(this.model.volatiles)) {
7163
segments.push(`
72-
instance["${key}"] = ${this.alias(`model.volatiles["${key}"]`)}.initializer(instance);
64+
this["${key}"] = ${this.alias(`model.volatiles["${key}"]`)}.initializer(this);
7365
`);
7466
}
7567

7668
const identifierProp = this.model.mstType.identifierAttribute;
7769
if (identifierProp) {
7870
segments.push(`
79-
const id = instance["${identifierProp}"];
80-
instance[$identifier] = id;
81-
context.referenceCache.set(id, instance);
71+
const id = this["${identifierProp}"];
72+
this[$identifier] = id;
73+
context.referenceCache.set(id, this);
8274
`);
8375
}
8476

85-
const innerFunc = `
86-
return function Instantiate${this.model.name}(instance, snapshot, context) {
87-
${segments.join("\n")}
77+
const defineClassStatement = `
78+
return class ${this.model.name} extends model {
79+
[$memos] = null;
80+
[$memoizedKeys] = null;
81+
82+
constructor(
83+
snapshot,
84+
context,
85+
parent,
86+
/** @hidden */ hackyPreventInitialization = false
87+
) {
88+
super(null, null, null, true);
89+
90+
if (hackyPreventInitialization) {
91+
return;
92+
}
93+
94+
this[$env] = context.env;
95+
this[$parent] = parent;
96+
97+
${segments.join("\n")}
98+
}
99+
100+
get [$readOnly]() {
101+
return true;
102+
}
103+
104+
get [$type]() {
105+
return this.constructor;
106+
}
88107
}
89108
`;
90109

91110
const aliasFuncBody = `
92-
const { QuickMap, QuickArray, $identifier } = imports;
111+
const { QuickMap, QuickArray, $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type } = imports;
112+
93113
${Array.from(this.aliases.entries())
94114
.map(([expression, alias]) => `const ${alias} = ${expression};`)
95115
.join("\n")}
96116
97-
${innerFunc}
117+
${defineClassStatement}
98118
`;
99119

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

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

111131
private expressionForDirectlyAssignableType(key: string, type: DirectlyAssignableType) {
@@ -132,7 +152,7 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
132152
if (${varName}) {
133153
const referencedInstance = context.referenceCache.get(${varName});
134154
if (referencedInstance) {
135-
instance["${key}"] = referencedInstance;
155+
this["${key}"] = referencedInstance;
136156
return;
137157
}
138158
}
@@ -162,14 +182,14 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
162182
let createExpression;
163183
if (isDirectlyAssignableType(type.type)) {
164184
createExpression = `
165-
instance["${key}"] = ${varName}
185+
this["${key}"] = ${varName}
166186
`;
167187
} else {
168188
createExpression = `
169-
instance["${key}"] = ${this.alias(`model.properties["${key}"].type`)}.instantiate(
189+
this["${key}"] = ${this.alias(`model.properties["${key}"].type`)}.instantiate(
170190
${varName},
171191
context,
172-
instance
192+
this
173193
);
174194
`;
175195
}
@@ -188,20 +208,20 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
188208
if (!isDirectlyAssignableType(type.childrenType) || type.childrenType instanceof DateType) {
189209
return `
190210
// instantiate fallback for ${key} of type ${type.name}
191-
instance["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
211+
this["${key}"] = ${this.alias(`model.properties["${key}"]`)}.instantiate(
192212
snapshot?.["${key}"],
193213
context,
194-
instance
214+
this
195215
);
196216
`;
197217
}
198218

199219
// Directly assignable types are primitives so we don't need to worry about setting parent/env/etc. Hence, we just
200220
// pass the snapshot straight through to the constructor.
201221
return `
202-
instance["${key}"] = new QuickArray(
222+
this["${key}"] = new QuickArray(
203223
${this.alias(`model.properties["${key}"]`)},
204-
instance,
224+
this,
205225
context.env,
206226
...(snapshot?.["${key}"] ?? [])
207227
);
@@ -212,8 +232,8 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
212232
const mapVarName = `map${key}`;
213233
const snapshotVarName = `snapshotValue${key}`;
214234
return `
215-
const ${mapVarName} = new QuickMap(${this.alias(`model.properties["${key}"]`)}, instance, context.env);
216-
instance["${key}"] = ${mapVarName};
235+
const ${mapVarName} = new QuickMap(${this.alias(`model.properties["${key}"]`)}, this, context.env);
236+
this["${key}"] = ${mapVarName};
217237
const ${snapshotVarName} = snapshot?.["${key}"];
218238
if (${snapshotVarName}) {
219239
for (const key in ${snapshotVarName}) {

src/symbols.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ export const $registered = Symbol.for("MQT_registered");
3737
* @hidden
3838
**/
3939
export const $volatileDefiner = Symbol.for("MQT_volatileDefiner");
40+
41+
/**
42+
* The values of memoized properties on an MQT instance
43+
* @hidden
44+
**/
45+
export const $memos = Symbol.for("mqt:class-model-memos");
46+
47+
/**
48+
* The list of properties which have been memoized
49+
* @hidden
50+
**/
51+
export const $memoizedKeys = Symbol.for("mqt:class-model-memoized-keys");

src/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { IInterceptor, IMapDidChange, IMapWillChange, Lambda } from "mobx";
22
import type { IAnyType as MSTAnyType } from "mobx-state-tree";
33
import type { VolatileMetadata } from "./class-model";
44
import type { $quickType, $registered, $type } from "./symbols";
5-
import { $fastInstantiator, CompiledInstantiator } from "./fast-instantiator";
65

76
export type { $quickType, $registered, $type } from "./symbols";
87
export type { IJsonPatch, IMiddlewareEvent, IPatchRecorder, ReferenceOptions, UnionOptions } from "mobx-state-tree";
@@ -129,7 +128,6 @@ export interface IClassModelType<
129128
> {
130129
readonly [$quickType]: undefined;
131130
readonly [$registered]: true;
132-
[$fastInstantiator]: CompiledInstantiator;
133131

134132
readonly InputType: InputType;
135133
readonly OutputType: OutputType;

0 commit comments

Comments
 (0)