modestbench 0.0.1
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 +45 -0
- package/LICENSE.md +55 -0
- package/README.md +699 -0
- package/dist/bootstrap.cjs +37 -0
- package/dist/bootstrap.cjs.map +1 -0
- package/dist/bootstrap.d.cts +17 -0
- package/dist/bootstrap.d.cts.map +1 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +33 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli/commands/history.cjs +459 -0
- package/dist/cli/commands/history.cjs.map +1 -0
- package/dist/cli/commands/history.d.cts +34 -0
- package/dist/cli/commands/history.d.cts.map +1 -0
- package/dist/cli/commands/history.d.ts +34 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +422 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/init.cjs +566 -0
- package/dist/cli/commands/init.cjs.map +1 -0
- package/dist/cli/commands/init.d.cts +26 -0
- package/dist/cli/commands/init.d.cts.map +1 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +562 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.cjs +285 -0
- package/dist/cli/commands/run.cjs.map +1 -0
- package/dist/cli/commands/run.d.cts +37 -0
- package/dist/cli/commands/run.d.cts.map +1 -0
- package/dist/cli/commands/run.d.ts +37 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +248 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.cjs +523 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +58 -0
- package/dist/cli/index.d.cts.map +1 -0
- package/dist/cli/index.d.ts +58 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +515 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/manager.cjs +370 -0
- package/dist/config/manager.cjs.map +1 -0
- package/dist/config/manager.d.cts +46 -0
- package/dist/config/manager.d.cts.map +1 -0
- package/dist/config/manager.d.ts +46 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +333 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/schema.cjs +182 -0
- package/dist/config/schema.cjs.map +1 -0
- package/dist/config/schema.d.cts +51 -0
- package/dist/config/schema.d.cts.map +1 -0
- package/dist/config/schema.d.ts +51 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +145 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +10 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/benchmark-schema.cjs +135 -0
- package/dist/core/benchmark-schema.cjs.map +1 -0
- package/dist/core/benchmark-schema.d.cts +139 -0
- package/dist/core/benchmark-schema.d.cts.map +1 -0
- package/dist/core/benchmark-schema.d.ts +139 -0
- package/dist/core/benchmark-schema.d.ts.map +1 -0
- package/dist/core/benchmark-schema.js +132 -0
- package/dist/core/benchmark-schema.js.map +1 -0
- package/dist/core/engine.cjs +669 -0
- package/dist/core/engine.cjs.map +1 -0
- package/dist/core/engine.d.cts +128 -0
- package/dist/core/engine.d.cts.map +1 -0
- package/dist/core/engine.d.ts +128 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +632 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/engines/accurate-engine.cjs +292 -0
- package/dist/core/engines/accurate-engine.cjs.map +1 -0
- package/dist/core/engines/accurate-engine.d.cts +63 -0
- package/dist/core/engines/accurate-engine.d.cts.map +1 -0
- package/dist/core/engines/accurate-engine.d.ts +63 -0
- package/dist/core/engines/accurate-engine.d.ts.map +1 -0
- package/dist/core/engines/accurate-engine.js +288 -0
- package/dist/core/engines/accurate-engine.js.map +1 -0
- package/dist/core/engines/index.cjs +21 -0
- package/dist/core/engines/index.cjs.map +1 -0
- package/dist/core/engines/index.d.cts +16 -0
- package/dist/core/engines/index.d.cts.map +1 -0
- package/dist/core/engines/index.d.ts +16 -0
- package/dist/core/engines/index.d.ts.map +1 -0
- package/dist/core/engines/index.js +16 -0
- package/dist/core/engines/index.js.map +1 -0
- package/dist/core/engines/tinybench-engine.cjs +286 -0
- package/dist/core/engines/tinybench-engine.cjs.map +1 -0
- package/dist/core/engines/tinybench-engine.d.cts +18 -0
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
- package/dist/core/engines/tinybench-engine.d.ts +18 -0
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
- package/dist/core/engines/tinybench-engine.js +282 -0
- package/dist/core/engines/tinybench-engine.js.map +1 -0
- package/dist/core/error-manager.cjs +303 -0
- package/dist/core/error-manager.cjs.map +1 -0
- package/dist/core/error-manager.d.cts +77 -0
- package/dist/core/error-manager.d.cts.map +1 -0
- package/dist/core/error-manager.d.ts +77 -0
- package/dist/core/error-manager.d.ts.map +1 -0
- package/dist/core/error-manager.js +299 -0
- package/dist/core/error-manager.js.map +1 -0
- package/dist/core/loader.cjs +287 -0
- package/dist/core/loader.cjs.map +1 -0
- package/dist/core/loader.d.cts +55 -0
- package/dist/core/loader.d.cts.map +1 -0
- package/dist/core/loader.d.ts +55 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +250 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/stats-utils.cjs +99 -0
- package/dist/core/stats-utils.cjs.map +1 -0
- package/dist/core/stats-utils.d.cts +50 -0
- package/dist/core/stats-utils.d.cts.map +1 -0
- package/dist/core/stats-utils.d.ts +50 -0
- package/dist/core/stats-utils.d.ts.map +1 -0
- package/dist/core/stats-utils.js +94 -0
- package/dist/core/stats-utils.js.map +1 -0
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/progress/manager.cjs +325 -0
- package/dist/progress/manager.cjs.map +1 -0
- package/dist/progress/manager.d.cts +125 -0
- package/dist/progress/manager.d.cts.map +1 -0
- package/dist/progress/manager.d.ts +125 -0
- package/dist/progress/manager.d.ts.map +1 -0
- package/dist/progress/manager.js +321 -0
- package/dist/progress/manager.js.map +1 -0
- package/dist/reporters/csv.cjs +250 -0
- package/dist/reporters/csv.cjs.map +1 -0
- package/dist/reporters/csv.d.cts +92 -0
- package/dist/reporters/csv.d.cts.map +1 -0
- package/dist/reporters/csv.d.ts +92 -0
- package/dist/reporters/csv.d.ts.map +1 -0
- package/dist/reporters/csv.js +246 -0
- package/dist/reporters/csv.js.map +1 -0
- package/dist/reporters/human.cjs +516 -0
- package/dist/reporters/human.cjs.map +1 -0
- package/dist/reporters/human.d.cts +86 -0
- package/dist/reporters/human.d.cts.map +1 -0
- package/dist/reporters/human.d.ts +86 -0
- package/dist/reporters/human.d.ts.map +1 -0
- package/dist/reporters/human.js +509 -0
- package/dist/reporters/human.js.map +1 -0
- package/dist/reporters/index.cjs +17 -0
- package/dist/reporters/index.cjs.map +1 -0
- package/dist/reporters/index.d.cts +10 -0
- package/dist/reporters/index.d.cts.map +1 -0
- package/dist/reporters/index.d.ts +10 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +10 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json.cjs +215 -0
- package/dist/reporters/json.cjs.map +1 -0
- package/dist/reporters/json.d.cts +79 -0
- package/dist/reporters/json.d.cts.map +1 -0
- package/dist/reporters/json.d.ts +79 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +211 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/registry.cjs +255 -0
- package/dist/reporters/registry.cjs.map +1 -0
- package/dist/reporters/registry.d.cts +155 -0
- package/dist/reporters/registry.d.cts.map +1 -0
- package/dist/reporters/registry.d.ts +155 -0
- package/dist/reporters/registry.d.ts.map +1 -0
- package/dist/reporters/registry.js +249 -0
- package/dist/reporters/registry.js.map +1 -0
- package/dist/reporters/simple.cjs +328 -0
- package/dist/reporters/simple.cjs.map +1 -0
- package/dist/reporters/simple.d.cts +51 -0
- package/dist/reporters/simple.d.cts.map +1 -0
- package/dist/reporters/simple.d.ts +51 -0
- package/dist/reporters/simple.d.ts.map +1 -0
- package/dist/reporters/simple.js +321 -0
- package/dist/reporters/simple.js.map +1 -0
- package/dist/schema/modestbench-config.schema.json +162 -0
- package/dist/storage/history.cjs +456 -0
- package/dist/storage/history.cjs.map +1 -0
- package/dist/storage/history.d.cts +99 -0
- package/dist/storage/history.d.cts.map +1 -0
- package/dist/storage/history.d.ts +99 -0
- package/dist/storage/history.d.ts.map +1 -0
- package/dist/storage/history.js +452 -0
- package/dist/storage/history.js.map +1 -0
- package/dist/types/cli.cjs +21 -0
- package/dist/types/cli.cjs.map +1 -0
- package/dist/types/cli.d.cts +296 -0
- package/dist/types/cli.d.cts.map +1 -0
- package/dist/types/cli.d.ts +296 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +18 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/core.cjs +14 -0
- package/dist/types/core.cjs.map +1 -0
- package/dist/types/core.d.cts +380 -0
- package/dist/types/core.d.cts.map +1 -0
- package/dist/types/core.d.ts +380 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +13 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.cjs +27 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +11 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interfaces.cjs +10 -0
- package/dist/types/interfaces.cjs.map +1 -0
- package/dist/types/interfaces.d.cts +381 -0
- package/dist/types/interfaces.d.cts.map +1 -0
- package/dist/types/interfaces.d.ts +381 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/interfaces.js +9 -0
- package/dist/types/interfaces.js.map +1 -0
- package/dist/types/utility.cjs +92 -0
- package/dist/types/utility.cjs.map +1 -0
- package/dist/types/utility.d.cts +330 -0
- package/dist/types/utility.d.cts.map +1 -0
- package/dist/types/utility.d.ts +330 -0
- package/dist/types/utility.d.ts.map +1 -0
- package/dist/types/utility.js +78 -0
- package/dist/types/utility.js.map +1 -0
- package/package.json +211 -0
- package/src/bootstrap.ts +35 -0
- package/src/cli/commands/history.ts +569 -0
- package/src/cli/commands/init.ts +658 -0
- package/src/cli/commands/run.ts +346 -0
- package/src/cli/index.ts +642 -0
- package/src/config/manager.ts +387 -0
- package/src/config/schema.ts +188 -0
- package/src/constants.ts +21 -0
- package/src/core/benchmark-schema.ts +185 -0
- package/src/core/engine.ts +888 -0
- package/src/core/engines/accurate-engine.ts +408 -0
- package/src/core/engines/index.ts +16 -0
- package/src/core/engines/tinybench-engine.ts +335 -0
- package/src/core/error-manager.ts +372 -0
- package/src/core/loader.ts +324 -0
- package/src/core/stats-utils.ts +135 -0
- package/src/index.ts +46 -0
- package/src/progress/manager.ts +415 -0
- package/src/reporters/csv.ts +368 -0
- package/src/reporters/human.ts +707 -0
- package/src/reporters/index.ts +10 -0
- package/src/reporters/json.ts +302 -0
- package/src/reporters/registry.ts +349 -0
- package/src/reporters/simple.ts +459 -0
- package/src/storage/history.ts +600 -0
- package/src/types/cli.ts +312 -0
- package/src/types/core.ts +414 -0
- package/src/types/index.ts +18 -0
- package/src/types/interfaces.ts +451 -0
- package/src/types/utility.ts +446 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench History Storage
|
|
3
|
+
*
|
|
4
|
+
* File-based storage system for benchmark run history and results. Provides
|
|
5
|
+
* querying, cleanup, and export capabilities for historical data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
BenchmarkRun,
|
|
20
|
+
CleanupResult,
|
|
21
|
+
HistoryQuery,
|
|
22
|
+
HistoryStorage,
|
|
23
|
+
RetentionPolicy,
|
|
24
|
+
} from '../types/index.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Index entry for stored benchmark runs
|
|
28
|
+
*/
|
|
29
|
+
interface IndexEntry {
|
|
30
|
+
readonly date: Date;
|
|
31
|
+
readonly filename: string;
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly sizeBytes: number;
|
|
34
|
+
readonly summary: string;
|
|
35
|
+
readonly tags: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Storage index structure
|
|
40
|
+
*/
|
|
41
|
+
interface StorageIndex {
|
|
42
|
+
readonly created: Date;
|
|
43
|
+
readonly entries: IndexEntry[];
|
|
44
|
+
readonly lastModified: Date;
|
|
45
|
+
readonly version: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* File-based history storage implementation
|
|
50
|
+
*/
|
|
51
|
+
export class FileHistoryStorage implements HistoryStorage {
|
|
52
|
+
private index: null | StorageIndex = null;
|
|
53
|
+
|
|
54
|
+
private readonly indexFile: string;
|
|
55
|
+
|
|
56
|
+
private readonly maxFileSize: number;
|
|
57
|
+
|
|
58
|
+
private readonly storageDir: string;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
options: {
|
|
62
|
+
maxFileSize?: number;
|
|
63
|
+
storageDir?: string;
|
|
64
|
+
} = {},
|
|
65
|
+
) {
|
|
66
|
+
this.storageDir =
|
|
67
|
+
options.storageDir || join(process.cwd(), '.modestbench', 'history');
|
|
68
|
+
this.indexFile = join(this.storageDir, 'index.json');
|
|
69
|
+
this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB default
|
|
70
|
+
|
|
71
|
+
this.ensureStorageDir();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static isValidBenchmarkRun(obj: unknown): obj is BenchmarkRun {
|
|
75
|
+
// Type guard function that checks properties on unknown object
|
|
76
|
+
return (
|
|
77
|
+
!!obj &&
|
|
78
|
+
typeof obj === 'object' &&
|
|
79
|
+
'id' in obj &&
|
|
80
|
+
typeof obj.id === 'string' &&
|
|
81
|
+
'files' in obj &&
|
|
82
|
+
Array.isArray(obj.files) &&
|
|
83
|
+
'startTime' in obj &&
|
|
84
|
+
!!obj.startTime &&
|
|
85
|
+
'endTime' in obj &&
|
|
86
|
+
!!obj.endTime &&
|
|
87
|
+
'environment' in obj &&
|
|
88
|
+
!!obj.environment &&
|
|
89
|
+
'summary' in obj &&
|
|
90
|
+
!!obj.summary
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clean up old data according to retention policy
|
|
96
|
+
*/
|
|
97
|
+
async cleanup(policy: RetentionPolicy): Promise<CleanupResult> {
|
|
98
|
+
try {
|
|
99
|
+
const index = await this.loadIndex();
|
|
100
|
+
const entriesToRemove: IndexEntry[] = [];
|
|
101
|
+
let totalSize = 0;
|
|
102
|
+
|
|
103
|
+
// Calculate current storage metrics
|
|
104
|
+
for (const entry of index.entries) {
|
|
105
|
+
totalSize += entry.sizeBytes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Sort entries by date (oldest first) for cleanup
|
|
109
|
+
const sortedEntries = [...index.entries].sort(
|
|
110
|
+
(a, b) => a.date.getTime() - b.date.getTime(),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Apply retention policies
|
|
114
|
+
for (const entry of sortedEntries) {
|
|
115
|
+
let shouldRemove = false;
|
|
116
|
+
|
|
117
|
+
// Check max age
|
|
118
|
+
if (
|
|
119
|
+
policy.maxAge &&
|
|
120
|
+
Date.now() - entry.date.getTime() > policy.maxAge
|
|
121
|
+
) {
|
|
122
|
+
shouldRemove = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check max runs count (remove oldest)
|
|
126
|
+
if (
|
|
127
|
+
policy.maxRuns &&
|
|
128
|
+
index.entries.length - entriesToRemove.length > policy.maxRuns
|
|
129
|
+
) {
|
|
130
|
+
shouldRemove = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check max size (remove oldest until under limit)
|
|
134
|
+
if (policy.maxSize && totalSize > policy.maxSize) {
|
|
135
|
+
shouldRemove = true;
|
|
136
|
+
totalSize -= entry.sizeBytes;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (shouldRemove) {
|
|
140
|
+
entriesToRemove.push(entry);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Remove files and update index
|
|
145
|
+
const removedFiles: string[] = [];
|
|
146
|
+
let freedBytes = 0;
|
|
147
|
+
|
|
148
|
+
for (const entry of entriesToRemove) {
|
|
149
|
+
const filePath = join(this.storageDir, entry.filename);
|
|
150
|
+
try {
|
|
151
|
+
if (existsSync(filePath)) {
|
|
152
|
+
unlinkSync(filePath);
|
|
153
|
+
removedFiles.push(entry.filename);
|
|
154
|
+
freedBytes += entry.sizeBytes;
|
|
155
|
+
}
|
|
156
|
+
await this.removeFromIndex(entry.id);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Log but continue with other deletions
|
|
159
|
+
console.warn(`Failed to remove file ${entry.filename}: ${error}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
freedBytes,
|
|
165
|
+
removedFiles,
|
|
166
|
+
removedRuns: entriesToRemove.length,
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Failed to cleanup storage: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Export historical data
|
|
177
|
+
*/
|
|
178
|
+
async export(format: 'csv' | 'json', query?: HistoryQuery): Promise<string> {
|
|
179
|
+
try {
|
|
180
|
+
const runs = await this.queryRuns(query || {});
|
|
181
|
+
|
|
182
|
+
if (format === 'json') {
|
|
183
|
+
return JSON.stringify(runs, null, 2);
|
|
184
|
+
} else if (format === 'csv') {
|
|
185
|
+
return this.exportToCsv(runs);
|
|
186
|
+
} else {
|
|
187
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Failed to export data: ${error instanceof Error ? error.message : String(error)}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get index of all stored runs
|
|
198
|
+
*/
|
|
199
|
+
async getIndex(): Promise<
|
|
200
|
+
Array<{ date: Date; id: string; summary: string }>
|
|
201
|
+
> {
|
|
202
|
+
try {
|
|
203
|
+
const index = await this.loadIndex();
|
|
204
|
+
return index.entries.map((entry) => ({
|
|
205
|
+
date: entry.date,
|
|
206
|
+
id: entry.id,
|
|
207
|
+
summary: entry.summary,
|
|
208
|
+
}));
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Failed to get storage index: ${error instanceof Error ? error.message : String(error)}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get storage statistics
|
|
218
|
+
*/
|
|
219
|
+
async getStats(): Promise<{
|
|
220
|
+
newestRun?: Date | undefined;
|
|
221
|
+
oldestRun?: Date | undefined;
|
|
222
|
+
totalRuns: number;
|
|
223
|
+
totalSize: number;
|
|
224
|
+
}> {
|
|
225
|
+
try {
|
|
226
|
+
const index = await this.loadIndex();
|
|
227
|
+
const dates = index.entries
|
|
228
|
+
.map((e) => e.date)
|
|
229
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
newestRun: dates[dates.length - 1],
|
|
233
|
+
oldestRun: dates[0],
|
|
234
|
+
totalRuns: index.entries.length,
|
|
235
|
+
totalSize: index.entries.reduce(
|
|
236
|
+
(total, entry) => total + entry.sizeBytes,
|
|
237
|
+
0,
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
} catch {
|
|
241
|
+
return { totalRuns: 0, totalSize: 0 };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get storage directory path
|
|
247
|
+
*/
|
|
248
|
+
getStorageDir(): string {
|
|
249
|
+
return this.storageDir;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get total storage size in bytes
|
|
254
|
+
*/
|
|
255
|
+
async getStorageSize(): Promise<number> {
|
|
256
|
+
try {
|
|
257
|
+
const index = await this.loadIndex();
|
|
258
|
+
return index.entries.reduce((total, entry) => total + entry.sizeBytes, 0);
|
|
259
|
+
} catch {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Load a specific benchmark run
|
|
266
|
+
*/
|
|
267
|
+
async loadRun(id: string): Promise<BenchmarkRun | null> {
|
|
268
|
+
try {
|
|
269
|
+
const index = await this.loadIndex();
|
|
270
|
+
const entry = index.entries.find((e) => e.id === id);
|
|
271
|
+
|
|
272
|
+
if (!entry) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const filePath = join(this.storageDir, entry.filename);
|
|
277
|
+
|
|
278
|
+
if (!existsSync(filePath)) {
|
|
279
|
+
// File missing, clean up index
|
|
280
|
+
await this.removeFromIndex(id);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const data = readFileSync(filePath, 'utf8');
|
|
285
|
+
const run = JSON.parse(data) as BenchmarkRun;
|
|
286
|
+
|
|
287
|
+
// Validate the loaded run
|
|
288
|
+
if (!FileHistoryStorage.isValidBenchmarkRun(run)) {
|
|
289
|
+
throw new Error(`Invalid benchmark run data in file ${entry.filename}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return run;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Failed to load benchmark run ${id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Query historical runs
|
|
302
|
+
*/
|
|
303
|
+
async queryRuns(query: HistoryQuery): Promise<BenchmarkRun[]> {
|
|
304
|
+
try {
|
|
305
|
+
const index = await this.loadIndex();
|
|
306
|
+
let filteredEntries = [...index.entries];
|
|
307
|
+
|
|
308
|
+
// Apply filters
|
|
309
|
+
if (query.since) {
|
|
310
|
+
filteredEntries = filteredEntries.filter((e) => e.date >= query.since!);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (query.until) {
|
|
314
|
+
filteredEntries = filteredEntries.filter((e) => e.date <= query.until!);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (query.pattern) {
|
|
318
|
+
const regex = new RegExp(query.pattern, 'i');
|
|
319
|
+
filteredEntries = filteredEntries.filter((e) => regex.test(e.summary));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (query.tags && query.tags.length > 0) {
|
|
323
|
+
filteredEntries = filteredEntries.filter((e) =>
|
|
324
|
+
query.tags!.some((tag) => e.tags.includes(tag)),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Apply sorting
|
|
329
|
+
const sortBy = query.sortBy || 'date';
|
|
330
|
+
const sort = query.sort || 'desc';
|
|
331
|
+
|
|
332
|
+
filteredEntries.sort((a, b) => {
|
|
333
|
+
let comparison = 0;
|
|
334
|
+
|
|
335
|
+
switch (sortBy) {
|
|
336
|
+
case 'date':
|
|
337
|
+
comparison = a.date.getTime() - b.date.getTime();
|
|
338
|
+
break;
|
|
339
|
+
case 'name':
|
|
340
|
+
comparison = a.summary.localeCompare(b.summary);
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
comparison = a.date.getTime() - b.date.getTime();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return sort === 'desc' ? -comparison : comparison;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Apply pagination
|
|
350
|
+
const offset = query.offset || 0;
|
|
351
|
+
const limit = query.limit || filteredEntries.length;
|
|
352
|
+
const paginatedEntries = filteredEntries.slice(offset, offset + limit);
|
|
353
|
+
|
|
354
|
+
// Load the actual runs
|
|
355
|
+
const runs: BenchmarkRun[] = [];
|
|
356
|
+
for (const entry of paginatedEntries) {
|
|
357
|
+
const run = await this.loadRun(entry.id);
|
|
358
|
+
if (run) {
|
|
359
|
+
runs.push(run);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return runs;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Failed to query benchmark runs: ${error instanceof Error ? error.message : String(error)}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Save a benchmark run to storage
|
|
373
|
+
*/
|
|
374
|
+
async saveRun(run: BenchmarkRun): Promise<void> {
|
|
375
|
+
try {
|
|
376
|
+
this.ensureStorageDir();
|
|
377
|
+
|
|
378
|
+
// Generate filename based on run ID and timestamp
|
|
379
|
+
const filename = this.generateFilename(run);
|
|
380
|
+
const filePath = join(this.storageDir, filename);
|
|
381
|
+
|
|
382
|
+
// Serialize the run data
|
|
383
|
+
const data = JSON.stringify(run, null, 2);
|
|
384
|
+
|
|
385
|
+
// Check file size limit
|
|
386
|
+
if (Buffer.byteLength(data, 'utf8') > this.maxFileSize) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Benchmark run data exceeds maximum file size of ${this.maxFileSize} bytes`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Write the run data
|
|
393
|
+
writeFileSync(filePath, data, 'utf8');
|
|
394
|
+
|
|
395
|
+
// Update the index
|
|
396
|
+
await this.updateIndex(run, filename, Buffer.byteLength(data, 'utf8'));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Failed to save benchmark run: ${error instanceof Error ? error.message : String(error)}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Ensure storage directory exists
|
|
406
|
+
*/
|
|
407
|
+
private ensureStorageDir(): void {
|
|
408
|
+
if (!existsSync(this.storageDir)) {
|
|
409
|
+
mkdirSync(this.storageDir, { recursive: true });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Export runs to CSV format
|
|
415
|
+
*/
|
|
416
|
+
private exportToCsv(runs: BenchmarkRun[]): string {
|
|
417
|
+
const headers = [
|
|
418
|
+
'runId',
|
|
419
|
+
'startTime',
|
|
420
|
+
'endTime',
|
|
421
|
+
'duration',
|
|
422
|
+
'files',
|
|
423
|
+
'suites',
|
|
424
|
+
'tasks',
|
|
425
|
+
'passed',
|
|
426
|
+
'failed',
|
|
427
|
+
'nodeVersion',
|
|
428
|
+
'platform',
|
|
429
|
+
'arch',
|
|
430
|
+
'gitCommit',
|
|
431
|
+
'gitBranch',
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const rows = runs.map((run) => [
|
|
435
|
+
run.id,
|
|
436
|
+
run.startTime.toISOString(),
|
|
437
|
+
run.endTime.toISOString(),
|
|
438
|
+
run.duration.toString(),
|
|
439
|
+
run.summary.totalFiles.toString(),
|
|
440
|
+
run.summary.totalSuites.toString(),
|
|
441
|
+
run.summary.totalTasks.toString(),
|
|
442
|
+
run.summary.passedTasks.toString(),
|
|
443
|
+
run.summary.failedTasks.toString(),
|
|
444
|
+
run.environment.nodeVersion,
|
|
445
|
+
run.environment.platform,
|
|
446
|
+
run.environment.arch,
|
|
447
|
+
run.git?.commit || '',
|
|
448
|
+
run.git?.branch || '',
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
const csvLines = [headers, ...rows];
|
|
452
|
+
return csvLines
|
|
453
|
+
.map((row) =>
|
|
454
|
+
row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(','),
|
|
455
|
+
)
|
|
456
|
+
.join('\n');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Generate filename for a benchmark run
|
|
461
|
+
*/
|
|
462
|
+
private generateFilename(run: BenchmarkRun): string {
|
|
463
|
+
const timestamp = run.startTime.toISOString().replace(/[:.]/g, '-');
|
|
464
|
+
const hash = createHash('md5').update(run.id).digest('hex').substring(0, 8);
|
|
465
|
+
return `run-${timestamp}-${hash}.json`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Generate a human-readable summary for a run
|
|
470
|
+
*/
|
|
471
|
+
private generateSummary(run: BenchmarkRun): string {
|
|
472
|
+
const fileCount = run.files.length;
|
|
473
|
+
const taskCount = run.summary.totalTasks;
|
|
474
|
+
const failedCount = run.summary.failedTasks;
|
|
475
|
+
|
|
476
|
+
if (failedCount > 0) {
|
|
477
|
+
return `${fileCount} files, ${taskCount} tasks (${failedCount} failed)`;
|
|
478
|
+
} else {
|
|
479
|
+
return `${fileCount} files, ${taskCount} tasks`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Load the storage index
|
|
485
|
+
*/
|
|
486
|
+
private async loadIndex(): Promise<StorageIndex> {
|
|
487
|
+
if (this.index) {
|
|
488
|
+
return this.index;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!existsSync(this.indexFile)) {
|
|
492
|
+
this.index = {
|
|
493
|
+
created: new Date(),
|
|
494
|
+
entries: [],
|
|
495
|
+
lastModified: new Date(),
|
|
496
|
+
version: '1.0.0',
|
|
497
|
+
};
|
|
498
|
+
await this.saveIndex();
|
|
499
|
+
return this.index; // We just assigned it, so it's not null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const data = readFileSync(this.indexFile, 'utf8');
|
|
504
|
+
const parsed = JSON.parse(data) as {
|
|
505
|
+
[key: string]: unknown;
|
|
506
|
+
created: string;
|
|
507
|
+
entries: Array<{ [key: string]: unknown; date: string }>;
|
|
508
|
+
lastModified: string;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Convert date strings back to Date objects
|
|
512
|
+
this.index = {
|
|
513
|
+
created: new Date(parsed.created),
|
|
514
|
+
entries: parsed.entries.map((entry) => ({
|
|
515
|
+
...entry,
|
|
516
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
|
517
|
+
date: new Date((entry as any).date),
|
|
518
|
+
})) as IndexEntry[],
|
|
519
|
+
lastModified: new Date(parsed.lastModified),
|
|
520
|
+
version: '1.0.0',
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
return this.index; // We just assigned it, so it's not null
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`Failed to load storage index: ${error instanceof Error ? error.message : String(error)}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Remove an entry from the index
|
|
533
|
+
*/
|
|
534
|
+
private async removeFromIndex(id: string): Promise<void> {
|
|
535
|
+
const index = await this.loadIndex();
|
|
536
|
+
const entryIndex = index.entries.findIndex((e) => e.id === id);
|
|
537
|
+
|
|
538
|
+
if (entryIndex >= 0) {
|
|
539
|
+
index.entries.splice(entryIndex, 1);
|
|
540
|
+
this.index = index;
|
|
541
|
+
await this.saveIndex();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Save the storage index
|
|
547
|
+
*/
|
|
548
|
+
private async saveIndex(): Promise<void> {
|
|
549
|
+
if (!this.index) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
this.index = {
|
|
555
|
+
...this.index,
|
|
556
|
+
lastModified: new Date(),
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const data = JSON.stringify(this.index, null, 2);
|
|
560
|
+
writeFileSync(this.indexFile, data, 'utf8');
|
|
561
|
+
} catch (error) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`Failed to save storage index: ${error instanceof Error ? error.message : String(error)}`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Update index with a new run
|
|
570
|
+
*/
|
|
571
|
+
private async updateIndex(
|
|
572
|
+
run: BenchmarkRun,
|
|
573
|
+
filename: string,
|
|
574
|
+
sizeBytes: number,
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
const index = await this.loadIndex();
|
|
577
|
+
|
|
578
|
+
const entry: IndexEntry = {
|
|
579
|
+
date: run.startTime,
|
|
580
|
+
filename,
|
|
581
|
+
id: run.id,
|
|
582
|
+
sizeBytes,
|
|
583
|
+
summary: this.generateSummary(run),
|
|
584
|
+
tags: run.tags || [],
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Remove existing entry if it exists
|
|
588
|
+
const existingIndex = index.entries.findIndex((e) => e.id === run.id);
|
|
589
|
+
if (existingIndex >= 0) {
|
|
590
|
+
index.entries.splice(existingIndex, 1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Add new entry
|
|
594
|
+
index.entries.push(entry);
|
|
595
|
+
|
|
596
|
+
// Update index
|
|
597
|
+
this.index = index;
|
|
598
|
+
await this.saveIndex();
|
|
599
|
+
}
|
|
600
|
+
}
|