From 1490d2ba89bc3b04c370c944cc4bb567a1a2d706 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 7 Nov 2023 17:41:09 -0500 Subject: [PATCH] Improve instantiation speed by not using a megamorphic dispatch in the instance constructor --- src/class-model.ts | 46 ++++++-------------- src/fast-instantiator.ts | 92 ++++++++++++++++++++++++---------------- src/symbols.ts | 12 ++++++ src/types.ts | 2 - 4 files changed, 80 insertions(+), 72 deletions(-) diff --git a/src/class-model.ts b/src/class-model.ts index c8e5cfa..b04504f 100644 --- a/src/class-model.ts +++ b/src/class-model.ts @@ -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, @@ -23,8 +25,6 @@ import type { IAnyType, IClassModelType, IStateTreeNode, - InputTypesForModelProps, - InputsForModel, InstantiateContext, ModelPropertiesDeclaration, ModelViews, @@ -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 @@ -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>; /** @hidden */ readonly [$env]?: any; /** @hidden */ readonly [$parent]?: IStateTreeNode | null; /** @hidden */ - [$memos] = null; + [$identifier]?: any; /** @hidden */ - [$memoizedKeys] = null; + [$memos]!: Record | null; /** @hidden */ - [$identifier]?: any; - - constructor( - snapshot: InputsForModel>> | undefined, - context: InstantiateContext, - parent: IStateTreeNode | null, - /** @hidden */ hackyPreventInitialization = false - ) { - if (hackyPreventInitialization) { - return; - } - - this[$env] = context.env; - this[$parent] = parent; - - (this.constructor as IClassModelType)[$fastInstantiator](this as any, snapshot, context); - } - - get [$readOnly]() { - return true; - } - - get [$type]() { - return this.constructor as IClassModelType>; - } + [$memoizedKeys]!: Record | null; } /** @@ -162,7 +139,7 @@ export function register, name?: string ) { - const klass = object as any as IClassModelType; + let klass = object as any as IClassModelType; const mstActions: ModelActions = {}; const mstViews: ModelViews = {}; const mstVolatiles: Record = {}; @@ -263,6 +240,7 @@ export function register initializeVolatiles({}, self, mstVolatiles)); } - klass[$fastInstantiator] = buildFastInstantiator(klass); + klass = buildFastInstantiator(klass); (klass as any)[$registered] = true; return klass as any; diff --git a/src/fast-instantiator.ts b/src/fast-instantiator.ts index 8a385f5..e2e487f 100644 --- a/src/fast-instantiator.ts +++ b/src/fast-instantiator.ts @@ -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 = ( - instance: Instance, - snapshot: SnapshotIn, - 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 = , any, any>>(model: T): CompiledInstantiator => { +export const buildFastInstantiator = , any, any>>(model: T): T => { return new InstantiatorBuilder(model).build(); }; @@ -38,15 +30,15 @@ class InstantiatorBuilder, an constructor(readonly model: T) {} - build(): CompiledInstantiator { + 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) { @@ -58,10 +50,10 @@ class InstantiatorBuilder, 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 ); `); } @@ -69,32 +61,60 @@ class InstantiatorBuilder, an 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"); @@ -105,7 +125,7 @@ class InstantiatorBuilder, an const aliasFunc = new Function("model", "imports", aliasFuncBody); // evaluate aliases and get created inner function - return aliasFunc(this.model, { $identifier, QuickMap, QuickArray }) as CompiledInstantiator; + return aliasFunc(this.model, { $identifier, $env, $parent, $memos, $memoizedKeys, $readOnly, $type, QuickMap, QuickArray }) as T; } private expressionForDirectlyAssignableType(key: string, type: DirectlyAssignableType) { @@ -132,7 +152,7 @@ class InstantiatorBuilder, an if (${varName}) { const referencedInstance = context.referenceCache.get(${varName}); if (referencedInstance) { - instance["${key}"] = referencedInstance; + this["${key}"] = referencedInstance; return; } } @@ -162,14 +182,14 @@ class InstantiatorBuilder, 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 ); `; } @@ -188,10 +208,10 @@ class InstantiatorBuilder, 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 ); `; } @@ -199,9 +219,9 @@ class InstantiatorBuilder, an // 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}"] ?? []) ); @@ -212,8 +232,8 @@ class InstantiatorBuilder, 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}) { diff --git a/src/symbols.ts b/src/symbols.ts index 4e45e1f..852cebc 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -37,3 +37,15 @@ export const $registered = Symbol.for("MQT_registered"); * @hidden **/ export const $volatileDefiner = Symbol.for("MQT_volatileDefiner"); + +/** + * The values of memoized properties on an MQT instance + * @hidden + **/ +export const $memos = Symbol.for("mqt:class-model-memos"); + +/** + * The list of properties which have been memoized + * @hidden + **/ +export const $memoizedKeys = Symbol.for("mqt:class-model-memoized-keys"); diff --git a/src/types.ts b/src/types.ts index 7cdd45e..bade302 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,6 @@ import type { IInterceptor, IMapDidChange, IMapWillChange, Lambda } from "mobx"; import type { IAnyType as MSTAnyType } from "mobx-state-tree"; import type { VolatileMetadata } from "./class-model"; import type { $quickType, $registered, $type } from "./symbols"; -import { $fastInstantiator, CompiledInstantiator } from "./fast-instantiator"; export type { $quickType, $registered, $type } from "./symbols"; export type { IJsonPatch, IMiddlewareEvent, IPatchRecorder, ReferenceOptions, UnionOptions } from "mobx-state-tree"; @@ -129,7 +128,6 @@ export interface IClassModelType< > { readonly [$quickType]: undefined; readonly [$registered]: true; - [$fastInstantiator]: CompiledInstantiator; readonly InputType: InputType; readonly OutputType: OutputType;