Skip to content

300x performance improvement deepkit/injector, 20x faster and smaller deepkit/rpc, new packages deepkit/bench, deepkit/run #640

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion packages/app/tests/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ test('scoped injector', () => {

{
const injector = serviceContainer.getInjector(module);
expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but has no value`);
expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but is not available in scope global`);
}

{
Expand Down
2 changes: 1 addition & 1 deletion packages/app/tests/service-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ test('scopes', () => {
const serviceContainer = new ServiceContainer(myModule);
const sessionInjector = serviceContainer.getInjectorContext().createChildScope('rpc');

expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but has no value`);
expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but is not available`);
expect(sessionInjector.get(SessionHandler)).toBeInstanceOf(SessionHandler);

expect(serviceContainer.getInjectorContext().get(MyService)).toBeInstanceOf(MyService);
Expand Down
1 change: 1 addition & 0 deletions packages/bench/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests
1 change: 1 addition & 0 deletions packages/bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Bench
Empty file added packages/bench/dist/.gitkeep
Empty file.
259 changes: 259 additions & 0 deletions packages/bench/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
export const AsyncFunction = (async () => {
}).constructor as { new(...args: string[]): Function };

function noop() {
}

type Benchmark = {
name: string;
fn: () => void;
iterations: number;
avgTime: number;
variance: number;
rme: number;
samples: number[],
heapDiff: number;
gcEvents: number[];
}

const benchmarks: Benchmark[] = [{
name: '',
fn: noop,
gcEvents: [],
samples: [],
iterations: 0,
avgTime: 0,
heapDiff: 0,
rme: 0,
variance: 0,
}];
let benchmarkCurrent = 1;
let current = benchmarks[0];

const blocks = ['▁', '▂', '▄', '▅', '▆', '▇', '█'];

function getBlocks(stats: number[]): string {
const max = Math.max(...stats);
let res = '';
for (const n of stats) {
const cat = Math.ceil(n / max * 6);
res += (blocks[cat - 1]);
}

return res;
}

const Reset = '\x1b[0m';
const FgGreen = '\x1b[32m';
const FgYellow = '\x1b[33m';

function green(text: string): string {
return `${FgGreen}${text}${Reset}`;
}

function yellow(text: string): string {
return `${FgYellow}${text}${Reset}`;
}

function print(...args: any[]) {
process.stdout.write(args.join(' ') + '\n');
}

const callGc = global.gc ? global.gc : () => undefined;

function report(benchmark: Benchmark) {
const hz = 1000 / benchmark.avgTime;

print(
' 🏎',
'x', green(hz.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(14)), 'ops/sec',
'\xb1' + benchmark.rme.toFixed(2).padStart(5) + '%',
yellow(benchmark.avgTime.toLocaleString(undefined, { minimumFractionDigits: 6, maximumFractionDigits: 6 }).padStart(10)), 'ms/op',
'\t' + getBlocks(benchmark.samples),
green(benchmark.name) + (current.fn instanceof AsyncFunction ? ' (async)' : ''),
`\t${benchmark.iterations} samples`,
benchmark.gcEvents.length ? `\t${benchmark.gcEvents.length} gc (${benchmark.gcEvents.reduce((a, b) => a + b, 0)}ms)` : '',
);
}

export function benchmark(name: string, fn: () => void) {
benchmarks.push({
name, fn,
gcEvents: [],
samples: [],
iterations: 0,
avgTime: 0,
heapDiff: 0,
rme: 0,
variance: 0,
});
}

export async function run(seconds: number = 1) {
print('Node', process.version);

while (benchmarkCurrent < benchmarks.length) {
current = benchmarks[benchmarkCurrent];
try {
if (current.fn instanceof AsyncFunction) {
await testAsync(seconds);
} else {
test(seconds);
}
} catch (error) {
print(`Benchmark ${current.name} failed`, error);
}
benchmarkCurrent++;
report(current);
}

console.log('done');
}

const executors = [
getExecutor(1),
getExecutor(10),
getExecutor(100),
getExecutor(1000),
getExecutor(10000),
getExecutor(100000),
getExecutor(1000000),
];

const asyncExecutors = [
getAsyncExecutor(1),
getAsyncExecutor(10),
getAsyncExecutor(100),
getAsyncExecutor(1000),
getAsyncExecutor(10000),
getAsyncExecutor(100000),
getAsyncExecutor(1000000),
];

const gcObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
current.gcEvents.push(entry.duration);
}
});
const a = gcObserver.observe({ entryTypes: ['gc'] });

function test(seconds: number) {
let iterations = 1;
let samples: number[] = [];
const max = seconds * 1000;

let executorId = 0;
let executor = executors[executorId];
//check which executor to use, go up until one round takes more than 5ms
do {
const candidate = executors[executorId++];
if (!candidate) break;
const start = performance.now();
candidate(current.fn);
const end = performance.now();
const time = end - start;
if (time > 5) break;
executor = candidate;
} while (true);

// warmup
for (let i = 0; i < 100; i++) {
executor(current.fn);
}

let consumed = 0;
const beforeHeap = process.memoryUsage().heapUsed;
callGc();
do {
const start = performance.now();
const r = executor(current.fn);
const end = performance.now();
const time = end - start;
consumed += time;
samples.push(time / r);
iterations += r;
} while (consumed < max);

// console.log('executionTimes', executionTimes);
collect(current, beforeHeap, samples, iterations);
}

function collect(current: Benchmark, beforeHeap: number, samples: number[], iterations: number) {
// remove first 10% of samples
const allSamples = samples.slice();
samples = samples.slice(Math.floor(samples.length * 0.9));

const avgTime = samples.reduce((sum, t) => sum + t, 0) / samples.length;
samples.sort((a, b) => a - b);

const variance = samples.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / samples.length;
const rme = (Math.sqrt(variance) / avgTime) * 100; // Relative Margin of Error (RME)

const afterHeap = process.memoryUsage().heapUsed;
const heapDiff = afterHeap - beforeHeap;

current.avgTime = avgTime;
current.variance = variance;
current.rme = rme;
current.heapDiff = heapDiff;
current.iterations = iterations;
// pick 20 samples from allSamples, make sure the first and last are included
current.samples = allSamples.filter((v, i) => i === 0 || i === allSamples.length - 1 || i % Math.floor(allSamples.length / 20) === 0);
// current.samples = allSamples;
}

async function testAsync(seconds: number) {
let iterations = 1;
let samples: number[] = [];
const max = seconds * 1000;

let executorId = 0;
let executor = asyncExecutors[executorId];
//check which executor to use, go up until one round takes more than 5ms
do {
const candidate = asyncExecutors[executorId++];
if (!candidate) break;
const start = performance.now();
await candidate(current.fn);
const end = performance.now();
const time = end - start;
if (time > 5) break;
executor = candidate;
} while (true);

// warmup
for (let i = 0; i < 100; i++) {
executor(current.fn);
}

let consumed = 0;
const beforeHeap = process.memoryUsage().heapUsed;
callGc();
do {
const start = performance.now();
const r = await executor(current.fn);
const end = performance.now();
const time = end - start;
consumed += time;
samples.push(time / r);
iterations += r;
} while (consumed < max);

collect(current, beforeHeap, samples, iterations);
}

function getExecutor(times: number) {
let code = '';
for (let i = 0; i < times; i++) {
code += 'fn();';
}
return new Function('fn', code + '; return ' + times);
}

function getAsyncExecutor(times: number) {
let code = '';
for (let i = 0; i < times; i++) {
code += 'await fn();';
}
return new AsyncFunction('fn', code + '; return ' + times);
}
26 changes: 26 additions & 0 deletions packages/bench/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@deepkit/bench",
"version": "1.0.3",
"description": "Deepkit Bench",
"type": "commonjs",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"types": "./dist/cjs/index.d.ts",
"require": "./dist/cjs/index.js",
"default": "./dist/esm/index.js"
}
},
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "echo '{\"type\": \"module\"}' > ./dist/esm/package.json"
},
"repository": "https://github.com/deepkit/deepkit-framework",
"author": "Marc J. Schmidt <[email protected]>",
"license": "MIT"
}
15 changes: 15 additions & 0 deletions packages/bench/tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm",
"module": "ES2020"
},
"references": [
{
"path": "../core/tsconfig.esm.json"
},
{
"path": "../type/tsconfig.esm.json"
}
]
}
31 changes: 31 additions & 0 deletions packages/bench/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"noImplicitAny": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"target": "es2020",
"module": "CommonJS",
"esModuleInterop": true,
"outDir": "./dist/cjs",
"declaration": true,
"composite": true,
"types": [
"node"
]
},
"reflection": true,
"include": [
"index.ts"
],
"exclude": [
"tests"
],
"references": [
]
}
2 changes: 2 additions & 0 deletions packages/bson/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
export * from './src/model.js';
export * from './src/bson-parser.js';
export { BaseParser } from './src/bson-parser.js';
export { seekElementSize } from './src/continuation.js';
export { BSONType } from './src/utils.js';
export * from './src/bson-deserializer.js';
export * from './src/bson-serializer.js';
export * from './src/strings.js';
Expand Down
Loading
Loading