Skip to content

Instantly share code, notes, and snippets.

@H4ad
Last active February 4, 2023 15:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save H4ad/2c07b7e88f4126d49a3b6e43fa44d233 to your computer and use it in GitHub Desktop.
Save H4ad/2c07b7e88f4126d49a3b6e43fa44d233 to your computer and use it in GitHub Desktop.
Module Token Factory Performance Analysis v2
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,
});
// 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