@durable-streams/benchmarks 0.1.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 durable-stream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
@@ -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,37 +1,60 @@
1
1
  {
2
2
  "name": "@durable-streams/benchmarks",
3
- "version": "0.1.0",
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
- "type": "module",
8
- "main": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "import": "./dist/index.js",
13
- "types": "./dist/index.d.ts"
14
- }
15
- },
16
- "scripts": {
17
- "build": "tsdown",
18
- "dev": "tsdown --watch",
19
- "typecheck": "tsc --noEmit"
6
+ "bugs": {
7
+ "url": "https://github.com/durable-streams/durable-streams/issues"
20
8
  },
21
9
  "dependencies": {
22
- "@durable-streams/client": "workspace:*",
23
- "vitest": "^3.2.4"
10
+ "vitest": "^3.2.4",
11
+ "@durable-streams/client": "0.1.2"
24
12
  },
25
13
  "devDependencies": {
26
14
  "@types/node": "^22.15.21",
27
15
  "tsdown": "^0.9.0",
28
16
  "typescript": "^5.0.0"
29
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
+ },
30
34
  "files": [
31
35
  "dist",
32
36
  "src"
33
37
  ],
34
- "engines": {
35
- "node": ">=18.0.0"
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"
51
+ },
52
+ "sideEffects": false,
53
+ "type": "module",
54
+ "types": "./dist/index.d.ts",
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "dev": "tsdown --watch",
58
+ "typecheck": "tsc --noEmit"
36
59
  }
37
- }
60
+ }