@auraindustry/aurajs 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/benchmarks/perf-thresholds.json +102 -0
- package/package.json +47 -0
- package/src/.gitkeep +0 -0
- package/src/asset-pack.mjs +316 -0
- package/src/build-contract.mjs +71 -0
- package/src/bundler.mjs +698 -0
- package/src/cli.mjs +764 -0
- package/src/config.mjs +460 -0
- package/src/conformance-runner.mjs +322 -0
- package/src/conformance.mjs +5407 -0
- package/src/headless-test.mjs +4314 -0
- package/src/host-binary.mjs +423 -0
- package/src/perf-benchmark-runner.mjs +233 -0
- package/src/perf-benchmark.mjs +549 -0
- package/src/postinstall.mjs +26 -0
- package/src/scaffold.mjs +74 -0
- package/templates/starter/aura.config.json +28 -0
- package/templates/starter/src/main.js +226 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
accessSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
constants as fsConstants,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
const HOST_PACKAGE_SPECS = Object.freeze([
|
|
20
|
+
Object.freeze({
|
|
21
|
+
platform: 'darwin',
|
|
22
|
+
arch: 'arm64',
|
|
23
|
+
target: 'darwin-arm64',
|
|
24
|
+
packageName: '@aurajs/darwin-arm64',
|
|
25
|
+
binaryRelativePath: 'bin/aurajs-host',
|
|
26
|
+
}),
|
|
27
|
+
Object.freeze({
|
|
28
|
+
platform: 'darwin',
|
|
29
|
+
arch: 'x64',
|
|
30
|
+
target: 'darwin-x64',
|
|
31
|
+
packageName: '@aurajs/darwin-x64',
|
|
32
|
+
binaryRelativePath: 'bin/aurajs-host',
|
|
33
|
+
}),
|
|
34
|
+
Object.freeze({
|
|
35
|
+
platform: 'linux',
|
|
36
|
+
arch: 'x64',
|
|
37
|
+
target: 'linux-x64',
|
|
38
|
+
packageName: '@aurajs/linux-x64',
|
|
39
|
+
binaryRelativePath: 'bin/aurajs-host',
|
|
40
|
+
}),
|
|
41
|
+
Object.freeze({
|
|
42
|
+
platform: 'win32',
|
|
43
|
+
arch: 'x64',
|
|
44
|
+
target: 'win32-x64',
|
|
45
|
+
packageName: '@aurajs/win32-x64',
|
|
46
|
+
binaryRelativePath: 'bin/aurajs-host.exe',
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function readJsonFile(path) {
|
|
51
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findPackageRootFromResolvedFile(resolvedFile, packageName, readPackageJson) {
|
|
55
|
+
let cursor = dirname(resolvedFile);
|
|
56
|
+
while (true) {
|
|
57
|
+
const packageJsonPath = join(cursor, 'package.json');
|
|
58
|
+
try {
|
|
59
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
60
|
+
if (packageJson?.name === packageName) {
|
|
61
|
+
return cursor;
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
|
|
65
|
+
const parent = dirname(cursor);
|
|
66
|
+
if (parent === cursor) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
cursor = parent;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function supportedTargetsLabel() {
|
|
74
|
+
return HOST_PACKAGE_SPECS.map((spec) => `${spec.platform}-${spec.arch}`).join(', ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hashFile(path) {
|
|
78
|
+
const hash = createHash('sha256');
|
|
79
|
+
hash.update(readFileSync(path));
|
|
80
|
+
return hash.digest('hex');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function defaultCacheRoot() {
|
|
84
|
+
return process.env.AURAJS_HOST_CACHE_DIR || join(homedir(), '.cache', 'aurajs', 'host-binaries');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveOverrideHostBinaryPath({
|
|
88
|
+
platform = process.platform,
|
|
89
|
+
arch = process.arch,
|
|
90
|
+
fileExists = existsSync,
|
|
91
|
+
} = {}) {
|
|
92
|
+
const overridePath = process.env.AURAJS_HOST_BINARY_PATH;
|
|
93
|
+
if (!overridePath) return null;
|
|
94
|
+
|
|
95
|
+
const resolvedOverridePath = resolve(overridePath);
|
|
96
|
+
if (!fileExists(resolvedOverridePath)) {
|
|
97
|
+
throw new HostBinaryResolutionError(
|
|
98
|
+
`AURAJS_HOST_BINARY_PATH is set, but file is missing: "${resolvedOverridePath}".`,
|
|
99
|
+
{
|
|
100
|
+
code: 'AURA_HOST_BINARY_OVERRIDE_MISSING',
|
|
101
|
+
platform,
|
|
102
|
+
arch,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const spec = getHostPackageSpec({ platform, arch });
|
|
108
|
+
return {
|
|
109
|
+
...spec,
|
|
110
|
+
packageRoot: dirname(resolvedOverridePath),
|
|
111
|
+
binaryPath: resolvedOverridePath,
|
|
112
|
+
override: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safeUnlink(path) {
|
|
117
|
+
try {
|
|
118
|
+
unlinkSync(path);
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveCacheInfo(spec, binaryPath, packageRoot, packageVersion, cacheRoot) {
|
|
123
|
+
const binaryStats = statSync(binaryPath);
|
|
124
|
+
const cacheDir = resolve(cacheRoot, spec.target, packageVersion || 'unknown');
|
|
125
|
+
const cachedBinaryPath = resolve(cacheDir, basename(binaryPath));
|
|
126
|
+
const manifestPath = resolve(cacheDir, 'manifest.json');
|
|
127
|
+
return {
|
|
128
|
+
cacheDir,
|
|
129
|
+
cachedBinaryPath,
|
|
130
|
+
manifestPath,
|
|
131
|
+
sourceStat: {
|
|
132
|
+
size: binaryStats.size,
|
|
133
|
+
mtimeMs: binaryStats.mtimeMs,
|
|
134
|
+
mode: binaryStats.mode,
|
|
135
|
+
},
|
|
136
|
+
sourceBinaryPath: binaryPath,
|
|
137
|
+
packageRoot,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class HostBinaryResolutionError extends Error {
|
|
142
|
+
constructor(message, { code = 'AURA_HOST_BINARY_ERROR', platform, arch, packageName, cause } = {}) {
|
|
143
|
+
super(message, cause ? { cause } : undefined);
|
|
144
|
+
this.name = 'HostBinaryResolutionError';
|
|
145
|
+
this.code = code;
|
|
146
|
+
this.platform = platform;
|
|
147
|
+
this.arch = arch;
|
|
148
|
+
this.packageName = packageName;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getHostPackageSpec({ platform = process.platform, arch = process.arch } = {}) {
|
|
153
|
+
const spec = HOST_PACKAGE_SPECS.find((entry) => entry.platform === platform && entry.arch === arch);
|
|
154
|
+
if (!spec) {
|
|
155
|
+
throw new HostBinaryResolutionError(
|
|
156
|
+
`Unsupported host target "${platform}-${arch}". Supported targets: ${supportedTargetsLabel()}. Build on a supported platform or use CI cross-platform artifacts.`,
|
|
157
|
+
{ code: 'AURA_HOST_UNSUPPORTED_TARGET', platform, arch },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return spec;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveHostBinaryPath({
|
|
164
|
+
platform = process.platform,
|
|
165
|
+
arch = process.arch,
|
|
166
|
+
requireResolve = (specifier) => requireFromHere.resolve(specifier),
|
|
167
|
+
fileExists = existsSync,
|
|
168
|
+
readPackageJson = readJsonFile,
|
|
169
|
+
resolvePackageRoot,
|
|
170
|
+
} = {}) {
|
|
171
|
+
const override = resolveOverrideHostBinaryPath({ platform, arch, fileExists });
|
|
172
|
+
if (override) {
|
|
173
|
+
return override;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const spec = getHostPackageSpec({ platform, arch });
|
|
177
|
+
const packageRoot = resolvePackageRoot
|
|
178
|
+
? resolvePackageRoot(spec.packageName)
|
|
179
|
+
: findInstalledPackageRoot(spec.packageName, { requireResolve, readPackageJson });
|
|
180
|
+
|
|
181
|
+
const binaryPath = resolve(packageRoot, spec.binaryRelativePath);
|
|
182
|
+
if (!fileExists(binaryPath)) {
|
|
183
|
+
throw new HostBinaryResolutionError(
|
|
184
|
+
`Host package "${spec.packageName}" is installed, but binary is missing at "${binaryPath}". Reinstall dependencies to restore platform binaries.`,
|
|
185
|
+
{
|
|
186
|
+
code: 'AURA_HOST_BINARY_MISSING',
|
|
187
|
+
platform,
|
|
188
|
+
arch,
|
|
189
|
+
packageName: spec.packageName,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...spec,
|
|
196
|
+
packageRoot,
|
|
197
|
+
binaryPath,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function resolveAndCacheHostBinary({
|
|
202
|
+
platform = process.platform,
|
|
203
|
+
arch = process.arch,
|
|
204
|
+
cacheRoot = defaultCacheRoot(),
|
|
205
|
+
packageJsonVersion,
|
|
206
|
+
resolvePackageRoot,
|
|
207
|
+
} = {}) {
|
|
208
|
+
const resolved = resolveHostBinaryPath({ platform, arch, resolvePackageRoot });
|
|
209
|
+
|
|
210
|
+
if (resolved.override) {
|
|
211
|
+
return {
|
|
212
|
+
...resolved,
|
|
213
|
+
cacheDir: dirname(resolved.binaryPath),
|
|
214
|
+
cachedBinaryPath: resolved.binaryPath,
|
|
215
|
+
manifestPath: null,
|
|
216
|
+
cacheStatus: 'override',
|
|
217
|
+
diagnostics: ['Using AURAJS_HOST_BINARY_PATH override; cache step skipped.'],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let packageVersion = packageJsonVersion;
|
|
222
|
+
try {
|
|
223
|
+
const packageJson = readJsonFile(resolve(resolved.packageRoot, 'package.json'));
|
|
224
|
+
packageVersion = packageVersion || packageJson.version;
|
|
225
|
+
} catch {}
|
|
226
|
+
|
|
227
|
+
const cache = resolveCacheInfo(
|
|
228
|
+
resolved,
|
|
229
|
+
resolved.binaryPath,
|
|
230
|
+
resolved.packageRoot,
|
|
231
|
+
packageVersion,
|
|
232
|
+
cacheRoot,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
mkdirSync(cache.cacheDir, { recursive: true });
|
|
236
|
+
|
|
237
|
+
const sourceHash = hashFile(cache.sourceBinaryPath);
|
|
238
|
+
const manifest = {
|
|
239
|
+
packageName: resolved.packageName,
|
|
240
|
+
target: resolved.target,
|
|
241
|
+
sourceBinaryPath: cache.sourceBinaryPath,
|
|
242
|
+
packageRoot: cache.packageRoot,
|
|
243
|
+
size: cache.sourceStat.size,
|
|
244
|
+
mtimeMs: cache.sourceStat.mtimeMs,
|
|
245
|
+
mode: cache.sourceStat.mode,
|
|
246
|
+
sha256: sourceHash,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
let cacheHit = false;
|
|
250
|
+
if (existsSync(cache.cachedBinaryPath) && existsSync(cache.manifestPath)) {
|
|
251
|
+
try {
|
|
252
|
+
const prior = readJsonFile(cache.manifestPath);
|
|
253
|
+
const cachedStats = statSync(cache.cachedBinaryPath);
|
|
254
|
+
const cachedHash = hashFile(cache.cachedBinaryPath);
|
|
255
|
+
cacheHit = (
|
|
256
|
+
prior?.sha256 === sourceHash
|
|
257
|
+
&& prior?.size === cache.sourceStat.size
|
|
258
|
+
&& cachedStats.size === cache.sourceStat.size
|
|
259
|
+
&& cachedHash === sourceHash
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
cacheHit = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!cacheHit) {
|
|
267
|
+
safeUnlink(cache.cachedBinaryPath);
|
|
268
|
+
copyFileSync(cache.sourceBinaryPath, cache.cachedBinaryPath);
|
|
269
|
+
writeFileSync(cache.manifestPath, JSON.stringify(manifest, null, 2));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
...resolved,
|
|
274
|
+
cacheDir: cache.cacheDir,
|
|
275
|
+
cachedBinaryPath: cache.cachedBinaryPath,
|
|
276
|
+
manifestPath: cache.manifestPath,
|
|
277
|
+
cacheStatus: cacheHit ? 'hit' : 'refreshed',
|
|
278
|
+
binaryPath: cache.cachedBinaryPath,
|
|
279
|
+
diagnostics: cacheHit
|
|
280
|
+
? []
|
|
281
|
+
: ['Host binary cache was missing/stale/corrupt and has been refreshed from optionalDependencies.'],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findInstalledPackageRoot(packageName, { requireResolve, readPackageJson }) {
|
|
286
|
+
try {
|
|
287
|
+
const packageJsonPath = requireResolve(`${packageName}/package.json`);
|
|
288
|
+
return dirname(packageJsonPath);
|
|
289
|
+
} catch {
|
|
290
|
+
// Fall through to entrypoint search.
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let resolvedEntrypoint;
|
|
294
|
+
try {
|
|
295
|
+
resolvedEntrypoint = requireResolve(packageName);
|
|
296
|
+
} catch (cause) {
|
|
297
|
+
throw new HostBinaryResolutionError(
|
|
298
|
+
`Missing host package "${packageName}". Run "npm install" to install optional platform binaries.`,
|
|
299
|
+
{ code: 'AURA_HOST_PACKAGE_MISSING', packageName, cause },
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const packageRoot = findPackageRootFromResolvedFile(resolvedEntrypoint, packageName, readPackageJson);
|
|
304
|
+
if (!packageRoot) {
|
|
305
|
+
throw new HostBinaryResolutionError(
|
|
306
|
+
`Could not determine package root for "${packageName}" from "${resolvedEntrypoint}".`,
|
|
307
|
+
{ code: 'AURA_HOST_PACKAGE_INVALID', packageName },
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return packageRoot;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
function resolveLocalHostBinaryCandidates(searchRoot, binaryName) {
|
|
316
|
+
return [
|
|
317
|
+
resolve(searchRoot, 'src', 'rust-host', 'target', 'debug', binaryName),
|
|
318
|
+
resolve(searchRoot, 'src', 'rust-host', 'target', 'release', binaryName),
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function canExecuteFile(path) {
|
|
323
|
+
try {
|
|
324
|
+
accessSync(path, fsConstants.X_OK);
|
|
325
|
+
return true;
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findRepoRootWithRustHost(startDir) {
|
|
332
|
+
let cursor = resolve(startDir || process.cwd());
|
|
333
|
+
while (true) {
|
|
334
|
+
const cargoToml = resolve(cursor, 'src', 'rust-host', 'Cargo.toml');
|
|
335
|
+
if (existsSync(cargoToml)) {
|
|
336
|
+
return cursor;
|
|
337
|
+
}
|
|
338
|
+
const parent = dirname(cursor);
|
|
339
|
+
if (parent === cursor) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
cursor = parent;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resolveGateHostBinary({
|
|
347
|
+
platform = process.platform,
|
|
348
|
+
arch = process.arch,
|
|
349
|
+
searchFrom = process.cwd(),
|
|
350
|
+
cacheRoot,
|
|
351
|
+
resolveCachedBinary = (opts) => resolveAndCacheHostBinary(opts),
|
|
352
|
+
resolvePackagedBinary = (opts) => resolveHostBinaryPath(opts),
|
|
353
|
+
} = {}) {
|
|
354
|
+
const diagnostics = [];
|
|
355
|
+
const spec = getHostPackageSpec({ platform, arch });
|
|
356
|
+
const binaryName = basename(spec.binaryRelativePath);
|
|
357
|
+
const repoRoot = findRepoRootWithRustHost(searchFrom);
|
|
358
|
+
const localCandidates = repoRoot ? resolveLocalHostBinaryCandidates(repoRoot, binaryName) : [];
|
|
359
|
+
|
|
360
|
+
for (const candidate of localCandidates) {
|
|
361
|
+
if (!existsSync(candidate)) continue;
|
|
362
|
+
if (!canExecuteFile(candidate)) {
|
|
363
|
+
diagnostics.push(`Detected local host binary but it is not executable: ${candidate}`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
...spec,
|
|
369
|
+
binaryPath: candidate,
|
|
370
|
+
packageRoot: dirname(candidate),
|
|
371
|
+
source: 'local-build',
|
|
372
|
+
diagnostics,
|
|
373
|
+
localBuild: {
|
|
374
|
+
repoRoot,
|
|
375
|
+
checked: localCandidates,
|
|
376
|
+
},
|
|
377
|
+
resolvedAt: new Date().toISOString(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const buildHint = repoRoot
|
|
382
|
+
? `Missing local host build. Build one with: (cd ${repoRoot} && npm run build:rust)`
|
|
383
|
+
: 'Missing local host build. Build one with: npm run build:rust (from the aurascript package root).';
|
|
384
|
+
diagnostics.push(buildHint);
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const cached = resolveCachedBinary({ platform, arch, ...(cacheRoot ? { cacheRoot } : {}) });
|
|
388
|
+
return {
|
|
389
|
+
...cached,
|
|
390
|
+
source: cached.override ? 'override' : 'cache',
|
|
391
|
+
diagnostics: [...diagnostics, ...(cached.diagnostics || [])],
|
|
392
|
+
localBuild: {
|
|
393
|
+
repoRoot,
|
|
394
|
+
checked: localCandidates,
|
|
395
|
+
},
|
|
396
|
+
resolvedAt: new Date().toISOString(),
|
|
397
|
+
};
|
|
398
|
+
} catch (cacheError) {
|
|
399
|
+
diagnostics.push(`Cache fallback unavailable: ${cacheError.message || String(cacheError)}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const packaged = resolvePackagedBinary({ platform, arch });
|
|
403
|
+
return {
|
|
404
|
+
...packaged,
|
|
405
|
+
source: 'package',
|
|
406
|
+
diagnostics,
|
|
407
|
+
localBuild: {
|
|
408
|
+
repoRoot,
|
|
409
|
+
checked: localCandidates,
|
|
410
|
+
},
|
|
411
|
+
resolvedAt: new Date().toISOString(),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function listSupportedHostTargets() {
|
|
416
|
+
return HOST_PACKAGE_SPECS.map((spec) => Object.freeze({
|
|
417
|
+
platform: spec.platform,
|
|
418
|
+
arch: spec.arch,
|
|
419
|
+
target: spec.target,
|
|
420
|
+
packageName: spec.packageName,
|
|
421
|
+
binaryRelativePath: spec.binaryRelativePath,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PERF_EVALUATION_PATH,
|
|
8
|
+
evaluatePerfReport,
|
|
9
|
+
loadPerfThresholds,
|
|
10
|
+
runPerfBenchmarks,
|
|
11
|
+
} from './perf-benchmark.mjs';
|
|
12
|
+
|
|
13
|
+
function parseCliArgs(argv) {
|
|
14
|
+
let strict = false;
|
|
15
|
+
let repeats = 5;
|
|
16
|
+
let thresholdPath = null;
|
|
17
|
+
let reportPath = null;
|
|
18
|
+
let workspaceRoot = null;
|
|
19
|
+
let evaluationPath = DEFAULT_PERF_EVALUATION_PATH;
|
|
20
|
+
let assetMode = null;
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
23
|
+
const token = argv[i];
|
|
24
|
+
if (token === '--strict') {
|
|
25
|
+
strict = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (token === '--repeats' && argv[i + 1]) {
|
|
29
|
+
repeats = Number.parseInt(argv[i + 1], 10);
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (token === '--thresholds' && argv[i + 1]) {
|
|
34
|
+
thresholdPath = argv[i + 1];
|
|
35
|
+
i += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (token === '--report-path' && argv[i + 1]) {
|
|
39
|
+
reportPath = argv[i + 1];
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (token === '--workspace-root' && argv[i + 1]) {
|
|
44
|
+
workspaceRoot = argv[i + 1];
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (token === '--evaluation-path' && argv[i + 1]) {
|
|
49
|
+
evaluationPath = argv[i + 1];
|
|
50
|
+
i += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (token === '--asset-mode' && argv[i + 1]) {
|
|
54
|
+
assetMode = argv[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Unknown benchmark option: ${token}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!Number.isInteger(repeats) || repeats < 1) {
|
|
62
|
+
throw new Error('--repeats must be a positive integer');
|
|
63
|
+
}
|
|
64
|
+
if (strict && repeats < 2) {
|
|
65
|
+
throw new Error('--strict requires --repeats >= 2 to evaluate warm-cache thresholds');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
strict,
|
|
70
|
+
repeats,
|
|
71
|
+
thresholdPath,
|
|
72
|
+
reportPath,
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
assetMode,
|
|
75
|
+
evaluationPath: resolve(process.cwd(), evaluationPath),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mapApplicableScenes(report, sceneThresholds = {}) {
|
|
80
|
+
const reportSceneIds = new Set((report?.scenes || []).map((scene) => scene.id));
|
|
81
|
+
return Object.entries(sceneThresholds)
|
|
82
|
+
.filter(([sceneId]) => reportSceneIds.has(sceneId))
|
|
83
|
+
.map(([sceneId, limits]) => ({
|
|
84
|
+
id: sceneId,
|
|
85
|
+
maxAvgFrameTimeMs: limits.maxAvgFrameTimeMs,
|
|
86
|
+
maxJitterMs: limits.maxJitterMs,
|
|
87
|
+
minFps: limits.minFps,
|
|
88
|
+
maxP50FrameTimeMs: limits.maxP50FrameTimeMs,
|
|
89
|
+
maxP95FrameTimeMs: limits.maxP95FrameTimeMs,
|
|
90
|
+
maxP99FrameTimeMs: limits.maxP99FrameTimeMs,
|
|
91
|
+
maxStutterBurstCount: limits.maxStutterBurstCount,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bucketFailures(failures) {
|
|
96
|
+
return {
|
|
97
|
+
coldStart: failures.filter((entry) => String(entry.type || '').startsWith('coldStart.')),
|
|
98
|
+
warmCache: failures.filter((entry) => String(entry.type || '').startsWith('warmCache.')),
|
|
99
|
+
other: failures.filter((entry) => !String(entry.type || '').startsWith('coldStart.') && !String(entry.type || '').startsWith('warmCache.')),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildThresholdContext(report, thresholds, thresholdPath) {
|
|
104
|
+
const thresholdScenes = thresholds?.scenes || {};
|
|
105
|
+
return {
|
|
106
|
+
path: thresholdPath,
|
|
107
|
+
schemaVersion: thresholds?.schemaVersion || null,
|
|
108
|
+
scenePolicy: thresholds?.scenePolicy || null,
|
|
109
|
+
sceneCount: Object.keys(thresholdScenes).length,
|
|
110
|
+
applicableScenes: mapApplicableScenes(report, thresholdScenes),
|
|
111
|
+
coldStart: {
|
|
112
|
+
applicableScenes: mapApplicableScenes(report, thresholds?.coldStart?.scenes || {}),
|
|
113
|
+
},
|
|
114
|
+
warmCache: {
|
|
115
|
+
applicableScenes: mapApplicableScenes(report, thresholds?.warmCache?.scenes || {}),
|
|
116
|
+
},
|
|
117
|
+
latency: thresholds?.latency || null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeEvaluationArtifact(artifactPath, artifact) {
|
|
122
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
123
|
+
writeFileSync(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const {
|
|
128
|
+
strict,
|
|
129
|
+
repeats,
|
|
130
|
+
thresholdPath,
|
|
131
|
+
reportPath,
|
|
132
|
+
workspaceRoot,
|
|
133
|
+
assetMode,
|
|
134
|
+
evaluationPath,
|
|
135
|
+
} = parseCliArgs(process.argv.slice(2));
|
|
136
|
+
const report = await runPerfBenchmarks({
|
|
137
|
+
projectRoot: process.cwd(),
|
|
138
|
+
repeats,
|
|
139
|
+
reportPath,
|
|
140
|
+
workspaceRoot,
|
|
141
|
+
assetMode,
|
|
142
|
+
});
|
|
143
|
+
const { path: resolvedThresholdPath, thresholds } = loadPerfThresholds(thresholdPath);
|
|
144
|
+
const failures = evaluatePerfReport(report, thresholds);
|
|
145
|
+
const failureBuckets = bucketFailures(failures);
|
|
146
|
+
const status = strict && failures.length > 0 ? 'fail' : 'pass';
|
|
147
|
+
const evaluationArtifact = {
|
|
148
|
+
generatedAt: new Date().toISOString(),
|
|
149
|
+
status,
|
|
150
|
+
strict,
|
|
151
|
+
repeats,
|
|
152
|
+
thresholdPath: resolvedThresholdPath,
|
|
153
|
+
thresholdContext: buildThresholdContext(report, thresholds, resolvedThresholdPath),
|
|
154
|
+
reportPath: report.reportPath,
|
|
155
|
+
hostBinary: report.hostBinary || null,
|
|
156
|
+
assetMode: report?.config?.assetMode || null,
|
|
157
|
+
runtimeCacheDiagnostics: report.runtimeCacheDiagnostics || null,
|
|
158
|
+
failureCount: failures.length,
|
|
159
|
+
failureBuckets: {
|
|
160
|
+
coldStart: {
|
|
161
|
+
count: failureBuckets.coldStart.length,
|
|
162
|
+
failures: failureBuckets.coldStart,
|
|
163
|
+
},
|
|
164
|
+
warmCache: {
|
|
165
|
+
count: failureBuckets.warmCache.length,
|
|
166
|
+
failures: failureBuckets.warmCache,
|
|
167
|
+
},
|
|
168
|
+
other: {
|
|
169
|
+
count: failureBuckets.other.length,
|
|
170
|
+
failures: failureBuckets.other,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
failures,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
writeEvaluationArtifact(evaluationPath, evaluationArtifact);
|
|
177
|
+
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(' aura benchmark: complete');
|
|
180
|
+
console.log(` Report: ${report.reportPath}`);
|
|
181
|
+
console.log(` Thresholds: ${resolvedThresholdPath}`);
|
|
182
|
+
console.log(` Threshold evaluation: ${evaluationPath}`);
|
|
183
|
+
if (report.hostBinary) {
|
|
184
|
+
console.log(` Host binary source: ${report.hostBinary.source || 'unknown'}`);
|
|
185
|
+
console.log(` Host binary path: ${report.hostBinary.path}`);
|
|
186
|
+
for (const diagnostic of report.hostBinary.diagnostics || []) {
|
|
187
|
+
console.log(` Host diagnostic: ${diagnostic}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const scene of report.scenes) {
|
|
192
|
+
console.log(` Scene ${scene.id}: avgFrameTime=${scene.avgFrameTimeMs.toFixed(4)}ms jitter=${scene.jitterMs.toFixed(4)}ms avgFps=${scene.avgFps.toFixed(2)}`);
|
|
193
|
+
if (scene.coldStart) {
|
|
194
|
+
console.log(` coldStart: avgFrameTime=${scene.coldStart.avgFrameTimeMs.toFixed(4)}ms jitter=${scene.coldStart.jitterMs.toFixed(4)}ms avgFps=${scene.coldStart.avgFps.toFixed(2)} samples=${scene.coldStart.sampleCount}`);
|
|
195
|
+
}
|
|
196
|
+
if (scene.warmCache) {
|
|
197
|
+
console.log(` warmCache: avgFrameTime=${scene.warmCache.avgFrameTimeMs.toFixed(4)}ms jitter=${scene.warmCache.jitterMs.toFixed(4)}ms avgFps=${scene.warmCache.avgFps.toFixed(2)} samples=${scene.warmCache.sampleCount}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(` Synthetic latency: input=${report.latency.inputQueryMs.toFixed(6)}ms audio=${report.latency.audioCallMs.toFixed(6)}ms`);
|
|
202
|
+
|
|
203
|
+
if (failures.length > 0) {
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(` Threshold failures: ${failures.length}`);
|
|
206
|
+
console.log(` coldStart: ${failureBuckets.coldStart.length}`);
|
|
207
|
+
console.log(` warmCache: ${failureBuckets.warmCache.length}`);
|
|
208
|
+
console.log(` other: ${failureBuckets.other.length}`);
|
|
209
|
+
for (const failure of failures) {
|
|
210
|
+
if (failure.scene) {
|
|
211
|
+
console.log(` - ${failure.type} (${failure.scene}): actual=${failure.actual} threshold=${failure.threshold}`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(` - ${failure.type}: actual=${failure.actual} threshold=${failure.threshold}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (strict) {
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(' aura benchmark: FAIL (strict mode)');
|
|
220
|
+
console.log('');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(' aura benchmark: PASS');
|
|
227
|
+
console.log('');
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('');
|
|
230
|
+
console.error(` aura benchmark error: ${error.message}`);
|
|
231
|
+
console.error('');
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|