@durable-streams/benchmarks 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +269 -0
- package/dist/index.d.cts +12 -0
- package/package.json +34 -25
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
const node_fs = __toESM(require("node:fs"));
|
|
26
|
+
const vitest = __toESM(require("vitest"));
|
|
27
|
+
const __durable_streams_client = __toESM(require("@durable-streams/client"));
|
|
28
|
+
|
|
29
|
+
//#region src/index.ts
|
|
30
|
+
const results = new Map();
|
|
31
|
+
function recordResult(name, value, unit) {
|
|
32
|
+
if (!results.has(name)) results.set(name, {
|
|
33
|
+
values: [],
|
|
34
|
+
unit
|
|
35
|
+
});
|
|
36
|
+
results.get(name).values.push(value);
|
|
37
|
+
}
|
|
38
|
+
function calculateStats(values) {
|
|
39
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
40
|
+
const min = sorted[0];
|
|
41
|
+
const max = sorted[sorted.length - 1];
|
|
42
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
43
|
+
const p50 = sorted[Math.floor(sorted.length * .5)];
|
|
44
|
+
const p75 = sorted[Math.floor(sorted.length * .75)];
|
|
45
|
+
const p99 = sorted[Math.floor(sorted.length * .99)];
|
|
46
|
+
return {
|
|
47
|
+
min,
|
|
48
|
+
max,
|
|
49
|
+
mean,
|
|
50
|
+
p50,
|
|
51
|
+
p75,
|
|
52
|
+
p99
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function printResults() {
|
|
56
|
+
console.log(`\n=== RESULTS SO FAR ===`);
|
|
57
|
+
const tableData = {};
|
|
58
|
+
for (const [name, data] of results.entries()) {
|
|
59
|
+
if (data.values.length === 0) continue;
|
|
60
|
+
const stats = calculateStats(data.values);
|
|
61
|
+
tableData[name] = {
|
|
62
|
+
Min: `${stats.min.toFixed(2)} ${data.unit}`,
|
|
63
|
+
Max: `${stats.max.toFixed(2)} ${data.unit}`,
|
|
64
|
+
Mean: `${stats.mean.toFixed(2)} ${data.unit}`,
|
|
65
|
+
P50: `${stats.p50.toFixed(2)} ${data.unit}`,
|
|
66
|
+
P75: `${stats.p75.toFixed(2)} ${data.unit}`,
|
|
67
|
+
P99: `${stats.p99.toFixed(2)} ${data.unit}`,
|
|
68
|
+
Iterations: data.values.length
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
console.table(tableData);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run the full benchmark suite against a server
|
|
75
|
+
*/
|
|
76
|
+
function runBenchmarks(options) {
|
|
77
|
+
const { baseUrl, environment = `unknown` } = options;
|
|
78
|
+
(0, vitest.afterAll)(() => {
|
|
79
|
+
const statsOutput = {};
|
|
80
|
+
for (const [name, data] of results.entries()) {
|
|
81
|
+
const stats = calculateStats(data.values);
|
|
82
|
+
statsOutput[name] = {
|
|
83
|
+
...stats,
|
|
84
|
+
unit: data.unit,
|
|
85
|
+
iterations: data.values.length
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const output = {
|
|
89
|
+
environment,
|
|
90
|
+
baseUrl,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
results: statsOutput
|
|
93
|
+
};
|
|
94
|
+
(0, node_fs.writeFileSync)(`benchmark-results.json`, JSON.stringify(output, null, 2), `utf-8`);
|
|
95
|
+
console.log(`\n\n=== BENCHMARK RESULTS ===`);
|
|
96
|
+
console.log(`Environment: ${output.environment}`);
|
|
97
|
+
console.log(`Base URL: ${output.baseUrl}`);
|
|
98
|
+
console.log(``);
|
|
99
|
+
const finalTableData = {};
|
|
100
|
+
for (const [name, stats] of Object.entries(statsOutput)) finalTableData[name] = {
|
|
101
|
+
Min: `${stats.min.toFixed(2)} ${stats.unit}`,
|
|
102
|
+
Max: `${stats.max.toFixed(2)} ${stats.unit}`,
|
|
103
|
+
Mean: `${stats.mean.toFixed(2)} ${stats.unit}`,
|
|
104
|
+
P50: `${stats.p50.toFixed(2)} ${stats.unit}`,
|
|
105
|
+
P75: `${stats.p75.toFixed(2)} ${stats.unit}`,
|
|
106
|
+
P99: `${stats.p99.toFixed(2)} ${stats.unit}`,
|
|
107
|
+
Iterations: stats.iterations
|
|
108
|
+
};
|
|
109
|
+
console.table(finalTableData);
|
|
110
|
+
console.log(`\n\nResults saved to benchmark-results.json`);
|
|
111
|
+
});
|
|
112
|
+
(0, vitest.describe)(`Latency - Round-trip Time`, () => {
|
|
113
|
+
(0, vitest.bench)(`baseline ping (round-trip network latency)`, async () => {
|
|
114
|
+
const startTime = performance.now();
|
|
115
|
+
await fetch(`${baseUrl}/health`);
|
|
116
|
+
const endTime = performance.now();
|
|
117
|
+
const pingTime = endTime - startTime;
|
|
118
|
+
recordResult(`Baseline Ping`, pingTime, `ms`);
|
|
119
|
+
}, {
|
|
120
|
+
iterations: 5,
|
|
121
|
+
time: 5e3
|
|
122
|
+
});
|
|
123
|
+
(0, vitest.bench)(`append and receive via long-poll (100 bytes)`, async () => {
|
|
124
|
+
const streamPath = `/v1/stream/latency-bench-${Date.now()}-${Math.random()}`;
|
|
125
|
+
const stream = await __durable_streams_client.DurableStream.create({
|
|
126
|
+
url: `${baseUrl}${streamPath}`,
|
|
127
|
+
contentType: `application/octet-stream`
|
|
128
|
+
});
|
|
129
|
+
const message = new Uint8Array(100).fill(42);
|
|
130
|
+
let offset = (await stream.head()).offset;
|
|
131
|
+
const pingStart = performance.now();
|
|
132
|
+
await fetch(`${baseUrl}/health`);
|
|
133
|
+
const pingEnd = performance.now();
|
|
134
|
+
const pingTime = pingEnd - pingStart;
|
|
135
|
+
const warmupPromise = (async () => {
|
|
136
|
+
const res = await stream.stream({
|
|
137
|
+
offset,
|
|
138
|
+
live: `long-poll`
|
|
139
|
+
});
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
const unsubscribe = res.subscribeBytes(async (chunk) => {
|
|
142
|
+
if (chunk.data.length > 0) {
|
|
143
|
+
offset = chunk.offset;
|
|
144
|
+
unsubscribe();
|
|
145
|
+
res.cancel();
|
|
146
|
+
resolve();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
})();
|
|
151
|
+
await stream.append(message);
|
|
152
|
+
await warmupPromise;
|
|
153
|
+
const readPromise = (async () => {
|
|
154
|
+
const res = await stream.stream({
|
|
155
|
+
offset,
|
|
156
|
+
live: `long-poll`
|
|
157
|
+
});
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
const unsubscribe = res.subscribeBytes(async (chunk) => {
|
|
160
|
+
if (chunk.data.length > 0) {
|
|
161
|
+
unsubscribe();
|
|
162
|
+
res.cancel();
|
|
163
|
+
resolve();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
})();
|
|
168
|
+
const startTime = performance.now();
|
|
169
|
+
await stream.append(message);
|
|
170
|
+
await readPromise;
|
|
171
|
+
const endTime = performance.now();
|
|
172
|
+
await stream.delete();
|
|
173
|
+
const totalLatency = endTime - startTime;
|
|
174
|
+
const overhead = totalLatency - pingTime;
|
|
175
|
+
recordResult(`Latency - Total RTT`, totalLatency, `ms`);
|
|
176
|
+
recordResult(`Latency - Ping`, pingTime, `ms`);
|
|
177
|
+
recordResult(`Latency - Overhead`, overhead, `ms`);
|
|
178
|
+
}, {
|
|
179
|
+
iterations: 10,
|
|
180
|
+
time: 15e3
|
|
181
|
+
});
|
|
182
|
+
(0, vitest.afterAll)(() => {
|
|
183
|
+
printResults();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
(0, vitest.describe)(`Message Throughput`, () => {
|
|
187
|
+
(0, vitest.bench)(`small messages (100 bytes)`, async () => {
|
|
188
|
+
const streamPath = `/v1/stream/msg-small-${Date.now()}-${Math.random()}`;
|
|
189
|
+
const stream = await __durable_streams_client.DurableStream.create({
|
|
190
|
+
url: `${baseUrl}${streamPath}`,
|
|
191
|
+
contentType: `application/octet-stream`
|
|
192
|
+
});
|
|
193
|
+
const message = new Uint8Array(100).fill(42);
|
|
194
|
+
const messageCount = 1e3;
|
|
195
|
+
const concurrency = 75;
|
|
196
|
+
const startTime = performance.now();
|
|
197
|
+
for (let batch = 0; batch < messageCount / concurrency; batch++) await Promise.all(Array.from({ length: concurrency }, () => stream.append(message)));
|
|
198
|
+
const endTime = performance.now();
|
|
199
|
+
const elapsedSeconds = (endTime - startTime) / 1e3;
|
|
200
|
+
const messagesPerSecond = messageCount / elapsedSeconds;
|
|
201
|
+
await stream.delete();
|
|
202
|
+
recordResult(`Throughput - Small Messages`, messagesPerSecond, `msg/sec`);
|
|
203
|
+
}, {
|
|
204
|
+
iterations: 3,
|
|
205
|
+
time: 1e4
|
|
206
|
+
});
|
|
207
|
+
(0, vitest.bench)(`large messages (1MB)`, async () => {
|
|
208
|
+
const streamPath = `/v1/stream/msg-large-${Date.now()}-${Math.random()}`;
|
|
209
|
+
const stream = await __durable_streams_client.DurableStream.create({
|
|
210
|
+
url: `${baseUrl}${streamPath}`,
|
|
211
|
+
contentType: `application/octet-stream`
|
|
212
|
+
});
|
|
213
|
+
const message = new Uint8Array(1024 * 1024).fill(42);
|
|
214
|
+
const messageCount = 50;
|
|
215
|
+
const concurrency = 15;
|
|
216
|
+
const startTime = performance.now();
|
|
217
|
+
for (let batch = 0; batch < messageCount / concurrency; batch++) await Promise.all(Array.from({ length: concurrency }, () => stream.append(message)));
|
|
218
|
+
const endTime = performance.now();
|
|
219
|
+
const elapsedSeconds = (endTime - startTime) / 1e3;
|
|
220
|
+
const messagesPerSecond = messageCount / elapsedSeconds;
|
|
221
|
+
await stream.delete();
|
|
222
|
+
recordResult(`Throughput - Large Messages`, messagesPerSecond, `msg/sec`);
|
|
223
|
+
}, {
|
|
224
|
+
iterations: 2,
|
|
225
|
+
time: 1e4
|
|
226
|
+
});
|
|
227
|
+
(0, vitest.afterAll)(() => {
|
|
228
|
+
printResults();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
vitest.describe.skip(`Byte Throughput`, () => {
|
|
232
|
+
(0, vitest.bench)(`streaming throughput - appendStream`, async () => {
|
|
233
|
+
const streamPath = `/v1/stream/byte-stream-${Date.now()}-${Math.random()}`;
|
|
234
|
+
const stream = await __durable_streams_client.DurableStream.create({
|
|
235
|
+
url: `${baseUrl}${streamPath}`,
|
|
236
|
+
contentType: `application/octet-stream`
|
|
237
|
+
});
|
|
238
|
+
const chunkSize = 64 * 1024;
|
|
239
|
+
const chunk = new Uint8Array(chunkSize).fill(42);
|
|
240
|
+
const totalChunks = 100;
|
|
241
|
+
const startTime = performance.now();
|
|
242
|
+
const appends = [];
|
|
243
|
+
for (let i = 0; i < totalChunks; i++) appends.push(stream.append(chunk));
|
|
244
|
+
await Promise.all(appends);
|
|
245
|
+
const endTime = performance.now();
|
|
246
|
+
let bytesRead = 0;
|
|
247
|
+
const readRes = await stream.stream({ live: false });
|
|
248
|
+
const reader = readRes.bodyStream().getReader();
|
|
249
|
+
let result = await reader.read();
|
|
250
|
+
while (!result.done) {
|
|
251
|
+
bytesRead += result.value.length;
|
|
252
|
+
result = await reader.read();
|
|
253
|
+
}
|
|
254
|
+
const elapsedSeconds = (endTime - startTime) / 1e3;
|
|
255
|
+
const mbPerSecond = bytesRead / (1024 * 1024) / elapsedSeconds;
|
|
256
|
+
await stream.delete();
|
|
257
|
+
recordResult(`Throughput - Streaming (appendStream)`, mbPerSecond, `MB/sec`);
|
|
258
|
+
}, {
|
|
259
|
+
iterations: 3,
|
|
260
|
+
time: 1e4
|
|
261
|
+
});
|
|
262
|
+
(0, vitest.afterAll)(() => {
|
|
263
|
+
printResults();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
//#endregion
|
|
269
|
+
exports.runBenchmarks = runBenchmarks
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface BenchmarkOptions {
|
|
3
|
+
/** Base URL of the server to benchmark */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/** Environment name (e.g., "production", "local") */
|
|
6
|
+
environment?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run the full benchmark suite against a server
|
|
10
|
+
*/
|
|
11
|
+
declare function runBenchmarks(options: BenchmarkOptions): void; //#endregion
|
|
12
|
+
export { BenchmarkOptions, runBenchmarks };
|
package/package.json
CHANGED
|
@@ -1,48 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@durable-streams/benchmarks",
|
|
3
|
-
"version": "0.1.1",
|
|
4
3
|
"description": "Performance benchmark suite for Durable Streams server implementations",
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
|
-
"license": "Apache-2.0",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/durable-streams/durable-streams.git",
|
|
10
|
-
"directory": "packages/benchmarks"
|
|
11
|
-
},
|
|
12
6
|
"bugs": {
|
|
13
7
|
"url": "https://github.com/durable-streams/durable-streams/issues"
|
|
14
8
|
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"durable-streams",
|
|
17
|
-
"benchmarks",
|
|
18
|
-
"performance",
|
|
19
|
-
"typescript"
|
|
20
|
-
],
|
|
21
|
-
"type": "module",
|
|
22
|
-
"main": "./dist/index.js",
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"import": "./dist/index.js",
|
|
27
|
-
"types": "./dist/index.d.ts"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
9
|
"dependencies": {
|
|
31
10
|
"vitest": "^3.2.4",
|
|
32
|
-
"@durable-streams/client": "0.1.
|
|
11
|
+
"@durable-streams/client": "0.1.2"
|
|
33
12
|
},
|
|
34
13
|
"devDependencies": {
|
|
35
14
|
"@types/node": "^22.15.21",
|
|
36
15
|
"tsdown": "^0.9.0",
|
|
37
16
|
"typescript": "^5.0.0"
|
|
38
17
|
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.cts",
|
|
29
|
+
"default": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
39
34
|
"files": [
|
|
40
35
|
"dist",
|
|
41
36
|
"src"
|
|
42
37
|
],
|
|
43
|
-
"
|
|
44
|
-
"
|
|
38
|
+
"keywords": [
|
|
39
|
+
"benchmarks",
|
|
40
|
+
"durable-streams",
|
|
41
|
+
"performance",
|
|
42
|
+
"typescript"
|
|
43
|
+
],
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"main": "./dist/index.cjs",
|
|
46
|
+
"module": "./dist/index.js",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/durable-streams/durable-streams.git",
|
|
50
|
+
"directory": "packages/benchmarks"
|
|
45
51
|
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"type": "module",
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
46
55
|
"scripts": {
|
|
47
56
|
"build": "tsdown",
|
|
48
57
|
"dev": "tsdown --watch",
|