Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster cached view version 7: values right on the instance #94

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌎 🚀 🌔

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");
Loading