@uploadista/data-store-s3 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/observability.d.ts +45 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +155 -0
- package/dist/s3-store-old.d.ts +51 -0
- package/dist/s3-store-old.d.ts.map +1 -0
- package/dist/s3-store-old.js +765 -0
- package/dist/s3-store.d.ts +9 -0
- package/dist/s3-store.d.ts.map +1 -0
- package/dist/s3-store.js +666 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts +44 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts.map +1 -0
- package/dist/services/__mocks__/s3-client-mock.service.js +379 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/s3-client.service.d.ts +68 -0
- package/dist/services/s3-client.service.d.ts.map +1 -0
- package/dist/services/s3-client.service.js +209 -0
- package/dist/test-observability.d.ts +6 -0
- package/dist/test-observability.d.ts.map +1 -0
- package/dist/test-observability.js +62 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/calculations.d.ts +7 -0
- package/dist/utils/calculations.d.ts.map +1 -0
- package/dist/utils/calculations.js +41 -0
- package/dist/utils/error-handling.d.ts +7 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/error-handling.js +29 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/stream-adapter.d.ts +14 -0
- package/dist/utils/stream-adapter.d.ts.map +1 -0
- package/dist/utils/stream-adapter.js +41 -0
- package/package.json +36 -0
- package/src/__tests__/integration/s3-store.integration.test.ts +548 -0
- package/src/__tests__/multipart-logic.test.ts +395 -0
- package/src/__tests__/s3-store.edge-cases.test.ts +681 -0
- package/src/__tests__/s3-store.performance.test.ts +622 -0
- package/src/__tests__/s3-store.test.ts +662 -0
- package/src/__tests__/utils/performance-helpers.ts +459 -0
- package/src/__tests__/utils/test-data-generator.ts +331 -0
- package/src/__tests__/utils/test-setup.ts +256 -0
- package/src/index.ts +1 -0
- package/src/s3-store.ts +1059 -0
- package/src/services/__mocks__/s3-client-mock.service.ts +604 -0
- package/src/services/index.ts +1 -0
- package/src/services/s3-client.service.ts +359 -0
- package/src/types.ts +96 -0
- package/src/utils/calculations.ts +61 -0
- package/src/utils/error-handling.ts +52 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/stream-adapter.ts +50 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface PerformanceMetrics {
|
|
4
|
+
startTime: number;
|
|
5
|
+
endTime: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
bytesProcessed: number;
|
|
8
|
+
throughputBps: number; // bytes per second
|
|
9
|
+
throughputMbps: number; // megabits per second
|
|
10
|
+
memoryDelta: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MemoryMetrics {
|
|
14
|
+
heapUsedBefore: number;
|
|
15
|
+
heapUsedAfter: number;
|
|
16
|
+
heapUsedDelta: number;
|
|
17
|
+
heapTotalBefore: number;
|
|
18
|
+
heapTotalAfter: number;
|
|
19
|
+
external: number;
|
|
20
|
+
arrayBuffers: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ConcurrentMetrics {
|
|
24
|
+
totalOperations: number;
|
|
25
|
+
totalDuration: number;
|
|
26
|
+
successfulOperations: number;
|
|
27
|
+
failedOperations: number;
|
|
28
|
+
averageDuration: number;
|
|
29
|
+
maxDuration: number;
|
|
30
|
+
minDuration: number;
|
|
31
|
+
concurrencyLevel: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Measure performance of an Effect operation
|
|
36
|
+
*/
|
|
37
|
+
export const measurePerformance = <A, E>(
|
|
38
|
+
operation: Effect.Effect<A, E>,
|
|
39
|
+
bytesProcessed: number = 0,
|
|
40
|
+
): Effect.Effect<{ result: A; metrics: PerformanceMetrics }, E> =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const startTime = performance.now();
|
|
43
|
+
const startMemory = process.memoryUsage();
|
|
44
|
+
|
|
45
|
+
const result = yield* operation;
|
|
46
|
+
|
|
47
|
+
const endTime = performance.now();
|
|
48
|
+
const endMemory = process.memoryUsage();
|
|
49
|
+
|
|
50
|
+
const durationMs = endTime - startTime;
|
|
51
|
+
const throughputBps =
|
|
52
|
+
bytesProcessed > 0 ? (bytesProcessed * 1000) / durationMs : 0;
|
|
53
|
+
const throughputMbps = (throughputBps * 8) / (1024 * 1024);
|
|
54
|
+
const memoryDelta = endMemory.heapUsed - startMemory.heapUsed;
|
|
55
|
+
|
|
56
|
+
const metrics: PerformanceMetrics = {
|
|
57
|
+
startTime,
|
|
58
|
+
endTime,
|
|
59
|
+
durationMs,
|
|
60
|
+
bytesProcessed,
|
|
61
|
+
throughputBps,
|
|
62
|
+
throughputMbps,
|
|
63
|
+
memoryDelta,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { result, metrics };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Measure memory usage of an Effect operation
|
|
71
|
+
*/
|
|
72
|
+
export const measureMemory = <A, E>(
|
|
73
|
+
operation: Effect.Effect<A, E>,
|
|
74
|
+
): Effect.Effect<{ result: A; memory: MemoryMetrics }, E> =>
|
|
75
|
+
Effect.gen(function* () {
|
|
76
|
+
// Force garbage collection if available (for more accurate measurements)
|
|
77
|
+
if (global.gc) {
|
|
78
|
+
global.gc();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const memoryBefore = process.memoryUsage();
|
|
82
|
+
|
|
83
|
+
const result = yield* operation;
|
|
84
|
+
|
|
85
|
+
// Force garbage collection again
|
|
86
|
+
if (global.gc) {
|
|
87
|
+
global.gc();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const memoryAfter = process.memoryUsage();
|
|
91
|
+
|
|
92
|
+
const memory: MemoryMetrics = {
|
|
93
|
+
heapUsedBefore: memoryBefore.heapUsed,
|
|
94
|
+
heapUsedAfter: memoryAfter.heapUsed,
|
|
95
|
+
heapUsedDelta: memoryAfter.heapUsed - memoryBefore.heapUsed,
|
|
96
|
+
heapTotalBefore: memoryBefore.heapTotal,
|
|
97
|
+
heapTotalAfter: memoryAfter.heapTotal,
|
|
98
|
+
external: memoryAfter.external,
|
|
99
|
+
arrayBuffers: memoryAfter.arrayBuffers,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return { result, memory };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Measure concurrent operations performance
|
|
107
|
+
*/
|
|
108
|
+
export const measureConcurrentOps = <A, E>(
|
|
109
|
+
operations: Effect.Effect<A, E>[],
|
|
110
|
+
concurrency: number = operations.length,
|
|
111
|
+
): Effect.Effect<{ results: A[]; metrics: ConcurrentMetrics }, E> =>
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const startTime = performance.now();
|
|
114
|
+
const individualMetrics: PerformanceMetrics[] = [];
|
|
115
|
+
|
|
116
|
+
const results = yield* Effect.forEach(
|
|
117
|
+
operations,
|
|
118
|
+
(operation) =>
|
|
119
|
+
measurePerformance(operation).pipe(
|
|
120
|
+
Effect.tap(({ metrics }) =>
|
|
121
|
+
Effect.sync(() => individualMetrics.push(metrics)),
|
|
122
|
+
),
|
|
123
|
+
Effect.map(({ result }) => result),
|
|
124
|
+
),
|
|
125
|
+
{ concurrency },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const endTime = performance.now();
|
|
129
|
+
const totalDuration = endTime - startTime;
|
|
130
|
+
|
|
131
|
+
const successfulOps = individualMetrics.length;
|
|
132
|
+
const failedOps = operations.length - successfulOps;
|
|
133
|
+
|
|
134
|
+
const durations = individualMetrics.map((m) => m.durationMs);
|
|
135
|
+
const averageDuration =
|
|
136
|
+
durations.reduce((sum, dur) => sum + dur, 0) / durations.length;
|
|
137
|
+
const maxDuration = Math.max(...durations);
|
|
138
|
+
const minDuration = Math.min(...durations);
|
|
139
|
+
|
|
140
|
+
const metrics: ConcurrentMetrics = {
|
|
141
|
+
totalOperations: operations.length,
|
|
142
|
+
successfulOperations: successfulOps,
|
|
143
|
+
failedOperations: failedOps,
|
|
144
|
+
totalDuration,
|
|
145
|
+
averageDuration,
|
|
146
|
+
maxDuration,
|
|
147
|
+
minDuration,
|
|
148
|
+
concurrencyLevel: concurrency,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return { results, metrics };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Performance benchmark for upload operations
|
|
156
|
+
*/
|
|
157
|
+
export interface UploadBenchmark {
|
|
158
|
+
fileSize: number;
|
|
159
|
+
partSize?: number;
|
|
160
|
+
concurrency?: number;
|
|
161
|
+
expectedThroughputMbps?: number;
|
|
162
|
+
maxDurationMs?: number;
|
|
163
|
+
maxMemoryUsageMB?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const benchmarkUpload = (
|
|
167
|
+
uploadOperation: Effect.Effect<unknown, unknown>,
|
|
168
|
+
benchmark: UploadBenchmark,
|
|
169
|
+
): Effect.Effect<
|
|
170
|
+
{
|
|
171
|
+
success: boolean;
|
|
172
|
+
metrics: PerformanceMetrics;
|
|
173
|
+
memory: MemoryMetrics;
|
|
174
|
+
issues: string[];
|
|
175
|
+
},
|
|
176
|
+
unknown
|
|
177
|
+
> =>
|
|
178
|
+
Effect.gen(function* () {
|
|
179
|
+
const issues: string[] = [];
|
|
180
|
+
|
|
181
|
+
const { metrics, memory } = yield* measureMemory(
|
|
182
|
+
measurePerformance(uploadOperation, benchmark.fileSize),
|
|
183
|
+
).pipe(
|
|
184
|
+
Effect.map(({ result: { result, metrics }, memory }) => ({
|
|
185
|
+
result,
|
|
186
|
+
metrics,
|
|
187
|
+
memory,
|
|
188
|
+
})),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Check performance benchmarks
|
|
192
|
+
if (
|
|
193
|
+
benchmark.expectedThroughputMbps &&
|
|
194
|
+
metrics.throughputMbps < benchmark.expectedThroughputMbps
|
|
195
|
+
) {
|
|
196
|
+
issues.push(
|
|
197
|
+
`Throughput ${metrics.throughputMbps.toFixed(2)} Mbps below expected ` +
|
|
198
|
+
`${benchmark.expectedThroughputMbps} Mbps`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
benchmark.maxDurationMs &&
|
|
204
|
+
metrics.durationMs > benchmark.maxDurationMs
|
|
205
|
+
) {
|
|
206
|
+
issues.push(
|
|
207
|
+
`Duration ${metrics.durationMs.toFixed(2)}ms exceeds maximum ` +
|
|
208
|
+
`${benchmark.maxDurationMs}ms`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const memoryUsageMB = memory.heapUsedDelta / (1024 * 1024);
|
|
213
|
+
if (
|
|
214
|
+
benchmark.maxMemoryUsageMB &&
|
|
215
|
+
memoryUsageMB > benchmark.maxMemoryUsageMB
|
|
216
|
+
) {
|
|
217
|
+
issues.push(
|
|
218
|
+
`Memory usage ${memoryUsageMB.toFixed(2)}MB exceeds maximum ` +
|
|
219
|
+
`${benchmark.maxMemoryUsageMB}MB`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: issues.length === 0,
|
|
225
|
+
metrics,
|
|
226
|
+
memory,
|
|
227
|
+
issues,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Utility to format performance metrics for test output
|
|
233
|
+
*/
|
|
234
|
+
export const formatMetrics = (metrics: PerformanceMetrics): string => {
|
|
235
|
+
const sizeFormatted = (metrics.bytesProcessed / (1024 * 1024)).toFixed(2);
|
|
236
|
+
const throughputFormatted = metrics.throughputMbps.toFixed(2);
|
|
237
|
+
const durationFormatted = metrics.durationMs.toFixed(2);
|
|
238
|
+
|
|
239
|
+
return `${sizeFormatted}MB in ${durationFormatted}ms (${throughputFormatted} Mbps)`;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Utility to format memory metrics for test output
|
|
244
|
+
*/
|
|
245
|
+
export const formatMemoryMetrics = (memory: MemoryMetrics): string => {
|
|
246
|
+
const heapDeltaMB = (memory.heapUsedDelta / (1024 * 1024)).toFixed(2);
|
|
247
|
+
const heapAfterMB = (memory.heapUsedAfter / (1024 * 1024)).toFixed(2);
|
|
248
|
+
const externalMB = (memory.external / (1024 * 1024)).toFixed(2);
|
|
249
|
+
|
|
250
|
+
return `Heap: ${heapAfterMB}MB (+${heapDeltaMB}MB), External: ${externalMB}MB`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Utility to format concurrent metrics for test output
|
|
255
|
+
*/
|
|
256
|
+
export const formatConcurrentMetrics = (metrics: ConcurrentMetrics): string => {
|
|
257
|
+
const successRate = (
|
|
258
|
+
(metrics.successfulOperations / metrics.totalOperations) *
|
|
259
|
+
100
|
|
260
|
+
).toFixed(1);
|
|
261
|
+
const avgDuration = metrics.averageDuration.toFixed(2);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
`${metrics.successfulOperations}/${metrics.totalOperations} ops (${successRate}%) ` +
|
|
265
|
+
`avg ${avgDuration}ms (${metrics.minDuration.toFixed(2)}-${metrics.maxDuration.toFixed(2)}ms)`
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create performance benchmarks for different file sizes
|
|
271
|
+
*/
|
|
272
|
+
export const createPerformanceBenchmarks = (): Record<
|
|
273
|
+
string,
|
|
274
|
+
UploadBenchmark
|
|
275
|
+
> => ({
|
|
276
|
+
tiny: {
|
|
277
|
+
fileSize: 1024, // 1KB
|
|
278
|
+
expectedThroughputMbps: 0.1, // Much more relaxed for test environment
|
|
279
|
+
maxDurationMs: 1000,
|
|
280
|
+
maxMemoryUsageMB: 5, // More realistic for test environment
|
|
281
|
+
},
|
|
282
|
+
small: {
|
|
283
|
+
fileSize: 1024 * 1024, // 1MB
|
|
284
|
+
expectedThroughputMbps: 1, // More relaxed
|
|
285
|
+
maxDurationMs: 5000,
|
|
286
|
+
maxMemoryUsageMB: 10,
|
|
287
|
+
},
|
|
288
|
+
medium: {
|
|
289
|
+
fileSize: 10 * 1024 * 1024, // 10MB
|
|
290
|
+
expectedThroughputMbps: 5, // Much more relaxed
|
|
291
|
+
maxDurationMs: 10000,
|
|
292
|
+
maxMemoryUsageMB: 25,
|
|
293
|
+
},
|
|
294
|
+
large: {
|
|
295
|
+
fileSize: 50 * 1024 * 1024, // 50MB
|
|
296
|
+
expectedThroughputMbps: 10, // Much more relaxed
|
|
297
|
+
maxDurationMs: 30000,
|
|
298
|
+
maxMemoryUsageMB: 50,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Progress tracker for testing upload progress callbacks
|
|
304
|
+
*/
|
|
305
|
+
export class ProgressTracker {
|
|
306
|
+
private updates: { offset: number; timestamp: number }[] = [];
|
|
307
|
+
|
|
308
|
+
readonly onProgress = (offset: number) => {
|
|
309
|
+
this.updates.push({ offset, timestamp: performance.now() });
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
getUpdates() {
|
|
313
|
+
return [...this.updates];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getProgressRate(): number {
|
|
317
|
+
if (this.updates.length < 2) return 0;
|
|
318
|
+
|
|
319
|
+
const first = this.updates[0];
|
|
320
|
+
const last = this.updates[this.updates.length - 1];
|
|
321
|
+
|
|
322
|
+
const bytesPerMs =
|
|
323
|
+
(last.offset - first.offset) / (last.timestamp - first.timestamp);
|
|
324
|
+
return bytesPerMs * 1000; // bytes per second
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getTotalBytesTracked(): number {
|
|
328
|
+
return this.updates.length > 0
|
|
329
|
+
? this.updates[this.updates.length - 1].offset
|
|
330
|
+
: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getUpdateCount(): number {
|
|
334
|
+
return this.updates.length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
clear() {
|
|
338
|
+
this.updates = [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Stress test configuration
|
|
344
|
+
*/
|
|
345
|
+
export interface StressTestConfig {
|
|
346
|
+
concurrentUploads: number;
|
|
347
|
+
fileSize: number;
|
|
348
|
+
totalFiles: number;
|
|
349
|
+
maxErrorRate: number; // 0-1, acceptable error rate
|
|
350
|
+
minThroughputMbps: number;
|
|
351
|
+
maxTestDurationMs: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run a stress test with multiple concurrent uploads
|
|
356
|
+
*/
|
|
357
|
+
export const runStressTest = <A, E>(
|
|
358
|
+
createUpload: () => Effect.Effect<A, E>,
|
|
359
|
+
config: StressTestConfig,
|
|
360
|
+
): Effect.Effect<
|
|
361
|
+
{
|
|
362
|
+
success: boolean;
|
|
363
|
+
metrics: ConcurrentMetrics;
|
|
364
|
+
errorRate: number;
|
|
365
|
+
totalThroughputMbps: number;
|
|
366
|
+
issues: string[];
|
|
367
|
+
},
|
|
368
|
+
E
|
|
369
|
+
> =>
|
|
370
|
+
Effect.gen(function* () {
|
|
371
|
+
const issues: string[] = [];
|
|
372
|
+
const startTime = performance.now();
|
|
373
|
+
|
|
374
|
+
// Create batches of concurrent uploads
|
|
375
|
+
const batches = Math.ceil(config.totalFiles / config.concurrentUploads);
|
|
376
|
+
let totalResults: A[] = [];
|
|
377
|
+
const allMetrics: ConcurrentMetrics[] = [];
|
|
378
|
+
|
|
379
|
+
for (let batch = 0; batch < batches; batch++) {
|
|
380
|
+
const remainingFiles =
|
|
381
|
+
config.totalFiles - batch * config.concurrentUploads;
|
|
382
|
+
const batchSize = Math.min(config.concurrentUploads, remainingFiles);
|
|
383
|
+
|
|
384
|
+
const batchOperations = Array.from({ length: batchSize }, () =>
|
|
385
|
+
createUpload(),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const { results, metrics } = yield* measureConcurrentOps(
|
|
389
|
+
batchOperations,
|
|
390
|
+
config.concurrentUploads,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
totalResults = [...totalResults, ...results];
|
|
394
|
+
allMetrics.push(metrics);
|
|
395
|
+
|
|
396
|
+
// Check if we're exceeding time limit
|
|
397
|
+
const currentTime = performance.now();
|
|
398
|
+
if (currentTime - startTime > config.maxTestDurationMs) {
|
|
399
|
+
issues.push(
|
|
400
|
+
`Test exceeded maximum duration ${config.maxTestDurationMs}ms`,
|
|
401
|
+
);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Aggregate metrics
|
|
407
|
+
const totalOps = allMetrics.reduce((sum, m) => sum + m.totalOperations, 0);
|
|
408
|
+
|
|
409
|
+
const totalSuccessful = allMetrics.reduce(
|
|
410
|
+
(sum, m) => sum + m.successfulOperations,
|
|
411
|
+
0,
|
|
412
|
+
);
|
|
413
|
+
const totalFailed = allMetrics.reduce(
|
|
414
|
+
(sum, m) => sum + m.failedOperations,
|
|
415
|
+
0,
|
|
416
|
+
);
|
|
417
|
+
const avgDuration =
|
|
418
|
+
allMetrics.reduce((sum, m) => sum + m.averageDuration, 0) /
|
|
419
|
+
allMetrics.length;
|
|
420
|
+
|
|
421
|
+
const errorRate = totalFailed / totalOps;
|
|
422
|
+
const endTime = performance.now();
|
|
423
|
+
const totalDurationMs = endTime - startTime;
|
|
424
|
+
const totalBytes = config.fileSize * totalSuccessful;
|
|
425
|
+
const totalThroughputMbps =
|
|
426
|
+
(totalBytes * 8) / (totalDurationMs * 1024 * 1024);
|
|
427
|
+
|
|
428
|
+
const aggregatedMetrics: ConcurrentMetrics = {
|
|
429
|
+
totalOperations: totalOps,
|
|
430
|
+
totalDuration: totalDurationMs,
|
|
431
|
+
successfulOperations: totalSuccessful,
|
|
432
|
+
failedOperations: totalFailed,
|
|
433
|
+
averageDuration: avgDuration,
|
|
434
|
+
maxDuration: Math.max(...allMetrics.map((m) => m.maxDuration)),
|
|
435
|
+
minDuration: Math.min(...allMetrics.map((m) => m.minDuration)),
|
|
436
|
+
concurrencyLevel: config.concurrentUploads,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Check benchmarks
|
|
440
|
+
if (errorRate > config.maxErrorRate) {
|
|
441
|
+
issues.push(
|
|
442
|
+
`Error rate ${(errorRate * 100).toFixed(1)}% exceeds maximum ${(config.maxErrorRate * 100).toFixed(1)}%`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (totalThroughputMbps < config.minThroughputMbps) {
|
|
447
|
+
issues.push(
|
|
448
|
+
`Total throughput ${totalThroughputMbps.toFixed(2)} Mbps below minimum ${config.minThroughputMbps} Mbps`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
success: issues.length === 0,
|
|
454
|
+
metrics: aggregatedMetrics,
|
|
455
|
+
errorRate,
|
|
456
|
+
totalThroughputMbps,
|
|
457
|
+
issues,
|
|
458
|
+
};
|
|
459
|
+
});
|