From 45d656ccc0e4ba36fe362784e60ca58c6b2da31d Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 12 Jan 2024 13:47:51 +0100 Subject: [PATCH] feat(type): add new fast path to resolveReceiveType and made it 5x faster on average use case. previously the cost of passing the type via type arguments (e.g. cast, is, ...) 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. --- packages/type/src/reflection/processor.ts | 18 ++--- packages/type/src/reflection/reflection.ts | 29 ++++++++- packages/type/src/reflection/type.ts | 3 +- packages/type/tests/receive-type.spec.ts | 12 ++++ packages/type/tests/validation.spec.ts | 76 +++++++++++++++++++--- 5 files changed, 112 insertions(+), 26 deletions(-) diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index 4bbd85d39..eb39142d4 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -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; @@ -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 }); diff --git a/packages/type/src/reflection/reflection.ts b/packages/type/src/reflection/reflection.ts index 4c48cb7d4..7a457de01 100644 --- a/packages/type/src/reflection/reflection.ts +++ b/packages/type/src/reflection/reflection.ts @@ -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'; @@ -75,11 +75,25 @@ export type ReceiveType = Packed | Type | ClassType; export function resolveReceiveType(type?: Packed | Type | ClassType | AbstractClassType | ReflectionClass): 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(), 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; + } + } + 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 { @@ -1419,6 +1433,17 @@ export class ReflectionClass { } } +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]+)/); + if (match) { + return match[1]; + } + return 'UnknownTypeName:' + str; +} + + // old function to decorate an interface // export function decorate(decorate: { [P in keyof T]?: FreeDecoratorFn }, p?: ReceiveType): ReflectionClass { // const type = typeOf([], p); diff --git a/packages/type/src/reflection/type.ts b/packages/type/src/reflection/type.ts index 18aaae763..f259c2755 100644 --- a/packages/type/src/reflection/type.ts +++ b/packages/type/src/reflection/type.ts @@ -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, @@ -2743,6 +2744,6 @@ export function stringifyType(type: Type, stateIn: Partial export function annotateClass(clazz: ClassType | AbstractClassType, type?: ReceiveType) { (clazz as any).__type = isClass(type) ? (type as any).__type || [] : []; - type = resolveReceiveType(type); + type = resolveRuntimeType(type); (clazz as any).__type.__type = type; } diff --git a/packages/type/tests/receive-type.spec.ts b/packages/type/tests/receive-type.spec.ts index 522497037..e9634aa61 100644 --- a/packages/type/tests/receive-type.spec.ts +++ b/packages/type/tests/receive-type.spec.ts @@ -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(); @@ -30,6 +31,17 @@ test('method call', () => { expect(type).toEqual({ kind: ReflectionKind.string }); }); +test('maintain name', () => { + interface User {} + + function c(type?: ReceiveType) { + return resolveReceiveType(type); + } + + const t = c(); + expect(t.typeName).toBe('User'); +}); + test('decorator call', () => { let got: Type | undefined; diff --git a/packages/type/tests/validation.spec.ts b/packages/type/tests/validation.spec.ts index e04f1a28b..e6119ae2c 100644 --- a/packages/type/tests/validation.spec.ts +++ b/packages/type/tests/validation.spec.ts @@ -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('Hello')).toEqual([]); @@ -163,7 +163,12 @@ test('path', () => { // expect(validate({ configs: [{ name: 'a', value: 3 }] })).toEqual([]); expect(validate({ configs: {} })).toEqual([{ code: 'type', message: 'Not an array', path: 'configs', value: {} }]); - expect(validate({ configs: [{ name: 'a', value: 123 }, { name: '12' }] })).toEqual([{ code: 'type', message: 'Not a number', path: 'configs.1.value', value: undefined }]); + expect(validate({ 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 } = {}; @@ -179,7 +184,12 @@ test('class with union literal', () => { } expect(validate({ readConcernLevel: 'majority' })).toEqual([]); - expect(validate({ readConcernLevel: 'invalid' })).toEqual([{ code: 'type', message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', path: 'readConcernLevel', value: 'invalid' }]); + expect(validate({ readConcernLevel: 'invalid' })).toEqual([{ + code: 'type', + message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', + path: 'readConcernLevel', + value: 'invalid' + }]); }); test('named tuple', () => { @@ -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({name: 'Peter', age: 32})).toEqual([]); - expect(validate({name: 'Peter', age: 'sdd'})).toEqual([{code: 'type', message: 'Not a number', path: 'age', value: 'sdd'}]); + + expect(validate({ name: 'Peter', age: 32 })).toEqual([]); + expect(validate({ name: 'Peter', age: 'sdd' })).toEqual([{ code: 'type', message: 'Not a number', path: 'age', value: 'sdd' }]); }); test('class with statics', () => { @@ -287,8 +299,8 @@ test('class with statics', () => { } } - expect(validate({value: 34})).toEqual([]); - expect(validate({value: '33'})).toEqual([{code: 'type', message: 'Not a number', path: 'value', value: '33'}]); + expect(validate({ value: 34 })).toEqual([]); + expect(validate({ value: '33' })).toEqual([{ code: 'type', message: 'Not a number', path: 'value', value: '33' }]); }); test('date', () => { @@ -296,5 +308,49 @@ test('date', () => { public name!: string; public createdAt!: Date; } - expect(validate({name: "jack", createdAt: 'asd'})).toEqual([{code: 'type', message: 'Not a Date', path: 'createdAt', value: 'asd'}]); + + expect(validate({ 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(); + 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({ + 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); });