@framedash/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -5
- package/dist/commands/alerts.d.ts.map +1 -1
- package/dist/commands/alerts.js +2 -1
- package/dist/commands/alerts.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +25 -2
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/builds.d.ts +2 -0
- package/dist/commands/builds.d.ts.map +1 -0
- package/dist/commands/builds.js +31 -0
- package/dist/commands/builds.js.map +1 -0
- package/dist/commands/content.d.ts.map +1 -1
- package/dist/commands/content.js +2 -2
- package/dist/commands/content.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +3 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/funnel.d.ts.map +1 -1
- package/dist/commands/funnel.js +7 -8
- package/dist/commands/funnel.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +142 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +79 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/map-capture.d.ts +2 -1
- package/dist/commands/map-capture.d.ts.map +1 -1
- package/dist/commands/map-capture.js +42 -12
- package/dist/commands/map-capture.js.map +1 -1
- package/dist/commands/perf-diff.d.ts +2 -0
- package/dist/commands/perf-diff.d.ts.map +1 -0
- package/dist/commands/perf-diff.js +125 -0
- package/dist/commands/perf-diff.js.map +1 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +12 -0
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/retention.d.ts.map +1 -1
- package/dist/commands/retention.js +3 -3
- package/dist/commands/retention.js.map +1 -1
- package/dist/commands/run-profile-test.d.ts +7 -0
- package/dist/commands/run-profile-test.d.ts.map +1 -0
- package/dist/commands/run-profile-test.js +503 -0
- package/dist/commands/run-profile-test.js.map +1 -0
- package/dist/commands/threshold-profiles.d.ts +2 -0
- package/dist/commands/threshold-profiles.d.ts.map +1 -0
- package/dist/commands/threshold-profiles.js +24 -0
- package/dist/commands/threshold-profiles.js.map +1 -0
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +42 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +44 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/create-client.d.ts +27 -1
- package/dist/lib/create-client.d.ts.map +1 -1
- package/dist/lib/create-client.js +167 -24
- package/dist/lib/create-client.js.map +1 -1
- package/dist/lib/formatters.js +93 -1
- package/dist/lib/formatters.js.map +1 -1
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +4 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/oauth/loopback-server.d.ts +27 -0
- package/dist/lib/oauth/loopback-server.d.ts.map +1 -0
- package/dist/lib/oauth/loopback-server.js +164 -0
- package/dist/lib/oauth/loopback-server.js.map +1 -0
- package/dist/lib/oauth/manager.d.ts +36 -0
- package/dist/lib/oauth/manager.d.ts.map +1 -0
- package/dist/lib/oauth/manager.js +130 -0
- package/dist/lib/oauth/manager.js.map +1 -0
- package/dist/lib/oauth/pkce.d.ts +15 -0
- package/dist/lib/oauth/pkce.d.ts.map +1 -0
- package/dist/lib/oauth/pkce.js +24 -0
- package/dist/lib/oauth/pkce.js.map +1 -0
- package/dist/lib/oauth/token-endpoint.d.ts +39 -0
- package/dist/lib/oauth/token-endpoint.d.ts.map +1 -0
- package/dist/lib/oauth/token-endpoint.js +117 -0
- package/dist/lib/oauth/token-endpoint.js.map +1 -0
- package/dist/lib/oauth/token-store.d.ts +34 -0
- package/dist/lib/oauth/token-store.d.ts.map +1 -0
- package/dist/lib/oauth/token-store.js +164 -0
- package/dist/lib/oauth/token-store.js.map +1 -0
- package/dist/lib/perf-diff-eval.d.ts +58 -0
- package/dist/lib/perf-diff-eval.d.ts.map +1 -0
- package/dist/lib/perf-diff-eval.js +49 -0
- package/dist/lib/perf-diff-eval.js.map +1 -0
- package/dist/lib/run-command.js +2 -2
- package/dist/lib/run-command.js.map +1 -1
- package/dist/lib/run-profile-test-lib.d.ts +156 -0
- package/dist/lib/run-profile-test-lib.d.ts.map +1 -0
- package/dist/lib/run-profile-test-lib.js +171 -0
- package/dist/lib/run-profile-test-lib.js.map +1 -0
- package/dist/lib/uploader.d.ts +14 -1
- package/dist/lib/uploader.d.ts.map +1 -1
- package/dist/lib/uploader.js +26 -12
- package/dist/lib/uploader.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
/** Resolve the credentials file path (honors XDG_CONFIG_HOME at call time). */
|
|
6
|
+
export function credentialsFilePath() {
|
|
7
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
8
|
+
const configHome = xdg && xdg.trim() !== "" ? xdg : join(homedir(), ".config");
|
|
9
|
+
return join(configHome, "framedash", "credentials.json");
|
|
10
|
+
}
|
|
11
|
+
function isStoredTokenEntry(value) {
|
|
12
|
+
if (typeof value !== "object" || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
const entry = value;
|
|
15
|
+
return (typeof entry.access_token === "string" &&
|
|
16
|
+
entry.access_token !== "" &&
|
|
17
|
+
typeof entry.refresh_token === "string" &&
|
|
18
|
+
entry.refresh_token !== "" &&
|
|
19
|
+
typeof entry.expires_at === "number" &&
|
|
20
|
+
Number.isFinite(entry.expires_at) &&
|
|
21
|
+
typeof entry.scope === "string");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the whole token store. Missing, unreadable, or corrupt files -- and
|
|
25
|
+
* individual malformed entries -- degrade to "absent" rather than throwing:
|
|
26
|
+
* the caller then falls back to other credential sources or asks the user to
|
|
27
|
+
* log in again.
|
|
28
|
+
*/
|
|
29
|
+
export function readTokenStore() {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(credentialsFilePath(), "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const store = {};
|
|
48
|
+
for (const [origin, entry] of Object.entries(parsed)) {
|
|
49
|
+
if (isStoredTokenEntry(entry)) {
|
|
50
|
+
store[origin] = entry;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return store;
|
|
54
|
+
}
|
|
55
|
+
/** Read the stored entry for one base-URL origin, if any. */
|
|
56
|
+
export function readStoredEntry(origin) {
|
|
57
|
+
const store = readTokenStore();
|
|
58
|
+
// Own-property guard: a prototype-chain key (e.g. "toString") must never
|
|
59
|
+
// masquerade as a stored entry.
|
|
60
|
+
return Object.hasOwn(store, origin) ? store[origin] : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reap orphaned credentials.json.*.tmp files left by crashed writes -- they
|
|
64
|
+
* hold the same token material as the store itself. Called from every write
|
|
65
|
+
* and from clearTokenStore, so any mutation heals a previous crash.
|
|
66
|
+
*/
|
|
67
|
+
function reapOrphanTmps() {
|
|
68
|
+
const filePath = credentialsFilePath();
|
|
69
|
+
const base = `${filePath}.`;
|
|
70
|
+
let names;
|
|
71
|
+
try {
|
|
72
|
+
names = readdirSync(dirname(filePath));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return; // Config dir itself is gone: nothing to reap.
|
|
76
|
+
}
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
const full = join(dirname(filePath), name);
|
|
79
|
+
if (full.startsWith(base) && full.endsWith(".tmp")) {
|
|
80
|
+
// Best-effort: reaping runs AFTER the atomic rename has already
|
|
81
|
+
// committed the real write, so a cleanup failure (locked file on
|
|
82
|
+
// Windows, odd FS state) must never propagate and make a successful
|
|
83
|
+
// saveStoredEntry look failed. recursive covers a dir-shaped orphan.
|
|
84
|
+
try {
|
|
85
|
+
rmSync(full, { force: true, recursive: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Leave the orphan; the next mutation will retry the reap.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Persist the whole store atomically: write a temp file in the target
|
|
95
|
+
* directory, then rename over the destination (rename replaces existing
|
|
96
|
+
* files on both POSIX and Windows), so readers always see a complete JSON
|
|
97
|
+
* document. Concurrent writers are last-writer-wins by design (see the
|
|
98
|
+
* concurrency model note above).
|
|
99
|
+
*/
|
|
100
|
+
function writeTokenStore(store) {
|
|
101
|
+
const filePath = credentialsFilePath();
|
|
102
|
+
const dir = dirname(filePath);
|
|
103
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
104
|
+
try {
|
|
105
|
+
// mkdirSync's mode only applies on creation; tighten a pre-existing
|
|
106
|
+
// directory too so credentials.json never sits in a world-readable dir.
|
|
107
|
+
// Best-effort no-op on Windows.
|
|
108
|
+
chmodSync(dir, 0o700);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best effort only.
|
|
112
|
+
}
|
|
113
|
+
const tmpPath = `${filePath}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(tmpPath, `${JSON.stringify(store, null, "\t")}\n`, { mode: 0o600 });
|
|
116
|
+
renameSync(tmpPath, filePath);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
rmSync(tmpPath, { force: true });
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Re-assert the mode in case the file pre-existed with wider permissions;
|
|
124
|
+
// best-effort no-op on Windows.
|
|
125
|
+
chmodSync(filePath, 0o600);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Best effort only.
|
|
129
|
+
}
|
|
130
|
+
reapOrphanTmps();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Insert or replace the entry for an origin (atomic write). Async signature
|
|
134
|
+
* kept for call-site stability even though the body is synchronous.
|
|
135
|
+
*/
|
|
136
|
+
export async function saveStoredEntry(origin, entry) {
|
|
137
|
+
const store = readTokenStore();
|
|
138
|
+
store[origin] = entry;
|
|
139
|
+
writeTokenStore(store);
|
|
140
|
+
}
|
|
141
|
+
/** Remove the entry for an origin. Returns true if an entry was removed. */
|
|
142
|
+
export async function deleteStoredEntry(origin) {
|
|
143
|
+
const store = readTokenStore();
|
|
144
|
+
// Object.hasOwn (not `in`): prototype-chain keys are not stored entries
|
|
145
|
+
// and must not trigger a rewrite.
|
|
146
|
+
if (!Object.hasOwn(store, origin))
|
|
147
|
+
return false;
|
|
148
|
+
delete store[origin];
|
|
149
|
+
writeTokenStore(store);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Delete the whole credentials file (logout --all), including any orphaned
|
|
154
|
+
* credentials.json.*.tmp files a crashed write left behind -- those hold the
|
|
155
|
+
* same token material as the store itself.
|
|
156
|
+
*/
|
|
157
|
+
export async function clearTokenStore() {
|
|
158
|
+
// recursive so a credentials path that is unexpectedly a directory
|
|
159
|
+
// (corruption / manual tampering) is still removed rather than throwing
|
|
160
|
+
// and blocking logout --all from clearing on-disk token material.
|
|
161
|
+
rmSync(credentialsFilePath(), { force: true, recursive: true });
|
|
162
|
+
reapOrphanTmps();
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../../src/lib/oauth/token-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure regression-gate logic for `framedash perf-diff`, kept free of I/O so the
|
|
3
|
+
* CI pass/fail decision is unit-testable. The shapes mirror the builds/compare
|
|
4
|
+
* API response (apps/web .../builds/compare); the CLI cannot import the web
|
|
5
|
+
* app's types, so they are restated here.
|
|
6
|
+
*/
|
|
7
|
+
/** Lower-is-better performance metrics perf-diff can gate on. */
|
|
8
|
+
export type RegressionMetric = "frame_time" | "memory" | "gpu_time";
|
|
9
|
+
export declare const REGRESSION_METRICS: readonly RegressionMetric[];
|
|
10
|
+
export declare function isRegressionMetric(value: string): value is RegressionMetric;
|
|
11
|
+
/** One metric's diff, as returned by the builds/compare API. */
|
|
12
|
+
export interface ApiMetricDiff {
|
|
13
|
+
metric: RegressionMetric;
|
|
14
|
+
baselineP50: number | null;
|
|
15
|
+
candidateP50: number | null;
|
|
16
|
+
/** Signed percent change of candidate P50 vs baseline P50; null = not comparable. */
|
|
17
|
+
diffPct: number | null;
|
|
18
|
+
isRegression: boolean;
|
|
19
|
+
baselineTail: number | null;
|
|
20
|
+
candidateTail: number | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ApiBuildComparison {
|
|
23
|
+
baseline: {
|
|
24
|
+
build_id: string;
|
|
25
|
+
};
|
|
26
|
+
candidate: {
|
|
27
|
+
build_id: string;
|
|
28
|
+
};
|
|
29
|
+
diffs: ApiMetricDiff[];
|
|
30
|
+
}
|
|
31
|
+
export interface RegressionVerdict {
|
|
32
|
+
/** True when at least one evaluated metric regressed beyond the threshold. */
|
|
33
|
+
failed: boolean;
|
|
34
|
+
/** Metrics that regressed beyond the threshold. */
|
|
35
|
+
offenders: ApiMetricDiff[];
|
|
36
|
+
/** Metrics actually evaluated (matching the metric filter, with comparable data). */
|
|
37
|
+
evaluated: ApiMetricDiff[];
|
|
38
|
+
}
|
|
39
|
+
export interface EvaluateOptions {
|
|
40
|
+
/** Restrict the gate to a single metric; default checks all. */
|
|
41
|
+
metric?: RegressionMetric;
|
|
42
|
+
/**
|
|
43
|
+
* A metric fails only when its P50 worsened by MORE than this percentage,
|
|
44
|
+
* letting CI tolerate run-to-run noise. Default 0 = any worsening fails.
|
|
45
|
+
*/
|
|
46
|
+
thresholdPct?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Decide whether a build comparison should fail a CI gate. All regression
|
|
50
|
+
* metrics are lower-is-better, so a positive diffPct (candidate P50 above
|
|
51
|
+
* baseline) is a regression. A null diffPct (no baseline data, or a metric
|
|
52
|
+
* unavailable on the platform, e.g. GPU timing) is not comparable and is
|
|
53
|
+
* skipped -- neither an offender nor counted as evaluated.
|
|
54
|
+
*/
|
|
55
|
+
export declare function evaluateRegression(comparison: ApiBuildComparison, options?: EvaluateOptions): RegressionVerdict;
|
|
56
|
+
/** A one-line human summary of a single metric diff (for CI logs). */
|
|
57
|
+
export declare function formatMetricDiff(d: ApiMetricDiff): string;
|
|
58
|
+
//# sourceMappingURL=perf-diff-eval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perf-diff-eval.d.ts","sourceRoot":"","sources":["../../src/lib/perf-diff-eval.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEpE,eAAO,MAAM,kBAAkB,EAAE,SAAS,gBAAgB,EAAyC,CAAC;AAEpG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,qFAAqF;IACrF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,KAAK,EAAE,aAAa,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,qFAAqF;IACrF,SAAS,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,GAAE,eAAoB,GAC3B,iBAAiB,CAmBnB;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAQzD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure regression-gate logic for `framedash perf-diff`, kept free of I/O so the
|
|
3
|
+
* CI pass/fail decision is unit-testable. The shapes mirror the builds/compare
|
|
4
|
+
* API response (apps/web .../builds/compare); the CLI cannot import the web
|
|
5
|
+
* app's types, so they are restated here.
|
|
6
|
+
*/
|
|
7
|
+
export const REGRESSION_METRICS = ["frame_time", "memory", "gpu_time"];
|
|
8
|
+
export function isRegressionMetric(value) {
|
|
9
|
+
return REGRESSION_METRICS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decide whether a build comparison should fail a CI gate. All regression
|
|
13
|
+
* metrics are lower-is-better, so a positive diffPct (candidate P50 above
|
|
14
|
+
* baseline) is a regression. A null diffPct (no baseline data, or a metric
|
|
15
|
+
* unavailable on the platform, e.g. GPU timing) is not comparable and is
|
|
16
|
+
* skipped -- neither an offender nor counted as evaluated.
|
|
17
|
+
*/
|
|
18
|
+
export function evaluateRegression(comparison, options = {}) {
|
|
19
|
+
// Clamp a non-finite or negative threshold to 0 (the strictest gate) so a bad
|
|
20
|
+
// value fails safe rather than silently disabling the gate -- `x > Infinity`
|
|
21
|
+
// and `x > NaN` are both always false, which would pass every regression. The
|
|
22
|
+
// command validates --threshold too; this keeps the exported helper robust on
|
|
23
|
+
// its own.
|
|
24
|
+
const threshold = Number.isFinite(options.thresholdPct) && options.thresholdPct >= 0
|
|
25
|
+
? options.thresholdPct
|
|
26
|
+
: 0;
|
|
27
|
+
// Defensive: a malformed response (missing diffs) yields no evaluable metrics,
|
|
28
|
+
// which the caller treats as "nothing comparable" -> fail-closed. `typeof ===
|
|
29
|
+
// "number"` (not `!== null`) also skips an undefined/missing diffPct safely.
|
|
30
|
+
const diffs = Array.isArray(comparison?.diffs) ? comparison.diffs : [];
|
|
31
|
+
const evaluated = diffs.filter((d) => (!options.metric || d.metric === options.metric) && typeof d.diffPct === "number");
|
|
32
|
+
const offenders = evaluated.filter((d) => d.diffPct > threshold);
|
|
33
|
+
return { failed: offenders.length > 0, offenders, evaluated };
|
|
34
|
+
}
|
|
35
|
+
/** A one-line human summary of a single metric diff (for CI logs). */
|
|
36
|
+
export function formatMetricDiff(d) {
|
|
37
|
+
// `typeof !== "number"` covers a null OR an undefined/missing diffPct, so the
|
|
38
|
+
// toFixed() below can never throw on a malformed row.
|
|
39
|
+
if (typeof d.diffPct !== "number")
|
|
40
|
+
return `${d.metric}: no comparable data`;
|
|
41
|
+
const sign = d.diffPct >= 0 ? "+" : "";
|
|
42
|
+
return `${d.metric}: ${sign}${d.diffPct.toFixed(1)}% (P50 ${fmtValue(d.baselineP50)} -> ${fmtValue(d.candidateP50)})`;
|
|
43
|
+
}
|
|
44
|
+
function fmtValue(value) {
|
|
45
|
+
// `typeof !== "number"` covers null AND a missing/undefined value on a
|
|
46
|
+
// malformed row, so toFixed() can never throw here.
|
|
47
|
+
return typeof value !== "number" ? "n/a" : value.toFixed(2);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=perf-diff-eval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perf-diff-eval.js","sourceRoot":"","sources":["../../src/lib/perf-diff-eval.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,CAAC,MAAM,kBAAkB,GAAgC,CAAC,YAAY,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;AAEpG,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC/C,OAAQ,kBAAwC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAuCD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CACjC,UAA8B,EAC9B,UAA2B,EAAE;IAE7B,8EAA8E;IAC9E,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,WAAW;IACX,MAAM,SAAS,GACd,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,IAAK,OAAO,CAAC,YAAuB,IAAI,CAAC;QAC7E,CAAC,CAAE,OAAO,CAAC,YAAuB;QAClC,CAAC,CAAC,CAAC,CAAC;IACN,+EAA+E;IAC/E,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CACxF,CAAC;IACF,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAC,CAAC,OAAkB,GAAG,SAAS,CAAC,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AAC/D,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,CAAgB;IAChD,8EAA8E;IAC9E,sDAAsD;IACtD,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC,CAAC,MAAM,sBAAsB,CAAC;IAC5E,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACvC,OAAO,GAAG,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,QAAQ,CACjG,CAAC,CAAC,YAAY,CACd,GAAG,CAAC;AACN,CAAC;AAED,SAAS,QAAQ,CAAC,KAAgC;IACjD,uEAAuE;IACvE,oDAAoD;IACpD,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC"}
|
package/dist/lib/run-command.js
CHANGED
|
@@ -20,12 +20,12 @@ export async function runCommand(opts, handler) {
|
|
|
20
20
|
if (opts.noProject) {
|
|
21
21
|
const baseConfig = resolveConfigWithoutProject(typedValues);
|
|
22
22
|
const config = { ...baseConfig, projectId: "" };
|
|
23
|
-
const client = createClient(config.baseUrl, config.
|
|
23
|
+
const client = createClient(config.baseUrl, config.credential, "");
|
|
24
24
|
await handler({ config, client, values: typedValues, positionals });
|
|
25
25
|
}
|
|
26
26
|
else {
|
|
27
27
|
const config = resolveConfig(typedValues);
|
|
28
|
-
const client = createClient(config.baseUrl, config.
|
|
28
|
+
const client = createClient(config.baseUrl, config.credential, config.projectId);
|
|
29
29
|
await handler({ config, client, values: typedValues, positionals });
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../src/lib/run-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,SAAS,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAEN,cAAc,EACd,aAAa,EACb,2BAA2B,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAiBzC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,IAAoB,EACpB,OAA+C;IAE/C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACzC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,EAAE,GAAG,cAAc,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE;QAC/C,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,KAAK;KAChD,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,OAAO;IACR,CAAC;IAED,MAAM,WAAW,GAAG,MAAsD,CAAC;IAE3E,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,UAAU,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAc,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QAC3D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../src/lib/run-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,SAAS,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAEN,cAAc,EACd,aAAa,EACb,2BAA2B,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAiBzC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,IAAoB,EACpB,OAA+C;IAE/C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;QACzC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,EAAE,GAAG,cAAc,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE;QAC/C,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,KAAK;KAChD,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,OAAO;IACR,CAAC;IAED,MAAM,WAAW,GAAG,MAAsD,CAAC;IAE3E,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACpB,MAAM,UAAU,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAc,EAAE,GAAG,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QAC3D,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACnE,MAAM,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC;IACrE,CAAC;SAAM,CAAC;QACP,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;QACjF,MAAM,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC;IACrE,CAAC;AACF,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAY,EACZ,IAAY,EACZ,WAA0B;IAE1B,OAAO,KAAK,EAAE,IAAc,EAAiB,EAAE;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAE3B,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACnE,GAAG,CAAC,IAAI,CAAC,CAAC;YACV,OAAO;QACR,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,WAAW,IAAI,gBAAgB,UAAU,EAAE,CAAC,CAAC;YACnD,GAAG,CAAC,IAAI,CAAC,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,OAAO;QACR,CAAC;QAED,+EAA+E;QAC/E,MAAM,WAAW,CAAC,UAAU,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `framedash run-profile-test`, kept free of process/network
|
|
3
|
+
* I/O so the env-contract and ingest-wait logic is unit-testable. The command
|
|
4
|
+
* (commands/run-profile-test.ts) layers git resolution, the child-process
|
|
5
|
+
* spawn, and the perf-diff gate around these.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The FRAMEDASH_* environment contract the SDK's
|
|
9
|
+
* BeginAutomatedSessionFromEnvironment() reads to stamp the automated session.
|
|
10
|
+
* The build id becomes the first-class `build_id`; branch/commit/scenario ride
|
|
11
|
+
* the attributes map as ci.branch / ci.commit / ci.scenario.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ENV_BUILD_ID = "FRAMEDASH_BUILD_ID";
|
|
14
|
+
export declare const ENV_GIT_BRANCH = "FRAMEDASH_GIT_BRANCH";
|
|
15
|
+
export declare const ENV_GIT_COMMIT = "FRAMEDASH_GIT_COMMIT";
|
|
16
|
+
export declare const ENV_TEST_SCENARIO = "FRAMEDASH_TEST_SCENARIO";
|
|
17
|
+
/**
|
|
18
|
+
* The full FRAMEDASH_* session contract. The runner is authoritative for all of
|
|
19
|
+
* these: it clears any it does not set so the launched game can never inherit a
|
|
20
|
+
* stale value (e.g. a FRAMEDASH_GIT_BRANCH left in the CI environment) when the
|
|
21
|
+
* current run omits that field.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SESSION_ENV_KEYS: readonly ["FRAMEDASH_BUILD_ID", "FRAMEDASH_GIT_BRANCH", "FRAMEDASH_GIT_COMMIT", "FRAMEDASH_TEST_SCENARIO"];
|
|
24
|
+
/** Resolved build identity stamped onto the profiling run. */
|
|
25
|
+
export interface ProfileIdentity {
|
|
26
|
+
/** Becomes FRAMEDASH_BUILD_ID and the perf-diff candidate; always set. */
|
|
27
|
+
buildId: string;
|
|
28
|
+
/** FRAMEDASH_GIT_BRANCH (ci.branch); omitted from the env when empty. */
|
|
29
|
+
branch?: string;
|
|
30
|
+
/** FRAMEDASH_GIT_COMMIT (ci.commit); omitted when empty. */
|
|
31
|
+
commit?: string;
|
|
32
|
+
/** FRAMEDASH_TEST_SCENARIO (ci.scenario); omitted when empty. */
|
|
33
|
+
scenario?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Raw identity flags as parsed from the command line (any may be unset). */
|
|
36
|
+
export interface IdentityInputs {
|
|
37
|
+
buildId?: string;
|
|
38
|
+
branch?: string;
|
|
39
|
+
commit?: string;
|
|
40
|
+
scenario?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Git fallbacks (best-effort `git rev-parse` outputs; either may be unset). */
|
|
43
|
+
export interface GitFallbacks {
|
|
44
|
+
branch?: string;
|
|
45
|
+
commit?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the build identity from explicit flags, falling back to git. The
|
|
49
|
+
* build id defaults to the resolved commit, so a CI run that sets only --commit
|
|
50
|
+
* (or relies on the git fallback) still gets a stable, first-class build_id.
|
|
51
|
+
* Returns null when no build id can be determined, so the caller can error out
|
|
52
|
+
* before launching the game.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveProfileIdentity(flags: IdentityInputs, git: GitFallbacks): ProfileIdentity | null;
|
|
55
|
+
/**
|
|
56
|
+
* The FRAMEDASH_* variables to overlay onto the child process environment. Only
|
|
57
|
+
* non-empty values are included so the SDK's env reader never picks up a blank
|
|
58
|
+
* branch/commit/scenario and stamps an empty ci.* tag.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildSessionEnv(identity: ProfileIdentity): Record<string, string>;
|
|
61
|
+
/** Result of validating a "seconds" CLI option value. */
|
|
62
|
+
export type SecondsValidation = {
|
|
63
|
+
value: number;
|
|
64
|
+
} | {
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Validate a seconds-valued option. Returns the parsed number, or an error
|
|
69
|
+
* message describing why it is invalid (the caller adds the --flag prefix and
|
|
70
|
+
* decides how to surface it). A non-string value (unset, or the boolean parseArgs
|
|
71
|
+
* yields for a value-less flag) falls back to `fallback`. With `allowZero` the
|
|
72
|
+
* value may be 0 (used by --command-timeout, where 0 disables the bound);
|
|
73
|
+
* otherwise it must be strictly positive (as --ingest-timeout / --poll-interval
|
|
74
|
+
* require).
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateSeconds(value: string | boolean | undefined, fallback: number, opts?: {
|
|
77
|
+
allowZero?: boolean;
|
|
78
|
+
max?: number;
|
|
79
|
+
}): SecondsValidation;
|
|
80
|
+
/**
|
|
81
|
+
* Platform-specific plan for terminating a launched process TREE by pid. A plain
|
|
82
|
+
* kill of the direct child orphans its grandchildren (a Unity/UE5 player launched
|
|
83
|
+
* via the shell spawns helper processes that survive), so on win32 we shell out
|
|
84
|
+
* to `taskkill /T /F` (whole tree, forced) and on POSIX we signal the child's
|
|
85
|
+
* process GROUP (the negative pid, valid because the child is spawned detached
|
|
86
|
+
* into its own group). Pure so the argv/decision is unit-testable without
|
|
87
|
+
* spawning anything.
|
|
88
|
+
*/
|
|
89
|
+
export type TreeKillPlan = {
|
|
90
|
+
platform: "win32";
|
|
91
|
+
command: "taskkill";
|
|
92
|
+
args: string[];
|
|
93
|
+
} | {
|
|
94
|
+
platform: "posix";
|
|
95
|
+
groupPid: number;
|
|
96
|
+
} | {
|
|
97
|
+
platform: "noop";
|
|
98
|
+
};
|
|
99
|
+
export declare function planTreeKill(platform: NodeJS.Platform, pid: number | undefined): TreeKillPlan;
|
|
100
|
+
/** Minimal shape of a builds-list row (mirrors apps/web BuildInfo). */
|
|
101
|
+
export interface BuildListEntry {
|
|
102
|
+
build_id: string;
|
|
103
|
+
/**
|
|
104
|
+
* Perf-event count. ClickHouse `count()` is a UInt64, which the JSON format
|
|
105
|
+
* serializes as a quoted string by default, so this can arrive as a number OR
|
|
106
|
+
* a numeric string -- buildEventCount() normalizes it.
|
|
107
|
+
*/
|
|
108
|
+
event_count?: number | string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The perf-bearing event count currently reported for a build_id, or 0 when the
|
|
112
|
+
* build is absent (or the row has no usable count). Normalizes the value with
|
|
113
|
+
* Number() so a JSON-quoted UInt64 string compares numerically, not
|
|
114
|
+
* lexicographically (otherwise "10" > "9" would be false). Used to snapshot the
|
|
115
|
+
* candidate's count before a run so the wait can require it to grow.
|
|
116
|
+
*/
|
|
117
|
+
export declare function buildEventCount(builds: BuildListEntry[] | null | undefined, buildId: string): number;
|
|
118
|
+
/**
|
|
119
|
+
* True once the candidate build_id has produced MORE than `minEventCount`
|
|
120
|
+
* perf-bearing events. With the default 0 this means "present with any events".
|
|
121
|
+
* Passing the build's pre-run count makes the wait require fresh events from
|
|
122
|
+
* THIS run, so a CI re-run for the same build_id is not satisfied by old data.
|
|
123
|
+
*/
|
|
124
|
+
export declare function hasIngestedBuild(builds: BuildListEntry[] | null | undefined, buildId: string, minEventCount?: number): boolean;
|
|
125
|
+
/** Injected dependencies for waitForIngest (so the poll loop is unit-testable). */
|
|
126
|
+
export interface WaitForIngestDeps {
|
|
127
|
+
/** Fetch the current builds list (one poll). */
|
|
128
|
+
fetchBuilds: () => Promise<BuildListEntry[]>;
|
|
129
|
+
/** Candidate build_id to wait for. */
|
|
130
|
+
buildId: string;
|
|
131
|
+
/**
|
|
132
|
+
* Require the candidate's event count to exceed this value (its count before
|
|
133
|
+
* the run), so the wait confirms fresh events rather than pre-existing data.
|
|
134
|
+
* Defaults to 0 ("present with any events").
|
|
135
|
+
*/
|
|
136
|
+
minEventCount?: number;
|
|
137
|
+
/** Give up after this many milliseconds. */
|
|
138
|
+
timeoutMs: number;
|
|
139
|
+
/** Delay between polls, in milliseconds. */
|
|
140
|
+
intervalMs: number;
|
|
141
|
+
/** Sleep helper (injected so tests can advance a fake clock). */
|
|
142
|
+
sleep: (ms: number) => Promise<void>;
|
|
143
|
+
/** Monotonic clock in milliseconds (injected for the same reason). */
|
|
144
|
+
now: () => number;
|
|
145
|
+
/** Optional per-attempt hook (for progress logging). */
|
|
146
|
+
onPoll?: (attempt: number) => void;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Poll the builds list until the candidate build's perf data has landed, or the
|
|
150
|
+
* timeout elapses. Returns true if the build appeared, false on timeout. Always
|
|
151
|
+
* polls at least once (so timeoutMs=0 still makes a single attempt). A transient
|
|
152
|
+
* fetch error (ingest / ClickHouse lag) is swallowed and retried until the
|
|
153
|
+
* deadline rather than failing the whole run on a blip.
|
|
154
|
+
*/
|
|
155
|
+
export declare function waitForIngest(deps: WaitForIngestDeps): Promise<boolean>;
|
|
156
|
+
//# sourceMappingURL=run-profile-test-lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-profile-test-lib.d.ts","sourceRoot":"","sources":["../../src/lib/run-profile-test-lib.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,YAAY,uBAAuB,CAAC;AACjD,eAAO,MAAM,cAAc,yBAAyB,CAAC;AACrD,eAAO,MAAM,cAAc,yBAAyB,CAAC;AACrD,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,4GAKnB,CAAC;AAEX,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC/B,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAUD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,YAAY,GACf,eAAe,GAAG,IAAI,CAUxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMjF;AAED,yDAAyD;AACzD,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC9B,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,EACnC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,iBAAiB,CAoBnB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GACrB;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1D;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAExB,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAa7F;AAED,uEAAuE;AACvE,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC9B,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,EAC3C,OAAO,EAAE,MAAM,GACb,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,EAC3C,OAAO,EAAE,MAAM,EACf,aAAa,SAAI,GACf,OAAO,CAET;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IACjC,gDAAgD;IAChD,WAAW,EAAE,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC7C,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,sEAAsE;IACtE,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAmB7E"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for `framedash run-profile-test`, kept free of process/network
|
|
3
|
+
* I/O so the env-contract and ingest-wait logic is unit-testable. The command
|
|
4
|
+
* (commands/run-profile-test.ts) layers git resolution, the child-process
|
|
5
|
+
* spawn, and the perf-diff gate around these.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The FRAMEDASH_* environment contract the SDK's
|
|
9
|
+
* BeginAutomatedSessionFromEnvironment() reads to stamp the automated session.
|
|
10
|
+
* The build id becomes the first-class `build_id`; branch/commit/scenario ride
|
|
11
|
+
* the attributes map as ci.branch / ci.commit / ci.scenario.
|
|
12
|
+
*/
|
|
13
|
+
export const ENV_BUILD_ID = "FRAMEDASH_BUILD_ID";
|
|
14
|
+
export const ENV_GIT_BRANCH = "FRAMEDASH_GIT_BRANCH";
|
|
15
|
+
export const ENV_GIT_COMMIT = "FRAMEDASH_GIT_COMMIT";
|
|
16
|
+
export const ENV_TEST_SCENARIO = "FRAMEDASH_TEST_SCENARIO";
|
|
17
|
+
/**
|
|
18
|
+
* The full FRAMEDASH_* session contract. The runner is authoritative for all of
|
|
19
|
+
* these: it clears any it does not set so the launched game can never inherit a
|
|
20
|
+
* stale value (e.g. a FRAMEDASH_GIT_BRANCH left in the CI environment) when the
|
|
21
|
+
* current run omits that field.
|
|
22
|
+
*/
|
|
23
|
+
export const SESSION_ENV_KEYS = [
|
|
24
|
+
ENV_BUILD_ID,
|
|
25
|
+
ENV_GIT_BRANCH,
|
|
26
|
+
ENV_GIT_COMMIT,
|
|
27
|
+
ENV_TEST_SCENARIO,
|
|
28
|
+
];
|
|
29
|
+
function clean(value) {
|
|
30
|
+
// Guard the type defensively: parseArgs only yields strings for these flags,
|
|
31
|
+
// but a non-string (boolean/null) must not crash .trim().
|
|
32
|
+
if (typeof value !== "string")
|
|
33
|
+
return undefined;
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the build identity from explicit flags, falling back to git. The
|
|
39
|
+
* build id defaults to the resolved commit, so a CI run that sets only --commit
|
|
40
|
+
* (or relies on the git fallback) still gets a stable, first-class build_id.
|
|
41
|
+
* Returns null when no build id can be determined, so the caller can error out
|
|
42
|
+
* before launching the game.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveProfileIdentity(flags, git) {
|
|
45
|
+
const commit = clean(flags.commit) ?? clean(git.commit);
|
|
46
|
+
const buildId = clean(flags.buildId) ?? commit;
|
|
47
|
+
if (!buildId)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
buildId,
|
|
51
|
+
branch: clean(flags.branch) ?? clean(git.branch),
|
|
52
|
+
commit,
|
|
53
|
+
scenario: clean(flags.scenario),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The FRAMEDASH_* variables to overlay onto the child process environment. Only
|
|
58
|
+
* non-empty values are included so the SDK's env reader never picks up a blank
|
|
59
|
+
* branch/commit/scenario and stamps an empty ci.* tag.
|
|
60
|
+
*/
|
|
61
|
+
export function buildSessionEnv(identity) {
|
|
62
|
+
const env = { [ENV_BUILD_ID]: identity.buildId };
|
|
63
|
+
if (identity.branch)
|
|
64
|
+
env[ENV_GIT_BRANCH] = identity.branch;
|
|
65
|
+
if (identity.commit)
|
|
66
|
+
env[ENV_GIT_COMMIT] = identity.commit;
|
|
67
|
+
if (identity.scenario)
|
|
68
|
+
env[ENV_TEST_SCENARIO] = identity.scenario;
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate a seconds-valued option. Returns the parsed number, or an error
|
|
73
|
+
* message describing why it is invalid (the caller adds the --flag prefix and
|
|
74
|
+
* decides how to surface it). A non-string value (unset, or the boolean parseArgs
|
|
75
|
+
* yields for a value-less flag) falls back to `fallback`. With `allowZero` the
|
|
76
|
+
* value may be 0 (used by --command-timeout, where 0 disables the bound);
|
|
77
|
+
* otherwise it must be strictly positive (as --ingest-timeout / --poll-interval
|
|
78
|
+
* require).
|
|
79
|
+
*/
|
|
80
|
+
export function validateSeconds(value, fallback, opts = {}) {
|
|
81
|
+
if (typeof value !== "string")
|
|
82
|
+
return { value: fallback };
|
|
83
|
+
const baseError = opts.allowZero
|
|
84
|
+
? "must be a non-negative number of seconds (0 disables the bound)"
|
|
85
|
+
: "must be a positive number of seconds";
|
|
86
|
+
// Reject empty / whitespace explicitly: Number("") and Number(" ") are 0, which
|
|
87
|
+
// would silently DISABLE the bound under allowZero (a fail-closed footgun for a
|
|
88
|
+
// mistyped --command-timeout= value), so treat a blank value as invalid.
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
if (trimmed === "")
|
|
91
|
+
return { error: baseError };
|
|
92
|
+
const n = Number(trimmed);
|
|
93
|
+
const minOk = opts.allowZero ? n >= 0 : n > 0;
|
|
94
|
+
if (!Number.isFinite(n) || !minOk)
|
|
95
|
+
return { error: baseError };
|
|
96
|
+
// Guard against a value so large that timeoutMs overflows setTimeout's signed
|
|
97
|
+
// 32-bit delay, which Node clamps to 1ms -- an "effectively unbounded" input
|
|
98
|
+
// would then fire almost immediately and kill the run at once.
|
|
99
|
+
if (opts.max !== undefined && n > opts.max) {
|
|
100
|
+
return { error: `must be at most ${opts.max} seconds` };
|
|
101
|
+
}
|
|
102
|
+
return { value: n };
|
|
103
|
+
}
|
|
104
|
+
export function planTreeKill(platform, pid) {
|
|
105
|
+
// A missing, non-integer, or non-positive pid is UNSAFE to kill: on POSIX,
|
|
106
|
+
// process.kill(0, sig) signals the caller's OWN process group and kill(-1, sig)
|
|
107
|
+
// signals every process the user owns, either of which could take down this CLI
|
|
108
|
+
// or the CI runner itself. Refuse to act rather than risk it.
|
|
109
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
110
|
+
return { platform: "noop" };
|
|
111
|
+
}
|
|
112
|
+
if (platform === "win32") {
|
|
113
|
+
return { platform: "win32", command: "taskkill", args: ["/pid", String(pid), "/t", "/f"] };
|
|
114
|
+
}
|
|
115
|
+
// Negative pid targets the whole process group (see detached spawn in the command).
|
|
116
|
+
return { platform: "posix", groupPid: -pid };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The perf-bearing event count currently reported for a build_id, or 0 when the
|
|
120
|
+
* build is absent (or the row has no usable count). Normalizes the value with
|
|
121
|
+
* Number() so a JSON-quoted UInt64 string compares numerically, not
|
|
122
|
+
* lexicographically (otherwise "10" > "9" would be false). Used to snapshot the
|
|
123
|
+
* candidate's count before a run so the wait can require it to grow.
|
|
124
|
+
*/
|
|
125
|
+
export function buildEventCount(builds, buildId) {
|
|
126
|
+
if (!Array.isArray(builds))
|
|
127
|
+
return 0;
|
|
128
|
+
const row = builds.find((b) => b.build_id === buildId);
|
|
129
|
+
const count = Number(row?.event_count);
|
|
130
|
+
return Number.isFinite(count) ? count : 0;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* True once the candidate build_id has produced MORE than `minEventCount`
|
|
134
|
+
* perf-bearing events. With the default 0 this means "present with any events".
|
|
135
|
+
* Passing the build's pre-run count makes the wait require fresh events from
|
|
136
|
+
* THIS run, so a CI re-run for the same build_id is not satisfied by old data.
|
|
137
|
+
*/
|
|
138
|
+
export function hasIngestedBuild(builds, buildId, minEventCount = 0) {
|
|
139
|
+
return buildEventCount(builds, buildId) > minEventCount;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Poll the builds list until the candidate build's perf data has landed, or the
|
|
143
|
+
* timeout elapses. Returns true if the build appeared, false on timeout. Always
|
|
144
|
+
* polls at least once (so timeoutMs=0 still makes a single attempt). A transient
|
|
145
|
+
* fetch error (ingest / ClickHouse lag) is swallowed and retried until the
|
|
146
|
+
* deadline rather than failing the whole run on a blip.
|
|
147
|
+
*/
|
|
148
|
+
export async function waitForIngest(deps) {
|
|
149
|
+
const deadline = deps.now() + deps.timeoutMs;
|
|
150
|
+
let attempt = 0;
|
|
151
|
+
while (true) {
|
|
152
|
+
attempt++;
|
|
153
|
+
deps.onPoll?.(attempt);
|
|
154
|
+
try {
|
|
155
|
+
const builds = await deps.fetchBuilds();
|
|
156
|
+
if (hasIngestedBuild(builds, deps.buildId, deps.minEventCount ?? 0))
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Transient ingest / aggregation lag -- keep polling until the deadline.
|
|
161
|
+
}
|
|
162
|
+
// Read the clock once so the deadline check and the sleep window are
|
|
163
|
+
// consistent (a second now() call could advance past the deadline and yield
|
|
164
|
+
// a negative remaining); never sleep past the deadline.
|
|
165
|
+
const remaining = deadline - deps.now();
|
|
166
|
+
if (remaining <= 0)
|
|
167
|
+
return false;
|
|
168
|
+
await deps.sleep(Math.min(deps.intervalMs, remaining));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=run-profile-test-lib.js.map
|