Last active
February 4, 2023 15:26
-
-
Save H4ad/2c07b7e88f4126d49a3b6e43fa44d233 to your computer and use it in GitHub Desktop.
Module Token Factory Performance Analysis v2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Benchmark = require('benchmark'); | |
import { DynamicModule } from '@nestjs/common'; | |
import { Type } from '@nestjs/common/interfaces/type.interface'; | |
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; | |
import { | |
isFunction, isSymbol | |
} from '@nestjs/common/utils/shared.utils'; | |
import { ModuleTokenFactory } from '@nestjs/core/injector/module-token-factory'; | |
import { createHash } from 'crypto'; | |
import stringify from 'fast-safe-stringify'; | |
import { xxh32 } from '@node-rs/xxhash'; | |
var suite = new Benchmark.Suite(); | |
class Test1 { | |
constructor() {} | |
} | |
class Test2 { | |
constructor() {} | |
} | |
class Test3 { | |
constructor() {} | |
} | |
const checkClass = 'class '; | |
const checkClassLength = checkClass.length; | |
export class BetterModuleTokenFactory { | |
private readonly moduleTokenCache = new Map<string, string>(); | |
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>(); | |
public create( | |
metatype: Type<unknown>, | |
dynamicModuleMetadata?: Partial<DynamicModule> | undefined, | |
): string { | |
const moduleId = this.getModuleId(metatype); | |
if (!dynamicModuleMetadata) | |
return this.getFastModuleToken(moduleId, this.getModuleName(metatype)); | |
const opaqueToken = { | |
id: moduleId, | |
module: this.getModuleName(metatype), | |
dynamic: dynamicModuleMetadata ? dynamicModuleMetadata : '', | |
}; | |
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken); | |
return this.hashString(opaqueTokenString); | |
} | |
public getFastModuleToken(moduleId: string, moduleName: string): string { | |
const key = `${moduleId}_${moduleName}`; | |
if (this.moduleTokenCache.has(key)) return this.moduleTokenCache.get(key); | |
const hash = this.hashString(key); | |
this.moduleTokenCache.set(key, hash); | |
return hash; | |
} | |
public getStringifiedOpaqueToken( | |
opaqueToken: object | undefined, | |
): string { | |
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules | |
// The replacer function is also required in order to obtain real class names | |
// instead of the unified "Function" key | |
return opaqueToken ? stringify(opaqueToken, this.replacer) : ''; | |
} | |
public getModuleId(metatype: Type<unknown>): string { | |
let moduleId = this.moduleIdsCache.get(metatype); | |
if (moduleId) { | |
return moduleId; | |
} | |
moduleId = randomStringGenerator(); | |
this.moduleIdsCache.set(metatype, moduleId); | |
return moduleId; | |
} | |
public getModuleName(metatype: Type<any>): string { | |
return metatype.name; | |
} | |
private hashString(value: string): string { | |
return createHash('sha1').update(value).digest('hex') | |
} | |
private replacer(key: string, value: any) { | |
if (isFunction(value)) { | |
const funcAsString = value.toString(); | |
const isClass = funcAsString.slice(0, checkClassLength) === checkClass; | |
if (isClass) { | |
return value.name; | |
} | |
return funcAsString; | |
} | |
if (isSymbol(value)) { | |
return value.toString(); | |
} | |
return value; | |
} | |
} | |
export class BetterModuleTokenFactoryWithNewHash { | |
private readonly moduleTokenCache = new Map<string, string>(); | |
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>(); | |
public create( | |
metatype: Type<unknown>, | |
dynamicModuleMetadata?: Partial<DynamicModule> | undefined, | |
): string { | |
const moduleId = this.getModuleId(metatype); | |
if (!dynamicModuleMetadata) | |
return this.getFastModuleToken(moduleId, this.getModuleName(metatype)); | |
const opaqueToken = { | |
id: moduleId, | |
module: this.getModuleName(metatype), | |
dynamic: dynamicModuleMetadata ? dynamicModuleMetadata : '', | |
}; | |
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken); | |
return this.hashString(opaqueTokenString); | |
} | |
public getFastModuleToken(moduleId: string, moduleName: string): string { | |
const key = `${moduleId}_${moduleName}`; | |
if (this.moduleTokenCache.has(key)) return this.moduleTokenCache.get(key); | |
const hash = this.hashString(key); | |
this.moduleTokenCache.set(key, hash); | |
return hash; | |
} | |
public getStringifiedOpaqueToken( | |
opaqueToken: object | undefined, | |
): string { | |
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules | |
// The replacer function is also required in order to obtain real class names | |
// instead of the unified "Function" key | |
return opaqueToken ? stringify(opaqueToken, this.replacer) : ''; | |
} | |
public getModuleId(metatype: Type<unknown>): string { | |
let moduleId = this.moduleIdsCache.get(metatype); | |
if (moduleId) { | |
return moduleId; | |
} | |
moduleId = randomStringGenerator(); | |
this.moduleIdsCache.set(metatype, moduleId); | |
return moduleId; | |
} | |
public getModuleName(metatype: Type<any>): string { | |
return metatype.name; | |
} | |
private hashString(value: string): string { | |
return xxh32(value).toString(); | |
} | |
private replacer(key: string, value: any) { | |
if (isFunction(value)) { | |
const funcAsString = value.toString(); | |
const isClass = funcAsString.slice(0, checkClassLength) === checkClass; | |
if (isClass) { | |
return value.name; | |
} | |
return funcAsString; | |
} | |
if (isSymbol(value)) { | |
return value.toString(); | |
} | |
return value; | |
} | |
} | |
suite.add('ModuleTokenFactory#create', async function () { | |
new ModuleTokenFactory().create(Test1, undefined); | |
}); | |
suite.add('BetterModuleTokenFactory#create', async function () { | |
new BetterModuleTokenFactory().create(Test1, undefined); | |
}); | |
suite.add('BetterModuleTokenFactoryWithNewHash#create', async function () { | |
new BetterModuleTokenFactoryWithNewHash().create(Test1, undefined); | |
}); | |
const metadata: DynamicModule = { | |
module: Test1, | |
controllers: [ | |
Test3, | |
], | |
providers: [ | |
Test2, | |
], | |
exports: [ | |
Test2, | |
], | |
}; | |
suite.add('ModuleTokenFactory#create with metadata', async function () { | |
new ModuleTokenFactory().create(Test1, metadata); | |
}); | |
suite.add('BetterModuleTokenFactory#create with metadata', async function () { | |
new BetterModuleTokenFactory().create(Test1, metadata); | |
}); | |
suite.add('BetterModuleTokenFactoryWithNewHash#create with metadata', async function () { | |
new BetterModuleTokenFactoryWithNewHash().create(Test1, metadata); | |
}); | |
suite | |
// add listeners | |
.on('cycle', function (event) { | |
console.log(String(event.target)); | |
}) | |
.on('complete', function () { | |
console.log('Fastest is ' + this.filter('fastest').map('name')); | |
}) | |
.run({ | |
async: true, | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// change the path of the import for your AppModule. | |
// to run this file, install microtime lib first. | |
// Then, you only need to compile and run with `node dist/perf-startup-10000.ts` | |
const { NestFactory } = require('@nestjs/core'); | |
const { AppModule } = require('./dist/src/app.module'); | |
const runs = 10_000; | |
async function main() { | |
const start = performance.now(); | |
for (let i = 0; i < runs; i++) | |
await NestFactory.create(AppModule, { logger: false }).then(r => r.close()); | |
const end = performance.now(); | |
console.log(`Diff: ${(end - start)/runs}ms`); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment