bunsane 0.3.1 → 0.4.0
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/CHANGELOG.md +445 -318
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1043
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +64 -6
- package/core/RequestLoaders.ts +187 -36
- package/core/SchedulerManager.ts +28 -600
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +85 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +15 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +310 -0
- package/core/app/restRegistry.ts +80 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +666 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +161 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +173 -267
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +4 -4
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +16 -8
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +364 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +377 -0
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/AccessLog.ts +8 -1
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +20 -5
- package/database/cancellable.ts +35 -0
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -0
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +240 -13
- package/query/OrNode.ts +6 -5
- package/query/Query.ts +203 -59
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -367
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- package/tests/utils/test-context.ts +0 -149
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Benchmark execution engine for stress testing
|
|
3
|
-
* Runs queries with statistical analysis
|
|
4
|
-
*/
|
|
5
|
-
import db from '../../database';
|
|
6
|
-
|
|
7
|
-
export interface BenchmarkResult {
|
|
8
|
-
name: string;
|
|
9
|
-
iterations: number;
|
|
10
|
-
totalRecords: number;
|
|
11
|
-
timings: {
|
|
12
|
-
min: number;
|
|
13
|
-
max: number;
|
|
14
|
-
mean: number;
|
|
15
|
-
median: number;
|
|
16
|
-
p95: number;
|
|
17
|
-
p99: number;
|
|
18
|
-
stdDev: number;
|
|
19
|
-
};
|
|
20
|
-
rowsReturned: number;
|
|
21
|
-
queriesPerSecond: number;
|
|
22
|
-
memoryUsedMB: number;
|
|
23
|
-
passed: boolean;
|
|
24
|
-
target?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface BenchmarkOptions {
|
|
28
|
-
iterations?: number;
|
|
29
|
-
warmupIterations?: number;
|
|
30
|
-
targetP95?: number;
|
|
31
|
-
collectMemory?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ConcurrentResult {
|
|
35
|
-
name: string;
|
|
36
|
-
concurrency: number;
|
|
37
|
-
totalQueries: number;
|
|
38
|
-
queriesPerSecond: number;
|
|
39
|
-
avgLatency: number;
|
|
40
|
-
errorRate: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class BenchmarkRunner {
|
|
44
|
-
private results: BenchmarkResult[] = [];
|
|
45
|
-
|
|
46
|
-
async run(
|
|
47
|
-
name: string,
|
|
48
|
-
queryFn: () => Promise<any[]>,
|
|
49
|
-
options: BenchmarkOptions = {}
|
|
50
|
-
): Promise<BenchmarkResult> {
|
|
51
|
-
const {
|
|
52
|
-
iterations = 20,
|
|
53
|
-
warmupIterations = 3,
|
|
54
|
-
targetP95,
|
|
55
|
-
collectMemory = true
|
|
56
|
-
} = options;
|
|
57
|
-
|
|
58
|
-
// Warmup phase
|
|
59
|
-
for (let i = 0; i < warmupIterations; i++) {
|
|
60
|
-
await queryFn();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Force GC if available
|
|
64
|
-
if (typeof global.gc === 'function') {
|
|
65
|
-
global.gc();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const times: number[] = [];
|
|
69
|
-
let rowCount = 0;
|
|
70
|
-
const memBefore = process.memoryUsage().heapUsed;
|
|
71
|
-
|
|
72
|
-
// Benchmark phase
|
|
73
|
-
for (let i = 0; i < iterations; i++) {
|
|
74
|
-
const start = performance.now();
|
|
75
|
-
const results = await queryFn();
|
|
76
|
-
times.push(performance.now() - start);
|
|
77
|
-
rowCount = Array.isArray(results) ? results.length : 0;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const memAfter = process.memoryUsage().heapUsed;
|
|
81
|
-
const sortedTimes = [...times].sort((a, b) => a - b);
|
|
82
|
-
|
|
83
|
-
const timings = {
|
|
84
|
-
min: sortedTimes[0] ?? 0,
|
|
85
|
-
max: sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
86
|
-
mean: times.reduce((a, b) => a + b, 0) / (times.length || 1),
|
|
87
|
-
median: sortedTimes[Math.floor(sortedTimes.length / 2)] ?? 0,
|
|
88
|
-
p95: sortedTimes[Math.floor(sortedTimes.length * 0.95)] ?? sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
89
|
-
p99: sortedTimes[Math.floor(sortedTimes.length * 0.99)] ?? sortedTimes[sortedTimes.length - 1] ?? 0,
|
|
90
|
-
stdDev: this.calculateStdDev(times)
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const result: BenchmarkResult = {
|
|
94
|
-
name,
|
|
95
|
-
iterations,
|
|
96
|
-
totalRecords: await this.getRecordCount(),
|
|
97
|
-
timings,
|
|
98
|
-
rowsReturned: rowCount,
|
|
99
|
-
queriesPerSecond: 1000 / (timings.mean || 1),
|
|
100
|
-
memoryUsedMB: collectMemory ? (memAfter - memBefore) / 1024 / 1024 : 0,
|
|
101
|
-
passed: targetP95 ? (timings.p95 ?? 0) <= targetP95 : true,
|
|
102
|
-
target: targetP95
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
this.results.push(result);
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async runConcurrent(
|
|
110
|
-
name: string,
|
|
111
|
-
queryFn: () => Promise<any[]>,
|
|
112
|
-
concurrency: number,
|
|
113
|
-
duration: number = 10000
|
|
114
|
-
): Promise<ConcurrentResult> {
|
|
115
|
-
const times: number[] = [];
|
|
116
|
-
let errors = 0;
|
|
117
|
-
const startTime = performance.now();
|
|
118
|
-
|
|
119
|
-
const worker = async () => {
|
|
120
|
-
while (performance.now() - startTime < duration) {
|
|
121
|
-
const queryStart = performance.now();
|
|
122
|
-
try {
|
|
123
|
-
await queryFn();
|
|
124
|
-
times.push(performance.now() - queryStart);
|
|
125
|
-
} catch {
|
|
126
|
-
errors++;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
await Promise.all(Array(concurrency).fill(null).map(() => worker()));
|
|
132
|
-
|
|
133
|
-
const totalTime = performance.now() - startTime;
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
name,
|
|
137
|
-
concurrency,
|
|
138
|
-
totalQueries: times.length + errors,
|
|
139
|
-
queriesPerSecond: (times.length / totalTime) * 1000,
|
|
140
|
-
avgLatency: times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0,
|
|
141
|
-
errorRate: (times.length + errors) > 0 ? errors / (times.length + errors) : 0
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Run a benchmark and print detailed results
|
|
147
|
-
*/
|
|
148
|
-
async runWithOutput(
|
|
149
|
-
name: string,
|
|
150
|
-
queryFn: () => Promise<any[]>,
|
|
151
|
-
options: BenchmarkOptions = {}
|
|
152
|
-
): Promise<BenchmarkResult> {
|
|
153
|
-
console.log(` Running: ${name}...`);
|
|
154
|
-
const result = await this.run(name, queryFn, options);
|
|
155
|
-
|
|
156
|
-
const status = result.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
157
|
-
console.log(
|
|
158
|
-
` ${status} p50=${result.timings.median.toFixed(1)}ms ` +
|
|
159
|
-
`p95=${result.timings.p95.toFixed(1)}ms ` +
|
|
160
|
-
`rows=${result.rowsReturned} ` +
|
|
161
|
-
`QPS=${result.queriesPerSecond.toFixed(0)}`
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
if (!result.passed && result.target) {
|
|
165
|
-
console.log(` Target: p95 <= ${result.target}ms`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return result;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private calculateStdDev(values: number[]): number {
|
|
172
|
-
if (values.length === 0) return 0;
|
|
173
|
-
const mean = values.reduce((a, b) => a + b) / values.length;
|
|
174
|
-
const squareDiffs = values.map(value => Math.pow(value - mean, 2));
|
|
175
|
-
return Math.sqrt(squareDiffs.reduce((a, b) => a + b) / values.length);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private async getRecordCount(): Promise<number> {
|
|
179
|
-
try {
|
|
180
|
-
const result = await db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NULL`;
|
|
181
|
-
return parseInt(result[0].count);
|
|
182
|
-
} catch {
|
|
183
|
-
return 0;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
getResults(): BenchmarkResult[] {
|
|
188
|
-
return [...this.results];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
clearResults(): void {
|
|
192
|
-
this.results = [];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Get summary statistics
|
|
197
|
-
*/
|
|
198
|
-
getSummary(): { passed: number; failed: number; total: number } {
|
|
199
|
-
const passed = this.results.filter(r => r.passed).length;
|
|
200
|
-
const failed = this.results.filter(r => !r.passed).length;
|
|
201
|
-
return { passed, failed, total: this.results.length };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data seeder for stress testing
|
|
3
|
-
* Performs optimized bulk inserts for maximum throughput
|
|
4
|
-
*/
|
|
5
|
-
import db from '../../database';
|
|
6
|
-
import { sql } from 'bun';
|
|
7
|
-
import { ComponentRegistry } from '../../core/components';
|
|
8
|
-
import { getMetadataStorage } from '../../core/metadata';
|
|
9
|
-
import { uuidv7 } from '../../utils/uuid';
|
|
10
|
-
import type { BaseComponent } from '../../core/components/BaseComponent';
|
|
11
|
-
|
|
12
|
-
export interface SeederOptions {
|
|
13
|
-
totalEntities: number;
|
|
14
|
-
batchSize: number;
|
|
15
|
-
onProgress?: (current: number, total: number, elapsedMs: number) => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface SeederResult {
|
|
19
|
-
entityIds: string[];
|
|
20
|
-
totalTime: number;
|
|
21
|
-
recordsPerSecond: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type ComponentConstructor = new () => BaseComponent;
|
|
25
|
-
|
|
26
|
-
export class DataSeeder {
|
|
27
|
-
/**
|
|
28
|
-
* Seeds the database with test entities and components
|
|
29
|
-
* Uses optimized bulk inserts for maximum throughput
|
|
30
|
-
*/
|
|
31
|
-
async seed<T extends BaseComponent>(
|
|
32
|
-
componentClass: ComponentConstructor,
|
|
33
|
-
dataGenerator: (index: number) => Record<string, any>,
|
|
34
|
-
options: SeederOptions
|
|
35
|
-
): Promise<SeederResult> {
|
|
36
|
-
const { totalEntities, batchSize, onProgress } = options;
|
|
37
|
-
const entityIds: string[] = [];
|
|
38
|
-
const startTime = performance.now();
|
|
39
|
-
|
|
40
|
-
// Ensure component is registered (just wait for readiness, don't trigger registration)
|
|
41
|
-
const componentName = componentClass.name;
|
|
42
|
-
await ComponentRegistry.getReadyPromise(componentName);
|
|
43
|
-
|
|
44
|
-
const storage = getMetadataStorage();
|
|
45
|
-
const typeId = storage.getComponentId(componentName);
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < totalEntities; i += batchSize) {
|
|
48
|
-
const currentBatch = Math.min(batchSize, totalEntities - i);
|
|
49
|
-
const now = new Date();
|
|
50
|
-
|
|
51
|
-
// Build batch data arrays
|
|
52
|
-
const entitiesToInsert: { id: string; created_at: Date; updated_at: Date }[] = [];
|
|
53
|
-
const componentsToInsert: { id: string; entity_id: string; type_id: string; name: string; data: any; created_at: Date; updated_at: Date }[] = [];
|
|
54
|
-
const entityComponentsToInsert: { entity_id: string; type_id: string; component_id: string; created_at: Date; updated_at: Date }[] = [];
|
|
55
|
-
|
|
56
|
-
// Generate batch data
|
|
57
|
-
for (let j = 0; j < currentBatch; j++) {
|
|
58
|
-
const entityId = uuidv7();
|
|
59
|
-
const componentId = uuidv7();
|
|
60
|
-
const data = dataGenerator(i + j);
|
|
61
|
-
|
|
62
|
-
entitiesToInsert.push({
|
|
63
|
-
id: entityId,
|
|
64
|
-
created_at: now,
|
|
65
|
-
updated_at: now
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
componentsToInsert.push({
|
|
69
|
-
id: componentId,
|
|
70
|
-
entity_id: entityId,
|
|
71
|
-
type_id: typeId,
|
|
72
|
-
name: componentName,
|
|
73
|
-
data: data,
|
|
74
|
-
created_at: now,
|
|
75
|
-
updated_at: now
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
entityComponentsToInsert.push({
|
|
79
|
-
entity_id: entityId,
|
|
80
|
-
type_id: typeId,
|
|
81
|
-
component_id: componentId,
|
|
82
|
-
created_at: now,
|
|
83
|
-
updated_at: now
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
entityIds.push(entityId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Bulk insert entities using Bun's sql helper
|
|
90
|
-
await db`INSERT INTO entities ${sql(entitiesToInsert, 'id', 'created_at', 'updated_at')}`;
|
|
91
|
-
|
|
92
|
-
// Bulk insert components
|
|
93
|
-
await db`INSERT INTO components ${sql(componentsToInsert, 'id', 'entity_id', 'type_id', 'name', 'data', 'created_at', 'updated_at')}`;
|
|
94
|
-
|
|
95
|
-
// Bulk insert entity_components index
|
|
96
|
-
await db`INSERT INTO entity_components ${sql(entityComponentsToInsert, 'entity_id', 'type_id', 'component_id', 'created_at', 'updated_at')} ON CONFLICT (entity_id, type_id) DO NOTHING`;
|
|
97
|
-
|
|
98
|
-
if (onProgress) {
|
|
99
|
-
onProgress(i + currentBatch, totalEntities, performance.now() - startTime);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const totalTime = performance.now() - startTime;
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
entityIds,
|
|
107
|
-
totalTime,
|
|
108
|
-
recordsPerSecond: (totalEntities / totalTime) * 1000
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Seeds multiple components for existing entities
|
|
114
|
-
*/
|
|
115
|
-
async seedAdditionalComponent<T extends BaseComponent>(
|
|
116
|
-
entityIds: string[],
|
|
117
|
-
componentClass: ComponentConstructor,
|
|
118
|
-
dataGenerator: (index: number, entityId: string) => Record<string, any>,
|
|
119
|
-
batchSize: number = 5000
|
|
120
|
-
): Promise<void> {
|
|
121
|
-
const componentName = componentClass.name;
|
|
122
|
-
await ComponentRegistry.getReadyPromise(componentName);
|
|
123
|
-
|
|
124
|
-
const storage = getMetadataStorage();
|
|
125
|
-
const typeId = storage.getComponentId(componentName);
|
|
126
|
-
|
|
127
|
-
for (let i = 0; i < entityIds.length; i += batchSize) {
|
|
128
|
-
const batchEntityIds = entityIds.slice(i, i + batchSize);
|
|
129
|
-
const now = new Date();
|
|
130
|
-
|
|
131
|
-
const componentsToInsert: { id: string; entity_id: string; type_id: string; name: string; data: any; created_at: Date; updated_at: Date }[] = [];
|
|
132
|
-
const entityComponentsToInsert: { entity_id: string; type_id: string; component_id: string; created_at: Date; updated_at: Date }[] = [];
|
|
133
|
-
|
|
134
|
-
for (let j = 0; j < batchEntityIds.length; j++) {
|
|
135
|
-
const entityId = batchEntityIds[j]!;
|
|
136
|
-
const componentId = uuidv7();
|
|
137
|
-
const data = dataGenerator(i + j, entityId);
|
|
138
|
-
|
|
139
|
-
componentsToInsert.push({
|
|
140
|
-
id: componentId,
|
|
141
|
-
entity_id: entityId,
|
|
142
|
-
type_id: typeId,
|
|
143
|
-
name: componentName,
|
|
144
|
-
data: data,
|
|
145
|
-
created_at: now,
|
|
146
|
-
updated_at: now
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
entityComponentsToInsert.push({
|
|
150
|
-
entity_id: entityId,
|
|
151
|
-
type_id: typeId,
|
|
152
|
-
component_id: componentId,
|
|
153
|
-
created_at: now,
|
|
154
|
-
updated_at: now
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
await db`INSERT INTO components ${sql(componentsToInsert, 'id', 'entity_id', 'type_id', 'name', 'data', 'created_at', 'updated_at')}`;
|
|
159
|
-
await db`INSERT INTO entity_components ${sql(entityComponentsToInsert, 'entity_id', 'type_id', 'component_id', 'created_at', 'updated_at')} ON CONFLICT (entity_id, type_id) DO NOTHING`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Cleans up seeded data
|
|
165
|
-
*/
|
|
166
|
-
async cleanup(entityIds: string[], batchSize: number = 10000): Promise<void> {
|
|
167
|
-
for (let i = 0; i < entityIds.length; i += batchSize) {
|
|
168
|
-
const batch = entityIds.slice(i, i + batchSize);
|
|
169
|
-
// Use individual deletes for reliability
|
|
170
|
-
await db`DELETE FROM entities WHERE id IN ${sql(batch.map(id => [id]))}`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Runs VACUUM ANALYZE for optimal query planning
|
|
176
|
-
*/
|
|
177
|
-
async optimize(): Promise<void> {
|
|
178
|
-
await db.unsafe('VACUUM ANALYZE entities');
|
|
179
|
-
await db.unsafe('VACUUM ANALYZE components');
|
|
180
|
-
await db.unsafe('VACUUM ANALYZE entity_components');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Gets the current record count
|
|
185
|
-
*/
|
|
186
|
-
async getRecordCount(): Promise<number> {
|
|
187
|
-
const result = await db`SELECT COUNT(*) as count FROM entities WHERE deleted_at IS NULL`;
|
|
188
|
-
return parseInt(result[0].count);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Report generator for stress test results
|
|
3
|
-
*/
|
|
4
|
-
import type { BenchmarkResult, ConcurrentResult } from './BenchmarkRunner';
|
|
5
|
-
|
|
6
|
-
export interface ReportMetadata {
|
|
7
|
-
recordCount: number;
|
|
8
|
-
environment: string;
|
|
9
|
-
duration?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class StressTestReporter {
|
|
13
|
-
generateReport(
|
|
14
|
-
results: BenchmarkResult[],
|
|
15
|
-
metadata: ReportMetadata
|
|
16
|
-
): string {
|
|
17
|
-
const lines: string[] = [];
|
|
18
|
-
|
|
19
|
-
lines.push('='.repeat(70));
|
|
20
|
-
lines.push(' BunSane Stress Test Report');
|
|
21
|
-
lines.push('='.repeat(70));
|
|
22
|
-
lines.push(`Date: ${new Date().toISOString()}`);
|
|
23
|
-
lines.push(`Records: ${metadata.recordCount.toLocaleString()}`);
|
|
24
|
-
lines.push(`Environment: ${metadata.environment}`);
|
|
25
|
-
if (metadata.duration) {
|
|
26
|
-
lines.push(`Duration: ${(metadata.duration / 1000).toFixed(1)}s`);
|
|
27
|
-
}
|
|
28
|
-
lines.push('');
|
|
29
|
-
lines.push('Query Performance:');
|
|
30
|
-
lines.push('-'.repeat(70));
|
|
31
|
-
lines.push(
|
|
32
|
-
this.padRight('Query', 35) +
|
|
33
|
-
this.padLeft('p50', 10) +
|
|
34
|
-
this.padLeft('p95', 10) +
|
|
35
|
-
this.padLeft('QPS', 8) +
|
|
36
|
-
this.padLeft('Status', 7)
|
|
37
|
-
);
|
|
38
|
-
lines.push('-'.repeat(70));
|
|
39
|
-
|
|
40
|
-
for (const result of results) {
|
|
41
|
-
const status = result.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
42
|
-
lines.push(
|
|
43
|
-
this.padRight(this.truncate(result.name, 34), 35) +
|
|
44
|
-
this.padLeft(`${result.timings.median.toFixed(1)}ms`, 10) +
|
|
45
|
-
this.padLeft(`${result.timings.p95.toFixed(1)}ms`, 10) +
|
|
46
|
-
this.padLeft(`${result.queriesPerSecond.toFixed(0)}`, 8) +
|
|
47
|
-
this.padLeft(status, 7)
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
lines.push('-'.repeat(70));
|
|
52
|
-
lines.push('');
|
|
53
|
-
|
|
54
|
-
// Summary
|
|
55
|
-
const passed = results.filter(r => r.passed).length;
|
|
56
|
-
const failed = results.filter(r => !r.passed).length;
|
|
57
|
-
|
|
58
|
-
lines.push(`Summary: ${passed} passed, ${failed} failed`);
|
|
59
|
-
|
|
60
|
-
if (failed > 0) {
|
|
61
|
-
lines.push('');
|
|
62
|
-
lines.push('Failed benchmarks:');
|
|
63
|
-
for (const result of results.filter(r => !r.passed)) {
|
|
64
|
-
lines.push(
|
|
65
|
-
` - ${result.name}: p95=${result.timings.p95.toFixed(1)}ms ` +
|
|
66
|
-
`(target: ${result.target}ms)`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Recommendations based on results
|
|
72
|
-
const recommendations = this.generateRecommendations(results);
|
|
73
|
-
if (recommendations.length > 0) {
|
|
74
|
-
lines.push('');
|
|
75
|
-
lines.push('Recommendations:');
|
|
76
|
-
for (const rec of recommendations) {
|
|
77
|
-
lines.push(` - ${rec}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return lines.join('\n');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
generateConcurrentReport(
|
|
85
|
-
results: ConcurrentResult[],
|
|
86
|
-
metadata: ReportMetadata
|
|
87
|
-
): string {
|
|
88
|
-
const lines: string[] = [];
|
|
89
|
-
|
|
90
|
-
lines.push('='.repeat(70));
|
|
91
|
-
lines.push(' BunSane Concurrent Load Test Report');
|
|
92
|
-
lines.push('='.repeat(70));
|
|
93
|
-
lines.push(`Date: ${new Date().toISOString()}`);
|
|
94
|
-
lines.push(`Records: ${metadata.recordCount.toLocaleString()}`);
|
|
95
|
-
lines.push(`Environment: ${metadata.environment}`);
|
|
96
|
-
lines.push('');
|
|
97
|
-
lines.push('Concurrent Performance:');
|
|
98
|
-
lines.push('-'.repeat(70));
|
|
99
|
-
lines.push(
|
|
100
|
-
this.padRight('Query', 25) +
|
|
101
|
-
this.padLeft('Concurrency', 12) +
|
|
102
|
-
this.padLeft('QPS', 10) +
|
|
103
|
-
this.padLeft('Avg Latency', 12) +
|
|
104
|
-
this.padLeft('Errors', 8)
|
|
105
|
-
);
|
|
106
|
-
lines.push('-'.repeat(70));
|
|
107
|
-
|
|
108
|
-
for (const result of results) {
|
|
109
|
-
lines.push(
|
|
110
|
-
this.padRight(this.truncate(result.name, 24), 25) +
|
|
111
|
-
this.padLeft(`${result.concurrency}`, 12) +
|
|
112
|
-
this.padLeft(`${result.queriesPerSecond.toFixed(0)}`, 10) +
|
|
113
|
-
this.padLeft(`${result.avgLatency.toFixed(1)}ms`, 12) +
|
|
114
|
-
this.padLeft(`${(result.errorRate * 100).toFixed(1)}%`, 8)
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
lines.push('-'.repeat(70));
|
|
119
|
-
|
|
120
|
-
return lines.join('\n');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
generateMarkdownReport(
|
|
124
|
-
results: BenchmarkResult[],
|
|
125
|
-
metadata: ReportMetadata
|
|
126
|
-
): string {
|
|
127
|
-
const lines: string[] = [];
|
|
128
|
-
|
|
129
|
-
lines.push('# BunSane Stress Test Report');
|
|
130
|
-
lines.push('');
|
|
131
|
-
lines.push(`**Date:** ${new Date().toISOString()}`);
|
|
132
|
-
lines.push(`**Records:** ${metadata.recordCount.toLocaleString()}`);
|
|
133
|
-
lines.push(`**Environment:** ${metadata.environment}`);
|
|
134
|
-
lines.push('');
|
|
135
|
-
lines.push('## Query Performance');
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push('| Query | p50 | p95 | p99 | QPS | Status |');
|
|
138
|
-
lines.push('|-------|-----|-----|-----|-----|--------|');
|
|
139
|
-
|
|
140
|
-
for (const result of results) {
|
|
141
|
-
const status = result.passed ? 'PASS' : 'FAIL';
|
|
142
|
-
lines.push(
|
|
143
|
-
`| ${result.name} | ` +
|
|
144
|
-
`${result.timings.median.toFixed(1)}ms | ` +
|
|
145
|
-
`${result.timings.p95.toFixed(1)}ms | ` +
|
|
146
|
-
`${result.timings.p99.toFixed(1)}ms | ` +
|
|
147
|
-
`${result.queriesPerSecond.toFixed(0)} | ` +
|
|
148
|
-
`${status} |`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
lines.push('');
|
|
153
|
-
|
|
154
|
-
// Summary
|
|
155
|
-
const passed = results.filter(r => r.passed).length;
|
|
156
|
-
const failed = results.filter(r => !r.passed).length;
|
|
157
|
-
|
|
158
|
-
lines.push('## Summary');
|
|
159
|
-
lines.push('');
|
|
160
|
-
lines.push(`- **Passed:** ${passed}`);
|
|
161
|
-
lines.push(`- **Failed:** ${failed}`);
|
|
162
|
-
lines.push(`- **Total:** ${results.length}`);
|
|
163
|
-
|
|
164
|
-
if (failed > 0) {
|
|
165
|
-
lines.push('');
|
|
166
|
-
lines.push('### Failed Benchmarks');
|
|
167
|
-
lines.push('');
|
|
168
|
-
for (const result of results.filter(r => !r.passed)) {
|
|
169
|
-
lines.push(
|
|
170
|
-
`- **${result.name}**: p95=${result.timings.p95.toFixed(1)}ms ` +
|
|
171
|
-
`(target: ${result.target}ms)`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return lines.join('\n');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private generateRecommendations(results: BenchmarkResult[]): string[] {
|
|
180
|
-
const recommendations: string[] = [];
|
|
181
|
-
|
|
182
|
-
// Check for slow non-indexed queries
|
|
183
|
-
const slowQueries = results.filter(r => r.timings.p95 > 500);
|
|
184
|
-
if (slowQueries.length > 0) {
|
|
185
|
-
recommendations.push(
|
|
186
|
-
`${slowQueries.length} queries have p95 > 500ms - consider adding indexes`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Check for high memory usage
|
|
191
|
-
const highMemory = results.filter(r => r.memoryUsedMB > 100);
|
|
192
|
-
if (highMemory.length > 0) {
|
|
193
|
-
recommendations.push(
|
|
194
|
-
`${highMemory.length} queries use >100MB memory - consider pagination or streaming`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Check for low QPS
|
|
199
|
-
const lowQps = results.filter(r => r.queriesPerSecond < 10);
|
|
200
|
-
if (lowQps.length > 0) {
|
|
201
|
-
recommendations.push(
|
|
202
|
-
`${lowQps.length} queries have QPS < 10 - investigate query plans with EXPLAIN ANALYZE`
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Check for high variance
|
|
207
|
-
const highVariance = results.filter(r => r.timings.stdDev > r.timings.mean * 0.5);
|
|
208
|
-
if (highVariance.length > 0) {
|
|
209
|
-
recommendations.push(
|
|
210
|
-
`${highVariance.length} queries show high variance - may indicate cache miss issues`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return recommendations;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private padRight(str: string, len: number): string {
|
|
218
|
-
return str.padEnd(len);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private padLeft(str: string, len: number): string {
|
|
222
|
-
return str.padStart(len);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private truncate(str: string, maxLen: number): string {
|
|
226
|
-
if (str.length <= maxLen) return str;
|
|
227
|
-
return str.slice(0, maxLen - 3) + '...';
|
|
228
|
-
}
|
|
229
|
-
}
|