@nikovirtala/typesafe-dynamodb 0.0.1 → 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/README.md +46 -9
- package/examples/schema-validated-delete-document.ts +63 -0
- package/examples/schema-validated-get-document.ts +59 -0
- package/examples/schema-validated-put-document.ts +64 -0
- package/examples/schema-validated-query-document.ts +69 -0
- package/examples/schema-validated-scan-document.ts +57 -0
- package/examples/schema-validated-stream-event.ts +60 -0
- package/examples/schema-validated-update-document.ts +75 -0
- package/lib/attribute-value.js +8 -17
- package/lib/client-v3.d.ts +8 -8
- package/lib/client-v3.js +2 -3
- package/lib/create-set.d.ts +1 -1
- package/lib/create-set.js +2 -3
- package/lib/delete-document-command.d.ts +2 -2
- package/lib/delete-document-command.js +4 -7
- package/lib/delete-item-command.d.ts +2 -2
- package/lib/delete-item-command.js +4 -7
- package/lib/delete-item.d.ts +3 -3
- package/lib/delete-item.js +2 -3
- package/lib/document-client-field-mappings.js +1 -2
- package/lib/document-client-v3.d.ts +8 -8
- package/lib/document-client-v3.js +2 -3
- package/lib/expression-attributes.d.ts +3 -3
- package/lib/expression-attributes.js +2 -3
- package/lib/expression.js +1 -2
- package/lib/get-command.d.ts +4 -4
- package/lib/get-command.js +2 -3
- package/lib/get-document-command.d.ts +2 -2
- package/lib/get-document-command.js +4 -7
- package/lib/get-item-command.d.ts +2 -2
- package/lib/get-item-command.js +4 -7
- package/lib/get-item.d.ts +5 -5
- package/lib/get-item.js +2 -3
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -6
- package/lib/json-format.d.ts +1 -1
- package/lib/json-format.js +3 -6
- package/lib/key.d.ts +2 -2
- package/lib/key.js +2 -3
- package/lib/letter.js +1 -2
- package/lib/marshall.d.ts +1 -1
- package/lib/marshall.js +4 -7
- package/lib/narrow.d.ts +2 -2
- package/lib/narrow.js +2 -3
- package/lib/projection.d.ts +2 -2
- package/lib/projection.js +2 -3
- package/lib/put-document-command.d.ts +2 -2
- package/lib/put-document-command.js +4 -7
- package/lib/put-item-command.d.ts +2 -2
- package/lib/put-item-command.js +4 -7
- package/lib/put-item.d.ts +2 -2
- package/lib/put-item.js +2 -3
- package/lib/query-command.d.ts +2 -2
- package/lib/query-command.js +4 -7
- package/lib/query-document-command.d.ts +2 -2
- package/lib/query-document-command.js +4 -7
- package/lib/query.d.ts +2 -2
- package/lib/query.js +2 -3
- package/lib/scan-command.d.ts +2 -2
- package/lib/scan-command.js +4 -7
- package/lib/scan-document-command.d.ts +2 -2
- package/lib/scan-document-command.js +4 -7
- package/lib/scan.d.ts +2 -2
- package/lib/scan.js +2 -3
- package/lib/schema-validated-delete-document-command.d.ts +6 -0
- package/lib/schema-validated-delete-document-command.js +7 -0
- package/lib/schema-validated-document-client.d.ts +35 -0
- package/lib/schema-validated-document-client.js +22 -0
- package/lib/schema-validated-get-document-command.d.ts +6 -0
- package/lib/schema-validated-get-document-command.js +7 -0
- package/lib/schema-validated-put-document-command.d.ts +6 -0
- package/lib/schema-validated-put-document-command.js +7 -0
- package/lib/schema-validated-query-document-command.d.ts +6 -0
- package/lib/schema-validated-query-document-command.js +7 -0
- package/lib/schema-validated-scan-document-command.d.ts +6 -0
- package/lib/schema-validated-scan-document-command.js +7 -0
- package/lib/schema-validated-stream-event.d.ts +4 -0
- package/lib/schema-validated-stream-event.js +26 -0
- package/lib/schema-validated-update-document-command.d.ts +6 -0
- package/lib/schema-validated-update-document-command.js +7 -0
- package/lib/simplify.js +1 -2
- package/lib/stream-event.d.ts +1 -1
- package/lib/stream-event.js +2 -3
- package/lib/update-document-command.d.ts +2 -2
- package/lib/update-document-command.js +4 -7
- package/lib/update-item-command.d.ts +2 -2
- package/lib/update-item-command.js +4 -7
- package/lib/update-item.d.ts +4 -4
- package/lib/update-item.js +2 -3
- package/package.json +20 -58
- package/perf/README.md +114 -0
- package/perf/performance-test.ts +563 -0
- package/test-reports/junit.xml +33 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
|
+
|
|
3
|
+
import { performance, PerformanceObserver } from "node:perf_hooks";
|
|
4
|
+
import {
|
|
5
|
+
DynamoDBClient,
|
|
6
|
+
CreateTableCommand,
|
|
7
|
+
DescribeTableCommand,
|
|
8
|
+
ResourceNotFoundException,
|
|
9
|
+
} from "@aws-sdk/client-dynamodb";
|
|
10
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { SchemaValidatedDocumentClient } from "../lib/schema-validated-document-client.js";
|
|
13
|
+
import { SchemaValidatedGetDocumentCommand } from "../lib/schema-validated-get-document-command.js";
|
|
14
|
+
import { SchemaValidatedPutDocumentCommand } from "../lib/schema-validated-put-document-command.js";
|
|
15
|
+
import { SchemaValidatedQueryDocumentCommand } from "../lib/schema-validated-query-document-command.js";
|
|
16
|
+
import { SchemaValidatedUpdateDocumentCommand } from "../lib/schema-validated-update-document-command.js";
|
|
17
|
+
import { SchemaValidatedDeleteDocumentCommand } from "../lib/schema-validated-delete-document-command.js";
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
|
|
21
|
+
interface Config {
|
|
22
|
+
id: number;
|
|
23
|
+
tableName: string;
|
|
24
|
+
createTable: boolean;
|
|
25
|
+
operations: Array<"get" | "put" | "query" | "update" | "delete">;
|
|
26
|
+
executions: number;
|
|
27
|
+
region: string;
|
|
28
|
+
outputFile: string;
|
|
29
|
+
markdownFile: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Result {
|
|
33
|
+
operation: string;
|
|
34
|
+
executions: number;
|
|
35
|
+
totalTime: number;
|
|
36
|
+
averageTime: number;
|
|
37
|
+
minTime: number;
|
|
38
|
+
maxTime: number;
|
|
39
|
+
p50: number;
|
|
40
|
+
p90: number;
|
|
41
|
+
p95: number;
|
|
42
|
+
p99: number;
|
|
43
|
+
measurements: number[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Summary {
|
|
47
|
+
config: Config;
|
|
48
|
+
timestamp: string;
|
|
49
|
+
results: Result[];
|
|
50
|
+
totals: {
|
|
51
|
+
totalOperations: number;
|
|
52
|
+
totalTime: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TestSchema = z.object({
|
|
57
|
+
pk: z.string(),
|
|
58
|
+
sk: z.string(),
|
|
59
|
+
data: z.string(),
|
|
60
|
+
timestamp: z.number(),
|
|
61
|
+
metadata: z.object({
|
|
62
|
+
version: z.number(),
|
|
63
|
+
tags: z.array(z.string()),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
type TestItem = z.infer<typeof TestSchema>;
|
|
68
|
+
|
|
69
|
+
class PerformanceTester {
|
|
70
|
+
private client: DynamoDBClient;
|
|
71
|
+
private documentClient: DynamoDBDocumentClient;
|
|
72
|
+
private schemaValidatedClient: SchemaValidatedDocumentClient;
|
|
73
|
+
private measurements: Map<string, number[]> = new Map();
|
|
74
|
+
|
|
75
|
+
constructor(region: string) {
|
|
76
|
+
this.client = new DynamoDBClient({ region });
|
|
77
|
+
this.documentClient = DynamoDBDocumentClient.from(this.client);
|
|
78
|
+
this.schemaValidatedClient = new SchemaValidatedDocumentClient(
|
|
79
|
+
this.documentClient,
|
|
80
|
+
);
|
|
81
|
+
this.setupPerformanceObserver();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private setupPerformanceObserver() {
|
|
85
|
+
const obs = new PerformanceObserver((list) => {
|
|
86
|
+
for (const entry of list.getEntries()) {
|
|
87
|
+
if (entry.name.startsWith("test-")) {
|
|
88
|
+
const operation = entry.name.split("-")[1]; // extract operation type (get, put, etc.)
|
|
89
|
+
if (!this.measurements.has(operation)) {
|
|
90
|
+
this.measurements.set(operation, []);
|
|
91
|
+
}
|
|
92
|
+
this.measurements.get(operation)!.push(entry.duration);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
obs.observe({ entryTypes: ["measure"] });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async ensureTableExists(tableName: string): Promise<void> {
|
|
100
|
+
try {
|
|
101
|
+
await this.client.send(
|
|
102
|
+
new DescribeTableCommand({ TableName: tableName }),
|
|
103
|
+
);
|
|
104
|
+
console.log(`Table ${tableName} already exists`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof ResourceNotFoundException) {
|
|
107
|
+
console.log(`Creating table ${tableName}...`);
|
|
108
|
+
await this.createTable(tableName);
|
|
109
|
+
await this.waitForTableActive(tableName);
|
|
110
|
+
console.log(`Table ${tableName} created successfully`);
|
|
111
|
+
} else {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async createTable(tableName: string): Promise<void> {
|
|
118
|
+
await this.client.send(
|
|
119
|
+
new CreateTableCommand({
|
|
120
|
+
TableName: tableName,
|
|
121
|
+
KeySchema: [
|
|
122
|
+
{ AttributeName: "pk", KeyType: "HASH" },
|
|
123
|
+
{ AttributeName: "sk", KeyType: "RANGE" },
|
|
124
|
+
],
|
|
125
|
+
AttributeDefinitions: [
|
|
126
|
+
{ AttributeName: "pk", AttributeType: "S" },
|
|
127
|
+
{ AttributeName: "sk", AttributeType: "S" },
|
|
128
|
+
],
|
|
129
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async waitForTableActive(tableName: string): Promise<void> {
|
|
135
|
+
let attempts = 0;
|
|
136
|
+
const maxAttempts = 30;
|
|
137
|
+
|
|
138
|
+
while (attempts < maxAttempts) {
|
|
139
|
+
const { Table } = await this.client.send(
|
|
140
|
+
new DescribeTableCommand({ TableName: tableName }),
|
|
141
|
+
);
|
|
142
|
+
if (Table?.TableStatus === "ACTIVE") {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
146
|
+
attempts++;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Table ${tableName} did not become active within ${maxAttempts} seconds`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private generateTestItem(index: number): TestItem {
|
|
154
|
+
return {
|
|
155
|
+
pk: `test-pk-${index}`,
|
|
156
|
+
sk: `test-sk-${index}`,
|
|
157
|
+
data: `test-data-${index}-${Date.now()}`,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
metadata: {
|
|
160
|
+
version: 1,
|
|
161
|
+
tags: [`tag-${index}`, "performance-test"],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async testGet(tableName: string, executions: number): Promise<void> {
|
|
167
|
+
const GetCommand = SchemaValidatedGetDocumentCommand<TestItem, "pk", "sk">(
|
|
168
|
+
TestSchema,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < executions; i++) {
|
|
172
|
+
const startMark = `get-start-${i}`;
|
|
173
|
+
const endMark = `get-end-${i}`;
|
|
174
|
+
const measureName = `test-get-${i}`;
|
|
175
|
+
|
|
176
|
+
performance.mark(startMark);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await this.schemaValidatedClient.send(
|
|
180
|
+
new GetCommand({
|
|
181
|
+
TableName: tableName,
|
|
182
|
+
Key: {
|
|
183
|
+
pk: `test-pk-${i}`,
|
|
184
|
+
sk: `test-sk-${i}`,
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
TestSchema,
|
|
188
|
+
);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error(e);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
performance.mark(endMark);
|
|
194
|
+
performance.measure(measureName, startMark, endMark);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async testPut(tableName: string, executions: number): Promise<void> {
|
|
199
|
+
const PutCommand = SchemaValidatedPutDocumentCommand<TestItem>(TestSchema);
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < executions; i++) {
|
|
202
|
+
const startMark = `put-start-${i}`;
|
|
203
|
+
const endMark = `put-end-${i}`;
|
|
204
|
+
const measureName = `test-put-${i}`;
|
|
205
|
+
|
|
206
|
+
performance.mark(startMark);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await this.schemaValidatedClient.send(
|
|
210
|
+
new PutCommand({
|
|
211
|
+
TableName: tableName,
|
|
212
|
+
Item: this.generateTestItem(i),
|
|
213
|
+
}),
|
|
214
|
+
TestSchema,
|
|
215
|
+
);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.error(e);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
performance.mark(endMark);
|
|
221
|
+
performance.measure(measureName, startMark, endMark);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async testQuery(
|
|
226
|
+
tableName: string,
|
|
227
|
+
executions: number,
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const QueryCommand =
|
|
230
|
+
SchemaValidatedQueryDocumentCommand<TestItem>(TestSchema);
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < executions; i++) {
|
|
233
|
+
const startMark = `query-start-${i}`;
|
|
234
|
+
const endMark = `query-end-${i}`;
|
|
235
|
+
const measureName = `test-query-${i}`;
|
|
236
|
+
|
|
237
|
+
performance.mark(startMark);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await this.schemaValidatedClient.send(
|
|
241
|
+
new QueryCommand({
|
|
242
|
+
TableName: tableName,
|
|
243
|
+
KeyConditionExpression: "pk = :pk",
|
|
244
|
+
ExpressionAttributeValues: {
|
|
245
|
+
":pk": `test-pk-${i % 10}`,
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
TestSchema,
|
|
249
|
+
);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error(e);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
performance.mark(endMark);
|
|
255
|
+
performance.measure(measureName, startMark, endMark);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async testUpdate(
|
|
260
|
+
tableName: string,
|
|
261
|
+
executions: number,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
const UpdateCommand = SchemaValidatedUpdateDocumentCommand<
|
|
264
|
+
TestItem,
|
|
265
|
+
"pk",
|
|
266
|
+
"sk"
|
|
267
|
+
>(TestSchema);
|
|
268
|
+
|
|
269
|
+
for (let i = 0; i < executions; i++) {
|
|
270
|
+
const startMark = `update-start-${i}`;
|
|
271
|
+
const endMark = `update-end-${i}`;
|
|
272
|
+
const measureName = `test-update-${i}`;
|
|
273
|
+
|
|
274
|
+
performance.mark(startMark);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await this.schemaValidatedClient.send(
|
|
278
|
+
new UpdateCommand({
|
|
279
|
+
TableName: tableName,
|
|
280
|
+
Key: {
|
|
281
|
+
pk: `test-pk-${i}`,
|
|
282
|
+
sk: `test-sk-${i}`,
|
|
283
|
+
},
|
|
284
|
+
UpdateExpression: "SET #data = :data, #timestamp = :timestamp",
|
|
285
|
+
ExpressionAttributeNames: {
|
|
286
|
+
"#data": "data",
|
|
287
|
+
"#timestamp": "timestamp",
|
|
288
|
+
},
|
|
289
|
+
ExpressionAttributeValues: {
|
|
290
|
+
":data": `updated-data-${i}`,
|
|
291
|
+
":timestamp": Date.now(),
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
TestSchema,
|
|
295
|
+
);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.error(e);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
performance.mark(endMark);
|
|
301
|
+
performance.measure(measureName, startMark, endMark);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async testDelete(
|
|
306
|
+
tableName: string,
|
|
307
|
+
executions: number,
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const DeleteCommand = SchemaValidatedDeleteDocumentCommand<
|
|
310
|
+
TestItem,
|
|
311
|
+
"pk",
|
|
312
|
+
"sk"
|
|
313
|
+
>(TestSchema);
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < executions; i++) {
|
|
316
|
+
const startMark = `delete-start-${i}`;
|
|
317
|
+
const endMark = `delete-end-${i}`;
|
|
318
|
+
const measureName = `test-delete-${i}`;
|
|
319
|
+
|
|
320
|
+
performance.mark(startMark);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await this.schemaValidatedClient.send(
|
|
324
|
+
new DeleteCommand({
|
|
325
|
+
TableName: tableName,
|
|
326
|
+
Key: {
|
|
327
|
+
pk: `test-pk-${i}`,
|
|
328
|
+
sk: `test-sk-${i}`,
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
TestSchema,
|
|
332
|
+
);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.error(e);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
performance.mark(endMark);
|
|
338
|
+
performance.measure(measureName, startMark, endMark);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private calculateStats(
|
|
343
|
+
measurements: number[],
|
|
344
|
+
): Omit<Result, "operation" | "executions"> {
|
|
345
|
+
const totalTime = measurements.reduce((sum, time) => sum + time, 0);
|
|
346
|
+
const averageTime = totalTime / measurements.length;
|
|
347
|
+
const minTime = Math.min(...measurements);
|
|
348
|
+
const maxTime = Math.max(...measurements);
|
|
349
|
+
|
|
350
|
+
const sorted = [...measurements].sort((a, b) => a - b);
|
|
351
|
+
const p50 = this.percentile(sorted, 50);
|
|
352
|
+
const p90 = this.percentile(sorted, 90);
|
|
353
|
+
const p95 = this.percentile(sorted, 95);
|
|
354
|
+
const p99 = this.percentile(sorted, 99);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
totalTime,
|
|
358
|
+
averageTime,
|
|
359
|
+
minTime,
|
|
360
|
+
maxTime,
|
|
361
|
+
p50,
|
|
362
|
+
p90,
|
|
363
|
+
p95,
|
|
364
|
+
p99,
|
|
365
|
+
measurements,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private percentile(sorted: number[], p: number): number {
|
|
370
|
+
const index = (p / 100) * (sorted.length - 1);
|
|
371
|
+
const lower = Math.floor(index);
|
|
372
|
+
const upper = Math.ceil(index);
|
|
373
|
+
const weight = index % 1;
|
|
374
|
+
|
|
375
|
+
if (upper >= sorted.length) return sorted[sorted.length - 1];
|
|
376
|
+
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private generateMarkdownSummary(summary: Summary): string {
|
|
380
|
+
const { config, timestamp, results, totals } = summary;
|
|
381
|
+
|
|
382
|
+
let markdown = `# Performance Test Results\n\n`;
|
|
383
|
+
markdown += `**Test ID:** ${config.id}\n`;
|
|
384
|
+
markdown += `**Timestamp:** ${timestamp}\n`;
|
|
385
|
+
markdown += `**Table:** ${config.tableName}\n`;
|
|
386
|
+
markdown += `**Region:** ${config.region}\n`;
|
|
387
|
+
markdown += `**Total Operations:** ${totals.totalOperations}\n`;
|
|
388
|
+
markdown += `**Total Time:** ${totals.totalTime.toFixed(2)}ms\n\n`;
|
|
389
|
+
|
|
390
|
+
markdown += `## Results\n\n`;
|
|
391
|
+
markdown += `| Operation | Executions | Total (ms) | Avg (ms) | Min (ms) | Max (ms) | P50 (ms) | P90 (ms) | P95 (ms) | P99 (ms) |\n`;
|
|
392
|
+
markdown += `|-----------|------------|------------|----------|----------|----------|----------|----------|----------|----------|\n`;
|
|
393
|
+
|
|
394
|
+
for (const result of results) {
|
|
395
|
+
markdown += `| ${result.operation.toUpperCase()} | ${result.executions} | ${result.totalTime.toFixed(2)} | ${result.averageTime.toFixed(2)} | ${result.minTime.toFixed(2)} | ${result.maxTime.toFixed(2)} | ${result.p50.toFixed(2)} | ${result.p90.toFixed(2)} | ${result.p95.toFixed(2)} | ${result.p99.toFixed(2)} |\n`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return markdown;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async runTests(config: Config): Promise<Summary> {
|
|
402
|
+
console.log(`Starting performance tests with config:`, config);
|
|
403
|
+
|
|
404
|
+
if (config.createTable) {
|
|
405
|
+
await this.ensureTableExists(config.tableName);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const startTime = performance.now();
|
|
409
|
+
this.measurements.clear();
|
|
410
|
+
|
|
411
|
+
// run tests for each operation (put first to ensure data exists)
|
|
412
|
+
const orderedOps = [...config.operations].sort((a, b) => {
|
|
413
|
+
const order = { put: 0, get: 1, query: 2, update: 3, delete: 4 };
|
|
414
|
+
return order[a] - order[b];
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
for (const operation of orderedOps) {
|
|
418
|
+
console.log(
|
|
419
|
+
`Running ${operation} tests (${config.executions} executions)...`,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
switch (operation) {
|
|
423
|
+
case "get":
|
|
424
|
+
await this.testGet(config.tableName, config.executions);
|
|
425
|
+
break;
|
|
426
|
+
case "put":
|
|
427
|
+
await this.testPut(config.tableName, config.executions);
|
|
428
|
+
break;
|
|
429
|
+
case "query":
|
|
430
|
+
await this.testQuery(config.tableName, config.executions);
|
|
431
|
+
break;
|
|
432
|
+
case "update":
|
|
433
|
+
await this.testUpdate(config.tableName, config.executions);
|
|
434
|
+
break;
|
|
435
|
+
case "delete":
|
|
436
|
+
await this.testDelete(config.tableName, config.executions);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const endTime = performance.now();
|
|
442
|
+
const totalTestTime = endTime - startTime;
|
|
443
|
+
|
|
444
|
+
// process results
|
|
445
|
+
const results: Result[] = [];
|
|
446
|
+
let totalOperations = 0;
|
|
447
|
+
|
|
448
|
+
for (const [operation, measurements] of this.measurements.entries()) {
|
|
449
|
+
const stats = this.calculateStats(measurements);
|
|
450
|
+
|
|
451
|
+
results.push({
|
|
452
|
+
operation,
|
|
453
|
+
executions: measurements.length,
|
|
454
|
+
...stats,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
totalOperations += measurements.length;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const testResults: Summary = {
|
|
461
|
+
config,
|
|
462
|
+
timestamp: new Date().toISOString(),
|
|
463
|
+
results,
|
|
464
|
+
totals: {
|
|
465
|
+
totalOperations,
|
|
466
|
+
totalTime: totalTestTime,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// save results
|
|
471
|
+
const outputPath = path.resolve(config.outputFile);
|
|
472
|
+
fs.writeFileSync(outputPath, JSON.stringify(testResults, null, 2));
|
|
473
|
+
console.log(`Results saved to: ${outputPath}`);
|
|
474
|
+
|
|
475
|
+
// save markdown summary
|
|
476
|
+
const markdownPath = path.resolve(config.markdownFile);
|
|
477
|
+
const markdown = this.generateMarkdownSummary(testResults);
|
|
478
|
+
fs.writeFileSync(markdownPath, markdown);
|
|
479
|
+
console.log(`Markdown summary saved to: ${markdownPath}`);
|
|
480
|
+
|
|
481
|
+
return testResults;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
destroy() {
|
|
485
|
+
this.client.destroy();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// CLI interface
|
|
490
|
+
async function main() {
|
|
491
|
+
const args = process.argv.slice(2);
|
|
492
|
+
|
|
493
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
494
|
+
console.log(`
|
|
495
|
+
Usage: node --experimental-strip-types performance-test.ts [options]
|
|
496
|
+
|
|
497
|
+
Options:
|
|
498
|
+
--table <name> DynamoDB Table name (default: auto-generated)
|
|
499
|
+
--operations <ops> Comma-separated operations: get,put,query,update,delete (default: all)
|
|
500
|
+
--executions <num> Number of executions per operation (default: 1000)
|
|
501
|
+
--region <region> AWS region (default: eu-west-1)
|
|
502
|
+
--output <file> Output JSON file path (default: auto-generated)
|
|
503
|
+
--help, -h Show this help message
|
|
504
|
+
|
|
505
|
+
Examples:
|
|
506
|
+
node --experimental-strip-types performance-test.ts
|
|
507
|
+
node --experimental-strip-types performance-test.ts --table my-table --operations get,put --executions 500
|
|
508
|
+
`);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function getArg(name: string): string | undefined {
|
|
513
|
+
const index = args.indexOf(name);
|
|
514
|
+
return index !== -1 && index + 1 < args.length
|
|
515
|
+
? args[index + 1]
|
|
516
|
+
: undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const id = Date.now();
|
|
520
|
+
const userProvidedTable = getArg("--table");
|
|
521
|
+
const config: Config = {
|
|
522
|
+
id,
|
|
523
|
+
tableName: userProvidedTable ?? `performance-test-${id}`,
|
|
524
|
+
operations: (getArg("--operations") ?? "get,put,query,update,delete").split(
|
|
525
|
+
",",
|
|
526
|
+
) as Config["operations"],
|
|
527
|
+
executions: parseInt(getArg("--executions") ?? "1000"),
|
|
528
|
+
region: getArg("--region") ?? "eu-west-1",
|
|
529
|
+
outputFile: getArg("--output") ?? `performance-test-results-${id}.json`,
|
|
530
|
+
markdownFile: `performance-test-results-${id}.md`,
|
|
531
|
+
createTable: !userProvidedTable,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const tester = new PerformanceTester(config.region);
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const results = await tester.runTests(config);
|
|
538
|
+
|
|
539
|
+
console.log("\n=== Performance Test Results ===");
|
|
540
|
+
console.log(`Total operations: ${results.totals.totalOperations}`);
|
|
541
|
+
console.log(`Total time: ${results.totals.totalTime.toFixed(2)}ms`);
|
|
542
|
+
|
|
543
|
+
console.log("\n=== Operation Details ===");
|
|
544
|
+
for (const result of results.results) {
|
|
545
|
+
console.log(`${result.operation.toUpperCase()}:`);
|
|
546
|
+
console.log(` Executions: ${result.executions}`);
|
|
547
|
+
console.log(` Total time: ${result.totalTime.toFixed(2)}ms`);
|
|
548
|
+
console.log(` Average time: ${result.averageTime.toFixed(2)}ms`);
|
|
549
|
+
console.log(` Min time: ${result.minTime.toFixed(2)}ms`);
|
|
550
|
+
console.log(` Max time: ${result.maxTime.toFixed(2)}ms`);
|
|
551
|
+
console.log(` P50: ${result.p50.toFixed(2)}ms`);
|
|
552
|
+
console.log(` P90: ${result.p90.toFixed(2)}ms`);
|
|
553
|
+
console.log(` P95: ${result.p95.toFixed(2)}ms`);
|
|
554
|
+
console.log(` P99: ${result.p99.toFixed(2)}ms`);
|
|
555
|
+
}
|
|
556
|
+
} finally {
|
|
557
|
+
tester.destroy();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
562
|
+
main().catch(console.error);
|
|
563
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<testsuites name="jest tests" tests="9" failures="0" errors="0" time="3.013">
|
|
3
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.514" tests="1">
|
|
4
|
+
<testcase classname=" dummy" name=" dummy" time="0.002">
|
|
5
|
+
</testcase>
|
|
6
|
+
</testsuite>
|
|
7
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.578" tests="3">
|
|
8
|
+
<testcase classname=" should marshall MyItem to ToAttributeMap<MyItem>" name=" should marshall MyItem to ToAttributeMap<MyItem>" time="0.003">
|
|
9
|
+
</testcase>
|
|
10
|
+
<testcase classname=" should unmarshall MyItem from ToAttributeMap<MyItem>" name=" should unmarshall MyItem from ToAttributeMap<MyItem>" time="0.001">
|
|
11
|
+
</testcase>
|
|
12
|
+
<testcase classname=" unmarshall should map numbers to string when wrapNumbers: true" name=" unmarshall should map numbers to string when wrapNumbers: true" time="0.001">
|
|
13
|
+
</testcase>
|
|
14
|
+
</testsuite>
|
|
15
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.618" tests="2">
|
|
16
|
+
<testcase classname=" should have schema attached" name=" should have schema attached" time="0.003">
|
|
17
|
+
</testcase>
|
|
18
|
+
<testcase classname=" should work with type-safe operations" name=" should work with type-safe operations" time="0">
|
|
19
|
+
</testcase>
|
|
20
|
+
</testsuite>
|
|
21
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.622" tests="1">
|
|
22
|
+
<testcase classname=" dummy" name=" dummy" time="0.003">
|
|
23
|
+
</testcase>
|
|
24
|
+
</testsuite>
|
|
25
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.631" tests="1">
|
|
26
|
+
<testcase classname=" dummy" name=" dummy" time="0.003">
|
|
27
|
+
</testcase>
|
|
28
|
+
</testsuite>
|
|
29
|
+
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2025-10-15T18:13:44" time="2.653" tests="1">
|
|
30
|
+
<testcase classname=" long UpdateExpression should compile" name=" long UpdateExpression should compile" time="0.005">
|
|
31
|
+
</testcase>
|
|
32
|
+
</testsuite>
|
|
33
|
+
</testsuites>
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineConfig } from "@nikovirtala/projen-vitest/lib/bundled-define-config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
bail: 0,
|
|
6
|
+
coverage: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
provider: "v8",
|
|
9
|
+
reporter: ["text","lcov"],
|
|
10
|
+
reportsDirectory: "coverage",
|
|
11
|
+
},
|
|
12
|
+
environment: "node",
|
|
13
|
+
exclude: ["**/node_modules/**","**/dist/**","**/cypress/**","**/.{idea,git,cache,output,temp}/**","**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*"],
|
|
14
|
+
globals: false,
|
|
15
|
+
include: ["**/*.{test,spec}.?(c|m)[jt]s?(x)"],
|
|
16
|
+
isolate: true,
|
|
17
|
+
passWithNoTests: true,
|
|
18
|
+
printConsoleTrace: true,
|
|
19
|
+
pool: "forks",
|
|
20
|
+
slowTestThreshold: 300,
|
|
21
|
+
typecheck: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
checker: "tsc --noEmit",
|
|
24
|
+
tsconfig: "tsconfig.dev.json",
|
|
25
|
+
},
|
|
26
|
+
update: true,
|
|
27
|
+
},
|
|
28
|
+
});
|