Skip to content

Commit

Permalink
feat(type): add new fast path to resolveReceiveType and made it 5x fa…
Browse files Browse the repository at this point in the history
…ster on average use case.

 previously the cost of passing the type via type arguments (e.g. cast<User>, is<User>, ...) was too high and involved the usage of the full type virtual machine in order to interpret `Op.inline`. Adding this fast path to directly forwards to referenced type making it a lot faster and bypassing the type virtual machine entirely if the referenced type was already computed.
  • Loading branch information
marcj committed Jan 12, 2024
1 parent ba17d1f commit 45d656c
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 26 deletions.
18 changes: 5 additions & 13 deletions packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { MappedModifier, ReflectionOp } from '@deepkit/type-spec';
import { isExtendable } from './extends.js';
import { ClassType, isArray, isClass, isFunction, stringifyValueWithType } from '@deepkit/core';
import { isWithDeferredDecorators } from '../decorator.js';
import { ReflectionClass, TData } from './reflection.js';
import { extractTypeNameFromFunction, ReflectionClass, TData } from './reflection.js';
import { state } from './state.js';

export type RuntimeStackEntry = Type | Object | (() => ClassType | Object) | string | number | boolean | bigint;
Expand Down Expand Up @@ -111,26 +111,18 @@ export function unpack(pack: Packed): PackStruct {
return { ops, stack: pack.length > 1 ? pack.slice(0, -1) : [] };
}

export function resolvePacked(type: Packed, args: any[] = []): Type {
return resolveRuntimeType(type, args) as Type;
export function resolvePacked(type: Packed, args: any[] = [], options?: ReflectOptions): Type {
return resolveRuntimeType(type, args, options) as Type;
}

function isPack(o: any): o is Packed {
return isArray(o);
}

function extractTypeNameFromFunction(fn: Function): string {
const str = fn.toString();
//either it starts with __Ω* or __\u{3a9}* (bun.js)
const match = str.match(/(?:__Ω|__\\u\{3a9\})([\w]+)/);
if (match) {
return match[1];
}
return 'UnknownTypeName:' + str;
}

/**
* Computes a type of given object. This function caches the result on the object itself.
* This is the slow path, using the full type virtual machine to resolve the type.
* If you want to handle some fast paths (including cache), try using resolveReceiveType() instead.
*/
export function resolveRuntimeType(o: ClassType | Function | Packed | any, args: any[] = [], options?: ReflectOptions): Type {
const type = Processor.get().reflect(o, args, options || { reuseCached: true });
Expand Down
29 changes: 27 additions & 2 deletions packages/type/src/reflection/reflection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {
TypeTemplateLiteral
} from './type.js';
import { AbstractClassType, arrayRemoveItem, ClassType, getClassName, isArray, isClass, isPrototypeOfBase, stringifyValueWithType } from '@deepkit/core';
import { Packed, resolvePacked, resolveRuntimeType } from './processor.js';
import { Packed, resolvePacked, resolveRuntimeType, unpack } from './processor.js';
import { NoTypeReceived } from '../utils.js';
import { findCommonLiteral } from '../inheritance.js';
import type { ValidateFunction } from '../validator.js';
Expand All @@ -75,11 +75,25 @@ export type ReceiveType<T> = Packed | Type | ClassType<T>;

export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractClassType | ReflectionClass<any>): Type {
if (!type) throw new NoTypeReceived();
let typeFn: Function | undefined = undefined;

if (isArray(type)){
if (type.__type) return type.__type;
// this is fast path for simple references to a type, e.g. cast<User>(), so that User is directly handled
// instead of running the VM to resolve to User first.
if (type[type.length - 1] === 'n!') {
//n! represents a simple inline: [Op.inline, 0]
typeFn = (type as any)[0] as Function;
type = typeFn() as Packed | Type | ClassType | AbstractClassType | ReflectionClass<any>;
}
}

if (type instanceof ReflectionClass) return type.type;
if (isArray(type) && type.__type) return type.__type;
if (isType(type)) return type as Type;
if (isClass(type)) return resolveRuntimeType(type) as Type;
return resolvePacked(type);
const typeName = typeFn ? extractTypeNameFromFunction(typeFn) : undefined;
return resolvePacked(type, undefined, {reuseCached: true, typeName});
}

export function reflect(o: any, ...args: any[]): Type {
Expand Down Expand Up @@ -1419,6 +1433,17 @@ export class ReflectionClass<T> {
}
}

export function extractTypeNameFromFunction(fn: Function): string {
const str = fn.toString();
//either it starts with __Ω* or __\u{3a9}* (bun.js)
const match = str.match(/(?:__Ω|__\\u\{3a9\})([\w]+)/);

This comment has been minimized.

Copy link
@marcus-sa

marcus-sa Jan 12, 2024

Contributor

I need to update this to include __ɵΩ and __\u{275}\u{3a9} for external library imports.
749d118 (#517)

if (match) {
return match[1];
}
return 'UnknownTypeName:' + str;
}


// old function to decorate an interface
// export function decorate<T>(decorate: { [P in keyof T]?: FreeDecoratorFn<any> }, p?: ReceiveType<T>): ReflectionClass<T> {
// const type = typeOf([], p);
Expand Down
3 changes: 2 additions & 1 deletion packages/type/src/reflection/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TypeNumberBrand } from '@deepkit/type-spec';
import { getProperty, ReceiveType, reflect, ReflectionClass, resolveReceiveType, toSignature } from './reflection.js';
import { isExtendable } from './extends.js';
import { state } from './state.js';
import { resolveRuntimeType } from './processor.js';

export enum ReflectionVisibility {
public,
Expand Down Expand Up @@ -2743,6 +2744,6 @@ export function stringifyType(type: Type, stateIn: Partial<StringifyTypeOptions>

export function annotateClass<T>(clazz: ClassType | AbstractClassType, type?: ReceiveType<T>) {
(clazz as any).__type = isClass(type) ? (type as any).__type || [] : [];
type = resolveReceiveType(type);
type = resolveRuntimeType(type);
(clazz as any).__type.__type = type;
}
12 changes: 12 additions & 0 deletions packages/type/tests/receive-type.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test } from '@jest/globals';
import { ReceiveType, resolveReceiveType, typeOf } from '../src/reflection/reflection.js';
import { ReflectionKind, Type } from '../src/reflection/type.js';
import { ReflectionOp } from '@deepkit/type-spec';

test('typeOf', () => {
const type = typeOf<string>();
Expand Down Expand Up @@ -30,6 +31,17 @@ test('method call', () => {
expect(type).toEqual({ kind: ReflectionKind.string });
});

test('maintain name', () => {
interface User {}

function c<T>(type?: ReceiveType<T>) {
return resolveReceiveType(type);
}

const t = c<User>();
expect(t.typeName).toBe('User');
});

test('decorator call', () => {
let got: Type | undefined;

Expand Down
76 changes: 66 additions & 10 deletions packages/type/tests/validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expect, test, jest } from '@jest/globals';
import { expect, jest, test } from '@jest/globals';
import { Email, MaxLength, MinLength, Positive, Validate, validate, validates, ValidatorError } from '../src/validator.js';
import { assert, is } from '../src/typeguard.js';
import { AutoIncrement, Excluded, Group, integer, PrimaryKey, Type, Unique } from '../src/reflection/type.js';
import { t } from '../src/decorator.js';
import { ReflectionClass, typeOf } from '../src/reflection/reflection.js';
import { validatedDeserialize } from '../src/serializer-facade.js';
import { cast, castFunction, validatedDeserialize } from '../src/serializer-facade.js';

test('primitives', () => {
expect(validate<string>('Hello')).toEqual([]);
Expand Down Expand Up @@ -163,7 +163,12 @@ test('path', () => {

// expect(validate<Container1>({ configs: [{ name: 'a', value: 3 }] })).toEqual([]);
expect(validate<Container1>({ configs: {} })).toEqual([{ code: 'type', message: 'Not an array', path: 'configs', value: {} }]);
expect(validate<Container1>({ configs: [{ name: 'a', value: 123 }, { name: '12' }] })).toEqual([{ code: 'type', message: 'Not a number', path: 'configs.1.value', value: undefined }]);
expect(validate<Container1>({ configs: [{ name: 'a', value: 123 }, { name: '12' }] })).toEqual([{
code: 'type',
message: 'Not a number',
path: 'configs.1.value',
value: undefined
}]);

class Container2 {
configs: { [name: string]: Config } = {};
Expand All @@ -179,7 +184,12 @@ test('class with union literal', () => {
}

expect(validate<ConnectionOptions>({ readConcernLevel: 'majority' })).toEqual([]);
expect(validate<ConnectionOptions>({ readConcernLevel: 'invalid' })).toEqual([{ code: 'type', message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', path: 'readConcernLevel', value: 'invalid' }]);
expect(validate<ConnectionOptions>({ readConcernLevel: 'invalid' })).toEqual([{
code: 'type',
message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'',
path: 'readConcernLevel',
value: 'invalid'
}]);
});

test('named tuple', () => {
Expand Down Expand Up @@ -269,10 +279,12 @@ test('inline object', () => {

test('readonly constructor properties', () => {
class Pilot {
constructor(readonly name: string, readonly age: number) {}
constructor(readonly name: string, readonly age: number) {
}
}
expect(validate<Pilot>({name: 'Peter', age: 32})).toEqual([]);
expect(validate<Pilot>({name: 'Peter', age: 'sdd'})).toEqual([{code: 'type', message: 'Not a number', path: 'age', value: 'sdd'}]);

expect(validate<Pilot>({ name: 'Peter', age: 32 })).toEqual([]);
expect(validate<Pilot>({ name: 'Peter', age: 'sdd' })).toEqual([{ code: 'type', message: 'Not a number', path: 'age', value: 'sdd' }]);
});

test('class with statics', () => {
Expand All @@ -287,14 +299,58 @@ test('class with statics', () => {
}
}

expect(validate<PilotId>({value: 34})).toEqual([]);
expect(validate<PilotId>({value: '33'})).toEqual([{code: 'type', message: 'Not a number', path: 'value', value: '33'}]);
expect(validate<PilotId>({ value: 34 })).toEqual([]);
expect(validate<PilotId>({ value: '33' })).toEqual([{ code: 'type', message: 'Not a number', path: 'value', value: '33' }]);
});

test('date', () => {
class Account {
public name!: string;
public createdAt!: Date;
}
expect(validate<Account>({name: "jack", createdAt: 'asd'})).toEqual([{code: 'type', message: 'Not a Date', path: 'createdAt', value: 'asd'}]);

expect(validate<Account>({ name: 'jack', createdAt: 'asd' })).toEqual([{ code: 'type', message: 'Not a Date', path: 'createdAt', value: 'asd' }]);
});

test('speed', () => {
interface User {
username: string;
logins: number;
created: string;
}

let start, end;

const iterations = 100_000;

const c = castFunction<User>();
start = performance.now();
for (let i = 1; i < iterations; i++) {
const user = c({
username: 'Peter',
logins: 23,
created: '2020-01-01',
});
}
end = performance.now();
const deepkitInlined = end - start;
console.log('deepkitInlined', deepkitInlined);

start = performance.now();
for (let i = 1; i < iterations; i++) {
const user = cast<User>({
username: 'Peter',
logins: 23,
created: '2020-01-01',
});
}
end = performance.now();
const deepkitRef = end - start;
console.log('deepkitRef', deepkitRef);

const ratio = deepkitRef / deepkitInlined;
console.log('ratio', ratio);
// there is a performance cost involved in passing User to cast,
// but inline vs ref must not be slower than 2.5 times.
expect(ratio).toBeLessThan(2.5);
});

0 comments on commit 45d656c

Please sign in to comment.