@keepsnappy/agent 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +33 -4
- package/dist/appium-driver.js +68 -17
- package/dist/keepsnappy.js +1317 -264
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// apps/agent/src/cli.ts
|
|
4
|
-
import { resolve as
|
|
4
|
+
import { resolve as resolve8 } from "node:path";
|
|
5
5
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
6
6
|
|
|
7
7
|
// apps/agent/src/config.ts
|
|
@@ -1664,6 +1664,36 @@ var MEASURES = {
|
|
|
1664
1664
|
decimals: 2,
|
|
1665
1665
|
what: "The share of samples at heat level serious or above"
|
|
1666
1666
|
},
|
|
1667
|
+
"sampler.thermal.raw_start": {
|
|
1668
|
+
unit: "state",
|
|
1669
|
+
worse: "none",
|
|
1670
|
+
at: ["journey"],
|
|
1671
|
+
producer: "derive",
|
|
1672
|
+
label: "Raw Android thermal status at the start (0–6)",
|
|
1673
|
+
short: "raw Android thermal status at the start (0–6)",
|
|
1674
|
+
decimals: 0,
|
|
1675
|
+
what: "The first raw Android thermal status reported during the journey"
|
|
1676
|
+
},
|
|
1677
|
+
"sampler.thermal.raw_max": {
|
|
1678
|
+
unit: "state",
|
|
1679
|
+
worse: "none",
|
|
1680
|
+
at: ["journey"],
|
|
1681
|
+
producer: "derive",
|
|
1682
|
+
label: "Raw Android thermal status (peak, 0–6)",
|
|
1683
|
+
short: "raw Android thermal status (peak, 0–6)",
|
|
1684
|
+
decimals: 0,
|
|
1685
|
+
what: "The highest raw Android thermal status reported during the journey"
|
|
1686
|
+
},
|
|
1687
|
+
"sampler.thermal.raw_light_share": {
|
|
1688
|
+
unit: "ratio",
|
|
1689
|
+
worse: "none",
|
|
1690
|
+
at: ["journey"],
|
|
1691
|
+
producer: "derive",
|
|
1692
|
+
label: "Share of Android samples at LIGHT or above",
|
|
1693
|
+
short: "share of Android samples at LIGHT or above",
|
|
1694
|
+
decimals: 2,
|
|
1695
|
+
what: "The share of samples with a raw Android thermal status of 1 (LIGHT) or above, among samples reporting that status"
|
|
1696
|
+
},
|
|
1667
1697
|
"sampler.thermal.heats_up": {
|
|
1668
1698
|
unit: "share",
|
|
1669
1699
|
worse: "higher",
|
|
@@ -2103,6 +2133,7 @@ var DIMENSIONS = {
|
|
|
2103
2133
|
};
|
|
2104
2134
|
var BREAKDOWN_MEASURES = {
|
|
2105
2135
|
self_ms: { unit: "ms", worse: "higher", label: "render time", decimals: 1 },
|
|
2136
|
+
rolled_up_ms: { unit: "ms", worse: "none", label: "render time from libraries", decimals: 1 },
|
|
2106
2137
|
total_ms: { unit: "ms", worse: "higher", label: "total render time", decimals: 1 },
|
|
2107
2138
|
renders: { unit: "count", worse: "higher", label: "renders", decimals: 0 },
|
|
2108
2139
|
mounts: { unit: "count", worse: "higher", label: "mounts", decimals: 0 },
|
|
@@ -2112,7 +2143,53 @@ var BREAKDOWN_MEASURES = {
|
|
|
2112
2143
|
var measureDef = (id) => isMeasureId(id) ? MEASURES[id] : undefined;
|
|
2113
2144
|
var HEAT_METRIC = "sampler.thermal.heats_up";
|
|
2114
2145
|
var HEAT_LABEL = MEASURES[HEAT_METRIC].label;
|
|
2146
|
+
var FULL_ROW_MS = 2.5;
|
|
2147
|
+
// packages/facts/dist/src/components.js
|
|
2148
|
+
var FRAGMENT_VERSION = 2;
|
|
2149
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2150
|
+
var portable = (value) => typeof value === "string" && value.length > 0 && !/^(?:\/|[A-Za-z]:)/.test(value) && !value.includes("\\");
|
|
2151
|
+
var componentFile = (file) => portable(file) && !file.split("/").some((part) => part === ".." || part === "." || part === "");
|
|
2152
|
+
var isComponentName = (name) => typeof name === "string" && /^[\p{Lu}\p{Lt}][\p{ID_Continue}$\u200C\u200D]*$/u.test(name);
|
|
2153
|
+
var encodePart = (part) => encodeURIComponent(part).replace(/%2F/g, "/").replace(/%40/g, "@").replace(/~/g, "%7E");
|
|
2154
|
+
function decodePart(part) {
|
|
2155
|
+
try {
|
|
2156
|
+
const decoded = decodeURIComponent(part);
|
|
2157
|
+
return encodePart(decoded) === part ? decoded : null;
|
|
2158
|
+
} catch {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function componentKey(name, file) {
|
|
2163
|
+
if (name !== "default" && !isComponentName(name) || !componentFile(file))
|
|
2164
|
+
throw new Error("invalid client component name or file");
|
|
2165
|
+
return `${name}@${encodePart(file)}`;
|
|
2166
|
+
}
|
|
2167
|
+
function parseComponentKey(key) {
|
|
2168
|
+
if (typeof key !== "string")
|
|
2169
|
+
return null;
|
|
2170
|
+
const separator = key.indexOf("@");
|
|
2171
|
+
const name = key.slice(0, separator);
|
|
2172
|
+
const file = decodePart(key.slice(separator + 1));
|
|
2173
|
+
if (separator < 1 || name !== "default" && !isComponentName(name) || !componentFile(file))
|
|
2174
|
+
return null;
|
|
2175
|
+
return componentKey(name, file) === key ? { name, file } : null;
|
|
2176
|
+
}
|
|
2177
|
+
function isComponentFragment(value) {
|
|
2178
|
+
if (!object(value) || value.version !== FRAGMENT_VERSION || !portable(value.source) || typeof value.hash !== "string" || !/^[a-f0-9]{64}$/.test(value.hash) || !Array.isArray(value.components))
|
|
2179
|
+
return false;
|
|
2180
|
+
return value.components.every((component) => {
|
|
2181
|
+
if (!object(component) || typeof component.key !== "string")
|
|
2182
|
+
return false;
|
|
2183
|
+
const parsed = parseComponentKey(component.key);
|
|
2184
|
+
return parsed !== null && parsed.name === component.name && parsed.file === component.file && Number.isInteger(component.line) && component.line > 0;
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
function isComponentConfiguration(value) {
|
|
2188
|
+
return object(value) && value.version === FRAGMENT_VERSION && typeof value.directory === "string" && /^[a-zA-Z0-9-]+$/.test(value.directory);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2115
2191
|
// packages/facts/dist/src/places.js
|
|
2192
|
+
var placeWithoutVisit = (id) => placeRef(id).stem.replace(/#(\d+|repeat)$/, "");
|
|
2116
2193
|
var AFTER_3S = "+3s";
|
|
2117
2194
|
var kindOf = (stem) => {
|
|
2118
2195
|
if (stem === "*")
|
|
@@ -2136,16 +2213,97 @@ function placeRef(id) {
|
|
|
2136
2213
|
const stem = after3s ? id.slice(0, -AFTER_3S.length) : id;
|
|
2137
2214
|
return { id, kind: kindOf(stem), stem, segment: after3s ? "after3s" : "whole" };
|
|
2138
2215
|
}
|
|
2216
|
+
var placeSegment = (name, file) => `${name}@${encodePart(file)}`;
|
|
2217
|
+
var buildPlaceId = (parts) => {
|
|
2218
|
+
if (parts.kind === "screen")
|
|
2219
|
+
return `screen:${parts.screen}#${parts.visit}`;
|
|
2220
|
+
if (parts.kind === "launch")
|
|
2221
|
+
return `launch:${parts.screen}`;
|
|
2222
|
+
return `${parts.kind}:${parts.screen}›${parts.parent ? parts.parent + "›" : ""}${parts.target}${parts.k ? "~" + parts.k : ""}`;
|
|
2223
|
+
};
|
|
2224
|
+
function parseClientKey(key) {
|
|
2225
|
+
const anonymous = key.startsWith("default@");
|
|
2226
|
+
const parsed = parseComponentKey(anonymous ? "Default" + key.slice(7) : key);
|
|
2227
|
+
if (!parsed)
|
|
2228
|
+
return null;
|
|
2229
|
+
const name = anonymous ? "default" : parsed.name;
|
|
2230
|
+
return placeSegment(name, parsed.file) === key ? { name, file: parsed.file } : null;
|
|
2231
|
+
}
|
|
2232
|
+
var decodePlaceSegment = (key) => {
|
|
2233
|
+
if (key === "(no screen)")
|
|
2234
|
+
return { key, name: key, file: null };
|
|
2235
|
+
const at = key.indexOf("@");
|
|
2236
|
+
if (at < 1 || at === key.length - 1)
|
|
2237
|
+
return null;
|
|
2238
|
+
try {
|
|
2239
|
+
const name = key.slice(0, at);
|
|
2240
|
+
const file = decodePart(key.slice(at + 1));
|
|
2241
|
+
if (file === null)
|
|
2242
|
+
return null;
|
|
2243
|
+
if (name !== "(outside app components)" && !parseClientKey(key))
|
|
2244
|
+
return null;
|
|
2245
|
+
return placeSegment(name, file) === key ? { key, name, file } : null;
|
|
2246
|
+
} catch {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
var placeParts = (id) => {
|
|
2251
|
+
const stem = id.replace(/\+3s$/, "");
|
|
2252
|
+
const suffix = /#(\d+|repeat)$/.exec(stem);
|
|
2253
|
+
const base = suffix ? stem.slice(0, suffix.index) : stem;
|
|
2254
|
+
const occurrence = /~(\d+)$/.exec(base);
|
|
2255
|
+
const body = occurrence ? base.slice(0, occurrence.index) : base;
|
|
2256
|
+
const colon = body.indexOf(":");
|
|
2257
|
+
const kind = body.slice(0, colon);
|
|
2258
|
+
if (kind !== "screen" && kind !== "launch" && kind !== "press" && kind !== "drag")
|
|
2259
|
+
return null;
|
|
2260
|
+
const segments = body.slice(colon + 1).split("›");
|
|
2261
|
+
if (kind === "screen" || kind === "launch" ? segments.length !== 1 : segments.length < 2 || segments.length > 3)
|
|
2262
|
+
return null;
|
|
2263
|
+
const parts = segments.map(decodePlaceSegment);
|
|
2264
|
+
if (parts.some((p) => p === null))
|
|
2265
|
+
return null;
|
|
2266
|
+
if (parts.slice(1).some((p) => p.name === "(no screen)"))
|
|
2267
|
+
return null;
|
|
2268
|
+
if (parts.length === 3 && parts[1].name === "(outside app components)")
|
|
2269
|
+
return null;
|
|
2270
|
+
const screen = parts[0];
|
|
2271
|
+
const target = kind === "press" || kind === "drag" ? parts.at(-1) : null;
|
|
2272
|
+
return {
|
|
2273
|
+
kind,
|
|
2274
|
+
screen,
|
|
2275
|
+
target,
|
|
2276
|
+
parent: parts.length === 3 ? parts[1] : null,
|
|
2277
|
+
k: occurrence ? Number(occurrence[1]) : null,
|
|
2278
|
+
visit: suffix ? suffix[1] === "repeat" ? "repeat" : Number(suffix[1]) : null,
|
|
2279
|
+
outsideHost: target?.name === "(outside app components)" ? target.file : null
|
|
2280
|
+
};
|
|
2281
|
+
};
|
|
2139
2282
|
// packages/facts/dist/src/frame.js
|
|
2140
2283
|
var UNKNOWN_REFRESH_HZ = 60;
|
|
2141
2284
|
var LIMIT_FRAME_MS = 1000 / 60;
|
|
2142
2285
|
function frameBudgetMs(refreshHz) {
|
|
2143
2286
|
return 1000 / (refreshHz !== null && refreshHz !== undefined && Number.isFinite(refreshHz) && refreshHz > 0 ? refreshHz : UNKNOWN_REFRESH_HZ);
|
|
2144
2287
|
}
|
|
2288
|
+
// packages/facts/dist/src/profile-items.js
|
|
2289
|
+
function encodeProfileItems(rows, fullRowMs = FULL_ROW_MS) {
|
|
2290
|
+
const components = [], small = [];
|
|
2291
|
+
for (const row of rows) {
|
|
2292
|
+
if (row.selfMs === null || row.selfMs >= fullRowMs)
|
|
2293
|
+
components.push(row);
|
|
2294
|
+
else
|
|
2295
|
+
small.push([row.key ?? row.name, row.selfMs]);
|
|
2296
|
+
}
|
|
2297
|
+
small.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2298
|
+
return { components, ...small.length ? { small } : {} };
|
|
2299
|
+
}
|
|
2145
2300
|
// packages/derive/dist/src/launch.js
|
|
2146
2301
|
var obj = (v) => typeof v === "object" && v !== null ? v : {};
|
|
2147
2302
|
var isScreenWindow = (w) => w.raw.kind === "screen" || w.name.startsWith("screen:");
|
|
2148
|
-
var launchPlaceOf = (name) =>
|
|
2303
|
+
var launchPlaceOf = (name) => {
|
|
2304
|
+
const p = placeParts(name);
|
|
2305
|
+
return p ? buildPlaceId({ kind: "launch", screen: p.screen.key }) : `launch:${placeWithoutVisit(name).replace(/^screen:/, "")}`;
|
|
2306
|
+
};
|
|
2149
2307
|
var isScreenName = (name) => name.startsWith("screen:") || name.startsWith("launch:");
|
|
2150
2308
|
function withLaunchNames(windows) {
|
|
2151
2309
|
const screens = windows.filter(isScreenWindow);
|
|
@@ -2154,6 +2312,9 @@ function withLaunchNames(windows) {
|
|
|
2154
2312
|
const isLaunch = (w) => marked ? obj(w.raw.screen).launch === true : w === first;
|
|
2155
2313
|
return windows.map((w) => isScreenWindow(w) && isLaunch(w) ? { ...w, name: launchPlaceOf(w.name) } : w);
|
|
2156
2314
|
}
|
|
2315
|
+
|
|
2316
|
+
// packages/derive/dist/src/model.js
|
|
2317
|
+
var DERIVATION_VERSION = 27;
|
|
2157
2318
|
// packages/stats/dist/src/core/rng.js
|
|
2158
2319
|
function splitmix32(seed) {
|
|
2159
2320
|
let s = seed >>> 0;
|
|
@@ -3130,7 +3291,7 @@ var obj3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v :
|
|
|
3130
3291
|
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
3131
3292
|
var round = (x, digits = 3) => Math.round(x * 10 ** digits) / 10 ** digits;
|
|
3132
3293
|
var HOST_COMPONENTS = new Set(["View", "Text", "Image", "TextInput", "ScrollView"]);
|
|
3133
|
-
var isHostComponent = (name) => HOST_COMPONENTS.has(name) || /^RCT[A-Z]/.test(name);
|
|
3294
|
+
var isHostComponent = (name) => !parseComponentKey(name) && (HOST_COMPONENTS.has(name) || /^RCT[A-Z]/.test(name));
|
|
3134
3295
|
function causeCounts(raw, mounts) {
|
|
3135
3296
|
const c = obj3(raw);
|
|
3136
3297
|
const out = {};
|
|
@@ -3150,7 +3311,7 @@ function causeCounts(raw, mounts) {
|
|
|
3150
3311
|
}
|
|
3151
3312
|
return out;
|
|
3152
3313
|
}
|
|
3153
|
-
function componentRow(row, causes) {
|
|
3314
|
+
function componentRow(row, causes, rollup, detail) {
|
|
3154
3315
|
if (!Array.isArray(row) || typeof row[0] !== "string")
|
|
3155
3316
|
return;
|
|
3156
3317
|
const six = row.length >= 6;
|
|
@@ -3158,15 +3319,57 @@ function componentRow(row, causes) {
|
|
|
3158
3319
|
const mounts = num2(row[2]) ?? 0;
|
|
3159
3320
|
return {
|
|
3160
3321
|
name: row[0],
|
|
3322
|
+
...detail !== false && Object.hasOwn(rollup, row[0]) ? rollupOf(rollup[row[0]]) : {},
|
|
3161
3323
|
renders,
|
|
3162
3324
|
mounts,
|
|
3163
3325
|
instances: six ? num2(row[3]) ?? null : null,
|
|
3164
3326
|
selfMs: num2(row[six ? 4 : 3]) ?? null,
|
|
3165
3327
|
totalMs: num2(row[six ? 5 : 4]) ?? null,
|
|
3166
|
-
causes: causeCounts(causes[row[0]], mounts)
|
|
3328
|
+
causes: detail === false ? {} : causeCounts(causes[row[0]], mounts),
|
|
3329
|
+
...detail === undefined ? {} : { detailRenders: detail ? renders : 0 }
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
function rollupOf(raw) {
|
|
3333
|
+
const x = obj3(raw), rest = obj3(x.rolledUpRest);
|
|
3334
|
+
const rolledUp = (Array.isArray(x.rolledUp) ? x.rolledUp : []).flatMap((row) => Array.isArray(row) && typeof row[0] === "string" && num2(row[1]) !== undefined && num2(row[2]) !== undefined ? [{ name: row[0], ms: row[1], renders: row[2] }] : []);
|
|
3335
|
+
return {
|
|
3336
|
+
rolledUp,
|
|
3337
|
+
rolledUpRest: { ms: num2(rest.ms) ?? 0, n: num2(rest.n) ?? 0, ...rest.saturated === true ? { saturated: true } : {} },
|
|
3338
|
+
...typeof x.parent === "string" ? { parent: x.parent, parentOccurrences: { [x.parent]: 1 } } : {},
|
|
3339
|
+
...num2(x.rolledUpMs) === undefined ? {} : { rolledUpMs: num2(x.rolledUpMs) }
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function mergeRollups(a, b) {
|
|
3343
|
+
const names = new Map;
|
|
3344
|
+
for (const row of [...a.rolledUp ?? [], ...b.rolledUp ?? []]) {
|
|
3345
|
+
const was = names.get(row.name);
|
|
3346
|
+
names.set(row.name, { name: row.name, ms: (was?.ms ?? 0) + row.ms, renders: (was?.renders ?? 0) + row.renders });
|
|
3347
|
+
}
|
|
3348
|
+
const x = a.rolledUpRest, y = b.rolledUpRest;
|
|
3349
|
+
return { rolledUp: [...names.values()], rolledUpRest: {
|
|
3350
|
+
ms: (x?.ms ?? 0) + (y?.ms ?? 0),
|
|
3351
|
+
n: Math.max(x?.n ?? 0, y?.n ?? 0),
|
|
3352
|
+
...x?.saturated || y?.saturated ? { saturated: true } : {},
|
|
3353
|
+
...x?.nAtLeast || y?.nAtLeast || x?.n && y?.n ? { nAtLeast: true } : {}
|
|
3354
|
+
} };
|
|
3355
|
+
}
|
|
3356
|
+
function shownRollup(r, n) {
|
|
3357
|
+
const ranked = [...r.rolledUp ?? []].sort((a, b) => b.ms - a.ms || a.name.localeCompare(b.name));
|
|
3358
|
+
const cut = ranked.slice(5), rest = r.rolledUpRest;
|
|
3359
|
+
return {
|
|
3360
|
+
rolledUp: ranked.slice(0, 5).map((x) => ({ name: x.name, ms: round(x.ms / n), renders: round(x.renders / n) })),
|
|
3361
|
+
rolledUpRest: { ms: round(((rest?.ms ?? 0) + cut.reduce((s, x) => s + x.ms, 0)) / n), n: Math.max(rest?.n ?? 0, cut.length), ...rest?.saturated ? { saturated: true } : {}, ...rest?.nAtLeast || rest?.n && cut.length ? { nAtLeast: true } : {} }
|
|
3167
3362
|
};
|
|
3168
3363
|
}
|
|
3169
|
-
function
|
|
3364
|
+
function outsideOf(profiles, n) {
|
|
3365
|
+
const parts = profiles.flatMap((p) => p.outside ? [p.outside] : []);
|
|
3366
|
+
if (!parts.length)
|
|
3367
|
+
return {};
|
|
3368
|
+
const merged = parts.reduce((a, b) => mergeRollups(a, b), {});
|
|
3369
|
+
return { outside: { ...n === undefined ? merged : shownRollup(merged, n), selfMs: round(parts.reduce((s, p) => s + p.selfMs, 0) / (n ?? 1)), mountSelfMs: round(parts.reduce((s, p) => s + p.mountSelfMs, 0) / (n ?? 1)) } };
|
|
3370
|
+
}
|
|
3371
|
+
var stampsOf = (ps) => ps.every((p) => p.stamped !== undefined) ? { stamped: ps.reduce((s, p) => s + p.stamped, 0) } : {};
|
|
3372
|
+
function windowProfile(raw, detailCoverage = true) {
|
|
3170
3373
|
const p = obj3(obj3(raw.react).profile);
|
|
3171
3374
|
if (p.renderer !== "profiling" && p.renderer !== "production")
|
|
3172
3375
|
return;
|
|
@@ -3187,11 +3390,14 @@ function windowProfile(raw) {
|
|
|
3187
3390
|
}
|
|
3188
3391
|
return {
|
|
3189
3392
|
renderer: p.renderer,
|
|
3393
|
+
...Number.isSafeInteger(p.slowestCommitSeq) && p.slowestCommitSeq >= 0 && num2(r.max) !== undefined && r.max >= 0 ? { slowestCommit: { seq: p.slowestCommitSeq, endAtMs: num2(p.slowestCommitEndAtMs) ?? null, ms: r.max } } : {},
|
|
3190
3394
|
commits: num2(p.commits) ?? 0,
|
|
3395
|
+
...num2(p.stamped) === undefined ? {} : { stamped: num2(p.stamped) },
|
|
3396
|
+
...p.outside && p.renderer === "profiling" ? { outside: { ...rollupOf(p.outside), selfMs: num2(obj3(p.outside).selfMs) ?? 0, mountSelfMs: num2(obj3(p.outside).mountSelfMs) ?? 0 } } : {},
|
|
3191
3397
|
renderMs: p.renderer === "profiling" && sum !== undefined ? { sum, max: num2(r.max) ?? sum, n: num2(r.n) ?? 0 } : null,
|
|
3192
3398
|
overBudget: Object.keys(overBudget).length ? overBudget : null,
|
|
3193
3399
|
updaters,
|
|
3194
|
-
components: (Array.isArray(p.components) ? p.components : []).map((row) => componentRow(row, causes)).filter((c) => !!c),
|
|
3400
|
+
components: (Array.isArray(p.components) ? p.components : []).map((row, index) => componentRow(row, causes, obj3(p.rollup), detailCoverage ? index < (num2(p.detailRows) ?? Infinity) : undefined)).filter((c) => !!c),
|
|
3195
3401
|
omitted: num2(p.omitted) ?? 0,
|
|
3196
3402
|
selfMsTotal: num2(p.selfMsTotal) ?? null,
|
|
3197
3403
|
mountSelfMs: num2(p.mountSelfMs) ?? null
|
|
@@ -3242,13 +3448,23 @@ function addComponent(into, c) {
|
|
|
3242
3448
|
into.set(c.name, {
|
|
3243
3449
|
name: c.name,
|
|
3244
3450
|
renders: was.renders + c.renders,
|
|
3451
|
+
...was.detailRenders === undefined && c.detailRenders === undefined ? {} : { detailRenders: (was.detailRenders ?? was.renders) + (c.detailRenders ?? c.renders) },
|
|
3245
3452
|
mounts: was.mounts + c.mounts,
|
|
3246
3453
|
instances: was.instances === null && c.instances === null ? null : Math.max(was.instances ?? 0, c.instances ?? 0),
|
|
3247
3454
|
selfMs: plus(was.selfMs, c.selfMs),
|
|
3248
3455
|
totalMs: plus(was.totalMs, c.totalMs),
|
|
3249
|
-
causes
|
|
3456
|
+
causes,
|
|
3457
|
+
...was.rolledUpMs === undefined && c.rolledUpMs === undefined ? {} : { rolledUpMs: (was.rolledUpMs ?? 0) + (c.rolledUpMs ?? 0), ...mergeRollups(was, c) },
|
|
3458
|
+
...mergedParent(was, c)
|
|
3250
3459
|
});
|
|
3251
3460
|
}
|
|
3461
|
+
function mergedParent(a, b) {
|
|
3462
|
+
const counts = { ...a.parentOccurrences };
|
|
3463
|
+
for (const [k, v] of Object.entries(b.parentOccurrences ?? {}))
|
|
3464
|
+
counts[k] = (counts[k] ?? 0) + v;
|
|
3465
|
+
const parent = Object.keys(counts).sort((a, b) => counts[b] - counts[a] || a.localeCompare(b))[0];
|
|
3466
|
+
return parent ? { parent, parentOccurrences: counts } : {};
|
|
3467
|
+
}
|
|
3252
3468
|
function exactMountOf(profiles) {
|
|
3253
3469
|
let selfMsTotal = 0;
|
|
3254
3470
|
let mountSelfMs = 0;
|
|
@@ -3262,17 +3478,20 @@ function exactMountOf(profiles) {
|
|
|
3262
3478
|
}
|
|
3263
3479
|
return { selfMsTotal, mountSelfMs };
|
|
3264
3480
|
}
|
|
3265
|
-
function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = false) {
|
|
3481
|
+
function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = false, allComponents = false) {
|
|
3266
3482
|
const picked = [];
|
|
3267
3483
|
for (const cl of clusters(windows)) {
|
|
3268
3484
|
const withProfile = cl.flatMap((w) => {
|
|
3269
|
-
const p = windowProfile(w.raw);
|
|
3485
|
+
const p = windowProfile(w.raw, allComponents);
|
|
3270
3486
|
return p ? [{ w, p }] : [];
|
|
3271
3487
|
});
|
|
3272
3488
|
if (!withProfile.length)
|
|
3273
3489
|
continue;
|
|
3274
3490
|
const weight = (x) => x.p.renderMs?.sum ?? x.p.components.reduce((s, c) => s + c.renders, 0);
|
|
3275
|
-
picked.push(withProfile.reduce((best, x) =>
|
|
3491
|
+
picked.push(withProfile.reduce((best, x) => {
|
|
3492
|
+
const span = (y) => y.w.endWall - y.w.startWall;
|
|
3493
|
+
return weight(x) > weight(best) || weight(x) === weight(best) && (span(x) > span(best) || allComponents && span(x) === span(best) && windowOrder(x.w, best.w) < 0) ? x : best;
|
|
3494
|
+
}));
|
|
3276
3495
|
}
|
|
3277
3496
|
if (!picked.length)
|
|
3278
3497
|
return;
|
|
@@ -3295,10 +3514,13 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
|
|
|
3295
3514
|
overFrame = f === undefined || overFrame === undefined ? undefined : overFrame + f;
|
|
3296
3515
|
}
|
|
3297
3516
|
}
|
|
3517
|
+
const chosen = [...picked].sort((a, b) => (b.p.renderMs?.max ?? -1) - (a.p.renderMs?.max ?? -1) || windowOrder(a.w, b.w))[0];
|
|
3298
3518
|
const renderMs = timed ? picked.reduce((s, x) => s + (x.p.renderMs?.sum ?? 0), 0) : allIdle ? 0 : undefined;
|
|
3299
3519
|
const parts = picked.map((x) => timerCarriedRenderMs(x.w.raw));
|
|
3300
3520
|
const carried = parts.every((v) => v !== undefined) ? parts.reduce((s, v) => s + v, 0) : undefined;
|
|
3301
3521
|
return {
|
|
3522
|
+
window: chosen.w,
|
|
3523
|
+
...chosen.p.slowestCommit ? { slowestCommit: chosen.p.slowestCommit } : {},
|
|
3302
3524
|
renderer: timed ? "profiling" : "production",
|
|
3303
3525
|
commits: picked.reduce((s, x) => s + x.p.commits, 0),
|
|
3304
3526
|
renderMs,
|
|
@@ -3308,20 +3530,24 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
|
|
|
3308
3530
|
updaters,
|
|
3309
3531
|
components,
|
|
3310
3532
|
omitted: picked.reduce((s, x) => s + x.p.omitted, 0),
|
|
3311
|
-
distinctInWindow: Math.max(...picked.map((x) => x.p.components.length + x.p.omitted)),
|
|
3533
|
+
distinctInWindow: Math.max(...picked.map((x) => (allComponents ? x.p.components.filter((c) => !isHostComponent(c.name)).length : x.p.components.length) + x.p.omitted)),
|
|
3312
3534
|
exactMount: exactMountOf(picked.map((x) => x.p)),
|
|
3535
|
+
...outsideOf(picked.map((x) => x.p)),
|
|
3536
|
+
...stampsOf(picked.map((x) => x.p)),
|
|
3313
3537
|
...allIdle ? { idle: true } : {},
|
|
3314
3538
|
...upper ? { timerCarriedMs: carried, renderMsUpper: renderMs === undefined || carried === undefined ? undefined : renderMs + carried } : {}
|
|
3315
3539
|
};
|
|
3316
3540
|
}
|
|
3317
3541
|
var UPDATERS_KEPT = 5;
|
|
3542
|
+
var CPU_FUNCTIONS_KEPT = 10;
|
|
3543
|
+
var UNTIMED_COMPONENTS_KEPT = 10;
|
|
3318
3544
|
var topCauses = (causes, renders) => {
|
|
3319
3545
|
if (renders <= 0)
|
|
3320
3546
|
return {};
|
|
3321
3547
|
const entries = Object.entries(causes).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3);
|
|
3322
3548
|
return Object.fromEntries(entries.map(([k, v]) => [k, round(v / renders)]));
|
|
3323
3549
|
};
|
|
3324
|
-
function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
3550
|
+
function stepProfile(step, occ, budgetMs, topN, cpu, exactMount, fullRowMs) {
|
|
3325
3551
|
const n = occ.length;
|
|
3326
3552
|
const mean = (x) => x / n;
|
|
3327
3553
|
const timed = occ.every((o) => o.renderer === "profiling");
|
|
@@ -3334,8 +3560,9 @@ function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
|
3334
3560
|
const host = [...all.values()].filter((c) => isHostComponent(c.name));
|
|
3335
3561
|
const rank = (a, b) => (timed ? (b.selfMs ?? 0) - (a.selfMs ?? 0) : 0) || b.renders - a.renders || a.name.localeCompare(b.name);
|
|
3336
3562
|
own.sort(rank);
|
|
3337
|
-
const
|
|
3338
|
-
const
|
|
3563
|
+
const limit = fullRowMs === undefined ? topN : timed ? own.length : UNTIMED_COMPONENTS_KEPT;
|
|
3564
|
+
const kept = own.slice(0, limit);
|
|
3565
|
+
const rest = own.slice(limit);
|
|
3339
3566
|
const ms = (x) => timed && x !== null ? round(mean(x)) : null;
|
|
3340
3567
|
const updaters = {};
|
|
3341
3568
|
for (const o of occ)
|
|
@@ -3361,31 +3588,36 @@ function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
|
3361
3588
|
cpuOut = {
|
|
3362
3589
|
busyMs: round(median(cpu.map((c) => c.busyMs))),
|
|
3363
3590
|
symbolicated: sym.length ? sym.every(Boolean) : null,
|
|
3364
|
-
functions: [...fns.values()].sort((a, b) => b.self - a.self || a.key.localeCompare(b.key)).slice(0, topN).map((f) => ({ key: f.key, name: f.name, selfMs: round(f.self / k), totalMs: f.total === null ? null : round(f.total / k) }))
|
|
3591
|
+
functions: [...fns.values()].sort((a, b) => b.self - a.self || a.key.localeCompare(b.key)).slice(0, fullRowMs === undefined ? topN : CPU_FUNCTIONS_KEPT).map((f) => ({ key: f.key, name: f.name, selfMs: round(f.self / k), totalMs: f.total === null ? null : round(f.total / k) }))
|
|
3365
3592
|
};
|
|
3366
3593
|
}
|
|
3594
|
+
const rows = kept.map((c) => ({
|
|
3595
|
+
key: c.name,
|
|
3596
|
+
...c.parent ? { parent: c.parent } : {},
|
|
3597
|
+
...c.rolledUpMs === undefined ? {} : { rolledUpMs: round(mean(c.rolledUpMs)), ...shownRollup(c, n) },
|
|
3598
|
+
name: c.name,
|
|
3599
|
+
renders: round(mean(c.renders)),
|
|
3600
|
+
mounts: round(mean(c.mounts)),
|
|
3601
|
+
instances: c.instances,
|
|
3602
|
+
selfMs: ms(c.selfMs),
|
|
3603
|
+
totalMs: ms(c.totalMs),
|
|
3604
|
+
causes: topCauses(c.causes, c.detailRenders ?? c.renders)
|
|
3605
|
+
}));
|
|
3367
3606
|
return {
|
|
3368
3607
|
step,
|
|
3369
3608
|
occurrences: n,
|
|
3609
|
+
...outsideOf(occ, n),
|
|
3610
|
+
...stampsOf(occ),
|
|
3370
3611
|
renderer,
|
|
3371
3612
|
frameBudgetMs: round(budgetMs, 1),
|
|
3372
3613
|
commits: round(mean(commits)),
|
|
3373
3614
|
renderMs: timed ? round(mean(occ.reduce((s, o) => s + (o.renderMs ?? 0), 0))) : null,
|
|
3374
3615
|
commitMaxMs: timed ? round(Math.max(...occ.map((o) => o.commitMaxMs ?? 0))) : null,
|
|
3375
|
-
|
|
3376
|
-
key: c.name,
|
|
3377
|
-
name: c.name,
|
|
3378
|
-
renders: round(mean(c.renders)),
|
|
3379
|
-
mounts: round(mean(c.mounts)),
|
|
3380
|
-
instances: c.instances,
|
|
3381
|
-
selfMs: ms(c.selfMs),
|
|
3382
|
-
totalMs: ms(c.totalMs),
|
|
3383
|
-
causes: topCauses(c.causes, c.renders)
|
|
3384
|
-
})),
|
|
3616
|
+
...fullRowMs !== undefined && timed ? encodeProfileItems(rows, fullRowMs) : { components: rows },
|
|
3385
3617
|
host: { renders: round(mean(host.reduce((s, c) => s + c.renders, 0))), selfMs: ms(host.reduce((s, c) => s + (c.selfMs ?? 0), 0)) },
|
|
3386
3618
|
omitted: {
|
|
3387
3619
|
count: rest.length,
|
|
3388
|
-
selfMs: ms(rest.reduce((s, c) => s + (c.selfMs ?? 0), 0)),
|
|
3620
|
+
selfMs: fullRowMs !== undefined && timed && occ.some((o) => o.omitted > 0) ? exact.every((e) => e !== undefined) ? Math.max(0, ms(exact.reduce((s, e) => s + e.selfMsTotal, 0) - [...all.values()].reduce((s, c) => s + (c.selfMs ?? 0), 0) - occ.reduce((s, o) => s + (o.outside?.selfMs ?? 0), 0))) : null : ms(rest.reduce((s, c) => s + (c.selfMs ?? 0), 0)),
|
|
3389
3621
|
sdkOmitted: occ.reduce((s, o) => s + o.omitted, 0)
|
|
3390
3622
|
},
|
|
3391
3623
|
updaters: commits > 0 ? Object.entries(updaters).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, UPDATERS_KEPT).map(([k, v]) => [k, round(v / commits)]) : [],
|
|
@@ -3423,7 +3655,7 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3423
3655
|
for (const [name, all] of [...byName].sort((a, b) => a[0].localeCompare(b[0])))
|
|
3424
3656
|
groups.push([name, all.map((w) => [w])]);
|
|
3425
3657
|
for (const [key, occs] of groups) {
|
|
3426
|
-
const found = occs.map((ws) => occurrence(ws.filter((w) => w.raw.endReason !== "timeout"), budget, options.idleWindowsNeutral, options.countFrameMs, options.upperBound === true)).filter((o) => !!o);
|
|
3658
|
+
const found = occs.map((ws) => occurrence(ws.filter((w) => w.raw.endReason !== "timeout"), budget, options.idleWindowsNeutral, options.countFrameMs, options.upperBound === true, options.fullRowMs !== undefined)).filter((o) => !!o);
|
|
3427
3659
|
const decided = found.some((o) => !o.idle) && found.every((o) => o.idle || o.renderer === "profiling");
|
|
3428
3660
|
const occ = decided ? found.map((o) => o.idle ? { ...o, renderer: "profiling" } : o) : found;
|
|
3429
3661
|
const cpu = options.cpu?.get(key);
|
|
@@ -3438,10 +3670,21 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3438
3670
|
if (vals.length && vals.every((v) => v !== undefined))
|
|
3439
3671
|
metrics.set(`${key}.${f}`, median(vals));
|
|
3440
3672
|
}
|
|
3441
|
-
|
|
3673
|
+
const profile = stepProfile(key, occ, budget, topN, cpu, options.exactMount === true, options.fullRowMs);
|
|
3674
|
+
const kind = placeRef(key).kind;
|
|
3675
|
+
const active = occ.filter((o) => !o.idle);
|
|
3676
|
+
const selected = middleBy((active.length ? active : occ).filter((o) => o.window), (o) => o.commitMaxMs, (o) => num2(o.window.raw.id) ?? 0, (o) => num2(o.window.raw.t0) ?? o.window.startWall);
|
|
3677
|
+
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && ["screen", "press", "drag", "other"].includes(kind) && selected?.slowestCommit;
|
|
3678
|
+
const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
|
|
3679
|
+
profiles.push({ ...profile, ...identity ? { slowestCommit: identity } : {}, ...openedBy ? { openedBy } : {} });
|
|
3442
3680
|
}
|
|
3443
3681
|
return { metrics, profiles };
|
|
3444
3682
|
}
|
|
3683
|
+
var windowOrder = (a, b) => (num2(a.raw.id) ?? 0) - (num2(b.raw.id) ?? 0) || (num2(a.raw.t0) ?? a.startWall) - (num2(b.raw.t0) ?? b.startWall);
|
|
3684
|
+
function middleBy(items, time, id, t0) {
|
|
3685
|
+
const ordered = [...items].sort((a, b) => (time(a) ?? -1) - (time(b) ?? -1) || id(a) - id(b) || t0(a) - t0(b));
|
|
3686
|
+
return ordered[Math.floor((ordered.length - 1) / 2)];
|
|
3687
|
+
}
|
|
3445
3688
|
|
|
3446
3689
|
// packages/derive/dist/src/facts.js
|
|
3447
3690
|
var ROUTE_CHANGE_ROW = "route_change";
|
|
@@ -4287,6 +4530,14 @@ function v6Metrics(windows, samples, t0, t1, version) {
|
|
|
4287
4530
|
out.set("sampler.thermal.start", thermal[0]);
|
|
4288
4531
|
out.set("sampler.thermal.hot_share", thermal.filter((v) => v >= 2).length / thermal.length);
|
|
4289
4532
|
}
|
|
4533
|
+
if (version >= 27) {
|
|
4534
|
+
const raw = inRun.map((s) => num7(s.raw.thermalRaw)).filter((v) => v !== undefined);
|
|
4535
|
+
if (raw.length) {
|
|
4536
|
+
out.set("sampler.thermal.raw_start", raw[0]);
|
|
4537
|
+
out.set("sampler.thermal.raw_max", Math.max(...raw));
|
|
4538
|
+
out.set("sampler.thermal.raw_light_share", raw.filter((v) => v >= 1).length / raw.length);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4290
4541
|
const hz = inRun.map((s) => num7(obj7(s.raw.frames).maxHz)).filter((v) => v !== undefined);
|
|
4291
4542
|
if (hz.length)
|
|
4292
4543
|
out.set("sampler.max_hz", Math.max(...hz));
|
|
@@ -4601,12 +4852,14 @@ function endsHome(windows, endCommandWall) {
|
|
|
4601
4852
|
}
|
|
4602
4853
|
function endMetrics(markers, events, samples, windows, launchMb, opts = {}) {
|
|
4603
4854
|
const out = new Map;
|
|
4604
|
-
const
|
|
4855
|
+
const declared = markers.some((m) => m.phase === "start");
|
|
4856
|
+
const readsEnd = declared || opts.markerless === true && events.gc.length > 0;
|
|
4857
|
+
const n = readsEnd ? runEndCommand(markers, events, windows) : null;
|
|
4605
4858
|
const command = n === null ? undefined : events.gc.find((g) => g.n === n && g.status === "ok");
|
|
4606
|
-
const home = endsHome(windows, command?.wall);
|
|
4859
|
+
const home = endsHome(windows, declared ? command?.wall : undefined);
|
|
4607
4860
|
if (home !== undefined)
|
|
4608
4861
|
out.set("sdk.journey_ends_home", home);
|
|
4609
|
-
if (!
|
|
4862
|
+
if (!readsEnd)
|
|
4610
4863
|
return out;
|
|
4611
4864
|
out.set("sdk.end_gc", command ? 1 : 0);
|
|
4612
4865
|
const g = n === null ? undefined : cleanOf(events, windows).get(n);
|
|
@@ -4614,6 +4867,8 @@ function endMetrics(markers, events, samples, windows, launchMb, opts = {}) {
|
|
|
4614
4867
|
return out;
|
|
4615
4868
|
const post = g.post === undefined ? undefined : footprintsBySeq(samples).get(g.post);
|
|
4616
4869
|
const manual = opts.manualRead ? manualReadFootprint(samples, g.n) : undefined;
|
|
4870
|
+
if (!declared && manual === undefined)
|
|
4871
|
+
return out;
|
|
4617
4872
|
const footprint = manual ?? post;
|
|
4618
4873
|
if (footprint !== undefined) {
|
|
4619
4874
|
if (opts.manualRead)
|
|
@@ -4678,8 +4933,7 @@ function keyFirstOpens(windows) {
|
|
|
4678
4933
|
}
|
|
4679
4934
|
|
|
4680
4935
|
// packages/derive/dist/src/derive.js
|
|
4681
|
-
var
|
|
4682
|
-
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
|
|
4936
|
+
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27];
|
|
4683
4937
|
var windowsForVersion = (windows, version = DERIVATION_VERSION) => version >= 22 ? keyFirstOpens(windows) : windows;
|
|
4684
4938
|
function stepWindowsFor(windows, version = DERIVATION_VERSION) {
|
|
4685
4939
|
return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version);
|
|
@@ -4698,6 +4952,22 @@ function samplerFrameBudgetMs(samples) {
|
|
|
4698
4952
|
}
|
|
4699
4953
|
return hz > 1 ? frameBudgetMs(Math.round(hz)) : undefined;
|
|
4700
4954
|
}
|
|
4955
|
+
function profileOptions(windows, version, processWindows = windows) {
|
|
4956
|
+
if (version < 26)
|
|
4957
|
+
return {};
|
|
4958
|
+
const processes = new Set(processWindows.map((w) => w.raw.react?.causes?.process).filter((p) => typeof p === "string"));
|
|
4959
|
+
const presses = windows.filter((w) => w.raw.kind === "press" && placeRef(w.name).kind === "press" && placeRef(w.name).segment !== "after3s").sort((a, b) => appClock(b)[0] - appClock(a)[0] || a.name.localeCompare(b.name));
|
|
4960
|
+
const openedBy = new Map;
|
|
4961
|
+
for (const screen of windows.filter((w) => placeRef(w.name).kind === "screen")) {
|
|
4962
|
+
const start = appClock(screen)[0], opener = presses.find((w) => {
|
|
4963
|
+
const [from, to] = appClock(w);
|
|
4964
|
+
return from <= start && start < to;
|
|
4965
|
+
});
|
|
4966
|
+
if (opener)
|
|
4967
|
+
openedBy.set(screen, opener.name);
|
|
4968
|
+
}
|
|
4969
|
+
return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, openedBy };
|
|
4970
|
+
}
|
|
4701
4971
|
function deriveRun(input) {
|
|
4702
4972
|
const { run, markers = [], ri, clockOffsetMs, samples = [], memWindow } = input;
|
|
4703
4973
|
const version = input.derivationVersion ?? DERIVATION_VERSION;
|
|
@@ -4719,7 +4989,9 @@ function deriveRun(input) {
|
|
|
4719
4989
|
return { rows, steps, run: final, profiles: [], derivationVersion: version };
|
|
4720
4990
|
const budget = input.frameBudgetMs ?? (version >= 6 ? samplerFrameBudgetMs(samplesInRun(samples, final.t0, final.t1)) : undefined);
|
|
4721
4991
|
const compareMs = version >= 20 ? LIMIT_FRAME_MS : budget ?? frameBudgetMs();
|
|
4992
|
+
const allComponents = profileOptions(windows, version, input.windows);
|
|
4722
4993
|
const prof = profileMetrics(windows, steps, {
|
|
4994
|
+
...allComponents,
|
|
4723
4995
|
frameBudgetMs: version >= 11 ? compareMs : Number(compareMs.toFixed(1)),
|
|
4724
4996
|
...version >= 12 ? { countFrameMs: LIMIT_FRAME_MS } : {},
|
|
4725
4997
|
...cpu === undefined ? {} : { cpu },
|
|
@@ -4729,6 +5001,7 @@ function deriveRun(input) {
|
|
|
4729
5001
|
...version >= 16 ? { upperBound: true } : {}
|
|
4730
5002
|
});
|
|
4731
5003
|
const bg = backgroundMetrics(background, {
|
|
5004
|
+
...allComponents,
|
|
4732
5005
|
frameBudgetMs: compareMs,
|
|
4733
5006
|
countFrameMs: LIMIT_FRAME_MS,
|
|
4734
5007
|
...input.profileTopN === undefined ? {} : { topN: input.profileTopN },
|
|
@@ -4759,7 +5032,7 @@ function deriveRun(input) {
|
|
|
4759
5032
|
if (version >= 19) {
|
|
4760
5033
|
const events = input.events ?? { gc: [], appState: [] };
|
|
4761
5034
|
const launch = extra.get("mem.launch_mb");
|
|
4762
|
-
for (const [k, v] of [...endMetrics(markers, events, samplesInRun(samples, final.t0, final.t1), sdkWindows, launch, { manualRead: version >= 23 }), ...jsHeapMetrics(events, sdkWindows)])
|
|
5035
|
+
for (const [k, v] of [...endMetrics(markers, events, samplesInRun(samples, final.t0, final.t1), sdkWindows, launch, { manualRead: version >= 23, markerless: version >= 24 }), ...jsHeapMetrics(events, sdkWindows)])
|
|
4763
5036
|
extra.set(k, v);
|
|
4764
5037
|
}
|
|
4765
5038
|
const base = { run_id: final.run_id, arm: final.arm, block: final.block, valid: final.valid, invalid_reason: final.invalid_reason };
|
|
@@ -4962,44 +5235,24 @@ async function uploadRuns(deps, outDir, runs, opts) {
|
|
|
4962
5235
|
return { uploaded, skipped };
|
|
4963
5236
|
}
|
|
4964
5237
|
|
|
4965
|
-
// packages/present/dist/src/
|
|
4966
|
-
var
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
tap: "Tap",
|
|
4970
|
-
scroll: "Scroll",
|
|
4971
|
-
step: "Test step",
|
|
4972
|
-
journey: "Whole journey",
|
|
4973
|
-
background: "Background",
|
|
4974
|
-
after3s: "3 s after"
|
|
4975
|
-
};
|
|
4976
|
-
var KIND_WORD = {
|
|
4977
|
-
journey: PLACE_KIND_WORDS.journey,
|
|
4978
|
-
screen: PLACE_KIND_WORDS.screen,
|
|
4979
|
-
launch: PLACE_KIND_WORDS.launch,
|
|
4980
|
-
press: PLACE_KIND_WORDS.tap,
|
|
4981
|
-
drag: PLACE_KIND_WORDS.scroll,
|
|
4982
|
-
step: PLACE_KIND_WORDS.step,
|
|
4983
|
-
background: PLACE_KIND_WORDS.background
|
|
4984
|
-
};
|
|
4985
|
-
var BACKGROUND_SOURCES = {
|
|
4986
|
-
websocket: "data pushed over a WebSocket",
|
|
4987
|
-
launch: "work started at app launch",
|
|
4988
|
-
link: "work of a deep link outside its screen",
|
|
4989
|
-
touch: "work of a tap outside its window",
|
|
4990
|
-
input: "work of typing outside its window"
|
|
4991
|
-
};
|
|
4992
|
-
var OPEN_MARK = /#(1|repeat)$/;
|
|
5238
|
+
// packages/present/dist/src/capital.js
|
|
5239
|
+
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5240
|
+
|
|
5241
|
+
// packages/present/dist/src/placeWords.js
|
|
4993
5242
|
var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
|
|
4994
5243
|
var ORDINAL_SUFFIXES = ["th", "st", "nd", "rd"];
|
|
4995
5244
|
var LAST_TEEN = 13;
|
|
4996
5245
|
var FIRST_TEEN = 11;
|
|
4997
5246
|
var words = (s) => s.replace(/[._\-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
4998
5247
|
var cap = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
5248
|
+
var nameWords = (s) => cap(words(s));
|
|
4999
5249
|
var ordinal = (n) => {
|
|
5000
5250
|
const inTeens = n % 100 >= FIRST_TEEN && n % 100 <= LAST_TEEN;
|
|
5001
5251
|
return `${n}${inTeens ? "th" : ORDINAL_SUFFIXES[n % 10] ?? "th"}`;
|
|
5002
5252
|
};
|
|
5253
|
+
function routeFileName(path) {
|
|
5254
|
+
return path.split("/").filter((p) => p && !/^\(.*\)$/.test(p) && p !== "index").map(nameWords).join(" ") || "App start";
|
|
5255
|
+
}
|
|
5003
5256
|
function screenName(path) {
|
|
5004
5257
|
const parts = path.split("/").filter((p) => p && p !== "__root");
|
|
5005
5258
|
const plain = parts.filter((p) => !/^\(.*\)$/.test(p) && !/^\[.*\]$/.test(p) && p !== "index");
|
|
@@ -5011,50 +5264,7 @@ function screenName(path) {
|
|
|
5011
5264
|
const name = cap(words(leaf));
|
|
5012
5265
|
return group && !GENERIC_GROUPS.has(group) && !plain.slice(0, -1).length ? `${cap(words(group))} › ${name}` : name;
|
|
5013
5266
|
}
|
|
5014
|
-
|
|
5015
|
-
if (kind === "journey")
|
|
5016
|
-
return PLACE_KIND_WORDS.journey;
|
|
5017
|
-
if (kind === "other")
|
|
5018
|
-
return words(stem);
|
|
5019
|
-
const rest = stem.slice(stem.indexOf(":") + 1).replace(kind === "press" || kind === "drag" ? OPEN_MARK : /$^/, "");
|
|
5020
|
-
if (kind === "background")
|
|
5021
|
-
return `${KIND_WORD.background}: ${BACKGROUND_SOURCES[rest] ?? words(rest)}`;
|
|
5022
|
-
if (kind === "launch")
|
|
5023
|
-
return `${KIND_WORD.launch}: ${screenName(rest)}`;
|
|
5024
|
-
if (kind !== "screen")
|
|
5025
|
-
return `${KIND_WORD[kind]}: ${words(rest)}`;
|
|
5026
|
-
const visitMark = /^(.*)#(\d+)$/.exec(rest);
|
|
5027
|
-
const visit = visitMark ? Number(visitMark[2]) : 1;
|
|
5028
|
-
return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
|
|
5029
|
-
}
|
|
5030
|
-
function windowName(id) {
|
|
5031
|
-
if (id === "flow")
|
|
5032
|
-
return PLACE_KIND_WORDS.journey;
|
|
5033
|
-
const ref = placeRef(id);
|
|
5034
|
-
const name = stemName(ref.stem, ref.kind);
|
|
5035
|
-
return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
|
|
5036
|
-
}
|
|
5037
|
-
function placeKindWord(id) {
|
|
5038
|
-
if (id === "flow")
|
|
5039
|
-
return PLACE_KIND_WORDS.journey;
|
|
5040
|
-
const { kind } = placeRef(id);
|
|
5041
|
-
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5042
|
-
}
|
|
5043
|
-
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5044
|
-
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5045
|
-
function placeLabel(id, screen) {
|
|
5046
|
-
if (id === "flow")
|
|
5047
|
-
return PLACE_KIND_WORDS.journey;
|
|
5048
|
-
const ref = placeRef(id);
|
|
5049
|
-
const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5050
|
-
if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
|
|
5051
|
-
const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
|
|
5052
|
-
return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
|
|
5053
|
-
}
|
|
5054
|
-
const name = windowName(ref.stem);
|
|
5055
|
-
const colon = name.indexOf(": ");
|
|
5056
|
-
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
|
|
5057
|
-
}
|
|
5267
|
+
|
|
5058
5268
|
// packages/present/dist/src/states.js
|
|
5059
5269
|
var STATE_WORDS = {
|
|
5060
5270
|
worse: "Worse",
|
|
@@ -5063,7 +5273,10 @@ var STATE_WORDS = {
|
|
|
5063
5273
|
"no-change": "No change",
|
|
5064
5274
|
"cant-tell": "Can’t tell"
|
|
5065
5275
|
};
|
|
5276
|
+
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5277
|
+
var isWorseState = (state) => state === "worse";
|
|
5066
5278
|
var NOT_BLOCKING = "not blocking";
|
|
5279
|
+
|
|
5067
5280
|
// packages/present/dist/src/values.js
|
|
5068
5281
|
var NO_VALUE = "–";
|
|
5069
5282
|
var FALLBACK_DECIMALS = 1;
|
|
@@ -5119,6 +5332,143 @@ function fieldValueText(field, v) {
|
|
|
5119
5332
|
const def = fieldDef(field.split(KEY_SEPARATOR)[0]);
|
|
5120
5333
|
return format(def?.unit, def?.decimals ?? FALLBACK_DECIMALS, v);
|
|
5121
5334
|
}
|
|
5335
|
+
|
|
5336
|
+
// packages/present/dist/src/rowWords.js
|
|
5337
|
+
var GONE_TAILS = {
|
|
5338
|
+
branch: { notInRun: "on main, not in this run", removed: "on main, not rendered in this run" },
|
|
5339
|
+
journey: { notInRun: "in the sessions before, not in the newest session", removed: "in the sessions before, not rendered in the newest session" }
|
|
5340
|
+
};
|
|
5341
|
+
var ROW_STATE_WORDS = {
|
|
5342
|
+
new: "New in this PR: not compared with main",
|
|
5343
|
+
notInRun: upperFirst(GONE_TAILS.branch.notInRun),
|
|
5344
|
+
removed: upperFirst(GONE_TAILS.branch.removed)
|
|
5345
|
+
};
|
|
5346
|
+
var JOURNEY_ROW_WORDS = {
|
|
5347
|
+
new: "New in the newest session: not compared with the sessions before",
|
|
5348
|
+
notInRun: upperFirst(GONE_TAILS.journey.notInRun),
|
|
5349
|
+
removed: upperFirst(GONE_TAILS.journey.removed)
|
|
5350
|
+
};
|
|
5351
|
+
function placeShownName(p, o = {}) {
|
|
5352
|
+
const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
|
|
5353
|
+
const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
|
|
5354
|
+
return o.onScreen ? `${named} on ${o.onScreen}` : named;
|
|
5355
|
+
}
|
|
5356
|
+
var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
|
|
5357
|
+
function componentShownName(name, file) {
|
|
5358
|
+
if (name !== "default")
|
|
5359
|
+
return name;
|
|
5360
|
+
const path = file.replace(/\.[^.\/]+$/, "");
|
|
5361
|
+
const route = /^(?:src\/)?app\/(.*)$/.exec(path)?.[1];
|
|
5362
|
+
const parts = path.split("/");
|
|
5363
|
+
const stem = parts.at(-1) ?? "";
|
|
5364
|
+
if (route !== undefined && stem !== "_layout")
|
|
5365
|
+
return routeFileName(route);
|
|
5366
|
+
const label = stem === "index" || stem === "_layout" ? parts.at(-2) ?? stem : stem;
|
|
5367
|
+
return nameWords(label);
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
// packages/present/dist/src/places.js
|
|
5371
|
+
var PLACE_KIND_WORDS = {
|
|
5372
|
+
screen: "Screen",
|
|
5373
|
+
launch: "Launch screen",
|
|
5374
|
+
tap: "Tap",
|
|
5375
|
+
scroll: "Scroll",
|
|
5376
|
+
step: "Test step",
|
|
5377
|
+
journey: "Whole journey",
|
|
5378
|
+
background: "Background",
|
|
5379
|
+
after3s: "3 s after"
|
|
5380
|
+
};
|
|
5381
|
+
var KIND_WORD = {
|
|
5382
|
+
journey: PLACE_KIND_WORDS.journey,
|
|
5383
|
+
screen: PLACE_KIND_WORDS.screen,
|
|
5384
|
+
launch: PLACE_KIND_WORDS.launch,
|
|
5385
|
+
press: PLACE_KIND_WORDS.tap,
|
|
5386
|
+
drag: PLACE_KIND_WORDS.scroll,
|
|
5387
|
+
step: PLACE_KIND_WORDS.step,
|
|
5388
|
+
background: PLACE_KIND_WORDS.background
|
|
5389
|
+
};
|
|
5390
|
+
var BACKGROUND_SOURCES = {
|
|
5391
|
+
websocket: "data pushed over a WebSocket",
|
|
5392
|
+
launch: "work started at app launch",
|
|
5393
|
+
link: "work of a deep link outside its screen",
|
|
5394
|
+
touch: "work of a tap outside its window",
|
|
5395
|
+
input: "work of typing outside its window"
|
|
5396
|
+
};
|
|
5397
|
+
var OPEN_MARK = /#(1|repeat)$/;
|
|
5398
|
+
function placeIdShownName(id, o = {}) {
|
|
5399
|
+
const p = placeParts(id);
|
|
5400
|
+
if (!p)
|
|
5401
|
+
return null;
|
|
5402
|
+
const component = (part) => ({ name: part.name, file: part.file ?? "" });
|
|
5403
|
+
const screen = p.screen.name === "(no screen)" ? "" : p.screen.name === "(outside app components)" ? screenName(p.screen.file ?? "") : componentShownName(p.screen.name, p.screen.file ?? "");
|
|
5404
|
+
if (p.kind === "screen" || p.kind === "launch")
|
|
5405
|
+
return screen + (p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "");
|
|
5406
|
+
return placeShownName({ kind: p.kind, target: p.outsideHost === null ? component(p.target) : null, parent: p.parent ? component(p.parent) : null, k: p.k, ...p.outsideHost === null ? {} : { host: p.outsideHost } }, { onScreen: o.onScreen === false ? undefined : screen });
|
|
5407
|
+
}
|
|
5408
|
+
var isLibraryPlace = (id) => placeParts(id)?.outsideHost != null;
|
|
5409
|
+
function stemName(stem, kind) {
|
|
5410
|
+
const shown = placeIdShownName(stem);
|
|
5411
|
+
if (shown !== null)
|
|
5412
|
+
return isLibraryPlace(stem) ? shown : `${KIND_WORD[kind]}: ${shown}`;
|
|
5413
|
+
if (kind === "journey")
|
|
5414
|
+
return PLACE_KIND_WORDS.journey;
|
|
5415
|
+
if (kind === "other")
|
|
5416
|
+
return words(stem);
|
|
5417
|
+
const rest = stem.slice(stem.indexOf(":") + 1).replace(kind === "press" || kind === "drag" ? OPEN_MARK : /$^/, "");
|
|
5418
|
+
if (kind === "background")
|
|
5419
|
+
return `${KIND_WORD.background}: ${BACKGROUND_SOURCES[rest] ?? words(rest)}`;
|
|
5420
|
+
if (kind === "launch")
|
|
5421
|
+
return `${KIND_WORD.launch}: ${screenName(rest)}`;
|
|
5422
|
+
if (kind !== "screen")
|
|
5423
|
+
return `${KIND_WORD[kind]}: ${words(rest)}`;
|
|
5424
|
+
const visitMark = /^(.*)#(\d+)$/.exec(rest);
|
|
5425
|
+
const visit = visitMark ? Number(visitMark[2]) : 1;
|
|
5426
|
+
return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
|
|
5427
|
+
}
|
|
5428
|
+
function windowName(id) {
|
|
5429
|
+
if (id === "flow")
|
|
5430
|
+
return PLACE_KIND_WORDS.journey;
|
|
5431
|
+
const ref = placeRef(id);
|
|
5432
|
+
const name = stemName(ref.stem, ref.kind);
|
|
5433
|
+
return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
|
|
5434
|
+
}
|
|
5435
|
+
function placeKindWord(id) {
|
|
5436
|
+
if (id === "flow")
|
|
5437
|
+
return PLACE_KIND_WORDS.journey;
|
|
5438
|
+
const { kind } = placeRef(id);
|
|
5439
|
+
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5440
|
+
}
|
|
5441
|
+
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5442
|
+
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5443
|
+
function placeLabel(id, screen) {
|
|
5444
|
+
if (id === "flow")
|
|
5445
|
+
return PLACE_KIND_WORDS.journey;
|
|
5446
|
+
const ref = placeRef(id);
|
|
5447
|
+
const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5448
|
+
const shown = placeIdShownName(ref.stem);
|
|
5449
|
+
if (shown !== null)
|
|
5450
|
+
return isLibraryPlace(ref.stem) ? `${shown}${after}` : `${KIND_WORD[ref.kind]} · ${shown}${after}`;
|
|
5451
|
+
if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
|
|
5452
|
+
const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
|
|
5453
|
+
return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
|
|
5454
|
+
}
|
|
5455
|
+
const name = windowName(ref.stem);
|
|
5456
|
+
const colon = name.indexOf(": ");
|
|
5457
|
+
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
|
|
5458
|
+
}
|
|
5459
|
+
// packages/present/dist/src/words.js
|
|
5460
|
+
var REASON_WORDS = {
|
|
5461
|
+
"no-baseline": "there are no main runs on this phone and journey yet",
|
|
5462
|
+
"too-few-runs": "there are too few valid runs on a side",
|
|
5463
|
+
"judged-measures-missing": "the judged measures are missing on a side",
|
|
5464
|
+
"measured-differently": "the sides were measured differently",
|
|
5465
|
+
"runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
|
|
5466
|
+
"facts-version-mismatch": "the sides were derived with different versions",
|
|
5467
|
+
"not-judged-on-simulators": "timing is not judged on simulators",
|
|
5468
|
+
"platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
|
|
5469
|
+
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5470
|
+
};
|
|
5471
|
+
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5122
5472
|
// packages/present/dist/src/stability.js
|
|
5123
5473
|
var UNSTABLE_WORDS = {
|
|
5124
5474
|
short: "main varies too much here",
|
|
@@ -5127,6 +5477,92 @@ var UNSTABLE_WORDS = {
|
|
|
5127
5477
|
ratioTip: "1.0 = main’s run-to-run noise alone would pass the warning step about once in 100 comparisons.",
|
|
5128
5478
|
what: "Main’s own runs vary too much here to tell, so a comparison there would mostly measure noise. It is shown with its values, never judged, never blocks and is never counted. It is judged again once main settles."
|
|
5129
5479
|
};
|
|
5480
|
+
// packages/present/dist/src/present.js
|
|
5481
|
+
var ASPECT_WORDS = {
|
|
5482
|
+
renderer: "React renderer",
|
|
5483
|
+
reactExport: "React export",
|
|
5484
|
+
causeSplit: "work split by its cause",
|
|
5485
|
+
windowModel: "window model",
|
|
5486
|
+
screenWindowModel: "screen window model",
|
|
5487
|
+
cpuSource: "CPU source",
|
|
5488
|
+
sdk: "SDK version",
|
|
5489
|
+
driver: "driver",
|
|
5490
|
+
driverVersion: "driver version",
|
|
5491
|
+
journeyVersion: "journey version",
|
|
5492
|
+
actionGc: "memory clean-up before each step",
|
|
5493
|
+
endGc: "memory clean-up at the journey end",
|
|
5494
|
+
cpuProfiling: "CPU profiling"
|
|
5495
|
+
};
|
|
5496
|
+
// packages/present/dist/src/main-history.js
|
|
5497
|
+
var MAIN_TIER_WORDS = {
|
|
5498
|
+
same: "In norm",
|
|
5499
|
+
better: STATE_WORDS.better,
|
|
5500
|
+
cant: STATE_WORDS["cant-tell"],
|
|
5501
|
+
slight: STATE_WORDS.warning,
|
|
5502
|
+
worse: STATE_WORDS.worse
|
|
5503
|
+
};
|
|
5504
|
+
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5505
|
+
function mainCantWords(reason, config) {
|
|
5506
|
+
if (!reason)
|
|
5507
|
+
return "";
|
|
5508
|
+
return reason.kind === "noisy_main" ? "main’s runs vary too much there" : `main has ${reason.earlier_sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${reason.since_label ? ` on ${reason.since_label}` : ""}`;
|
|
5509
|
+
}
|
|
5510
|
+
var waitingSessionsWords = (sessions) => `${sessions} older main ${sessions === 1 ? "session waits" : "sessions wait"} for a reprocess`;
|
|
5511
|
+
function mainPlaceReasonWords(reason, config) {
|
|
5512
|
+
switch (reason.kind) {
|
|
5513
|
+
case "no_baseline":
|
|
5514
|
+
case "noisy_main":
|
|
5515
|
+
return mainCantWords(reason, config);
|
|
5516
|
+
case "higher":
|
|
5517
|
+
return "Higher than main’s earlier sessions";
|
|
5518
|
+
case "lower":
|
|
5519
|
+
return "Lower than main’s earlier sessions";
|
|
5520
|
+
case "missing_value":
|
|
5521
|
+
return REASON_WORDS["runs-unknown"];
|
|
5522
|
+
case "measured_differently":
|
|
5523
|
+
return REASON_WORDS["measured-differently"];
|
|
5524
|
+
case "core":
|
|
5525
|
+
return reason.codes.map((code) => REASON_WORDS[code]).join("; ");
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
var MAIN_SUMMARY_PENDING = "Runs received; the summary is not computed yet.";
|
|
5529
|
+
var MAIN_NEWEST_SUMMARY_PENDING = `Newest: ${MAIN_SUMMARY_PENDING}`;
|
|
5530
|
+
var SHORT_ASPECT_WORDS = {
|
|
5531
|
+
...ASPECT_WORDS,
|
|
5532
|
+
sdk: "SDK",
|
|
5533
|
+
renderer: "Renderer",
|
|
5534
|
+
endGc: "End GC",
|
|
5535
|
+
causeSplit: "Cause split",
|
|
5536
|
+
driver: "Driver",
|
|
5537
|
+
driverVersion: "Driver",
|
|
5538
|
+
journeyVersion: "Journey"
|
|
5539
|
+
};
|
|
5540
|
+
function mainSessionCantWords(places, reason, config) {
|
|
5541
|
+
return `${STATE_WORDS["cant-tell"]} at ${places} place${places === 1 ? "" : "s"}: ${mainCantWords(reason, config)}`;
|
|
5542
|
+
}
|
|
5543
|
+
var KIND_PLURAL = {
|
|
5544
|
+
screen: "screens",
|
|
5545
|
+
press: "taps",
|
|
5546
|
+
drag: "scrolls",
|
|
5547
|
+
launch: "launch screens",
|
|
5548
|
+
step: "test steps",
|
|
5549
|
+
background: "background work",
|
|
5550
|
+
other: "other places"
|
|
5551
|
+
};
|
|
5552
|
+
var listWords = (kinds) => {
|
|
5553
|
+
const words = kinds.map((k) => KIND_PLURAL[k] ?? "other places");
|
|
5554
|
+
return words.length < 2 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`;
|
|
5555
|
+
};
|
|
5556
|
+
function mainCutWords(v) {
|
|
5557
|
+
const parts = [];
|
|
5558
|
+
if (v.cuts.length)
|
|
5559
|
+
parts.push(`Starts a new ${v.cuts.join(" and ")} baseline.`);
|
|
5560
|
+
if (v.partial_render) {
|
|
5561
|
+
const cut = listWords(v.partial_render.cut);
|
|
5562
|
+
parts.push(`${cut[0].toUpperCase()}${cut.slice(1)} start a new render baseline; ${listWords(v.partial_render.kept)} keep theirs.`);
|
|
5563
|
+
}
|
|
5564
|
+
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5565
|
+
}
|
|
5130
5566
|
// apps/agent/src/summary.ts
|
|
5131
5567
|
var fieldWord = (field) => measureDef(field)?.short ?? field;
|
|
5132
5568
|
function plainMetric(metric) {
|
|
@@ -5166,6 +5602,13 @@ function summarize(input) {
|
|
|
5166
5602
|
// apps/agent/src/remote.ts
|
|
5167
5603
|
var isBlocking = (s) => !!s?.verdicts && (s.verdicts.blocking ?? s.verdicts.state === "worse");
|
|
5168
5604
|
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
5605
|
+
function heatNote(sc) {
|
|
5606
|
+
const n = sc.heated_runs ?? 0;
|
|
5607
|
+
if (!n)
|
|
5608
|
+
return null;
|
|
5609
|
+
const phone = sc.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
5610
|
+
return `${INFO_WORDS.heat} in ${n} of ${sc.runs.valid + sc.runs.invalid} runs · ${sc.scenario ?? "journey"} · ${phone}`;
|
|
5611
|
+
}
|
|
5169
5612
|
function movedItems(v, sc) {
|
|
5170
5613
|
const flat = v.items.filter((i) => i.scenario === sc.scenario && (!sc.device || !i.device_id || i.device_id === sc.device.id));
|
|
5171
5614
|
const h = sc.headline;
|
|
@@ -5201,7 +5644,13 @@ function movedCounts(sc) {
|
|
|
5201
5644
|
}
|
|
5202
5645
|
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
5203
5646
|
var addCounts = (xs) => xs.reduce((a, c) => ({ worse: a.worse + c.worse, warnings: a.warnings + c.warnings, better: a.better + c.better, noChange: a.noChange + c.noChange, inconclusive: a.inconclusive + c.inconclusive }), { worse: 0, warnings: 0, better: 0, noChange: 0, inconclusive: 0 });
|
|
5204
|
-
var setupLine = (h) =>
|
|
5647
|
+
var setupLine = (h) => {
|
|
5648
|
+
const parts = [
|
|
5649
|
+
h?.measured_differently?.length ? `Measured differently (${h.measured_differently.join(", ")}) — ${NOT_BLOCKING}` : "",
|
|
5650
|
+
h?.waiting_for_reprocess ? waitingWords(h.waiting_for_reprocess) : ""
|
|
5651
|
+
].filter(Boolean);
|
|
5652
|
+
return parts.length ? parts.join("; ") : null;
|
|
5653
|
+
};
|
|
5205
5654
|
var keyHeader = (t) => ({ "x-keepsnappy-key": t.key });
|
|
5206
5655
|
async function createRunSet(t, body) {
|
|
5207
5656
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/run-sets", keyHeader(t), body);
|
|
@@ -5263,11 +5712,16 @@ function formatRunSet(s, top = 5) {
|
|
|
5263
5712
|
lines.push(` ${FEW_RUNS}`);
|
|
5264
5713
|
if (sc.role !== "adhoc" && !noCounts(movedCounts(sc)))
|
|
5265
5714
|
lines.push(` ${countsLine(movedCounts(sc))}`);
|
|
5266
|
-
|
|
5715
|
+
if (heatNote(sc))
|
|
5716
|
+
lines.push(` ${heatNote(sc)}`);
|
|
5717
|
+
const allMoved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
5718
|
+
const moved = allMoved.slice(0, top);
|
|
5267
5719
|
for (const i of moved) {
|
|
5268
5720
|
const what = i.label_text ? `${i.step && i.step !== "*" ? `${placeLabel(i.step)}: ` : ""}${i.label_text}` : i.step ? `${placeLabel(i.step)}: ${fieldWord(i.metric)}` : plainMetric(i.metric);
|
|
5269
5721
|
lines.push(` ${stateWord(i.state === "worse" && i.grade === "warn" ? "warn" : i.state).padEnd(WORD_WIDTH)} ${what}${i.change_text ? `: ${i.change_text}` : change(i.metric, i.baseline, i.candidate)}`);
|
|
5270
5722
|
}
|
|
5723
|
+
if (allMoved.length > moved.length)
|
|
5724
|
+
lines.push(` ${allMoved.length - moved.length} more`);
|
|
5271
5725
|
if (sc.dashboard_url)
|
|
5272
5726
|
lines.push(` ${sc.dashboard_url}`);
|
|
5273
5727
|
}
|
|
@@ -5288,6 +5742,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5288
5742
|
`;
|
|
5289
5743
|
}
|
|
5290
5744
|
const v = s.verdicts;
|
|
5745
|
+
const heated = v.scenarios.flatMap((sc) => heatNote(sc) ?? []);
|
|
5291
5746
|
const rows = v.scenarios.map((sc) => {
|
|
5292
5747
|
const c = movedCounts(sc);
|
|
5293
5748
|
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${word(sc.state, sc.word, sc.headline)}${sc.low_data ? ` (${FEW_RUNS})` : ""} | ${noCounts(c) ? "–" : `${c.worse} / ${c.warnings} / ${c.better} / ${c.noChange} / ${c.inconclusive}`} | ${sc.dashboard_url ? `[open](${sc.dashboard_url})` : "-"} |`;
|
|
@@ -5301,6 +5756,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5301
5756
|
`| Journey | Device | Result | ${STATE_WORDS.worse} / ${STATE_WORDS.warning} / ${STATE_WORDS.better} / ${STATE_WORDS["no-change"]} / ${STATE_WORDS["cant-tell"]} | Details |`,
|
|
5302
5757
|
"|---|---|---|---|---|",
|
|
5303
5758
|
...rows,
|
|
5759
|
+
...heated.length ? ["", ...heated.map((note) => `${note}.`)] : [],
|
|
5304
5760
|
"",
|
|
5305
5761
|
`[Open the dashboard](${s.dashboard_url})`,
|
|
5306
5762
|
""
|
|
@@ -8812,15 +9268,18 @@ ${r.stderr}` : ""}`);
|
|
|
8812
9268
|
}
|
|
8813
9269
|
|
|
8814
9270
|
// apps/agent/src/run.ts
|
|
8815
|
-
|
|
9271
|
+
var matchesMetric = (prefix, metric) => metric === prefix || metric.startsWith(`${prefix}.`);
|
|
9272
|
+
function renameMetrics(rows, rename, warn = console.warn) {
|
|
9273
|
+
for (const key of Object.keys(rename)) {
|
|
9274
|
+
if (!rows.some((r) => [key, `step:${key}`].some((prefix) => matchesMetric(prefix, r.metric))))
|
|
9275
|
+
warn(`steps.rename: key "${key}" matches no metric.`);
|
|
9276
|
+
}
|
|
8816
9277
|
const pairs = Object.entries(rename).flatMap(([a, b]) => [[a, b], [`step:${a}`, `step:${b}`]]);
|
|
8817
9278
|
if (pairs.length === 0)
|
|
8818
9279
|
return [...rows];
|
|
8819
9280
|
return rows.map((r) => {
|
|
8820
9281
|
for (const [from, to] of pairs) {
|
|
8821
|
-
if (r.metric
|
|
8822
|
-
return { ...r, metric: to };
|
|
8823
|
-
if (r.metric.startsWith(`${from}.`))
|
|
9282
|
+
if (matchesMetric(from, r.metric))
|
|
8824
9283
|
return { ...r, metric: `${to}${r.metric.slice(from.length)}` };
|
|
8825
9284
|
}
|
|
8826
9285
|
return r;
|
|
@@ -8838,7 +9297,7 @@ async function keepsnappyRun(deps, source, opts) {
|
|
|
8838
9297
|
...opts.markers ? { markers: opts.markers } : {},
|
|
8839
9298
|
...opts.clockOffsetMs === undefined ? {} : { clockOffsetMs: opts.clockOffsetMs }
|
|
8840
9299
|
});
|
|
8841
|
-
const rows = renameMetrics(raw, opts.config.steps.rename);
|
|
9300
|
+
const rows = renameMetrics(raw, opts.config.steps.rename, deps.log);
|
|
8842
9301
|
await deps.fs.writeFile(join8(opts.outDir, "metrics.csv"), toCsv(rows));
|
|
8843
9302
|
const summary = summarize({ runs, outDir: opts.outDir });
|
|
8844
9303
|
return { runs, rows, summary };
|
|
@@ -9257,10 +9716,20 @@ var nodeSpawner = {
|
|
|
9257
9716
|
let stdout = "";
|
|
9258
9717
|
let stderr = "";
|
|
9259
9718
|
let error;
|
|
9719
|
+
let timeout;
|
|
9720
|
+
let killTimeout;
|
|
9721
|
+
let closed = false;
|
|
9260
9722
|
const child = spawn(cmd, [...args], {
|
|
9261
|
-
env: { ...process.env, ...opts?.env }
|
|
9262
|
-
...opts?.timeoutMs ? { timeout: opts.timeoutMs } : {}
|
|
9723
|
+
env: { ...process.env, ...opts?.env }
|
|
9263
9724
|
});
|
|
9725
|
+
if (opts?.timeoutMs) {
|
|
9726
|
+
timeout = setTimeout(() => {
|
|
9727
|
+
error = `timed out after ${opts.timeoutMs}ms`;
|
|
9728
|
+
child.kill("SIGTERM");
|
|
9729
|
+
if (!closed)
|
|
9730
|
+
killTimeout = setTimeout(() => child.kill("SIGKILL"), opts.killAfterMs ?? 120000);
|
|
9731
|
+
}, opts.timeoutMs);
|
|
9732
|
+
}
|
|
9264
9733
|
child.stdout.on("data", (d) => {
|
|
9265
9734
|
stdout += d.toString();
|
|
9266
9735
|
opts?.onOutput?.(d.toString());
|
|
@@ -9271,8 +9740,11 @@ var nodeSpawner = {
|
|
|
9271
9740
|
});
|
|
9272
9741
|
child.on("error", (e) => error = e.message);
|
|
9273
9742
|
child.on("close", (code, signal) => {
|
|
9274
|
-
|
|
9275
|
-
|
|
9743
|
+
closed = true;
|
|
9744
|
+
clearTimeout(timeout);
|
|
9745
|
+
clearTimeout(killTimeout);
|
|
9746
|
+
if (signal)
|
|
9747
|
+
error = error ? `${error}; killed by ${signal}` : `killed by ${signal}`;
|
|
9276
9748
|
resolve({ code, stdout, stderr, ...error ? { error } : {} });
|
|
9277
9749
|
});
|
|
9278
9750
|
})
|
|
@@ -9315,7 +9787,7 @@ async function aa(deps, outDir, opts = {}) {
|
|
|
9315
9787
|
}
|
|
9316
9788
|
|
|
9317
9789
|
// apps/agent/src/farm.ts
|
|
9318
|
-
import { dirname as dirname3, join as
|
|
9790
|
+
import { dirname as dirname3, join as join13, resolve as resolve5 } from "node:path";
|
|
9319
9791
|
|
|
9320
9792
|
// packages/appium-driver/dist/src/links.js
|
|
9321
9793
|
var ALERT_POLL_MS = 250;
|
|
@@ -9548,6 +10020,7 @@ var ANDROID = {
|
|
|
9548
10020
|
"appium:autoGrantPermissions": true,
|
|
9549
10021
|
"appium:newCommandTimeout": NEW_COMMAND_TIMEOUT_S
|
|
9550
10022
|
}),
|
|
10023
|
+
absentAttributeText: "null",
|
|
9551
10024
|
settings: { waitForIdleTimeout: NO_WAIT_MS, actionAcknowledgmentTimeout: NO_WAIT_MS, waitForSelectorTimeout: NO_WAIT_MS, enableMultiWindows: true, ignoreUnimportantViews: true },
|
|
9552
10025
|
locators: androidLocators,
|
|
9553
10026
|
appArg: (appId) => ({ appId }),
|
|
@@ -9612,18 +10085,13 @@ class WdError extends Error {
|
|
|
9612
10085
|
this.wdError = wdError;
|
|
9613
10086
|
}
|
|
9614
10087
|
}
|
|
9615
|
-
// packages/appium-driver/dist/src/session.js
|
|
9616
|
-
var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
9617
|
-
var FLUSH_MARGIN_MS = 30000;
|
|
9618
|
-
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
9619
|
-
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
9620
10088
|
// packages/appium-driver/dist/src/version.js
|
|
9621
10089
|
import { createHash } from "node:crypto";
|
|
9622
10090
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9623
10091
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
9624
10092
|
import { fileURLToPath } from "node:url";
|
|
9625
10093
|
function driverVersion() {
|
|
9626
|
-
const built = "0.1.0-alpha.
|
|
10094
|
+
const built = "0.1.0-alpha.16";
|
|
9627
10095
|
if (built)
|
|
9628
10096
|
return built;
|
|
9629
10097
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -9672,8 +10140,93 @@ function journeyVersion(modulePath) {
|
|
|
9672
10140
|
hash.update(key).update("\x00").update(text).update("\x00");
|
|
9673
10141
|
return hash.digest("hex").slice(0, 12);
|
|
9674
10142
|
}
|
|
10143
|
+
|
|
10144
|
+
// packages/appium-driver/dist/src/session.js
|
|
10145
|
+
var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
10146
|
+
var FLUSH_MARGIN_MS = 30000;
|
|
10147
|
+
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10148
|
+
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
10149
|
+
// packages/appium-driver/dist/src/gestures.js
|
|
10150
|
+
var densities = new WeakMap;
|
|
10151
|
+
// apps/agent/src/device-model.ts
|
|
10152
|
+
import { tmpdir } from "node:os";
|
|
10153
|
+
import { join as join12 } from "node:path";
|
|
10154
|
+
function parseDevicectlList(json) {
|
|
10155
|
+
let data;
|
|
10156
|
+
try {
|
|
10157
|
+
data = JSON.parse(json);
|
|
10158
|
+
} catch {
|
|
10159
|
+
return [];
|
|
10160
|
+
}
|
|
10161
|
+
const devices = Array.isArray(data.result?.devices) ? data.result.devices : [];
|
|
10162
|
+
return devices.flatMap((d) => {
|
|
10163
|
+
const hw = d.hardwareProperties ?? {};
|
|
10164
|
+
const conn = d.connectionProperties ?? {};
|
|
10165
|
+
if (typeof hw.productType !== "string" || !hw.productType)
|
|
10166
|
+
return [];
|
|
10167
|
+
const text = (v) => typeof v === "string" ? v : "";
|
|
10168
|
+
return [{ productType: hw.productType, udid: text(hw.udid), identifier: text(d.identifier), serial: text(hw.serialNumber), connected: conn.tunnelState === "connected" }];
|
|
10169
|
+
});
|
|
10170
|
+
}
|
|
10171
|
+
var parseSimulators = (json) => {
|
|
10172
|
+
try {
|
|
10173
|
+
const data = JSON.parse(json);
|
|
10174
|
+
return Object.values(data.devices ?? {}).flat();
|
|
10175
|
+
} catch {
|
|
10176
|
+
return [];
|
|
10177
|
+
}
|
|
10178
|
+
};
|
|
10179
|
+
var withSimulatorWord = (name) => /simulator/i.test(name) ? name : `${name} Simulator`;
|
|
10180
|
+
var simulatorModel = (s) => withSimulatorWord(s.name);
|
|
10181
|
+
async function androidModel(deps, id) {
|
|
10182
|
+
const r = await deps.spawner.run("adb", [...id ? ["-s", id] : [], "shell", "getprop", "ro.product.model"]);
|
|
10183
|
+
const model = r.code === 0 ? (r.stdout.split(`
|
|
10184
|
+
`)[0] ?? "").trim() : "";
|
|
10185
|
+
return model || null;
|
|
10186
|
+
}
|
|
10187
|
+
async function simulators(deps, id) {
|
|
10188
|
+
const r = await deps.spawner.run("xcrun", ["simctl", "list", "devices", ...id ? [] : ["booted"], "-j"]);
|
|
10189
|
+
if (r.code !== 0)
|
|
10190
|
+
return [];
|
|
10191
|
+
return parseSimulators(r.stdout).filter((s) => id ? s.udid === id : s.state === "Booted");
|
|
10192
|
+
}
|
|
10193
|
+
async function phones(deps) {
|
|
10194
|
+
const out = join12(tmpdir(), `keepsnappy-devicectl-${deps.clock.now()}.json`);
|
|
10195
|
+
try {
|
|
10196
|
+
const r = await deps.spawner.run("xcrun", ["devicectl", "list", "devices", "--quiet", "--json-output", out]);
|
|
10197
|
+
const text = r.code === 0 ? await deps.fs.readFile(out).catch(() => "") : "";
|
|
10198
|
+
return parseDevicectlList(text);
|
|
10199
|
+
} finally {
|
|
10200
|
+
await deps.spawner.run("rm", ["-f", out]);
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
async function iosModel(deps, id) {
|
|
10204
|
+
const sims = await simulators(deps, id);
|
|
10205
|
+
if (id) {
|
|
10206
|
+
if (sims.length === 1)
|
|
10207
|
+
return simulatorModel(sims[0]);
|
|
10208
|
+
const found = (await phones(deps)).filter((p) => [p.udid, p.identifier, p.serial].some((x) => x.toLowerCase() === id.toLowerCase()));
|
|
10209
|
+
return found.length === 1 ? found[0].productType : null;
|
|
10210
|
+
}
|
|
10211
|
+
const connected = (await phones(deps)).filter((p) => p.connected);
|
|
10212
|
+
if (sims.length + connected.length !== 1)
|
|
10213
|
+
return null;
|
|
10214
|
+
return sims[0] ? simulatorModel(sims[0]) : connected[0].productType;
|
|
10215
|
+
}
|
|
10216
|
+
async function detectDeviceModel(deps, q) {
|
|
10217
|
+
if (q.platform === "android")
|
|
10218
|
+
return androidModel(deps, q.deviceId);
|
|
10219
|
+
if (q.platform === "ios")
|
|
10220
|
+
return iosModel(deps, q.deviceId);
|
|
10221
|
+
return null;
|
|
10222
|
+
}
|
|
10223
|
+
async function detectSimulator(deps, udid) {
|
|
10224
|
+
const sims = await simulators(deps, udid);
|
|
10225
|
+
return sims.length === 1 ? simulatorModel(sims[0]) : null;
|
|
10226
|
+
}
|
|
10227
|
+
|
|
9675
10228
|
// apps/agent/src/slack.ts
|
|
9676
|
-
var COLOR = { error: "#
|
|
10229
|
+
var COLOR = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
9677
10230
|
var MAX_ROWS = 3;
|
|
9678
10231
|
var NAME_W = 30;
|
|
9679
10232
|
var COLUMN_GAP = 1;
|
|
@@ -9685,8 +10238,6 @@ function metricWord(item) {
|
|
|
9685
10238
|
return measureDef(item.metric)?.short ?? item.metric.replace(/_/g, " ");
|
|
9686
10239
|
}
|
|
9687
10240
|
function change2(item) {
|
|
9688
|
-
if (item.metric === HEAT_METRIC && item.baseline !== null && item.candidate !== null)
|
|
9689
|
-
return `+${Math.round((item.candidate - item.baseline) * 100)} pp`;
|
|
9690
10241
|
if (item.change_text)
|
|
9691
10242
|
return item.change_text.replace(/ \(.*\)$/, "");
|
|
9692
10243
|
const { baseline: b, candidate: c } = item;
|
|
@@ -9699,13 +10250,13 @@ function change2(item) {
|
|
|
9699
10250
|
}
|
|
9700
10251
|
var size = (i) => i.baseline === null || i.candidate === null ? 0 : i.baseline === 0 ? i.candidate === 0 ? 0 : Number.POSITIVE_INFINITY : Math.abs(Math.log(Math.max(i.candidate, 0.000000001) / i.baseline));
|
|
9701
10252
|
var bySize = (a, b) => size(a) === size(b) ? 0 : size(a) > size(b) ? -1 : 1;
|
|
9702
|
-
var worseOrder = (a, b) => Number(a.grade === "warn") - Number(b.grade === "warn") ||
|
|
10253
|
+
var worseOrder = (a, b) => Number(a.grade === "warn") - Number(b.grade === "warn") || bySize(a, b);
|
|
9703
10254
|
var isWorse = (i) => i.state === "worse";
|
|
9704
10255
|
var isBetter = (i) => i.state === "better";
|
|
9705
10256
|
var differs = (sc) => sc?.headline?.measured_differently ?? [];
|
|
9706
10257
|
var scenarioBlocks = (sc) => sc?.headline?.blocking !== false;
|
|
9707
10258
|
var isRed = (i, sc) => i.grade !== "warn" && scenarioBlocks(sc);
|
|
9708
|
-
var mark = (i, sc) => isBetter(i) ? "\uD83D\uDFE2" : isRed(i, sc) ? "\uD83D\uDD34" : "\uD83D\
|
|
10259
|
+
var mark = (i, sc) => isBetter(i) ? "\uD83D\uDFE2" : isRed(i, sc) ? "\uD83D\uDD34" : i.grade !== "warn" ? "⚪" : "\uD83D\uDFE1";
|
|
9709
10260
|
var fit = (s, w) => s.length > w ? `${s.slice(0, w - 1)}…` : s.padEnd(w);
|
|
9710
10261
|
var right = (s, w) => s.padStart(w);
|
|
9711
10262
|
var phone = (sc) => sc?.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
@@ -9765,19 +10316,28 @@ function slackMessage(s, ctx) {
|
|
|
9765
10316
|
const worse = items.filter(isWorse).sort(worseOrder);
|
|
9766
10317
|
const better = items.filter(isBetter).sort(bySize);
|
|
9767
10318
|
const commit = commitLine(ctx);
|
|
10319
|
+
const heated = v.scenarios.filter((sc) => heatNote(sc));
|
|
10320
|
+
const heat = heated.map((sc) => context(heatNote(sc)));
|
|
9768
10321
|
const errorRows = worse.filter((i) => i.grade !== "warn");
|
|
9769
10322
|
const warningRows = worse.filter((i) => i.grade === "warn");
|
|
9770
10323
|
const errors = placeCount(errorRows);
|
|
9771
10324
|
const warns = placeCount(worse) - errors;
|
|
10325
|
+
const red = errorRows.some((i) => isRed(i, scenarioOf(i)));
|
|
10326
|
+
const notBlocking = errors > 0 && !red;
|
|
10327
|
+
const worseMark = red ? "\uD83D\uDD34" : warns > 0 ? "\uD83D\uDFE1" : "⚪";
|
|
10328
|
+
const worseColor = red ? COLOR.error : warns > 0 ? COLOR.warn : COLOR.none;
|
|
9772
10329
|
if (isPr) {
|
|
9773
|
-
if (!worse.length)
|
|
9774
|
-
|
|
9775
|
-
|
|
10330
|
+
if (!worse.length) {
|
|
10331
|
+
if (!heat.length)
|
|
10332
|
+
return null;
|
|
10333
|
+
const head = `⚪ ${ctx.branch}: ${INFO_WORDS.heat}`;
|
|
10334
|
+
const blocks = [section(`*${head}*`), ...heat, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0].dashboard_url ?? s.dashboard_url}|View the comparison>`)];
|
|
10335
|
+
return { text: head, attachments: [{ color: COLOR.none, blocks }] };
|
|
10336
|
+
}
|
|
9776
10337
|
const graded = worse.some((i) => i.grade);
|
|
9777
|
-
const notBlocking = errors > 0 && !red;
|
|
9778
10338
|
const places = (word, n) => `${word.toLowerCase()} in ${n} place${n > 1 ? "s" : ""}`;
|
|
9779
|
-
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : places(STATE_WORDS.worse, placeCount(worse))
|
|
9780
|
-
const head = `${
|
|
10339
|
+
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : `${places(STATE_WORDS.worse, placeCount(worse))}${notBlocking ? ` (${NOT_BLOCKING})` : ""}`;
|
|
10340
|
+
const head = `${worseMark} ${ctx.branch}: ${counts}`;
|
|
9781
10341
|
const blocks = [section(`*${head}*`)];
|
|
9782
10342
|
const groups = new Map;
|
|
9783
10343
|
for (const i of worse)
|
|
@@ -9793,21 +10353,24 @@ function slackMessage(s, ctx) {
|
|
|
9793
10353
|
const runs = nB && nC ? ` · ${nC} PR run${nC === 1 ? "" : "s"} vs ${nB} main run${nB === 1 ? "" : "s"}` : "";
|
|
9794
10354
|
const note = differs(sc).length ? `
|
|
9795
10355
|
_Measured differently (${differs(sc).join(", ")})${scenarioBlocks(sc) ? "" : ` — ${NOT_BLOCKING}`}_` : "";
|
|
9796
|
-
|
|
10356
|
+
const nonblocking = !scenarioBlocks(sc) && rows.some((i) => i.grade !== "warn") && !differs(sc).length ? ` · ${NOT_BLOCKING}` : "";
|
|
10357
|
+
blocks.push(section(`*${sc?.scenario ?? "journey"}*${nonblocking} · ${phone(sc)}${runs}${note}
|
|
9797
10358
|
${prTable(rows, sc)}`));
|
|
9798
10359
|
}
|
|
9799
10360
|
const more = worse.length - shown;
|
|
10361
|
+
blocks.push(...heat);
|
|
9800
10362
|
if (commit)
|
|
9801
10363
|
blocks.push(context(commit));
|
|
9802
10364
|
blocks.push(section(`\uD83D\uDC49 <${groups.keys().next().value?.dashboard_url ?? s.dashboard_url}|View the comparison>${more > 0 ? ` · ${more} more` : ""}`));
|
|
9803
|
-
return { text: head, attachments: [{ color:
|
|
10365
|
+
return { text: head, attachments: [{ color: worseColor, blocks }] };
|
|
9804
10366
|
}
|
|
9805
|
-
|
|
10367
|
+
const improved = better.length > 0 || v.state === "better";
|
|
10368
|
+
if (!worse.length && !improved && !ctx.always && !heat.length)
|
|
9806
10369
|
return null;
|
|
9807
|
-
const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
|
|
9808
|
-
const quiet = (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
|
|
9809
|
-
const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
|
|
9810
|
-
const head = `${worse.length ?
|
|
10370
|
+
const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
|
|
10371
|
+
const quiet = improved ? STATE_WORDS.better.toLowerCase() : (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
|
|
10372
|
+
const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : !improved && heat.length && !ctx.always ? `${ctx.branch}: ${INFO_WORDS.heat}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
|
|
10373
|
+
const head = `${worse.length ? worseMark : improved ? "\uD83D\uDFE2" : v.state === "inconclusive" && (ctx.always || !heat.length) ? "❔" : "⚪"} ${status}`;
|
|
9811
10374
|
const journeys = new Set(v.scenarios.map((sc) => `${sc.scenario}|${sc.device?.id}`)).size;
|
|
9812
10375
|
const blocks = [section(`*${head}*
|
|
9813
10376
|
${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
|
|
@@ -9818,15 +10381,16 @@ ${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
|
|
|
9818
10381
|
break;
|
|
9819
10382
|
shown++;
|
|
9820
10383
|
const sc = scenarioOf(i);
|
|
9821
|
-
blocks.push(section(`*${title}
|
|
10384
|
+
blocks.push(section(`*${title}*${i.grade !== "warn" && isWorse(i) && !scenarioBlocks(sc) ? ` (${NOT_BLOCKING})` : ""} · *${sc?.scenario ?? "journey"}* · ${phone(sc)}
|
|
9822
10385
|
${mainCard(i)}`));
|
|
9823
10386
|
}
|
|
9824
10387
|
}
|
|
9825
10388
|
const more = worse.length + better.length - shown;
|
|
10389
|
+
blocks.push(...heat);
|
|
9826
10390
|
if (commit)
|
|
9827
10391
|
blocks.push(context(commit));
|
|
9828
10392
|
blocks.push(section(`\uD83D\uDC49 <${s.dashboard_url}|Open KeepSnappy>${more > 0 ? ` · ${more} more changes` : ""}`));
|
|
9829
|
-
const color = worse.length ?
|
|
10393
|
+
const color = worse.length ? worseColor : improved ? COLOR.better : COLOR.none;
|
|
9830
10394
|
return { text: head, attachments: [{ color, blocks }] };
|
|
9831
10395
|
}
|
|
9832
10396
|
async function postToSlack(url, payload, fetchImpl = fetch) {
|
|
@@ -9973,9 +10537,20 @@ async function farmOpen(deps, f, rt) {
|
|
|
9973
10537
|
const git = gitContext({ ...f.branch ? { branch: f.branch } : {}, ...f.commit ? { commit: f.commit } : {} }, rt.env);
|
|
9974
10538
|
if (!git.branch)
|
|
9975
10539
|
problems.push("--branch is required outside GitHub Actions");
|
|
9976
|
-
|
|
10540
|
+
let provider = f["device-provider"] ?? "browserstack";
|
|
10541
|
+
let model = f["device-model"]?.trim();
|
|
10542
|
+
const simulator = f.platform === "ios" && provider !== "browserstack" && f["device-id"]?.trim() ? await detectSimulator(deps, f["device-id"].trim()) : null;
|
|
10543
|
+
if (simulator) {
|
|
10544
|
+
provider = "simulator";
|
|
10545
|
+
model = model ? withSimulatorWord(model) : simulator;
|
|
10546
|
+
}
|
|
10547
|
+
if (!model && ["local", "usb", "customer", "simulator", "emulator"].includes(provider)) {
|
|
10548
|
+
model = await detectDeviceModel(deps, { platform: f.platform, deviceId: f["device-id"]?.trim() }) ?? undefined;
|
|
10549
|
+
if (model)
|
|
10550
|
+
rt.print(`device model read from the device: ${model}`);
|
|
10551
|
+
}
|
|
9977
10552
|
if (!model)
|
|
9978
|
-
problems.push('--device-model is required: the journey history is keyed by the run set’s device (e.g. "
|
|
10553
|
+
problems.push('--device-model is required unless it can be read from a local device (--device-provider local|usb|customer|simulator|emulator, with --platform android|ios, adb or xcrun, and --device-id when more than one device is attached): the journey history is keyed by the run set’s device (e.g. "SM-S911B", "iPhone17,1")');
|
|
9979
10554
|
if (f.platform !== undefined && !["ios", "android", "web"].includes(f.platform))
|
|
9980
10555
|
problems.push("--platform must be ios, android or web");
|
|
9981
10556
|
const journeys = (f.journeys ?? "").split(",").map((j) => j.trim()).filter(Boolean);
|
|
@@ -10005,7 +10580,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
10005
10580
|
}
|
|
10006
10581
|
const api = apiTarget(url, f, rt);
|
|
10007
10582
|
const now = rt.now ?? Date.now;
|
|
10008
|
-
const device = { model, os: f["device-os"] ?? null, provider
|
|
10583
|
+
const device = { model, os: f["device-os"] ?? null, provider };
|
|
10009
10584
|
const journeyInfo = await farmJourneys(deps, f, rt, journeys);
|
|
10010
10585
|
const withInstrument = { ...f, ...instrumentFlags(f) };
|
|
10011
10586
|
const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
|
|
@@ -10263,7 +10838,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
10263
10838
|
await deps.fs.writeFile(statePath, `${JSON.stringify(state2, null, 2)}
|
|
10264
10839
|
`);
|
|
10265
10840
|
await deps.fs.mkdir(out);
|
|
10266
|
-
await deps.fs.writeFile(
|
|
10841
|
+
await deps.fs.writeFile(join13(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: state.run_set_id, dashboard_url: summary.dashboard_url, state: state2.result.state, session: { status: session.status, message: session.message }, summary }, null, 2)}
|
|
10267
10842
|
`);
|
|
10268
10843
|
await githubOutputs(deps, rt, { state: state2.result.state ?? "none", dashboard_url: summary.dashboard_url, run_set_id: state.run_set_id });
|
|
10269
10844
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
@@ -10309,11 +10884,11 @@ ${tail.join(" ")}
|
|
|
10309
10884
|
// apps/agent/src/farm-run.ts
|
|
10310
10885
|
import { existsSync as existsSync2 } from "node:fs";
|
|
10311
10886
|
import { createRequire } from "node:module";
|
|
10312
|
-
import { dirname as dirname4, join as
|
|
10887
|
+
import { dirname as dirname4, join as join16, resolve as resolve6 } from "node:path";
|
|
10313
10888
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10314
10889
|
|
|
10315
10890
|
// apps/agent/src/app-source.ts
|
|
10316
|
-
import { join as
|
|
10891
|
+
import { join as join14 } from "node:path";
|
|
10317
10892
|
var EAS_CLI_VERSION = "24.6.0";
|
|
10318
10893
|
var isEas = (spec) => spec.startsWith("eas:");
|
|
10319
10894
|
|
|
@@ -10325,7 +10900,7 @@ class DefaultAppResolver {
|
|
|
10325
10900
|
this.env = env;
|
|
10326
10901
|
}
|
|
10327
10902
|
async resolve(spec, o) {
|
|
10328
|
-
if (spec.startsWith("bs://"))
|
|
10903
|
+
if (spec.startsWith("bs://") || spec.startsWith("custom_id:"))
|
|
10329
10904
|
return { app: spec, commit: null };
|
|
10330
10905
|
if (!isEas(spec)) {
|
|
10331
10906
|
if (!await this.deps.fs.exists(spec))
|
|
@@ -10349,7 +10924,7 @@ class DefaultAppResolver {
|
|
|
10349
10924
|
const url = build.artifacts?.buildUrl ?? build.artifacts?.applicationArchiveUrl;
|
|
10350
10925
|
if (!url)
|
|
10351
10926
|
throw new Error(`EAS build ${id} has no downloadable artifact (is it finished?)`);
|
|
10352
|
-
const file =
|
|
10927
|
+
const file = join14(o.outDir, `app-${id.slice(0, 8)}${build.platform === "ANDROID" ? ".apk" : ".ipa"}`);
|
|
10353
10928
|
await this.deps.fs.mkdir(o.outDir);
|
|
10354
10929
|
const dl = await this.deps.spawner.run("curl", ["-fsSL", "-o", file, url]);
|
|
10355
10930
|
if (dl.code !== 0)
|
|
@@ -10360,7 +10935,7 @@ class DefaultAppResolver {
|
|
|
10360
10935
|
|
|
10361
10936
|
// apps/agent/src/browserstack.ts
|
|
10362
10937
|
import { randomUUID } from "node:crypto";
|
|
10363
|
-
import { basename as basename2, join as
|
|
10938
|
+
import { basename as basename2, join as join15 } from "node:path";
|
|
10364
10939
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
10365
10940
|
var BASE = "https://api-cloud.browserstack.com/app-automate/maestro/v2";
|
|
10366
10941
|
var TERMINAL = new Set(["passed", "failed", "error", "timedout", "skipped", "stopped"]);
|
|
@@ -10431,7 +11006,7 @@ class BrowserStack {
|
|
|
10431
11006
|
}
|
|
10432
11007
|
}
|
|
10433
11008
|
async uploadApp(app, customId) {
|
|
10434
|
-
if (app.startsWith("bs://"))
|
|
11009
|
+
if (app.startsWith("bs://") || app.startsWith("custom_id:"))
|
|
10435
11010
|
return app;
|
|
10436
11011
|
const form = new FormData;
|
|
10437
11012
|
form.append("file", new Blob([await this.bytes(app)]), basename2(app));
|
|
@@ -10444,7 +11019,7 @@ class BrowserStack {
|
|
|
10444
11019
|
}
|
|
10445
11020
|
async runMaestro(b) {
|
|
10446
11021
|
const appUrl = await this.uploadApp(b.app, b.customId);
|
|
10447
|
-
const work =
|
|
11022
|
+
const work = join15(this.o.tmpDir ?? "/tmp", `keepsnappy-suite-${(this.o.uniqueId ?? randomUUID)()}`);
|
|
10448
11023
|
let suiteUrl;
|
|
10449
11024
|
try {
|
|
10450
11025
|
const zip = await this.zipSuite(b, work);
|
|
@@ -10477,17 +11052,17 @@ class BrowserStack {
|
|
|
10477
11052
|
return this.poll(buildId, b);
|
|
10478
11053
|
}
|
|
10479
11054
|
async zipSuite(b, work) {
|
|
10480
|
-
const zipPath =
|
|
10481
|
-
const stage =
|
|
11055
|
+
const zipPath = join15(work, "suite.zip");
|
|
11056
|
+
const stage = join15(work, "stage");
|
|
10482
11057
|
await this.deps.fs.mkdir(stage);
|
|
10483
11058
|
const cp = await this.deps.spawner.run("cp", ["-R", b.suiteDir.replace(/\/$/, ""), stage]);
|
|
10484
11059
|
if (cp.code !== 0)
|
|
10485
11060
|
throw new FarmApiError(`cannot copy the suite ${b.suiteDir}: ${cp.stderr || cp.error}`);
|
|
10486
11061
|
const folder = basename2(b.suiteDir.replace(/\/$/, ""));
|
|
10487
|
-
if (!await this.deps.fs.exists(
|
|
11062
|
+
if (!await this.deps.fs.exists(join15(stage, folder, "config.yaml")))
|
|
10488
11063
|
throw new FarmApiError(`${b.suiteDir} has no config.yaml: is that the Maestro workspace?`);
|
|
10489
|
-
const target =
|
|
10490
|
-
await this.deps.fs.mkdir(
|
|
11064
|
+
const target = join15(stage, folder, b.tokensPath);
|
|
11065
|
+
await this.deps.fs.mkdir(join15(target, ".."));
|
|
10491
11066
|
await this.deps.fs.writeFile(target, await this.deps.fs.readFile(b.tokensJs));
|
|
10492
11067
|
const include = SUITE_EXTENSIONS.flatMap((e) => ["-i", `${folder}/*${e}`, "-i", `${folder}/**/*${e}`]);
|
|
10493
11068
|
const z = await this.deps.spawner.run("sh", ["-c", 'cd "$1" && shift && zip -qr "$@"', "sh", stage, zipPath, folder, ...include, "-x", `${folder}/runs/*`, "-x", "*/node_modules/*"]);
|
|
@@ -10546,9 +11121,9 @@ class BrowserStackLocal {
|
|
|
10546
11121
|
const url = TUNNEL_ZIP[this.o.platform ?? process.platform];
|
|
10547
11122
|
if (!url)
|
|
10548
11123
|
throw new FarmApiError(`no BrowserStackLocal binary known for ${this.o.platform ?? process.platform}`);
|
|
10549
|
-
const dir =
|
|
10550
|
-
const bin =
|
|
10551
|
-
const zip =
|
|
11124
|
+
const dir = join15(this.o.dir, identifier);
|
|
11125
|
+
const bin = join15(dir, "BrowserStackLocal");
|
|
11126
|
+
const zip = join15(dir, "bs-local.zip");
|
|
10552
11127
|
await this.deps.fs.mkdir(dir);
|
|
10553
11128
|
for (const [cmd, args] of [
|
|
10554
11129
|
["curl", ["-fsSL", "-o", zip, url]],
|
|
@@ -10582,12 +11157,12 @@ ${r.stderr}`;
|
|
|
10582
11157
|
// apps/agent/src/farm-run.ts
|
|
10583
11158
|
function appiumDriverEntry(o = {}) {
|
|
10584
11159
|
const exists = o.exists ?? existsSync2;
|
|
10585
|
-
const bundled =
|
|
11160
|
+
const bundled = join16(o.selfDir ?? dirname4(fileURLToPath2(import.meta.url)), "appium-driver.js");
|
|
10586
11161
|
if (exists(bundled))
|
|
10587
11162
|
return bundled;
|
|
10588
11163
|
const pkgDir = dirname4(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
|
|
10589
|
-
const built =
|
|
10590
|
-
return exists(built) ? built :
|
|
11164
|
+
const built = join16(pkgDir, "dist", "src", "cli.js");
|
|
11165
|
+
return exists(built) ? built : join16(pkgDir, "src", "cli.ts");
|
|
10591
11166
|
}
|
|
10592
11167
|
var driverRuntime = (entry) => entry.endsWith(".ts") ? process.execPath : "node";
|
|
10593
11168
|
function nodeFarmRunPorts(deps, rt) {
|
|
@@ -10596,7 +11171,7 @@ function nodeFarmRunPorts(deps, rt) {
|
|
|
10596
11171
|
if (!user || !key)
|
|
10597
11172
|
throw new ConfigError(["BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY must be set"]);
|
|
10598
11173
|
const tmp = rt.env.RUNNER_TEMP ?? rt.env.TMPDIR ?? "/tmp";
|
|
10599
|
-
const dir =
|
|
11174
|
+
const dir = join16(tmp, "keepsnappy-bs-local");
|
|
10600
11175
|
return {
|
|
10601
11176
|
apps: new DefaultAppResolver(deps, rt.env),
|
|
10602
11177
|
provider: new BrowserStack(deps, { credentials: { user, key }, tmpDir: tmp, ...rt.fetch ? { fetch: rt.fetch } : {}, ...rt.sleep ? { sleep: rt.sleep } : {}, ...rt.now ? { now: rt.now } : {} }),
|
|
@@ -10612,7 +11187,7 @@ var RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
|
10612
11187
|
function planFarmRun(f) {
|
|
10613
11188
|
const problems = [];
|
|
10614
11189
|
if (!f.app)
|
|
10615
|
-
problems.push("--app <eas:<build id>|path to .ipa/.apk|bs://id> is required");
|
|
11190
|
+
problems.push("--app <eas:<build id>|path to .ipa/.apk|bs://id|custom_id:id> is required");
|
|
10616
11191
|
const platform = f.platform ?? "ios";
|
|
10617
11192
|
if (platform !== "ios" && platform !== "android")
|
|
10618
11193
|
problems.push("--platform must be ios or android");
|
|
@@ -10658,14 +11233,15 @@ function planFarmRun(f) {
|
|
|
10658
11233
|
problems.push("--driver-script and --journey-module are two ways to run Appium; pass one");
|
|
10659
11234
|
} else if (!f["driver-script"] && !f["journey-module"]) {
|
|
10660
11235
|
problems.push("--driver appium needs --journey-module <file> (the built-in driver) or --driver-script <file> (your own driver)");
|
|
10661
|
-
} else if (f["journey-module"] && platform !== "ios") {
|
|
10662
|
-
problems.push("--journey-module: the built-in Appium driver drives iOS only");
|
|
10663
11236
|
}
|
|
10664
11237
|
if (f["run-id"] !== undefined && !RUN_ID.test(f["run-id"]))
|
|
10665
11238
|
problems.push("--run-id: letters, digits, . _ - only (it names the tunnel and its folder)");
|
|
10666
11239
|
if (f["fail-on-worse"] && !f.wait)
|
|
10667
11240
|
problems.push("--fail-on-worse needs --wait");
|
|
10668
11241
|
const timeoutMinutes = Number(f["timeout-minutes"] ?? 80);
|
|
11242
|
+
const killAfterSeconds = Number(f["kill-after-seconds"] ?? 120);
|
|
11243
|
+
if (!Number.isFinite(killAfterSeconds) || killAfterSeconds <= 0)
|
|
11244
|
+
problems.push("--kill-after-seconds must be finite and > 0");
|
|
10669
11245
|
const pollSeconds = Number(f["poll-seconds"] ?? 20);
|
|
10670
11246
|
if (!(timeoutMinutes > 0))
|
|
10671
11247
|
problems.push("--timeout-minutes must be > 0");
|
|
@@ -10685,6 +11261,7 @@ function planFarmRun(f) {
|
|
|
10685
11261
|
driverArgs: f["driver-arg"] ?? [],
|
|
10686
11262
|
out: resolve6(f.out ?? "farm-run"),
|
|
10687
11263
|
timeoutMinutes,
|
|
11264
|
+
killAfterSeconds,
|
|
10688
11265
|
pollSeconds
|
|
10689
11266
|
};
|
|
10690
11267
|
}
|
|
@@ -10708,8 +11285,8 @@ function printPlan(p, f, rt) {
|
|
|
10708
11285
|
const line = (l) => rt.print(redact(l, secrets));
|
|
10709
11286
|
line("keepsnappy farm run: dry run, no request is made and nothing is written");
|
|
10710
11287
|
line(` 1. validate: ${p.journeys.join(", ")} x ${p.repeat} on ${p.device} (${p.platform}), driver ${p.driver}`);
|
|
10711
|
-
line(` 2. resolve the app: ${p.app.startsWith("eas:") ? `eas-cli build:view ${p.app.slice(4)}, download into ${p.out}` : p.app.startsWith("bs://")
|
|
10712
|
-
line(` 3. farm open -> ${
|
|
11288
|
+
line(` 2. resolve the app: ${p.app.startsWith("eas:") ? `eas-cli build:view ${p.app.slice(4)}, download into ${p.out}` : p.app.startsWith("bs://") || p.app.startsWith("custom_id:") ? `${p.app} passes through` : `${p.app} (local file)`}`);
|
|
11289
|
+
line(` 3. farm open -> ${join16(p.out, "keepsnappy-tokens.js")}, ${join16(p.out, "keepsnappy-farm.json")} (POST /v1/run-sets, then POST /v1/runs x ${p.journeys.length * p.repeat})`);
|
|
10713
11290
|
line(` 4. tunnel: ${p.ports.length ? `BrowserStackLocal for bs-local.com ports ${p.ports.join(", ")}, identifier = run id` : "none (no --tunnel-for)"}`);
|
|
10714
11291
|
if (p.driver === "maestro") {
|
|
10715
11292
|
line(` 5. maestro on BrowserStack: upload app, zip ${f["suite-dir"]} with the tokens at ${f["tokens-path"] ?? "flows/keepsnappy/tokens.js"}, upload suite, POST build (execute ${f.execute}), poll`);
|
|
@@ -10718,7 +11295,7 @@ function printPlan(p, f, rt) {
|
|
|
10718
11295
|
}
|
|
10719
11296
|
for (const [k, v] of p.env)
|
|
10720
11297
|
line(` env ${k}=${substitute(v, {})}`);
|
|
10721
|
-
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${
|
|
11298
|
+
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${join16(p.out, `browserstack.${p.platform}.json`)}`);
|
|
10722
11299
|
line(" 7. always: stop the tunnel, put the empty stub back over the tokens file");
|
|
10723
11300
|
line(" exit: 0 any verdict, 1 infrastructure / driver failed / Blocked, 2 configuration, 3 --fail-on-worse");
|
|
10724
11301
|
}
|
|
@@ -10742,13 +11319,13 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10742
11319
|
timeoutMinutes: p.timeoutMinutes,
|
|
10743
11320
|
pollSeconds: p.pollSeconds,
|
|
10744
11321
|
onStarted: async (buildId) => {
|
|
10745
|
-
await deps.fs.writeFile(
|
|
11322
|
+
await deps.fs.writeFile(join16(ctx.out, `browserstack.${p.platform}.json`), `${JSON.stringify({ buildId, dashboard: `https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`, note: "written at submit; replaced by the full detail once the build ends", status: "unknown" }, null, 2)}
|
|
10746
11323
|
`);
|
|
10747
11324
|
rt.print(`build ${buildId} started: https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`);
|
|
10748
11325
|
},
|
|
10749
11326
|
onStatus: (buildId, status) => rt.print(`build ${buildId}: ${status}`)
|
|
10750
11327
|
});
|
|
10751
|
-
await deps.fs.writeFile(
|
|
11328
|
+
await deps.fs.writeFile(join16(ctx.out, `browserstack.${p.platform}.json`), `${redact(JSON.stringify(detail.detail, null, 2), secretsOf(p, f, rt.env))}
|
|
10752
11329
|
`);
|
|
10753
11330
|
return detail.status === "passed" ? { ok: true, message: null } : { ok: false, message: `the device build ${detail.buildId} ended ${detail.status}` };
|
|
10754
11331
|
}
|
|
@@ -10785,6 +11362,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10785
11362
|
const r = await deps.spawner.run(f["driver-script"] ? "node" : driverRuntime(entry), args, {
|
|
10786
11363
|
env,
|
|
10787
11364
|
timeoutMs: p.timeoutMinutes * 60000,
|
|
11365
|
+
killAfterMs: p.killAfterSeconds * 1000,
|
|
10788
11366
|
onOutput: (chunk) => {
|
|
10789
11367
|
streamed = true;
|
|
10790
11368
|
const lines = (pending + chunk).split(`
|
|
@@ -10799,11 +11377,11 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10799
11377
|
for (const l of `${r.stdout}${r.stderr}`.split(`
|
|
10800
11378
|
`))
|
|
10801
11379
|
emit(l);
|
|
10802
|
-
return r.code === 0 ? { ok: true, message: null } : { ok: false, message: `the driver script exited ${r.
|
|
11380
|
+
return r.code === 0 && !r.error ? { ok: true, message: null } : { ok: false, message: `the driver script exited ${r.error ?? r.code ?? "abnormally"}` };
|
|
10803
11381
|
}
|
|
10804
11382
|
async function farmRun(deps, f, rt, ports) {
|
|
10805
11383
|
const p = planFarmRun(f);
|
|
10806
|
-
const statePath =
|
|
11384
|
+
const statePath = join16(p.out, "keepsnappy-farm.json");
|
|
10807
11385
|
if (await deps.fs.exists(statePath))
|
|
10808
11386
|
throw new ConfigError([`${statePath} exists: a run set was opened here before; use another --out or remove ${p.out} (close it first with keepsnappy farm close)`]);
|
|
10809
11387
|
if (f["dry-run"]) {
|
|
@@ -10811,7 +11389,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10811
11389
|
return EXIT.ok;
|
|
10812
11390
|
}
|
|
10813
11391
|
const farm = ports ?? nodeFarmRunPorts(deps, rt);
|
|
10814
|
-
const tokensJs =
|
|
11392
|
+
const tokensJs = join16(p.out, "keepsnappy-tokens.js");
|
|
10815
11393
|
await deps.fs.mkdir(p.out);
|
|
10816
11394
|
const resolved = await farm.apps.resolve(p.app, { outDir: p.out });
|
|
10817
11395
|
const commit = f.commit ?? resolved.commit ?? undefined;
|
|
@@ -10842,9 +11420,9 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10842
11420
|
const runId = f["run-id"] ?? `keepsnappy-${p.platform}-${state.run_set_id.slice(0, 8)}`;
|
|
10843
11421
|
const ctx = { runId, out: p.out, tokensJs, runSetId: state.run_set_id, app: resolved.app };
|
|
10844
11422
|
try {
|
|
10845
|
-
await deps.spawner.run("rm", ["-f",
|
|
11423
|
+
await deps.spawner.run("rm", ["-f", join16(p.out, `browserstack.${p.platform}.json`)]);
|
|
10846
11424
|
if (p.ports.length) {
|
|
10847
|
-
tunnel = await farm.tunnel.open({ identifier: runId, ports: p.ports, logFile:
|
|
11425
|
+
tunnel = await farm.tunnel.open({ identifier: runId, ports: p.ports, logFile: join16(p.out, "browserstack-local.log") });
|
|
10848
11426
|
rt.print(`tunnel ${runId}: bs-local.com ports ${p.ports.join(", ")}`);
|
|
10849
11427
|
}
|
|
10850
11428
|
driver = await runDriver2(deps, rt, f, p, farm, { ...ctx, tunnelId: tunnel?.identifier ?? null });
|
|
@@ -10855,7 +11433,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10855
11433
|
rt.print(`device run failed: ${driver.message}; closing the run set with what arrived`);
|
|
10856
11434
|
closeCode = await farmClose(deps, {
|
|
10857
11435
|
state: statePath,
|
|
10858
|
-
"bs-build":
|
|
11436
|
+
"bs-build": join16(p.out, `browserstack.${p.platform}.json`),
|
|
10859
11437
|
out: p.out,
|
|
10860
11438
|
...f.wait ? { wait: true } : {},
|
|
10861
11439
|
...f["fail-on-worse"] ? { "fail-on-worse": true } : {},
|
|
@@ -10876,13 +11454,82 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10876
11454
|
}
|
|
10877
11455
|
|
|
10878
11456
|
// apps/agent/src/sourcemaps.ts
|
|
10879
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
10880
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
10881
|
-
import { basename as basename3 } from "node:path";
|
|
11457
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
11458
|
+
import { access, readFile as readFile4, readdir as readdir2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
11459
|
+
import { basename as basename3, join as join17, resolve as resolve7 } from "node:path";
|
|
10882
11460
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
10883
11461
|
import { gzipSync } from "node:zlib";
|
|
10884
11462
|
var MAX_COMPLETE_ROUNDS = 20;
|
|
10885
11463
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11464
|
+
async function prepareComponents(projectRoot, options = {}) {
|
|
11465
|
+
const root = resolve7(projectRoot);
|
|
11466
|
+
const directory = join17(root, ".keepsnappy", "components");
|
|
11467
|
+
const configurations = await readdir2(directory, { withFileTypes: true }).catch((error) => {
|
|
11468
|
+
if (error.code === "ENOENT")
|
|
11469
|
+
return [];
|
|
11470
|
+
throw error;
|
|
11471
|
+
});
|
|
11472
|
+
const candidates = [];
|
|
11473
|
+
for (const entry of configurations) {
|
|
11474
|
+
if (!entry.isDirectory() || !/^[a-zA-Z0-9-]+$/.test(entry.name))
|
|
11475
|
+
continue;
|
|
11476
|
+
try {
|
|
11477
|
+
await access(join17(directory, entry.name, ".config.json"));
|
|
11478
|
+
candidates.push(entry.name);
|
|
11479
|
+
} catch (error) {
|
|
11480
|
+
if (error.code !== "ENOENT")
|
|
11481
|
+
throw error;
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
candidates.sort();
|
|
11485
|
+
if (candidates.length === 0)
|
|
11486
|
+
return null;
|
|
11487
|
+
const selectedConfiguration = options.configuration ?? (candidates.length === 1 ? candidates[0] : undefined);
|
|
11488
|
+
if (!selectedConfiguration || !candidates.includes(selectedConfiguration)) {
|
|
11489
|
+
throw new ConfigError([`choose a component configuration with --components-config (${candidates.join(", ")})`]);
|
|
11490
|
+
}
|
|
11491
|
+
const selected = join17(directory, selectedConfiguration);
|
|
11492
|
+
const config = JSON.parse(await readFile4(join17(selected, ".config.json"), "utf8"));
|
|
11493
|
+
if (!isComponentConfiguration(config) || config.directory !== selectedConfiguration) {
|
|
11494
|
+
throw new ConfigError(["invalid KeepSnappy component fragment configuration"]);
|
|
11495
|
+
}
|
|
11496
|
+
const files = (await readdir2(selected)).filter((file) => file.endsWith(".json") && !file.startsWith(".")).sort();
|
|
11497
|
+
const components = [];
|
|
11498
|
+
const hashes = new Map;
|
|
11499
|
+
let matched = 0;
|
|
11500
|
+
for (const file of files) {
|
|
11501
|
+
const fragment = JSON.parse(await readFile4(join17(selected, file), "utf8"));
|
|
11502
|
+
if (!isComponentFragment(fragment))
|
|
11503
|
+
throw new ConfigError([`invalid KeepSnappy component fragment ${file}`]);
|
|
11504
|
+
if (!hashes.has(fragment.source)) {
|
|
11505
|
+
try {
|
|
11506
|
+
hashes.set(fragment.source, createHash2("sha256").update(await readFile4(resolve7(root, fragment.source))).digest("hex"));
|
|
11507
|
+
} catch (error) {
|
|
11508
|
+
if (error.code !== "ENOENT")
|
|
11509
|
+
throw error;
|
|
11510
|
+
hashes.set(fragment.source, null);
|
|
11511
|
+
}
|
|
11512
|
+
}
|
|
11513
|
+
if (hashes.get(fragment.source) !== fragment.hash)
|
|
11514
|
+
continue;
|
|
11515
|
+
matched++;
|
|
11516
|
+
components.push(...fragment.components);
|
|
11517
|
+
}
|
|
11518
|
+
if (files.length > 0 && matched === 0)
|
|
11519
|
+
options.warn?.("component fragments exist but none match the current sources");
|
|
11520
|
+
components.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.line - b.line);
|
|
11521
|
+
const file = join17(directory, "..", "components.json");
|
|
11522
|
+
const text = JSON.stringify(components);
|
|
11523
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11524
|
+
try {
|
|
11525
|
+
await writeFile2(temporary, text);
|
|
11526
|
+
await rename(temporary, file);
|
|
11527
|
+
} finally {
|
|
11528
|
+
await rm(temporary, { force: true });
|
|
11529
|
+
}
|
|
11530
|
+
const gzip = gzipSync(text);
|
|
11531
|
+
return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash2("sha256").update(gzip).digest("hex") };
|
|
11532
|
+
}
|
|
10886
11533
|
function stripSourcesContent(map) {
|
|
10887
11534
|
if (!isObject(map))
|
|
10888
11535
|
throw new ConfigError(["not a source map: the file is not a JSON object"]);
|
|
@@ -10958,21 +11605,35 @@ async function uploadSourcemaps(opts) {
|
|
|
10958
11605
|
}
|
|
10959
11606
|
return out;
|
|
10960
11607
|
}
|
|
10961
|
-
var SOURCEMAPS_USAGE =
|
|
11608
|
+
var SOURCEMAPS_USAGE = `keepsnappy sourcemaps upload --build <full commit sha|build id> --platform ios|android [--kind hermes|packager] [--flavour NAME] [--api URL] [--key-env KEEPSNAPPY_KEY] [--project-root PATH] [--components-config ID] <map files...>
|
|
11609
|
+
keepsnappy sourcemaps upload --eas (e2e EAS build: infer build, platform, map and kind; skips missing inputs and never fails the build)`;
|
|
10962
11610
|
async function sourcemapsCommand(args, rt) {
|
|
11611
|
+
try {
|
|
11612
|
+
return await uploadCommand(args, rt);
|
|
11613
|
+
} catch (error) {
|
|
11614
|
+
if (!args.includes("--eas"))
|
|
11615
|
+
throw error;
|
|
11616
|
+
rt.print("keepsnappy: source map upload failed (build not affected)");
|
|
11617
|
+
return 0;
|
|
11618
|
+
}
|
|
11619
|
+
}
|
|
11620
|
+
async function uploadCommand(args, rt) {
|
|
10963
11621
|
let parsed;
|
|
10964
11622
|
try {
|
|
10965
11623
|
parsed = parseArgs2({
|
|
10966
11624
|
args: [...args],
|
|
10967
11625
|
allowPositionals: true,
|
|
10968
11626
|
options: {
|
|
11627
|
+
eas: { type: "boolean" },
|
|
10969
11628
|
build: { type: "string" },
|
|
10970
11629
|
platform: { type: "string" },
|
|
10971
11630
|
kind: { type: "string" },
|
|
10972
11631
|
flavour: { type: "string" },
|
|
10973
11632
|
api: { type: "string" },
|
|
10974
11633
|
"ingest-url": { type: "string" },
|
|
10975
|
-
"key-env": { type: "string" }
|
|
11634
|
+
"key-env": { type: "string" },
|
|
11635
|
+
"project-root": { type: "string" },
|
|
11636
|
+
"components-config": { type: "string" }
|
|
10976
11637
|
}
|
|
10977
11638
|
});
|
|
10978
11639
|
} catch (e) {
|
|
@@ -10982,6 +11643,40 @@ async function sourcemapsCommand(args, rt) {
|
|
|
10982
11643
|
const [sub, ...files] = positionals;
|
|
10983
11644
|
if (sub !== "upload")
|
|
10984
11645
|
throw new ConfigError([SOURCEMAPS_USAGE]);
|
|
11646
|
+
const keyEnv = f["key-env"] ?? "KEEPSNAPPY_KEY";
|
|
11647
|
+
const key = projectKey(rt.env, keyEnv) || (f.eas && !f["key-env"] ? rt.env.KEEPSNAPPY_CI_KEY : undefined);
|
|
11648
|
+
const url = f.api ?? f["ingest-url"] ?? envValue(rt.env, "API") ?? (f.eas ? rt.env.EXPO_PUBLIC_KEEPSNAPPY_INGEST_URL ?? "" : DEFAULT_API_URL);
|
|
11649
|
+
let preparedFiles;
|
|
11650
|
+
if (f.eas) {
|
|
11651
|
+
const skip = (reason) => {
|
|
11652
|
+
rt.print(`keepsnappy: no source map upload (${reason})`);
|
|
11653
|
+
return 0;
|
|
11654
|
+
};
|
|
11655
|
+
if (rt.env.EAS_BUILD_PROFILE !== "e2e")
|
|
11656
|
+
return skip("profile is not e2e");
|
|
11657
|
+
f.platform ??= rt.env.EAS_BUILD_PLATFORM;
|
|
11658
|
+
if (f.platform !== "ios" && f.platform !== "android")
|
|
11659
|
+
return skip("platform must be ios or android");
|
|
11660
|
+
f.build ??= rt.env.EAS_BUILD_GIT_COMMIT_HASH;
|
|
11661
|
+
if (!key)
|
|
11662
|
+
return skip("key missing");
|
|
11663
|
+
if (!url)
|
|
11664
|
+
return skip("API URL missing");
|
|
11665
|
+
if (!f.build)
|
|
11666
|
+
return skip("commit missing");
|
|
11667
|
+
f.kind ??= f.platform === "ios" ? "packager" : "hermes";
|
|
11668
|
+
f.flavour ??= "e2e";
|
|
11669
|
+
if (files.length === 0)
|
|
11670
|
+
files.push(f.platform === "ios" ? "main.jsbundle.packager.map" : "android/app/build/generated/sourcemaps/react/release/index.android.bundle.map");
|
|
11671
|
+
preparedFiles = new Map;
|
|
11672
|
+
try {
|
|
11673
|
+
const read = rt.readFile ?? ((path) => readFile4(path, "utf8"));
|
|
11674
|
+
for (const file of files)
|
|
11675
|
+
preparedFiles.set(file, await read(file));
|
|
11676
|
+
} catch {
|
|
11677
|
+
return skip("map missing or unreadable");
|
|
11678
|
+
}
|
|
11679
|
+
}
|
|
10985
11680
|
const problems = [];
|
|
10986
11681
|
if (!f.build)
|
|
10987
11682
|
problems.push("--build <full commit sha (git rev-parse HEAD) or build id> is required");
|
|
@@ -10991,13 +11686,10 @@ async function sourcemapsCommand(args, rt) {
|
|
|
10991
11686
|
problems.push("--kind must be hermes or packager");
|
|
10992
11687
|
if (files.length === 0)
|
|
10993
11688
|
problems.push("give at least one source map file");
|
|
10994
|
-
const keyEnv = f["key-env"] ?? "KEEPSNAPPY_KEY";
|
|
10995
|
-
const key = projectKey(rt.env, keyEnv);
|
|
10996
11689
|
if (!key)
|
|
10997
11690
|
problems.push(`the project key (run scope) is missing: set the environment variable ${keyEnv}`);
|
|
10998
11691
|
if (problems.length)
|
|
10999
11692
|
throw new ConfigError(problems);
|
|
11000
|
-
const url = f.api ?? f["ingest-url"] ?? envValue(rt.env, "API") ?? DEFAULT_API_URL;
|
|
11001
11693
|
const done = await uploadSourcemaps({
|
|
11002
11694
|
url,
|
|
11003
11695
|
key,
|
|
@@ -11007,11 +11699,18 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11007
11699
|
...f.kind ? { kind: f.kind } : {},
|
|
11008
11700
|
...f.flavour ? { flavour: f.flavour } : {},
|
|
11009
11701
|
...rt.fetch ? { fetch: rt.fetch } : {},
|
|
11010
|
-
...rt.readFile ? { readFile: rt.readFile } : {}
|
|
11702
|
+
...preparedFiles ? { readFile: async (path) => preparedFiles.get(path) } : rt.readFile ? { readFile: rt.readFile } : {}
|
|
11011
11703
|
});
|
|
11012
11704
|
for (const d of done) {
|
|
11013
11705
|
rt.print(`${d.file}: uploaded to build ${d.buildId} (${d.gzBytes} bytes gzip, sourcesContent removed from ${d.strippedSources} sources)`);
|
|
11014
11706
|
}
|
|
11707
|
+
try {
|
|
11708
|
+
const components = await prepareComponents(f["project-root"] ?? process.cwd(), { configuration: f["components-config"], warn: (message) => rt.print(`warning: ${message}`) });
|
|
11709
|
+
if (components)
|
|
11710
|
+
rt.print(`${components.file}: prepared ${components.count} components; upload is not yet supported by the API`);
|
|
11711
|
+
} catch (error) {
|
|
11712
|
+
rt.print(`warning: component map preparation skipped: ${error.message}`);
|
|
11713
|
+
}
|
|
11015
11714
|
return 0;
|
|
11016
11715
|
}
|
|
11017
11716
|
|
|
@@ -11019,36 +11718,94 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11019
11718
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
11020
11719
|
|
|
11021
11720
|
// packages/mcp/dist/src/answer.js
|
|
11721
|
+
var SHORT_SESSIONS = 3;
|
|
11722
|
+
var SHORT_POINTS = 5;
|
|
11723
|
+
var SHORT_PLACES = 5;
|
|
11724
|
+
var SHORT_FORM_NOTES = {
|
|
11725
|
+
list_findings: "the how-sure lines, the place lines of findings that are not Worse and the lanes that were judged",
|
|
11726
|
+
get_finding: "the why, seen and layer lines, the component lines, the memory evidence lines other than the judged measure and the prompt",
|
|
11727
|
+
session_places: "the component lines",
|
|
11728
|
+
main_sessions: `the sessions older than the newest ${SHORT_SESSIONS}, the main changes of all but the newest session and, for one run_set, the places in norm`,
|
|
11729
|
+
main_vs_previous: `the movers after the first ${SHORT_PLACES} (every new, gone or multi-device place stays)`,
|
|
11730
|
+
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
11731
|
+
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
11732
|
+
pr_status: "the per-journey dashboard links and the heat note"
|
|
11733
|
+
};
|
|
11734
|
+
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
11735
|
+
|
|
11736
|
+
class Rows {
|
|
11737
|
+
rows = [];
|
|
11738
|
+
add(short, ...texts) {
|
|
11739
|
+
for (const t of texts)
|
|
11740
|
+
for (const l of t.split(`
|
|
11741
|
+
`))
|
|
11742
|
+
this.rows.push({ text: l, short });
|
|
11743
|
+
return this;
|
|
11744
|
+
}
|
|
11745
|
+
both(...texts) {
|
|
11746
|
+
return this.add(true, ...texts);
|
|
11747
|
+
}
|
|
11748
|
+
full(...texts) {
|
|
11749
|
+
return this.add(false, ...texts);
|
|
11750
|
+
}
|
|
11751
|
+
render(form, left = []) {
|
|
11752
|
+
if (form !== "short")
|
|
11753
|
+
return this.rows.map((r) => r.text).join(`
|
|
11754
|
+
`);
|
|
11755
|
+
const kept = [];
|
|
11756
|
+
for (const r of this.rows) {
|
|
11757
|
+
if (!r.short)
|
|
11758
|
+
continue;
|
|
11759
|
+
if (r.text === "" && (!kept.length || kept[kept.length - 1] === ""))
|
|
11760
|
+
continue;
|
|
11761
|
+
kept.push(r.text);
|
|
11762
|
+
}
|
|
11763
|
+
if (kept[kept.length - 1] === "")
|
|
11764
|
+
kept.pop();
|
|
11765
|
+
if (this.rows.some((r) => !r.short && r.text.trim() !== ""))
|
|
11766
|
+
kept.push(`Short form: ${andList(left.length ? left : ["other lines"])} left out; the full form shows them.`);
|
|
11767
|
+
return kept.join(`
|
|
11768
|
+
`);
|
|
11769
|
+
}
|
|
11770
|
+
}
|
|
11022
11771
|
var LABEL_W = 14;
|
|
11023
11772
|
var line = (label, text) => ` ${label.padEnd(LABEL_W)}${text}`;
|
|
11024
11773
|
var plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
11774
|
+
var promptText = (f) => f.what_to_do.prompt ?? "No prompt: this finding is an improvement.";
|
|
11025
11775
|
function formatFinding(f, opts = {}) {
|
|
11026
|
-
const
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
line("
|
|
11034
|
-
line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`),
|
|
11035
|
-
line("Id", f.id)
|
|
11036
|
-
];
|
|
11037
|
-
if (f.why.components.length > 1) {
|
|
11038
|
-
out.push(line("Components", ""));
|
|
11776
|
+
const r = new Rows;
|
|
11777
|
+
r.both(f.answer.headline, line("What changed", f.answer.what_changed), line("Where", f.answer.where), line("How sure", f.answer.how_sure));
|
|
11778
|
+
r.full(line("Why", f.answer.why));
|
|
11779
|
+
r.both(line("What to do", f.answer.what_to_do));
|
|
11780
|
+
r.full(line("Seen", f.answer.lifecycle), line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
11781
|
+
r.both(line("Id", f.id));
|
|
11782
|
+
if (f.why.components.length) {
|
|
11783
|
+
r.full(line("Components", ""));
|
|
11039
11784
|
for (const c of f.why.components) {
|
|
11040
|
-
|
|
11785
|
+
r.full(` ${c.component}: ${c.self_ms_before ?? "–"} → ${c.self_ms_after} ms own time, renders ${c.renders_before ?? "–"} → ${c.renders_after ?? "–"}${c.cause ? `; ${c.cause}` : ""}${c.flagged ? "" : " (not certain)"}`);
|
|
11786
|
+
}
|
|
11787
|
+
}
|
|
11788
|
+
const memory = f.why.memory ?? [];
|
|
11789
|
+
if (memory.length) {
|
|
11790
|
+
r.both(` Memory evidence: ${f.where.on === "main" ? "earlier → latest" : "main → PR"} (values are not a verdict)`);
|
|
11791
|
+
for (const m of memory) {
|
|
11792
|
+
const value = (n) => n === null ? "not stored" : fieldValueText(m.metric, n);
|
|
11793
|
+
r.add(m.metric === f.what.metric, ` ${m.label}: ${value(m.before)} → ${value(m.after)}${m.word ? ` (${m.word})` : ""}`);
|
|
11041
11794
|
}
|
|
11042
11795
|
}
|
|
11043
11796
|
if (opts.prompt)
|
|
11044
|
-
|
|
11045
|
-
return
|
|
11046
|
-
|
|
11797
|
+
r.full("", promptText(f));
|
|
11798
|
+
return r.render(opts.form, [
|
|
11799
|
+
"the why, seen and layer lines",
|
|
11800
|
+
...f.why.components.length ? [plural(f.why.components.length, "component line")] : [],
|
|
11801
|
+
...memory.some((m) => m.metric !== f.what.metric) ? [plural(memory.filter((m) => m.metric !== f.what.metric).length, "memory evidence line")] : [],
|
|
11802
|
+
...opts.prompt ? ["the prompt"] : []
|
|
11803
|
+
]);
|
|
11047
11804
|
}
|
|
11048
11805
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
11049
11806
|
var LANES_SHOWN = 8;
|
|
11050
11807
|
var STATE_WORDS2 = { present: "still present", resolved: "resolved", "not-judged": "not judged in the newest run set" };
|
|
11051
|
-
function
|
|
11808
|
+
function measuredLines(l, r) {
|
|
11052
11809
|
const f = l.filters;
|
|
11053
11810
|
const scope = [f.branch ?? "all branches", f.journey, f.phone].filter(Boolean).join(" · ");
|
|
11054
11811
|
const m = l.measured;
|
|
@@ -11056,56 +11813,107 @@ function measuredLine(l) {
|
|
|
11056
11813
|
if (!n) {
|
|
11057
11814
|
const on = f.branch ? f.branch === l.baseline_branch ? f.branch : `branch ${f.branch}` : "any branch";
|
|
11058
11815
|
const narrow = [f.journey, f.phone ? `phone “${f.phone}”` : null].filter(Boolean).join(", ");
|
|
11059
|
-
|
|
11816
|
+
r.both(m.run_sets ? `${scope}: ${plural(m.run_sets, "run set")} measured, but no verdict yet: nothing is known about its performance.` : `No KeepSnappy measurement on ${on}${narrow ? ` for ${narrow}` : ""}: nothing is known about its performance.`);
|
|
11817
|
+
return [];
|
|
11060
11818
|
}
|
|
11061
11819
|
const lane = (x) => {
|
|
11062
11820
|
const runs = x.runs.after !== null && x.runs.before !== null ? x.branch === l.baseline_branch ? `, ${x.runs.after} latest runs vs ${x.runs.before} earlier` : `, ${x.runs.after} branch runs vs ${x.runs.before} on ${l.baseline_branch}` : "";
|
|
11063
|
-
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}`;
|
|
11821
|
+
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
|
|
11064
11822
|
};
|
|
11065
|
-
if (m.lanes.length === 1)
|
|
11066
|
-
|
|
11823
|
+
if (m.lanes.length === 1) {
|
|
11824
|
+
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural(m.run_sets, "run set")} measured.`);
|
|
11825
|
+
return [];
|
|
11826
|
+
}
|
|
11067
11827
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
`);
|
|
11828
|
+
const unjudged = shown.filter((x) => !x.judged);
|
|
11829
|
+
r.add(unjudged.length > 0, `${scope}: ${plural(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
11830
|
+
for (const x of shown)
|
|
11831
|
+
r.add(!x.judged, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
11832
|
+
if (m.lanes.length > shown.length)
|
|
11833
|
+
r.full(` … ${m.lanes.length - shown.length} more (--journey / --phone narrow it)`);
|
|
11834
|
+
return [...shown.length > unjudged.length ? [plural(shown.length - unjudged.length, "judged lane")] : [], ...m.lanes.length > shown.length ? [plural(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []];
|
|
11074
11835
|
}
|
|
11075
11836
|
function countsLine2(l) {
|
|
11076
11837
|
const c = l.counts;
|
|
11077
11838
|
const parts = [
|
|
11078
|
-
c.worse ? `${c.worse} worse${c.not_blocking ? ` (${c.not_blocking} not blocking)` : ""}` : "",
|
|
11079
|
-
c.warn ?
|
|
11080
|
-
c.better ? `${c.better} better` : ""
|
|
11839
|
+
c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking} not blocking)` : ""}` : "",
|
|
11840
|
+
c.warn ? `${c.warn} ${STATE_WORDS.warning.toLowerCase()}` : "",
|
|
11841
|
+
c.better ? `${c.better} ${STATE_WORDS.better.toLowerCase()}` : ""
|
|
11081
11842
|
].filter(Boolean);
|
|
11082
11843
|
const state = l.filters.state;
|
|
11083
11844
|
const unjudged = l.measured.lanes.filter((x) => !x.judged);
|
|
11084
|
-
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` :
|
|
11845
|
+
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : `Nothing ${STATE_WORDS.worse.toLowerCase()}, nothing ${STATE_WORDS.warning.toLowerCase()}, nothing ${STATE_WORDS.better.toLowerCase()}`;
|
|
11085
11846
|
const heated = c.heat_runs ?? 0;
|
|
11086
11847
|
const more = [
|
|
11087
|
-
heated ? `${plural(heated, "run")}
|
|
11848
|
+
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural(heated, "run")} (run health, layer 3)` : "",
|
|
11088
11849
|
state === "present" && l.states.not_judged ? `${l.states.not_judged} more not judged in the newest run set (state not-judged)` : ""
|
|
11089
11850
|
].filter(Boolean);
|
|
11090
11851
|
const line = `${head}${more.length ? `; ${more.join("; ")}` : ""}.`;
|
|
11091
11852
|
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
11092
11853
|
}
|
|
11093
|
-
function formatFindingList(l) {
|
|
11094
|
-
const
|
|
11854
|
+
function formatFindingList(l, opts = {}) {
|
|
11855
|
+
const r = new Rows;
|
|
11856
|
+
const lanesLeft = measuredLines(l, r);
|
|
11095
11857
|
if (l.measured.newest || l.total)
|
|
11096
|
-
|
|
11858
|
+
r.both(countsLine2(l));
|
|
11097
11859
|
l.findings.forEach((f, i) => {
|
|
11098
|
-
|
|
11860
|
+
r.both("", `${l.offset + i + 1}. ${f.answer.headline}`);
|
|
11861
|
+
r.add(isWorseState(f.state), ` ${f.answer.where}`);
|
|
11862
|
+
r.full(` ${f.answer.how_sure}`);
|
|
11863
|
+
r.both(` ${f.answer.lifecycle} Id ${f.id}`);
|
|
11099
11864
|
});
|
|
11100
11865
|
const shown = l.findings.length ? `Showing ${l.offset + 1}–${l.offset + l.findings.length} of ${l.total}` : "";
|
|
11101
11866
|
if (l.next_offset !== null)
|
|
11102
|
-
|
|
11867
|
+
r.both("", `${shown || `Showing none of ${l.total}`}; the rest from offset ${l.next_offset} (--offset ${l.next_offset}).`);
|
|
11103
11868
|
else if (l.offset > 0)
|
|
11104
|
-
|
|
11869
|
+
r.both("", shown ? `${shown}.` : `Offset ${l.offset} is past the last of ${l.total}.`);
|
|
11105
11870
|
if (l.findings.length)
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11871
|
+
r.both("", "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt");
|
|
11872
|
+
const notWorse = l.findings.filter((f) => !isWorseState(f.state)).length;
|
|
11873
|
+
return r.render(opts.form, [
|
|
11874
|
+
...l.findings.length ? [plural(l.findings.length, "how-sure line")] : [],
|
|
11875
|
+
...notWorse ? [plural(notWorse, "place line")] : [],
|
|
11876
|
+
...lanesLeft
|
|
11877
|
+
]);
|
|
11878
|
+
}
|
|
11879
|
+
var STATUS_LEAD = {
|
|
11880
|
+
ok: "ok",
|
|
11881
|
+
warns: "nothing blocks, results to look at",
|
|
11882
|
+
blocks: "blocked",
|
|
11883
|
+
"cant-tell": "not ok yet, nothing was compared",
|
|
11884
|
+
"not-judged": "not ok yet, the newest run set was not judged",
|
|
11885
|
+
"never-measured": "not ok yet, never measured"
|
|
11886
|
+
};
|
|
11887
|
+
function formatBranchStatus(s, opts = {}) {
|
|
11888
|
+
const r = new Rows;
|
|
11889
|
+
r.both(`${s.branch}: ${STATUS_LEAD[s.result]}`, s.reason);
|
|
11890
|
+
if (s.lanes.length)
|
|
11891
|
+
r.both("");
|
|
11892
|
+
for (const l of s.lanes) {
|
|
11893
|
+
const badge = l.result === "blocks" ? " (blocks)" : l.word === STATE_WORDS.worse ? ` (${NOT_BLOCKING})` : "";
|
|
11894
|
+
const sha = l.commit?.sha?.slice(0, 7);
|
|
11895
|
+
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${l.word}${badge} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
|
|
11896
|
+
for (const f of l.worst)
|
|
11897
|
+
r.both(` ${f.id} ${f.headline}`);
|
|
11898
|
+
if (l.present_findings > l.worst.length)
|
|
11899
|
+
r.both(` … ${l.present_findings - l.worst.length} more: keepsnappy findings --branch ${s.branch}`);
|
|
11900
|
+
if (l.link)
|
|
11901
|
+
r.full(` ${l.link}`);
|
|
11902
|
+
}
|
|
11903
|
+
const links = s.lanes.filter((l) => l.link).length;
|
|
11904
|
+
const heated = s.counts.heat_runs ?? 0;
|
|
11905
|
+
const more = [
|
|
11906
|
+
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural(heated, "run")} (information, never blocks)` : ""],
|
|
11907
|
+
[true, s.not_judged_findings ? `${plural(s.not_judged_findings, "finding")} not judged in the newest run set; they may still be there: keepsnappy findings --branch ${s.branch} --state not-judged` : ""],
|
|
11908
|
+
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
11909
|
+
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
11910
|
+
];
|
|
11911
|
+
const shownMore = more.filter(([, text]) => text);
|
|
11912
|
+
if (shownMore.length)
|
|
11913
|
+
r.both("");
|
|
11914
|
+
for (const [short, text] of shownMore)
|
|
11915
|
+
r.add(short, text);
|
|
11916
|
+
return r.render(opts.form, [...links ? [plural(links, "dashboard link")] : [], ...heated ? ["the heat note"] : []]);
|
|
11109
11917
|
}
|
|
11110
11918
|
var commitText = (c) => !c ? "commit not recorded" : c.title ? `“${c.title}”${c.author ? ` by ${c.author}` : ""}${c.sha ? ` (${c.sha.slice(0, 7)})` : ""}` : c.sha ? c.sha.slice(0, 7) : "commit not recorded";
|
|
11111
11919
|
function formatCompare(r) {
|
|
@@ -11127,19 +11935,20 @@ function formatCompare(r) {
|
|
|
11127
11935
|
return out.join(`
|
|
11128
11936
|
`);
|
|
11129
11937
|
}
|
|
11130
|
-
function formatHistory(h) {
|
|
11131
|
-
const
|
|
11938
|
+
function formatHistory(h, opts = {}) {
|
|
11939
|
+
const r = new Rows;
|
|
11940
|
+
r.both(`${h.window_name} in ${h.journey_title ?? h.journey} on ${h.branch}: ${h.points.length ? `${plural(h.points.length, "run")}, oldest first` : h.note ?? "no runs"}`);
|
|
11132
11941
|
const ms = h.measures.slice(0, 4);
|
|
11133
11942
|
if (h.points.length) {
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
}
|
|
11943
|
+
r.both(` ${"when".padEnd(17)}${ms.map((m) => (m.label.length > 22 ? `${m.label.slice(0, 21)}…` : m.label).padStart(24)).join("")} commit`);
|
|
11944
|
+
h.points.forEach((p, i) => {
|
|
11945
|
+
r.add(i >= h.points.length - SHORT_POINTS, ` ${(p.at ?? "").slice(0, 16).replace("T", " ").padEnd(17)}${ms.map((m) => fieldValueText(m.metric, p.values[m.metric]).padStart(24)).join("")} ${commitText(p.commit)}`);
|
|
11946
|
+
});
|
|
11138
11947
|
}
|
|
11139
11948
|
if (h.findings.length)
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11949
|
+
r.both("", "Findings on this window:", ...h.findings.map((f) => ` ${f.answer.headline} — ${f.where.branch} (${f.id})`));
|
|
11950
|
+
const older = Math.max(0, h.points.length - SHORT_POINTS);
|
|
11951
|
+
return r.render(opts.form, older ? [plural(older, "older run")] : []);
|
|
11143
11952
|
}
|
|
11144
11953
|
function formatCommits(c) {
|
|
11145
11954
|
return [
|
|
@@ -11149,6 +11958,55 @@ function formatCommits(c) {
|
|
|
11149
11958
|
].join(`
|
|
11150
11959
|
`);
|
|
11151
11960
|
}
|
|
11961
|
+
var list = (xs) => xs.join(", ");
|
|
11962
|
+
function versionText(label, counts) {
|
|
11963
|
+
const values = Object.keys(counts).sort((a, b) => a === "unknown" ? -1 : b === "unknown" ? 1 : a < b ? -1 : a > b ? 1 : 0);
|
|
11964
|
+
const name = (k) => k === "unknown" ? "not recorded" : k;
|
|
11965
|
+
if (values.length === 0)
|
|
11966
|
+
return "";
|
|
11967
|
+
if (values.length === 1)
|
|
11968
|
+
return `${label} ${name(values[0])}`;
|
|
11969
|
+
return `${label} mixed: ${values.map((k) => `${name(k)} ×${counts[k]}`).join(", ")}`;
|
|
11970
|
+
}
|
|
11971
|
+
function formatSessionPlaces(p, opts = {}) {
|
|
11972
|
+
const s = p.session;
|
|
11973
|
+
const sha = s.commit?.sha?.slice(0, 7);
|
|
11974
|
+
const commit = s.commit ? `${s.commit.title ? `“${s.commit.title}”${s.commit.author ? ` by ${s.commit.author}` : ""}` : sha ?? "commit not recorded"}${s.commit.title && sha ? ` (${sha})` : ""}` : "commit not recorded";
|
|
11975
|
+
const v = s.versions;
|
|
11976
|
+
const codes = Object.keys(s.runs.invalid_codes);
|
|
11977
|
+
const r = new Rows().both(`Session ${s.completed_at ? `${when(s.completed_at)} ` : ""}on ${s.branch}: ${commit}${s.pull_request?.number ? `, PR #${s.pull_request.number}` : ""}; ${s.journey.title ?? s.journey.key} on ${s.phone.model}${s.phone.os ? ` · ${s.phone.os}` : ""}${s.status === "processed" ? "" : ` (run set ${s.status})`}.`, `Runs: ${s.runs.valid} valid${s.runs.invalid ? `, ${s.runs.invalid} invalid${codes.length ? ` (${list(codes)})` : ""}` : ""}. ${[
|
|
11978
|
+
versionText("SDK", v.sdk),
|
|
11979
|
+
versionText("driver", v.driver),
|
|
11980
|
+
versionText("driver version", v.driver_version),
|
|
11981
|
+
versionText("app", v.app_build),
|
|
11982
|
+
versionText("journey version", v.journey_version),
|
|
11983
|
+
`derivation ${v.derivation}`,
|
|
11984
|
+
`stats ${v.stats}`
|
|
11985
|
+
].filter(Boolean).join(", ")}.`);
|
|
11986
|
+
if (s.not_computed)
|
|
11987
|
+
return r.both("Not computed: this session has no stored statistics yet; nothing is known about its places (not zero).").render(opts.form);
|
|
11988
|
+
if (s.stale)
|
|
11989
|
+
return r.both("Stale: its statistics are of another derivation or stats version and are not served until the next recompute rewrites them; nothing is known about its places (not zero).").render(opts.form);
|
|
11990
|
+
const by = p.filters.measure;
|
|
11991
|
+
const byLabel = p.places.flatMap((x) => x.measures).find((m) => m.measure === by)?.label ?? by;
|
|
11992
|
+
r.both(`Stored per-session statistics: no verdict, no comparison. Places by ${byLabel} (${p.filters.sort}), highest first${p.filters.window ? `, matching “${p.filters.window}”` : ""}${p.without_measure ? `; ${p.without_measure} of ${p.total} places ${p.without_measure === 1 ? "has" : "have"} no value for it` : ""}.`, "");
|
|
11993
|
+
let componentLines = 0;
|
|
11994
|
+
p.places.forEach((place, i) => {
|
|
11995
|
+
const m = place.measures.find((x) => x.measure === by);
|
|
11996
|
+
const value = m ? `${fieldValueText(by, m.median)} (p90 ${fieldValueText(by, m.p90)}, n ${m.n})` : `no ${by}`;
|
|
11997
|
+
const comps = place.components.map((c) => `${c.name} ${valueText("self_ms", c.self_ms.median)} (p90 ${valueText("self_ms", c.self_ms.p90)}${c.renders_median !== null ? `, ${plural(c.renders_median, "render")}` : ""})`);
|
|
11998
|
+
r.both(`${p.offset + i + 1}. ${place.window_name}: ${value}`);
|
|
11999
|
+
if (comps.length) {
|
|
12000
|
+
r.full(` Components: ${list(comps)}`);
|
|
12001
|
+
componentLines++;
|
|
12002
|
+
}
|
|
12003
|
+
});
|
|
12004
|
+
if (!p.places.length)
|
|
12005
|
+
r.both(p.total ? `Offset ${p.offset} is past the last of ${p.total} places.` : "No place with stored statistics in this session matches.");
|
|
12006
|
+
else if (p.next_offset !== null)
|
|
12007
|
+
r.both("", `Showing ${p.offset + 1}–${p.offset + p.places.length} of ${p.total}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).`);
|
|
12008
|
+
return r.render(opts.form, componentLines ? [plural(componentLines, "component line")] : []);
|
|
12009
|
+
}
|
|
11152
12010
|
function formatLayers(l) {
|
|
11153
12011
|
return [
|
|
11154
12012
|
l.note,
|
|
@@ -11158,6 +12016,100 @@ function formatLayers(l) {
|
|
|
11158
12016
|
].join(`
|
|
11159
12017
|
`);
|
|
11160
12018
|
}
|
|
12019
|
+
var MAIN_TIERS_WORST_FIRST = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
12020
|
+
function formatMainSessions(h, opts = {}) {
|
|
12021
|
+
const baseline = { baseline: { sessions: h.baseline_sessions } };
|
|
12022
|
+
const r = new Rows().both(`Main sessions: tiers are verdicts vs ${h.baseline_sessions === undefined ? "earlier sessions" : `the ${h.baseline_sessions} sessions before`}.`);
|
|
12023
|
+
if (!h.sessions.length)
|
|
12024
|
+
r.both("No main sessions measured for this journey and phone.");
|
|
12025
|
+
const recent = new Set(h.sessions.slice(-SHORT_SESSIONS));
|
|
12026
|
+
const latest = h.sessions[h.sessions.length - 1];
|
|
12027
|
+
let otherChanges = 0;
|
|
12028
|
+
for (const s of h.sessions) {
|
|
12029
|
+
const k = recent.has(s);
|
|
12030
|
+
r.add(k, `${when(s.at)} ${s.run_set_id} ${commitText(s.commit ? { ...s.commit, author: null } : null)} (${plural(s.runs, "run")})`);
|
|
12031
|
+
if (!s.places)
|
|
12032
|
+
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
12033
|
+
else
|
|
12034
|
+
r.add(k, ` ${MAIN_TIERS_WORST_FIRST.map((tier) => `${MAIN_TIER_WORDS[tier]}: ${s.places[tier]}`).join("; ")}${s.cant_reason ? `; ${mainSessionCantWords(s.places.cant, s.cant_reason, baseline)}` : ""}`);
|
|
12035
|
+
if (s.waiting_sessions)
|
|
12036
|
+
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
12037
|
+
if (s.memory)
|
|
12038
|
+
r.add(k, ` Memory: ${MAIN_TIER_WORDS[s.memory.tier]}, ${s.memory.median} MB (n ${s.memory.n})${s.memory.cant_reason ? `; ${mainCantWords(s.memory.cant_reason, baseline)}` : ""}`);
|
|
12039
|
+
for (const v of h.versions.filter((v) => v.from_run_set_id === s.run_set_id))
|
|
12040
|
+
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}`);
|
|
12041
|
+
for (const c of h.changes.filter((c) => c.from_run_set_id === s.run_set_id)) {
|
|
12042
|
+
r.add(s === latest, ` Main change: ${c.place} ${c.ref}, ${c.before_median ?? "–"} → ${c.after_median ?? "–"}`);
|
|
12043
|
+
if (k && s !== latest)
|
|
12044
|
+
otherChanges++;
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
const older = h.sessions.length - recent.size;
|
|
12048
|
+
return r.render(opts.form, [...older ? [plural(older, "older session")] : [], ...otherChanges ? [plural(otherChanges, "main change") + " of the other recent sessions"] : []]);
|
|
12049
|
+
}
|
|
12050
|
+
function formatMainSessionPlaces(p, opts = {}) {
|
|
12051
|
+
const r = new Rows;
|
|
12052
|
+
if (p.status !== "ok")
|
|
12053
|
+
return r.both(`${MAIN_SUMMARY_PENDING}${p.summary_reason ? ` (${p.summary_reason})` : ""}`).render(opts.form);
|
|
12054
|
+
const baselineSessions = p.baseline_sessions;
|
|
12055
|
+
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
12056
|
+
for (const x of p.places)
|
|
12057
|
+
r.add(x.tier !== "same", ` ${x.place} ${x.ref}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}`);
|
|
12058
|
+
const inNorm = p.places.filter((x) => x.tier === "same").length;
|
|
12059
|
+
return r.render(opts.form, inNorm ? [plural(inNorm, "place") + " in norm"] : []);
|
|
12060
|
+
}
|
|
12061
|
+
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
12062
|
+
var previousLine = (p) => ` ${p.place} ${p.ref}${p.devices ? ` [devices ${p.devices.before.join(", ") || "–"} → ${p.devices.after.join(", ") || "–"}]` : ""}: ${p.status === "compared" ? "" : `${p.status}; `}${p.before ?? "–"} → ${p.after ?? "–"}; delta ${p.delta === null ? "–" : signed(p.delta)}; ${p.percent_change === null ? "change undefined" : `${signed(p.percent_change)}%`}; n ${p.n.before ?? "–"} → ${p.n.after ?? "–"}`;
|
|
12063
|
+
function formatMainVsPrevious(m, opts = {}) {
|
|
12064
|
+
const r = new Rows().both(`Information only: main session ${m.run_set_id} vs ${m.previous_run_set_id ?? "no previous session"}.`, `Measure: ${m.measure}; ${m.status}.`);
|
|
12065
|
+
if (m.cut === "unknown")
|
|
12066
|
+
r.both("History restart: unknown; current summaries unavailable.");
|
|
12067
|
+
else if (m.cut)
|
|
12068
|
+
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart.`);
|
|
12069
|
+
if (m.not_computed.length)
|
|
12070
|
+
r.both(`not_computed: ${m.not_computed.join(", ")}`);
|
|
12071
|
+
if (m.stale.length)
|
|
12072
|
+
r.both(`stale: ${m.stale.join(", ")}`);
|
|
12073
|
+
if (m.journey)
|
|
12074
|
+
r.both("Whole journey:", previousLine(m.journey));
|
|
12075
|
+
m.places.forEach((p, i) => r.add(i < SHORT_PLACES || p.status !== "compared", previousLine(p)));
|
|
12076
|
+
if (m.next_offset !== null)
|
|
12077
|
+
r.both(`Showing ${m.offset + 1}–${m.offset + m.places.length} of ${m.total}; next offset ${m.next_offset}.`);
|
|
12078
|
+
const smaller = m.places.filter((p, i) => i >= SHORT_PLACES && p.status === "compared").length;
|
|
12079
|
+
return r.render(opts.form, smaller ? [plural(smaller, "smaller mover")] : []);
|
|
12080
|
+
}
|
|
12081
|
+
function formatPlaceTrend(t, opts = {}) {
|
|
12082
|
+
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
12083
|
+
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
12084
|
+
const olderSessions = new Set;
|
|
12085
|
+
let olderRestarts = 0;
|
|
12086
|
+
for (const session of t.sessions) {
|
|
12087
|
+
const k = newest.has(session.run_set_id);
|
|
12088
|
+
if (session.restarts_known === false)
|
|
12089
|
+
r.both(`History restarts unknown: ${session.run_set_id} (no current main summary in the history window)`);
|
|
12090
|
+
for (const restart of t.restarts.filter((x) => x.from_run_set_id === session.run_set_id)) {
|
|
12091
|
+
r.add(k, `History restarts: ${restart.label} (${restart.cuts.join(", ")})`);
|
|
12092
|
+
if (!k)
|
|
12093
|
+
olderRestarts++;
|
|
12094
|
+
}
|
|
12095
|
+
for (const place of t.places) {
|
|
12096
|
+
const value = place.sessions.find((s) => s.run_set_id === session.run_set_id);
|
|
12097
|
+
if (!value)
|
|
12098
|
+
continue;
|
|
12099
|
+
const unit = place.unit ? ` ${place.unit}` : "";
|
|
12100
|
+
if (!k)
|
|
12101
|
+
olderSessions.add(session.run_set_id);
|
|
12102
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${place.window} · ${place.label} (${place.ref}): median ${value.median}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
if (!t.sessions.length)
|
|
12106
|
+
r.both("No main sessions.");
|
|
12107
|
+
if (t.not_computed.length)
|
|
12108
|
+
r.both(`Not computed: ${t.not_computed.join(", ")}`);
|
|
12109
|
+
if (t.stale.length)
|
|
12110
|
+
r.both(`Stale: ${t.stale.join(", ")}`);
|
|
12111
|
+
return r.render(opts.form, [...olderSessions.size ? [plural(olderSessions.size, "older session")] : [], ...olderRestarts ? [plural(olderRestarts, "older restart marker")] : []]);
|
|
12112
|
+
}
|
|
11161
12113
|
|
|
11162
12114
|
// packages/mcp/dist/src/client.js
|
|
11163
12115
|
class ApiError extends Error {
|
|
@@ -11172,9 +12124,13 @@ class ApiError extends Error {
|
|
|
11172
12124
|
}
|
|
11173
12125
|
async function get(t, path, query = {}) {
|
|
11174
12126
|
const u = new URL(path, t.url.endsWith("/") ? t.url : `${t.url}/`);
|
|
11175
|
-
for (const [k, v] of Object.entries(query))
|
|
11176
|
-
if (v
|
|
12127
|
+
for (const [k, v] of Object.entries(query)) {
|
|
12128
|
+
if (Array.isArray(v))
|
|
12129
|
+
for (const value of v)
|
|
12130
|
+
u.searchParams.append(k, value);
|
|
12131
|
+
else if (v !== undefined && v !== "")
|
|
11177
12132
|
u.searchParams.set(k, String(v));
|
|
12133
|
+
}
|
|
11178
12134
|
const r = await (t.fetch ?? fetch)(u, { headers: { "x-keepsnappy-key": t.key, accept: "application/json" } });
|
|
11179
12135
|
const text = await r.text();
|
|
11180
12136
|
const body = text ? JSON.parse(text) : {};
|
|
@@ -11187,19 +12143,47 @@ async function get(t, path, query = {}) {
|
|
|
11187
12143
|
return body;
|
|
11188
12144
|
}
|
|
11189
12145
|
var listFindings = (t, f = {}) => get(t, "v1/findings", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, state: f.state, limit: f.limit, offset: f.offset || undefined });
|
|
12146
|
+
var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
11190
12147
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
11191
12148
|
var compareRuns = (t, a, b) => get(t, "v1/compare", { a, b });
|
|
11192
12149
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
11193
12150
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
11194
12151
|
var layers = (t) => get(t, "v1/layers");
|
|
12152
|
+
var sessionPlaces = (t, f) => get(t, "v1/sessions/places", {
|
|
12153
|
+
run_set: f.run_set,
|
|
12154
|
+
journey: f.journey,
|
|
12155
|
+
phone: f.phone,
|
|
12156
|
+
branch: f.branch,
|
|
12157
|
+
measure: f.measure,
|
|
12158
|
+
sort: f.sort,
|
|
12159
|
+
window: f.window,
|
|
12160
|
+
limit: f.limit,
|
|
12161
|
+
offset: f.offset || undefined,
|
|
12162
|
+
top_components: f.top_components
|
|
12163
|
+
});
|
|
12164
|
+
var SESSION_SORTS = ["median", "p90", "mad"];
|
|
12165
|
+
var mainSessions = (t, q) => get(t, "v1/main/sessions", { ...q });
|
|
12166
|
+
var mainSessionPlaces = (t, runSetId, q) => get(t, `v1/main/sessions/${encodeURIComponent(runSetId)}/places`, { ...q });
|
|
12167
|
+
var mainVsPrevious = (t, q) => {
|
|
12168
|
+
const { run_set, ...query } = q;
|
|
12169
|
+
return get(t, `v1/main/sessions/${encodeURIComponent(run_set ?? "latest")}/vs-previous`, query);
|
|
12170
|
+
};
|
|
12171
|
+
var placeTrend = (t, q) => get(t, "v1/places/trend", { ...q });
|
|
11195
12172
|
|
|
11196
12173
|
// apps/agent/src/findings.ts
|
|
11197
|
-
var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KEY] [--phone MODEL] [--state present|not-judged|resolved|all] [--limit N] [--offset N] [--json]
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
12174
|
+
var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KEY] [--phone MODEL] [--state present|not-judged|resolved|all] [--limit N] [--offset N] [--short] [--json]
|
|
12175
|
+
status --branch NAME [--short] [--json] (is my PR ok? one answer: blocked / nothing blocks / ok, per journey × phone the word, the ids of the worst findings, dashboard links; never measured and not judged are not ok)
|
|
12176
|
+
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
12177
|
+
compare <runA> <runB> [--json] (run ids or external ids; one run against one, no statistics)
|
|
12178
|
+
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
12179
|
+
commits <from> [<to>] [--branch NAME] [--json] (measured commits between two run sets or commit hashes)
|
|
12180
|
+
places [--run-set ID | --journey KEY [--phone MODEL] [--branch NAME]] [--measure ID] [--sort median|p90|mad] [--window TEXT] [--limit N] [--offset N] [--top-components N] [--short] [--json]
|
|
12181
|
+
(every place of one session from its stored statistics: no verdict; the newest closed run set of the journey when --run-set is not given)
|
|
12182
|
+
main --journey KEY --phone MODEL [--sessions N | --session ID] [--vs-previous] [--measure REF] [--limit N] [--offset N] [--short] [--json]
|
|
11202
12183
|
layers [--json]
|
|
12184
|
+
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
12185
|
+
(the words after the journey are one window name; more windows with repeated --window)
|
|
12186
|
+
--short (findings, status, finding, history, places, main, trend: what decides, without the evidence detail; the same as the MCP tools' form short; --json is unchanged)
|
|
11203
12187
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
11204
12188
|
|
|
11205
12189
|
class UsageError extends ConfigError {
|
|
@@ -11211,7 +12195,8 @@ class UsageError extends ConfigError {
|
|
|
11211
12195
|
}
|
|
11212
12196
|
}
|
|
11213
12197
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
11214
|
-
var
|
|
12198
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "finding", "history", "places", "main", "trend"]);
|
|
12199
|
+
var FINDINGS_COMMANDS = new Set(["findings", "status", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
11215
12200
|
async function findingsCommand(cmd, argv, rt) {
|
|
11216
12201
|
const args = (() => {
|
|
11217
12202
|
try {
|
|
@@ -11224,12 +12209,22 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11224
12209
|
journey: { type: "string" },
|
|
11225
12210
|
phone: { type: "string" },
|
|
11226
12211
|
state: { type: "string" },
|
|
12212
|
+
session: { type: "string" },
|
|
12213
|
+
"vs-previous": { type: "boolean" },
|
|
11227
12214
|
limit: { type: "string" },
|
|
11228
12215
|
offset: { type: "string" },
|
|
12216
|
+
"run-set": { type: "string" },
|
|
12217
|
+
measure: { type: "string" },
|
|
12218
|
+
sort: { type: "string" },
|
|
12219
|
+
"top-components": { type: "string" },
|
|
11229
12220
|
json: { type: "boolean" },
|
|
12221
|
+
short: { type: "boolean" },
|
|
11230
12222
|
prompt: { type: "boolean" },
|
|
11231
12223
|
api: { type: "string" },
|
|
11232
|
-
"key-env": { type: "string" }
|
|
12224
|
+
"key-env": { type: "string" },
|
|
12225
|
+
window: { type: "string", multiple: true },
|
|
12226
|
+
ref: { type: "string", multiple: true },
|
|
12227
|
+
sessions: { type: "string" }
|
|
11233
12228
|
}
|
|
11234
12229
|
});
|
|
11235
12230
|
} catch (e) {
|
|
@@ -11242,8 +12237,9 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11242
12237
|
if (!key)
|
|
11243
12238
|
throw new ConfigError([`a project key with read scope is needed: set the environment variable ${keyEnv}`]);
|
|
11244
12239
|
const t = { url: o.api ?? envValue(rt.env, "API") ?? DEFAULT_API_URL, key, ...rt.fetch ? { fetch: rt.fetch } : {} };
|
|
12240
|
+
const answerForm = { form: o.short ? "short" : "full" };
|
|
11245
12241
|
const out = (v, text) => {
|
|
11246
|
-
rt.print(o.json ? JSON.stringify(v, null, 2) : text(v));
|
|
12242
|
+
rt.print(o.json ? JSON.stringify(v, null, 2) : text(v, answerForm));
|
|
11247
12243
|
return 0;
|
|
11248
12244
|
};
|
|
11249
12245
|
const need = (n, what) => {
|
|
@@ -11253,10 +12249,29 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11253
12249
|
const limit = o.limit === undefined ? undefined : Number(o.limit);
|
|
11254
12250
|
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200))
|
|
11255
12251
|
throw new UsageError(["--limit must be a whole number from 1 to 200 (page with --offset)"]);
|
|
12252
|
+
if (o.short && !SHORT_COMMANDS.has(cmd))
|
|
12253
|
+
throw new UsageError([`--short is for ${[...SHORT_COMMANDS].join(", ")}; keepsnappy ${cmd} has only one form`]);
|
|
11256
12254
|
const offset = o.offset === undefined ? undefined : Number(o.offset);
|
|
11257
12255
|
if (offset !== undefined && (!Number.isInteger(offset) || offset < 0))
|
|
11258
12256
|
throw new UsageError(["--offset must be a whole number >= 0"]);
|
|
11259
12257
|
switch (cmd) {
|
|
12258
|
+
case "main": {
|
|
12259
|
+
if (!o.journey)
|
|
12260
|
+
throw new UsageError(["keepsnappy main needs --journey KEY"]);
|
|
12261
|
+
if (!o.phone)
|
|
12262
|
+
throw new UsageError(["keepsnappy main needs --phone MODEL (exact device model)"]);
|
|
12263
|
+
const sessions = o.sessions === undefined ? undefined : Number(o.sessions);
|
|
12264
|
+
if (sessions !== undefined && (!Number.isInteger(sessions) || sessions < 1 || sessions > 20))
|
|
12265
|
+
throw new UsageError(["--sessions must be a whole number from 1 to 20"]);
|
|
12266
|
+
if (sessions !== undefined && (o.session || o["vs-previous"]))
|
|
12267
|
+
throw new UsageError(["--sessions is for the session list"]);
|
|
12268
|
+
const lane = { journey: o.journey, phone: o.phone };
|
|
12269
|
+
if (o["vs-previous"])
|
|
12270
|
+
return out(await mainVsPrevious(t, { ...lane, run_set: o.session, measure: o.measure, limit, offset }), formatMainVsPrevious);
|
|
12271
|
+
if (o.session)
|
|
12272
|
+
return out(await mainSessionPlaces(t, o.session, lane), formatMainSessionPlaces);
|
|
12273
|
+
return out(await mainSessions(t, { ...lane, sessions }), formatMainSessions);
|
|
12274
|
+
}
|
|
11260
12275
|
case "findings": {
|
|
11261
12276
|
if (o.branch && o.main)
|
|
11262
12277
|
throw new UsageError(["give --branch or --main, not both"]);
|
|
@@ -11273,14 +12288,18 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11273
12288
|
...offset ? { offset } : {}
|
|
11274
12289
|
}), formatFindingList);
|
|
11275
12290
|
}
|
|
12291
|
+
case "status":
|
|
12292
|
+
if (!o.branch)
|
|
12293
|
+
throw new UsageError(["keepsnappy status needs --branch NAME", FINDINGS_USAGE]);
|
|
12294
|
+
return out(await branchStatus(t, o.branch), formatBranchStatus);
|
|
11276
12295
|
case "finding": {
|
|
11277
12296
|
need(1, "<id>");
|
|
11278
12297
|
const f = await getFinding(t, p[0]);
|
|
11279
12298
|
if (o.prompt && !o.json) {
|
|
11280
|
-
rt.print(f
|
|
12299
|
+
rt.print(promptText(f));
|
|
11281
12300
|
return 0;
|
|
11282
12301
|
}
|
|
11283
|
-
return out(f,
|
|
12302
|
+
return out(f, formatFinding);
|
|
11284
12303
|
}
|
|
11285
12304
|
case "compare":
|
|
11286
12305
|
need(2, "<runA> <runB>");
|
|
@@ -11291,8 +12310,39 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11291
12310
|
case "commits":
|
|
11292
12311
|
need(1, "<from>");
|
|
11293
12312
|
return out(await commitsBetween(t, { from: p[0], ...p[1] ? { to: p[1] } : {}, ...o.branch ? { branch: o.branch } : {} }), formatCommits);
|
|
12313
|
+
case "places": {
|
|
12314
|
+
const sort = o.sort;
|
|
12315
|
+
if (sort && !SESSION_SORTS.includes(sort))
|
|
12316
|
+
throw new UsageError([`--sort is ${SESSION_SORTS.join(", ")}`]);
|
|
12317
|
+
const top = o["top-components"] === undefined ? undefined : Number(o["top-components"]);
|
|
12318
|
+
if (top !== undefined && (!Number.isInteger(top) || top < 0 || top > 20))
|
|
12319
|
+
throw new UsageError(["--top-components must be a whole number from 0 to 20"]);
|
|
12320
|
+
return out(await sessionPlaces(t, {
|
|
12321
|
+
...o["run-set"] ? { run_set: o["run-set"] } : {},
|
|
12322
|
+
...o.journey ? { journey: o.journey } : {},
|
|
12323
|
+
...o.phone ? { phone: o.phone } : {},
|
|
12324
|
+
...o.branch ? { branch: o.branch } : {},
|
|
12325
|
+
...o.measure ? { measure: o.measure } : {},
|
|
12326
|
+
...sort ? { sort } : {},
|
|
12327
|
+
...o.window?.length ? { window: o.window[0] } : {},
|
|
12328
|
+
...limit ? { limit } : {},
|
|
12329
|
+
...offset ? { offset } : {},
|
|
12330
|
+
...top !== undefined ? { top_components: top } : {}
|
|
12331
|
+
}), formatSessionPlaces);
|
|
12332
|
+
}
|
|
11294
12333
|
case "layers":
|
|
11295
12334
|
return out(await layers(t), formatLayers);
|
|
12335
|
+
case "trend": {
|
|
12336
|
+
const journey = o.journey ?? p[0];
|
|
12337
|
+
const words = p.slice(o.journey ? 0 : 1).join(" ");
|
|
12338
|
+
const windows = [...words ? [words] : [], ...o.window ?? []];
|
|
12339
|
+
if (!journey || !windows.length || !o.phone)
|
|
12340
|
+
throw new UsageError(["keepsnappy trend needs <journey> <window> --phone MODEL", FINDINGS_USAGE]);
|
|
12341
|
+
const sessions = o.sessions === undefined ? undefined : Number(o.sessions);
|
|
12342
|
+
if (sessions !== undefined && (!Number.isInteger(sessions) || sessions < 1 || sessions > 30))
|
|
12343
|
+
throw new UsageError(["--sessions must be a whole number from 1 to 30"]);
|
|
12344
|
+
return out(await placeTrend(t, { journey, phone: o.phone, window: windows, ref: o.ref, sessions }), formatPlaceTrend);
|
|
12345
|
+
}
|
|
11296
12346
|
default:
|
|
11297
12347
|
throw new UsageError([FINDINGS_USAGE]);
|
|
11298
12348
|
}
|
|
@@ -11308,17 +12358,19 @@ var USAGE = `keepsnappy <command>
|
|
|
11308
12358
|
[--device UDID] [--out DIR] [--timeout SEC] [--ri-capture observations.ndjson] [--mem-window NAME]
|
|
11309
12359
|
[--clock-offset-ms N] [--no-markers]
|
|
11310
12360
|
-- <test command...>
|
|
11311
|
-
farm open --api URL --device-model
|
|
12361
|
+
farm open --api URL [--device-model CODE] [--device-os V] [--device-id SERIAL|UDID] [--device-provider browserstack] [--platform ios|android] --journeys a,b [--repeat 5]
|
|
12362
|
+
(--device-model is read from the device with --platform and a local --device-provider: adb ro.product.model, devicectl machine id, simctl name; never for browserstack)
|
|
11312
12363
|
--tokens-js <file> --state <file> [--branch NAME --commit SHA --base-commit SHA] [--ttl-seconds 21600] [--key-env KEEPSNAPPY_KEY]
|
|
11313
12364
|
[--config keepsnappy.yml] [--flows <files|dirs|globs,...>] (journey titles: keepsnappy.yml flows.paths, else the flow's name: / top comment)
|
|
11314
12365
|
[--completion-marker] (the flow posts step run:complete at the end of each iteration; runs without it are invalid)
|
|
11315
12366
|
[--driver appium --journey-module FILE] [--driver-version V] [--app-build-id ID] [--harness-version V] [--journey-version V] [--flag-set LABEL] (setup fingerprint, plan 51)
|
|
11316
12367
|
[--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
|
|
11317
12368
|
farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
|
|
11318
|
-
farm run --app <eas:ID|file.ipa|bs://ID> --journeys a,b [--repeat 5] --device "iPhone 15 Pro-17" [--platform ios|android] [--driver maestro|appium]
|
|
12369
|
+
farm run --app <eas:ID|file.ipa|file.apk|bs://ID|custom_id:ID> --journeys a,b [--repeat 5] --device "iPhone 15 Pro-17" [--platform ios|android] [--driver maestro|appium]
|
|
11319
12370
|
[--tunnel-for localhost:PORT ...] [--env KEY=VALUE ...] (values may use {tunnel:PORT} {run_id} {out} {tokens_js} {run_set_id})
|
|
11320
12371
|
maestro: --suite-dir DIR --execute FLOW [--tokens-path flows/keepsnappy/tokens.js] [--maestro-version V]
|
|
11321
|
-
appium: --journey-module FILE | --driver-script FILE [--driver-arg ARG ...]
|
|
12372
|
+
appium: --journey-module FILE | --driver-script FILE [--driver-arg ARG ...] (iOS and Android)
|
|
12373
|
+
[--timeout-minutes 80] [--kill-after-seconds 120] (Appium: SIGTERM at timeout, then SIGKILL after grace)
|
|
11322
12374
|
[--branch NAME --commit SHA] [--wait [--fail-on-worse]] [--out DIR] [--dry-run] (open + tunnel + device + close; docs/plans/54a)
|
|
11323
12375
|
extract <out-dir> --ri-capture observations.ndjson [--mem-window NAME]
|
|
11324
12376
|
aa <out-dir> [--reps N] [--boot N]
|
|
@@ -11416,6 +12468,7 @@ async function main2(argv) {
|
|
|
11416
12468
|
"tokens-path": { type: "string" },
|
|
11417
12469
|
"maestro-version": { type: "string" },
|
|
11418
12470
|
"timeout-minutes": { type: "string" },
|
|
12471
|
+
"kill-after-seconds": { type: "string" },
|
|
11419
12472
|
"poll-seconds": { type: "string" },
|
|
11420
12473
|
"custom-id": { type: "string" },
|
|
11421
12474
|
"run-id": { type: "string" },
|
|
@@ -11449,9 +12502,9 @@ async function main2(argv) {
|
|
|
11449
12502
|
if (platform === "ios-device" && !o.device)
|
|
11450
12503
|
throw new Error("--device <udid> is required for ios-device");
|
|
11451
12504
|
const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
|
|
11452
|
-
const out =
|
|
12505
|
+
const out = resolve8(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
11453
12506
|
const udid = o.device ?? "booted";
|
|
11454
|
-
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid,
|
|
12507
|
+
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve8(out, ".scratch"));
|
|
11455
12508
|
const installer = platform === "ios-sim" ? new IosSimulatorInstaller(deps, udid) : new IosDeviceInstaller(deps, udid);
|
|
11456
12509
|
const bundleId = await bundleIdOf(deps, arms[0]?.target, o["bundle-id"]);
|
|
11457
12510
|
let markers;
|
|
@@ -11470,7 +12523,7 @@ async function main2(argv) {
|
|
|
11470
12523
|
outDir: out,
|
|
11471
12524
|
testCommand: positionals,
|
|
11472
12525
|
bundleId,
|
|
11473
|
-
...o["ri-capture"] ? { ri: new FileRiSource(deps,
|
|
12526
|
+
...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve8(o["ri-capture"])) } : {},
|
|
11474
12527
|
...o["mem-window"] ? { memWindow: o["mem-window"] } : {},
|
|
11475
12528
|
...o.timeout ? { testTimeoutMs: Number(o.timeout) * 1000 } : {}
|
|
11476
12529
|
});
|
|
@@ -11482,9 +12535,9 @@ async function main2(argv) {
|
|
|
11482
12535
|
const dir = positionals[0];
|
|
11483
12536
|
if (!dir)
|
|
11484
12537
|
throw new Error("extract needs <out-dir>");
|
|
11485
|
-
const ri = o["ri-capture"] ? new FileRiSource(deps,
|
|
11486
|
-
const rows = await extract(deps,
|
|
11487
|
-
console.log(`${rows.length} rows -> ${
|
|
12538
|
+
const ri = o["ri-capture"] ? new FileRiSource(deps, resolve8(o["ri-capture"])) : undefined;
|
|
12539
|
+
const rows = await extract(deps, resolve8(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
|
|
12540
|
+
console.log(`${rows.length} rows -> ${resolve8(dir, "metrics.csv")}`);
|
|
11488
12541
|
return 0;
|
|
11489
12542
|
}
|
|
11490
12543
|
if (cmd === "aa") {
|
|
@@ -11492,7 +12545,7 @@ async function main2(argv) {
|
|
|
11492
12545
|
if (!dir)
|
|
11493
12546
|
throw new Error("aa needs <out-dir>");
|
|
11494
12547
|
const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
|
|
11495
|
-
console.log((await aa(deps,
|
|
12548
|
+
console.log((await aa(deps, resolve8(dir), { config })).table);
|
|
11496
12549
|
return 0;
|
|
11497
12550
|
}
|
|
11498
12551
|
console.error(USAGE);
|