Skip to content

Commit 290b50b

Browse files
authored
Merge pull request #642 from deepkit/feat/injector
Faster injector, new `@deepkit/bench` and `@deepkit/run`
2 parents fc22a03 + b46e228 commit 290b50b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2278
-472
lines changed

packages/app/tests/module.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ test('scoped injector', () => {
387387

388388
{
389389
const injector = serviceContainer.getInjector(module);
390-
expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but has no value`);
390+
expect(() => injector.get(Service)).toThrow(`Service 'Service' is known but is not available in scope global. Available in scopes: http`);
391391
}
392392

393393
{

packages/app/tests/service-container.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ test('scopes', () => {
118118
const serviceContainer = new ServiceContainer(myModule);
119119
const sessionInjector = serviceContainer.getInjectorContext().createChildScope('rpc');
120120

121-
expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but has no value`);
121+
expect(() => serviceContainer.getInjectorContext().get(SessionHandler)).toThrow(`Service 'SessionHandler' is known but is not available in scope global. Available in scopes: rpc`);
122122
expect(sessionInjector.get(SessionHandler)).toBeInstanceOf(SessionHandler);
123123

124124
expect(serviceContainer.getInjectorContext().get(MyService)).toBeInstanceOf(MyService);

packages/bench/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests

packages/bench/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Bench
2+
3+
```typescript
4+
// benchmarks/test.ts
5+
import { benchmark, run } from '@deepkit/bench';
6+
7+
let i = 0;
8+
9+
benchmark('test', () => {
10+
i += 10;
11+
});
12+
13+
void run();
14+
```
15+
16+
```sh
17+
node --import @deepkit/run benchmarks/test.ts
18+
```

packages/bench/dist/.gitkeep

Whitespace-only changes.

packages/bench/index.ts

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
export const AsyncFunction = (async () => {
2+
}).constructor as { new(...args: string[]): Function };
3+
4+
function noop() {
5+
}
6+
7+
type Benchmark = {
8+
name: string;
9+
fn: () => void;
10+
iterations: number;
11+
avgTime: number;
12+
variance: number;
13+
rme: number;
14+
samples: number[],
15+
heapDiff: number;
16+
gcEvents: number[];
17+
}
18+
19+
const benchmarks: Benchmark[] = [{
20+
name: '',
21+
fn: noop,
22+
gcEvents: [],
23+
samples: [],
24+
iterations: 0,
25+
avgTime: 0,
26+
heapDiff: 0,
27+
rme: 0,
28+
variance: 0,
29+
}];
30+
let benchmarkCurrent = 1;
31+
let current = benchmarks[0];
32+
33+
const blocks = ['▁', '▂', '▄', '▅', '▆', '▇', '█'];
34+
35+
function getBlocks(stats: number[]): string {
36+
const max = Math.max(...stats);
37+
let res = '';
38+
for (const n of stats) {
39+
const cat = Math.ceil(n / max * 6);
40+
res += (blocks[cat - 1]);
41+
}
42+
43+
return res;
44+
}
45+
46+
const Reset = '\x1b[0m';
47+
const FgGreen = '\x1b[32m';
48+
const FgYellow = '\x1b[33m';
49+
50+
function green(text: string): string {
51+
return `${FgGreen}${text}${Reset}`;
52+
}
53+
54+
function yellow(text: string): string {
55+
return `${FgYellow}${text}${Reset}`;
56+
}
57+
58+
function print(...args: any[]) {
59+
process.stdout.write(args.join(' ') + '\n');
60+
}
61+
62+
const callGc = global.gc ? global.gc : () => undefined;
63+
64+
function report(benchmark: Benchmark) {
65+
const hz = 1000 / benchmark.avgTime;
66+
67+
print(
68+
' 🏎',
69+
'x', green(hz.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(14)), 'ops/sec',
70+
'\xb1' + benchmark.rme.toFixed(2).padStart(5) + '%',
71+
yellow(benchmark.avgTime.toLocaleString(undefined, { minimumFractionDigits: 6, maximumFractionDigits: 6 }).padStart(10)), 'ms/op',
72+
'\t' + getBlocks(benchmark.samples),
73+
green(benchmark.name) + (current.fn instanceof AsyncFunction ? ' (async)' : ''),
74+
`\t${benchmark.iterations} samples`,
75+
benchmark.gcEvents.length ? `\t${benchmark.gcEvents.length} gc (${benchmark.gcEvents.reduce((a, b) => a + b, 0)}ms)` : '',
76+
);
77+
}
78+
79+
export function benchmark(name: string, fn: () => void) {
80+
benchmarks.push({
81+
name, fn,
82+
gcEvents: [],
83+
samples: [],
84+
iterations: 0,
85+
avgTime: 0,
86+
heapDiff: 0,
87+
rme: 0,
88+
variance: 0,
89+
});
90+
}
91+
92+
export async function run(seconds: number = 1) {
93+
print('Node', process.version);
94+
95+
while (benchmarkCurrent < benchmarks.length) {
96+
current = benchmarks[benchmarkCurrent];
97+
try {
98+
if (current.fn instanceof AsyncFunction) {
99+
await testAsync(seconds);
100+
} else {
101+
test(seconds);
102+
}
103+
} catch (error) {
104+
print(`Benchmark ${current.name} failed`, error);
105+
}
106+
benchmarkCurrent++;
107+
report(current);
108+
}
109+
110+
console.log('done');
111+
}
112+
113+
const executors = [
114+
getExecutor(1),
115+
getExecutor(10),
116+
getExecutor(100),
117+
getExecutor(1000),
118+
getExecutor(10000),
119+
getExecutor(100000),
120+
getExecutor(1000000),
121+
];
122+
123+
const asyncExecutors = [
124+
getAsyncExecutor(1),
125+
getAsyncExecutor(10),
126+
getAsyncExecutor(100),
127+
getAsyncExecutor(1000),
128+
getAsyncExecutor(10000),
129+
getAsyncExecutor(100000),
130+
getAsyncExecutor(1000000),
131+
];
132+
133+
const gcObserver = new PerformanceObserver((list) => {
134+
for (const entry of list.getEntries()) {
135+
current.gcEvents.push(entry.duration);
136+
}
137+
});
138+
const a = gcObserver.observe({ entryTypes: ['gc'] });
139+
140+
function test(seconds: number) {
141+
let iterations = 1;
142+
let samples: number[] = [];
143+
const max = seconds * 1000;
144+
145+
let executorId = 0;
146+
let executor = executors[executorId];
147+
//check which executor to use, go up until one round takes more than 5ms
148+
do {
149+
const candidate = executors[executorId++];
150+
if (!candidate) break;
151+
const start = performance.now();
152+
candidate(current.fn);
153+
const end = performance.now();
154+
const time = end - start;
155+
if (time > 5) break;
156+
executor = candidate;
157+
} while (true);
158+
159+
// warmup
160+
for (let i = 0; i < 100; i++) {
161+
executor(current.fn);
162+
}
163+
164+
let consumed = 0;
165+
const beforeHeap = process.memoryUsage().heapUsed;
166+
callGc();
167+
do {
168+
const start = performance.now();
169+
const r = executor(current.fn);
170+
const end = performance.now();
171+
const time = end - start;
172+
consumed += time;
173+
samples.push(time / r);
174+
iterations += r;
175+
} while (consumed < max);
176+
177+
// console.log('executionTimes', executionTimes);
178+
collect(current, beforeHeap, samples, iterations);
179+
}
180+
181+
function collect(current: Benchmark, beforeHeap: number, samples: number[], iterations: number) {
182+
// remove first 10% of samples
183+
const allSamples = samples.slice();
184+
samples = samples.slice(Math.floor(samples.length * 0.9));
185+
186+
const avgTime = samples.reduce((sum, t) => sum + t, 0) / samples.length;
187+
samples.sort((a, b) => a - b);
188+
189+
const variance = samples.reduce((sum, t) => sum + Math.pow(t - avgTime, 2), 0) / samples.length;
190+
const rme = (Math.sqrt(variance) / avgTime) * 100; // Relative Margin of Error (RME)
191+
192+
const afterHeap = process.memoryUsage().heapUsed;
193+
const heapDiff = afterHeap - beforeHeap;
194+
195+
current.avgTime = avgTime;
196+
current.variance = variance;
197+
current.rme = rme;
198+
current.heapDiff = heapDiff;
199+
current.iterations = iterations;
200+
// pick 20 samples from allSamples, make sure the first and last are included
201+
current.samples = allSamples.filter((v, i) => i === 0 || i === allSamples.length - 1 || i % Math.floor(allSamples.length / 20) === 0);
202+
// current.samples = allSamples;
203+
}
204+
205+
async function testAsync(seconds: number) {
206+
let iterations = 1;
207+
let samples: number[] = [];
208+
const max = seconds * 1000;
209+
210+
let executorId = 0;
211+
let executor = asyncExecutors[executorId];
212+
//check which executor to use, go up until one round takes more than 5ms
213+
do {
214+
const candidate = asyncExecutors[executorId++];
215+
if (!candidate) break;
216+
const start = performance.now();
217+
await candidate(current.fn);
218+
const end = performance.now();
219+
const time = end - start;
220+
if (time > 5) break;
221+
executor = candidate;
222+
} while (true);
223+
224+
// warmup
225+
for (let i = 0; i < 100; i++) {
226+
executor(current.fn);
227+
}
228+
229+
let consumed = 0;
230+
const beforeHeap = process.memoryUsage().heapUsed;
231+
callGc();
232+
do {
233+
const start = performance.now();
234+
const r = await executor(current.fn);
235+
const end = performance.now();
236+
const time = end - start;
237+
consumed += time;
238+
samples.push(time / r);
239+
iterations += r;
240+
} while (consumed < max);
241+
242+
collect(current, beforeHeap, samples, iterations);
243+
}
244+
245+
function getExecutor(times: number) {
246+
let code = '';
247+
for (let i = 0; i < times; i++) {
248+
code += 'fn();';
249+
}
250+
return new Function('fn', code + '; return ' + times);
251+
}
252+
253+
function getAsyncExecutor(times: number) {
254+
let code = '';
255+
for (let i = 0; i < times; i++) {
256+
code += 'await fn();';
257+
}
258+
return new AsyncFunction('fn', code + '; return ' + times);
259+
}

packages/bench/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@deepkit/bench",
3+
"version": "1.0.3",
4+
"description": "Deepkit Bench",
5+
"type": "commonjs",
6+
"main": "./dist/cjs/index.js",
7+
"module": "./dist/esm/index.js",
8+
"types": "./dist/cjs/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/cjs/index.d.ts",
12+
"require": "./dist/cjs/index.js",
13+
"default": "./dist/esm/index.js"
14+
}
15+
},
16+
"sideEffects": false,
17+
"publishConfig": {
18+
"access": "public"
19+
},
20+
"scripts": {
21+
"build": "echo '{\"type\": \"module\"}' > ./dist/esm/package.json"
22+
},
23+
"repository": "https://github.com/deepkit/deepkit-framework",
24+
"author": "Marc J. Schmidt <marc@marcjschmidt.de>",
25+
"license": "MIT"
26+
}

packages/bench/tsconfig.esm.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist/esm",
5+
"module": "ES2020"
6+
}
7+
}

packages/bench/tsconfig.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
"forceConsistentCasingInFileNames": true,
4+
"strict": true,
5+
"noImplicitReturns": true,
6+
"noFallthroughCasesInSwitch": true,
7+
"sourceMap": true,
8+
"noImplicitAny": false,
9+
"experimentalDecorators": true,
10+
"emitDecoratorMetadata": true,
11+
"moduleResolution": "node",
12+
"target": "es2020",
13+
"module": "CommonJS",
14+
"esModuleInterop": true,
15+
"outDir": "./dist/cjs",
16+
"declaration": true,
17+
"composite": true,
18+
"types": [
19+
"node"
20+
]
21+
},
22+
"reflection": true,
23+
"include": [
24+
"index.ts"
25+
],
26+
"exclude": [
27+
"tests"
28+
],
29+
"references": [
30+
]
31+
}

packages/bson/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
export * from './src/model.js';
1212
export * from './src/bson-parser.js';
1313
export { BaseParser } from './src/bson-parser.js';
14+
export { seekElementSize } from './src/continuation.js';
15+
export { BSONType } from './src/utils.js';
1416
export * from './src/bson-deserializer.js';
1517
export * from './src/bson-serializer.js';
1618
export * from './src/strings.js';

0 commit comments

Comments
 (0)