Skip to content

Commit da54d63

Browse files
committed
feat(bench): add comprehensive benchmarking system with CI/CD integration
- Add Vitest Bench benchmarks for performance tracking - Create 4 benchmark suites: simple, complex, array, nested - Add GitHub Actions workflow for automated benchmark execution - Integrate github-action-benchmark for performance tracking - Add benchmark documentation in reports/benchmarks-setup.md - Update README with benchmark results - Configure Vitest to output benchmark results as JSON - Add npm scripts: bench, bench:watch - Install tinybench dependency for benchmarking Benchmark Results: - Simple mapping: ~30M ops/sec (competitive with vanilla) - Complex transformations: ~13M ops/sec - Array operations: ~1M ops/sec (100 items) - Deep nesting: ~2.5M ops/sec Features: - Automated execution on push and PR - Performance regression alerts (150% threshold) - Historical tracking and visualization - Artifact storage (30 days retention)
1 parent 4590135 commit da54d63

11 files changed

Lines changed: 704 additions & 9 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Benchmark
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: write
14+
deployments: write
15+
pull-requests: write
16+
17+
jobs:
18+
benchmark:
19+
name: Run Benchmarks
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: '20'
32+
cache: 'npm'
33+
34+
- name: Install dependencies
35+
run: npm ci
36+
37+
- name: Build project
38+
run: npm run build
39+
40+
- name: Run benchmarks
41+
run: npm run bench
42+
43+
- name: Store benchmark result
44+
uses: benchmark-action/github-action-benchmark@v1
45+
with:
46+
name: Vitest Benchmark
47+
tool: 'benchmarkjs'
48+
output-file-path: bench-results.json
49+
github-token: ${{ secrets.GITHUB_TOKEN }}
50+
auto-push: true
51+
# Show alert with commit comment on detecting possible performance regression
52+
alert-threshold: '150%'
53+
comment-on-alert: true
54+
fail-on-alert: false
55+
alert-comment-cc-users: '@Isqanderm'
56+
# Enable Job Summary for PRs
57+
summary-always: true
58+
59+
- name: Upload benchmark results
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: benchmark-results
63+
path: bench-results.json
64+
retention-days: 30
65+

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
node_modules
33
coverage
44
build
5+
bench-results.json

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ console.log(result); // { fullName: 'John Doe', isAdult: true }
7474

7575
Performance of om-data-mapper is almost identical to a native, hand-written “vanilla” mapper—demonstrating near-native speeds even in “safe” mode. Enabling Unsafe Mode (useUnsafe: true) removes all try/catch overhead and pushes performance even higher.
7676

77+
### Benchmark Results
78+
79+
We use automated benchmarks to track performance across different scenarios:
80+
81+
- **Simple Mapping**: ~30M ops/sec (competitive with vanilla)
82+
- **Complex Transformations**: ~13M ops/sec (with custom functions)
83+
- **Array Operations**: ~1M ops/sec (100 items)
84+
- **Deep Nesting**: ~2.4M ops/sec (4-level deep)
85+
86+
All benchmarks run automatically on every commit via GitHub Actions. See [Benchmark Setup Guide](./reports/benchmarks-setup.md) for details.
87+
7788
[![Benchmark Chart](https://raw.githubusercontent.com/Isqanderm/data-mapper/659ae4ac86f3a44bc16475867ad26efaa8dd6177/benchmarks/benckmarks.png)](https://raw.githubusercontent.com/Isqanderm/data-mapper/659ae4ac86f3a44bc16475867ad26efaa8dd6177/benchmarks/benckmarks.png)
7889

7990
## Features

bench/array.bench.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { bench, describe } from 'vitest';
2+
import { Mapper } from '../src';
3+
4+
interface Item {
5+
id: number;
6+
name: string;
7+
price: number;
8+
}
9+
10+
interface ItemDTO {
11+
itemId: number;
12+
itemName: string;
13+
cost: number;
14+
}
15+
16+
const items: Item[] = Array.from({ length: 100 }, (_, i) => ({
17+
id: i + 1,
18+
name: `Item ${i + 1}`,
19+
price: Math.random() * 100,
20+
}));
21+
22+
const itemMapper = Mapper.create<Item, ItemDTO>({
23+
itemId: 'id',
24+
itemName: 'name',
25+
cost: 'price',
26+
});
27+
28+
function vanillaArrayMapper(items: Item[]): ItemDTO[] {
29+
return items.map((item) => ({
30+
itemId: item.id,
31+
itemName: item.name,
32+
cost: item.price,
33+
}));
34+
}
35+
36+
describe('Array Mapping Benchmark', () => {
37+
bench('OmDataMapper - Map 100 items', () => {
38+
items.map((item) => itemMapper.execute(item).result);
39+
});
40+
41+
bench('Vanilla - Map 100 items', () => {
42+
vanillaArrayMapper(items);
43+
});
44+
});
45+

bench/complex.bench.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { bench, describe } from 'vitest';
2+
import { Mapper } from '../src';
3+
4+
interface ComplexSource {
5+
id: number;
6+
user: {
7+
firstName: string;
8+
lastName: string;
9+
profile: {
10+
age: number;
11+
email: string;
12+
};
13+
};
14+
orders: Array<{
15+
orderId: number;
16+
amount: number;
17+
items: Array<{
18+
productId: number;
19+
quantity: number;
20+
}>;
21+
}>;
22+
metadata: {
23+
createdAt: string;
24+
updatedAt: string;
25+
};
26+
}
27+
28+
interface ComplexTarget {
29+
userId: number;
30+
fullName: string;
31+
age: number;
32+
email: string;
33+
orderIds: number[];
34+
totalAmount: number;
35+
productCount: number;
36+
created: string;
37+
updated: string;
38+
}
39+
40+
const complexSourceData: ComplexSource = {
41+
id: 1,
42+
user: {
43+
firstName: 'John',
44+
lastName: 'Doe',
45+
profile: {
46+
age: 30,
47+
email: 'john@example.com',
48+
},
49+
},
50+
orders: [
51+
{
52+
orderId: 101,
53+
amount: 250,
54+
items: [
55+
{ productId: 1, quantity: 2 },
56+
{ productId: 2, quantity: 1 },
57+
],
58+
},
59+
{
60+
orderId: 102,
61+
amount: 150,
62+
items: [{ productId: 3, quantity: 3 }],
63+
},
64+
],
65+
metadata: {
66+
createdAt: '2024-01-01',
67+
updatedAt: '2024-01-15',
68+
},
69+
};
70+
71+
const complexMapper = Mapper.create<ComplexSource, ComplexTarget>({
72+
userId: 'id',
73+
fullName: (src) => `${src.user.firstName} ${src.user.lastName}`,
74+
age: 'user.profile.age',
75+
email: 'user.profile.email',
76+
orderIds: (src) => src.orders.map((o) => o.orderId),
77+
totalAmount: (src) => src.orders.reduce((sum, o) => sum + o.amount, 0),
78+
productCount: (src) => src.orders.reduce((sum, o) => sum + o.items.length, 0),
79+
created: 'metadata.createdAt',
80+
updated: 'metadata.updatedAt',
81+
});
82+
83+
function vanillaComplexMapper(source: ComplexSource): ComplexTarget {
84+
return {
85+
userId: source.id,
86+
fullName: `${source.user.firstName} ${source.user.lastName}`,
87+
age: source.user.profile.age,
88+
email: source.user.profile.email,
89+
orderIds: source.orders.map((o) => o.orderId),
90+
totalAmount: source.orders.reduce((sum, o) => sum + o.amount, 0),
91+
productCount: source.orders.reduce((sum, o) => sum + o.items.length, 0),
92+
created: source.metadata.createdAt,
93+
updated: source.metadata.updatedAt,
94+
};
95+
}
96+
97+
describe('Complex Mapping Benchmark', () => {
98+
bench('OmDataMapper - Complex mapping with transformers', () => {
99+
complexMapper.execute(complexSourceData);
100+
});
101+
102+
bench('Vanilla - Complex mapping with transformers', () => {
103+
vanillaComplexMapper(complexSourceData);
104+
});
105+
});
106+

bench/nested.bench.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { bench, describe } from 'vitest';
2+
import { Mapper } from '../src';
3+
4+
interface NestedSource {
5+
level1: {
6+
level2: {
7+
level3: {
8+
level4: {
9+
value: string;
10+
number: number;
11+
};
12+
};
13+
};
14+
};
15+
array: Array<{
16+
items: Array<{
17+
data: string;
18+
}>;
19+
}>;
20+
}
21+
22+
interface NestedTarget {
23+
deepValue: string;
24+
deepNumber: number;
25+
flattenedData: string[];
26+
}
27+
28+
const nestedSourceData: NestedSource = {
29+
level1: {
30+
level2: {
31+
level3: {
32+
level4: {
33+
value: 'deep value',
34+
number: 42,
35+
},
36+
},
37+
},
38+
},
39+
array: [
40+
{
41+
items: [{ data: 'item1' }, { data: 'item2' }],
42+
},
43+
{
44+
items: [{ data: 'item3' }],
45+
},
46+
],
47+
};
48+
49+
const nestedMapper = Mapper.create<NestedSource, NestedTarget>({
50+
deepValue: 'level1.level2.level3.level4.value',
51+
deepNumber: 'level1.level2.level3.level4.number',
52+
flattenedData: (src) => src.array.flatMap((a) => a.items.map((i) => i.data)),
53+
});
54+
55+
function vanillaNestedMapper(source: NestedSource): NestedTarget {
56+
return {
57+
deepValue: source.level1.level2.level3.level4.value,
58+
deepNumber: source.level1.level2.level3.level4.number,
59+
flattenedData: source.array.flatMap((a) => a.items.map((i) => i.data)),
60+
};
61+
}
62+
63+
describe('Nested Mapping Benchmark', () => {
64+
bench('OmDataMapper - Deep nested access', () => {
65+
nestedMapper.execute(nestedSourceData);
66+
});
67+
68+
bench('Vanilla - Deep nested access', () => {
69+
vanillaNestedMapper(nestedSourceData);
70+
});
71+
});
72+

bench/simple.bench.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { bench, describe } from 'vitest';
2+
import { Mapper } from '../src';
3+
4+
interface Source {
5+
id: number;
6+
name: string;
7+
details: {
8+
age: number;
9+
address: string;
10+
};
11+
}
12+
13+
interface Target {
14+
userId: number;
15+
fullName: string;
16+
age: number;
17+
location: string;
18+
}
19+
20+
const sourceData: Source = {
21+
id: 1,
22+
name: 'John Doe',
23+
details: {
24+
age: 30,
25+
address: '123 Main St',
26+
},
27+
};
28+
29+
const mapper = Mapper.create<Source, Target>({
30+
userId: 'id',
31+
fullName: 'name',
32+
age: 'details.age',
33+
location: 'details.address',
34+
});
35+
36+
function vanillaMapper(source: Source): Target {
37+
return {
38+
userId: source.id,
39+
fullName: source.name,
40+
age: source.details.age,
41+
location: source.details.address,
42+
};
43+
}
44+
45+
describe('Simple Mapping Benchmark', () => {
46+
bench('OmDataMapper - Simple mapping', () => {
47+
mapper.execute(sourceData);
48+
});
49+
50+
bench('Vanilla - Simple mapping', () => {
51+
vanillaMapper(sourceData);
52+
});
53+
});
54+

0 commit comments

Comments
 (0)