Skip to content

Commit

Permalink
Merge pull request #94 from gadget-inc/faster-get-7
Browse files Browse the repository at this point in the history
Faster cached view version 7: values right on the instance
  • Loading branch information
airhorns authored May 31, 2024
2 parents f183291 + 998d49a commit a32d11b
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 39 deletions.
29 changes: 9 additions & 20 deletions src/class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import type { IModelType as MSTIModelType, ModelActions } from "mobx-state-tree"
import { types as mstTypes } from "mobx-state-tree";
import { RegistrationError } from "./errors";
import { buildFastInstantiator } from "./fast-instantiator";
import { FastGetBuilder } from "./fast-getter";
import { defaultThrowAction, mstPropsFromQuickProps, propsFromModelPropsDeclaration } from "./model";
import {
$context,
$identifier,
$memoizedKeys,
$memos,
$originalDescriptor,
$parent,
$quickType,
Expand Down Expand Up @@ -40,7 +39,7 @@ type ActionMetadata = {
};

/** @internal */
type ViewMetadata = {
export type ViewMetadata = {
type: "view";
property: string;
};
Expand All @@ -53,7 +52,8 @@ export type VolatileMetadata = {
};

type VolatileInitializer<T> = (instance: T) => Record<string, any>;
type PropertyMetadata = ActionMetadata | ViewMetadata | VolatileMetadata;
/** @internal */
export type PropertyMetadata = ActionMetadata | ViewMetadata | VolatileMetadata;

const metadataPrefix = "mqt:properties";
const viewKeyPrefix = `${metadataPrefix}:view`;
Expand Down Expand Up @@ -90,10 +90,6 @@ class BaseClassModel {
readonly [$parent]?: IStateTreeNode | null;
/** @hidden */
[$identifier]?: any;
/** @hidden */
[$memos]!: Record<string, any> | null;
/** @hidden */
[$memoizedKeys]!: Record<string, boolean> | null;
}

/**
Expand Down Expand Up @@ -158,6 +154,8 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
});
}

const fastGetters = new FastGetBuilder(metadatas, klass);

for (const metadata of metadatas) {
switch (metadata.type) {
case "view": {
Expand All @@ -171,16 +169,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
if (descriptor.get) {
Object.defineProperty(klass.prototype, property, {
...descriptor,
get() {
if (!this[$readOnly]) return descriptor.get!.call(this);
if (this[$memoizedKeys]?.[property]) return this[$memos][property];
this[$memoizedKeys] ??= {};
this[$memos] ??= {};
const value: any = descriptor.get!.call(this);
this[$memoizedKeys][property] = true;
this[$memos][property] = value;
return value;
},
get: fastGetters.buildGetter(property, descriptor),
});
}

Expand Down Expand Up @@ -278,7 +267,7 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
// - .createReadOnly
// - .is
// - .instantiate
return buildFastInstantiator(klass) as any;
return buildFastInstantiator(klass, fastGetters) as any;
}

/**
Expand Down Expand Up @@ -449,7 +438,7 @@ function allPrototypeFunctionProperties(obj: any): string[] {
* Get the property descriptor for a property from anywhere in the prototype chain
* Similar to Object.getOwnPropertyDescriptor, but without the own bit
*/
function getPropertyDescriptor(obj: any, property: string) {
export function getPropertyDescriptor(obj: any, property: string) {
while (obj) {
const descriptor = Object.getOwnPropertyDescriptor(obj, property);
if (descriptor) {
Expand Down
68 changes: 68 additions & 0 deletions src/fast-getter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { PropertyMetadata, ViewMetadata } from "./class-model";
import { getPropertyDescriptor } from "./class-model";
import { RegistrationError } from "./errors";
import { $notYetMemoized, $readOnly } from "./symbols";

/** Assemble a function for getting the value of a readonly instance very quickly with static dispatch to properties */
export class FastGetBuilder {
memoizableProperties: string[];

constructor(
metadatas: PropertyMetadata[],
readonly klass: { new (...args: any[]): any },
) {
this.memoizableProperties = metadatas
.filter((metadata): metadata is ViewMetadata => {
if (metadata.type !== "view") return false;
const property = metadata.property;
const descriptor = getPropertyDescriptor(klass.prototype, property);
if (!descriptor) {
throw new RegistrationError(`Property ${property} not found on ${klass} prototype, can't register view for class model`);
}
return descriptor.get !== undefined;
})
.map((metadata) => metadata.property);
}

outerClosureStatements(className: string) {
return this.memoizableProperties
.map(
(property) => `
const ${property}Memo = Symbol.for("mqt/${property}-memo");
${className}.prototype[${property}Memo] = $notYetMemoized;
`,
)
.join("\n");
}

buildGetter(property: string, descriptor: PropertyDescriptor) {
const $memo = Symbol.for(`mqt/${property}-memo`);
const source = `
(
function build({ $readOnly, $memo, $notYetMemoized, getValue }) {
return function get${property}(model, imports) {
if (!this[$readOnly]) return getValue.call(this);
let value = this[$memo];
if (value !== $notYetMemoized) {
return value;
}
value = getValue.call(this);
this[$memo] = value;
return value;
}
}
)
//# sourceURL=mqt-eval/dynamic/${this.klass.name}-${property}-get.js
`;

try {
const builder = eval(source);
return builder({ $readOnly, $memo, $notYetMemoized, getValue: descriptor.get });
} catch (error) {
console.error(`Error building getter for ${this.klass.name}#${property}`);
console.error(`Compiled source:\n${source}`);
throw error;
}
}
}
29 changes: 18 additions & 11 deletions src/fast-instantiator.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { ArrayType, QuickArray } from "./array";
import type { FastGetBuilder } from "./fast-getter";
import { FrozenType } from "./frozen";
import { MapType, QuickMap } from "./map";
import { OptionalType } from "./optional";
import { ReferenceType, SafeReferenceType } from "./reference";
import { DateType, IntegerType, LiteralType, SimpleType } from "./simple";
import { $context, $identifier, $memoizedKeys, $memos, $parent, $readOnly, $type } from "./symbols";
import { $context, $identifier, $notYetMemoized, $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): T => {
return new InstantiatorBuilder(model).build();
export const buildFastInstantiator = <T extends IClassModelType<Record<string, IAnyType>, any, any>>(
model: T,
fastGetters: FastGetBuilder,
): T => {
return new InstantiatorBuilder(model, fastGetters).build();
};

type DirectlyAssignableType = SimpleType<any> | IntegerType | LiteralType<any> | DateType;
Expand All @@ -28,7 +32,10 @@ const isDirectlyAssignableType = (type: IAnyType): type is DirectlyAssignableTyp
class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, any, any>> {
aliases = new Map<string, string>();

constructor(readonly model: T) {}
constructor(
readonly model: T,
readonly getters: FastGetBuilder,
) {}

build(): T {
const segments: string[] = [];
Expand Down Expand Up @@ -80,10 +87,7 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
}

const defineClassStatement = `
return class ${className} extends model {
[$memos] = null;
[$memoizedKeys] = null;
class ${className} extends model {
static createReadOnly = (snapshot, env) => {
const context = {
referenceCache: new Map(),
Expand Down Expand Up @@ -137,13 +141,17 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
`;

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

// console.log(`function for ${this.model.name}`, "\n\n\n", aliasFuncBody, "\n\n\n");
Expand All @@ -166,10 +174,9 @@ class InstantiatorBuilder<T extends IClassModelType<Record<string, IAnyType>, an
$identifier,
$context,
$parent,
$memos,
$memoizedKeys,
$readOnly,
$type,
$notYetMemoized,
QuickMap,
QuickArray,
}) as T;
Expand Down
10 changes: 2 additions & 8 deletions src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,7 @@ export const $registered = Symbol.for("MQT_registered");
export const $volatileDefiner = Symbol.for("MQT_volatileDefiner");

/**
* The values of memoized properties on an MQT instance
* The value we use in the memos map when we haven't populated the memo yet
* @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");
export const $notYetMemoized = Symbol.for("mqt:not-yet-memoized");

0 comments on commit a32d11b

Please sign in to comment.