@keepsnappy/agent 0.1.0-alpha.18 → 0.1.0-alpha.20
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 +2 -2
- package/dist/appium-driver.js +356 -35
- package/dist/keepsnappy.js +747 -282
- 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 resolve9 } from "node:path";
|
|
5
5
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
6
6
|
|
|
7
7
|
// apps/agent/src/config.ts
|
|
@@ -1953,6 +1953,16 @@ var MEASURES = {
|
|
|
1953
1953
|
decimals: 1,
|
|
1954
1954
|
what: "The part of the slowest screen update that React spent outside every component's own render, in its own bookkeeping"
|
|
1955
1955
|
},
|
|
1956
|
+
jank_ms: {
|
|
1957
|
+
unit: "ms",
|
|
1958
|
+
worse: "higher",
|
|
1959
|
+
at: ["screen", "launch", "press", "drag", "step", "other", "background"],
|
|
1960
|
+
producer: "tracesift",
|
|
1961
|
+
label: "Jank time",
|
|
1962
|
+
short: "jank time",
|
|
1963
|
+
decimals: 1,
|
|
1964
|
+
what: "The time the window's screen updates spent past one 60 Hz frame, added up, read from the full React export"
|
|
1965
|
+
},
|
|
1956
1966
|
commits_over_2_frames: {
|
|
1957
1967
|
unit: "count",
|
|
1958
1968
|
worse: "higher",
|
|
@@ -3598,6 +3608,7 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
|
|
|
3598
3608
|
...upper ? { timerCarriedMs: carried, renderMsUpper: renderMs === undefined || carried === undefined ? undefined : renderMs + carried } : {}
|
|
3599
3609
|
};
|
|
3600
3610
|
}
|
|
3611
|
+
var SAME_COMMIT_MS = 0.01;
|
|
3601
3612
|
var UPDATERS_KEPT = 5;
|
|
3602
3613
|
var CPU_FUNCTIONS_KEPT = 10;
|
|
3603
3614
|
var UNTIMED_COMPONENTS_KEPT = 10;
|
|
@@ -3736,7 +3747,9 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3736
3747
|
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);
|
|
3737
3748
|
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && COMMIT_IDENTITY_KINDS.includes(kind) && selected?.slowestCommit;
|
|
3738
3749
|
const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
|
|
3739
|
-
|
|
3750
|
+
const streamed = identity ? options.commitTops?.get(identity.seq) : undefined;
|
|
3751
|
+
const tops = identity && streamed && Math.abs(streamed.ms - identity.ms) <= SAME_COMMIT_MS ? streamed : undefined;
|
|
3752
|
+
profiles.push({ ...profile, ...identity ? { slowestCommit: { ...identity, ...tops?.top ? { top: tops.top } : {}, ...tops?.updater ? { updater: tops.updater } : {} } } : {}, ...openedBy ? { openedBy } : {} });
|
|
3740
3753
|
}
|
|
3741
3754
|
return { metrics, profiles };
|
|
3742
3755
|
}
|
|
@@ -5012,7 +5025,7 @@ function samplerFrameBudgetMs(samples) {
|
|
|
5012
5025
|
}
|
|
5013
5026
|
return hz > 1 ? frameBudgetMs(Math.round(hz)) : undefined;
|
|
5014
5027
|
}
|
|
5015
|
-
function profileOptions(windows, version, processWindows = windows) {
|
|
5028
|
+
function profileOptions(windows, version, processWindows = windows, commitStreams = []) {
|
|
5016
5029
|
if (version < 26)
|
|
5017
5030
|
return {};
|
|
5018
5031
|
const processes = new Set(processWindows.map((w) => w.raw.react?.causes?.process).filter((p) => typeof p === "string"));
|
|
@@ -5026,7 +5039,9 @@ function profileOptions(windows, version, processWindows = windows) {
|
|
|
5026
5039
|
if (opener)
|
|
5027
5040
|
openedBy.set(screen, opener.name);
|
|
5028
5041
|
}
|
|
5029
|
-
|
|
5042
|
+
const [only] = processes.size === 1 ? processes : [];
|
|
5043
|
+
const commitTops = new Map(commitStreams.filter((s) => s.process === only).flatMap((s) => s.commits.map((c) => [c.seq, c])));
|
|
5044
|
+
return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, ...commitTops.size ? { commitTops } : {}, openedBy };
|
|
5030
5045
|
}
|
|
5031
5046
|
function deriveRun(input) {
|
|
5032
5047
|
const { run, markers = [], ri, clockOffsetMs, clockErrorMs, samples = [], memWindow } = input;
|
|
@@ -5049,7 +5064,7 @@ function deriveRun(input) {
|
|
|
5049
5064
|
return { rows, steps, run: final, profiles: [], derivationVersion: version };
|
|
5050
5065
|
const budget = input.frameBudgetMs ?? (version >= 6 ? samplerFrameBudgetMs(samplesInRun(samples, final.t0, final.t1)) : undefined);
|
|
5051
5066
|
const compareMs = version >= 20 ? LIMIT_FRAME_MS : budget ?? frameBudgetMs();
|
|
5052
|
-
const allComponents = profileOptions(windows, version, input.windows);
|
|
5067
|
+
const allComponents = profileOptions(windows, version, input.windows, input.commitStreams);
|
|
5053
5068
|
const prof = profileMetrics(windows, steps, {
|
|
5054
5069
|
...allComponents,
|
|
5055
5070
|
frameBudgetMs: version >= 11 ? compareMs : Number(compareMs.toFixed(1)),
|
|
@@ -5203,6 +5218,26 @@ var markerEnv = (url, runId) => ({
|
|
|
5203
5218
|
|
|
5204
5219
|
// packages/present/dist/src/capital.js
|
|
5205
5220
|
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5221
|
+
var lowerFirst = (s) => s ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
5222
|
+
|
|
5223
|
+
// packages/present/dist/src/names.js
|
|
5224
|
+
var textPart = (text) => ({ text });
|
|
5225
|
+
var componentPart = (text) => ({ text, component: true });
|
|
5226
|
+
var namePartsText = (parts) => parts.map((p) => p.text).join("");
|
|
5227
|
+
var plainParts = (text) => [textPart(text)];
|
|
5228
|
+
function nameParts(parts) {
|
|
5229
|
+
const out = [];
|
|
5230
|
+
for (const p of parts) {
|
|
5231
|
+
if (!p.text)
|
|
5232
|
+
continue;
|
|
5233
|
+
const last = out.at(-1);
|
|
5234
|
+
if (last && !last.component && !p.component)
|
|
5235
|
+
out[out.length - 1] = textPart(last.text + p.text);
|
|
5236
|
+
else
|
|
5237
|
+
out.push(p);
|
|
5238
|
+
}
|
|
5239
|
+
return out;
|
|
5240
|
+
}
|
|
5206
5241
|
|
|
5207
5242
|
// packages/present/dist/src/placeWords.js
|
|
5208
5243
|
var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
|
|
@@ -5239,6 +5274,7 @@ var STATE_WORDS = {
|
|
|
5239
5274
|
"no-change": "No change",
|
|
5240
5275
|
"cant-tell": "Can’t tell"
|
|
5241
5276
|
};
|
|
5277
|
+
var STATE_ORDER = ["worse", "warning", "cant-tell", "better", "no-change"];
|
|
5242
5278
|
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5243
5279
|
var isWorseState = (state) => state === "worse";
|
|
5244
5280
|
var NOT_BLOCKING = "not blocking";
|
|
@@ -5247,6 +5283,7 @@ var notBlockingBadge = (word, blocking) => word === STATE_WORDS.worse && blockin
|
|
|
5247
5283
|
var BLOCKED_WORD = "Blocked";
|
|
5248
5284
|
var NOT_COMPUTED_WORD = "Verdict not computed yet (computed on demand)";
|
|
5249
5285
|
var NO_BASELINE_WORD = "Waiting for main";
|
|
5286
|
+
var NOT_COMPARED_WORD = "Not compared with main";
|
|
5250
5287
|
var NOT_JUDGED_WORD = "Not judged";
|
|
5251
5288
|
|
|
5252
5289
|
// packages/present/dist/src/values.js
|
|
@@ -5310,6 +5347,13 @@ function fieldValueText(field, v) {
|
|
|
5310
5347
|
const def = fieldDef(field.split(KEY_SEPARATOR)[0]);
|
|
5311
5348
|
return format(def?.unit, def?.decimals ?? FALLBACK_DECIMALS, v);
|
|
5312
5349
|
}
|
|
5350
|
+
function timeMove(field, a, b) {
|
|
5351
|
+
if (a === null || b === null || !(a > 0))
|
|
5352
|
+
return null;
|
|
5353
|
+
const breakdown = breakdownMeasure(field);
|
|
5354
|
+
const unit = breakdown ? BREAKDOWN_MEASURES[breakdown].unit : fieldDef(field.split(KEY_SEPARATOR)[0])?.unit;
|
|
5355
|
+
return unit === "ms" ? Math.abs(b - a) / a : null;
|
|
5356
|
+
}
|
|
5313
5357
|
|
|
5314
5358
|
// packages/present/dist/src/rowWords.js
|
|
5315
5359
|
var GONE_TAILS = {
|
|
@@ -5364,16 +5408,19 @@ function leadBody(subject, measure, main, pr) {
|
|
|
5364
5408
|
var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_STATE[tier]]}: ${leadBody(subject, measure, main, pr)}`;
|
|
5365
5409
|
var FOR_INFORMATION = "For information";
|
|
5366
5410
|
var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
|
|
5367
|
-
function foldedText(n, rows, page) {
|
|
5368
|
-
return `${n} ${GONE_TAILS[page][rows === "components" ? "removed" : "notInRun"]}`;
|
|
5369
|
-
}
|
|
5370
5411
|
function placeShownName(p, o = {}) {
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5412
|
+
return namePartsText(placeShownNameParts(p, o));
|
|
5413
|
+
}
|
|
5414
|
+
function placeShownNameParts(p, o = {}) {
|
|
5415
|
+
const target = p.target ? componentNameParts(p.target.name, p.target.file) : plainParts(libraryViewName(p.kind, p.host ?? ""));
|
|
5416
|
+
const named = p.target && p.parent ? [...target, textPart(" in "), ...componentNameParts(p.parent.name, p.parent.file), textPart(p.k ? `, ${ordinal(p.k)}` : "")] : target;
|
|
5417
|
+
const screen = typeof o.onScreen === "string" ? plainParts(o.onScreen) : o.onScreen;
|
|
5418
|
+
return nameParts(screen?.length && namePartsText(screen) ? [...named, textPart(" on "), ...screen] : named);
|
|
5374
5419
|
}
|
|
5375
5420
|
var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
|
|
5376
|
-
|
|
5421
|
+
var componentShownName = (name, file) => componentFileName(name, file);
|
|
5422
|
+
var componentNameParts = (name, file) => [componentPart(componentFileName(name, file))];
|
|
5423
|
+
function componentFileName(name, file) {
|
|
5377
5424
|
if (name !== "default" || !file)
|
|
5378
5425
|
return name;
|
|
5379
5426
|
const path = file.replace(/\.[^.\/]+$/, "");
|
|
@@ -5427,21 +5474,49 @@ var placeOpenWord = (id) => {
|
|
|
5427
5474
|
const open = placeOpen(id);
|
|
5428
5475
|
return open ? OPEN_WORDS[open] : null;
|
|
5429
5476
|
};
|
|
5430
|
-
|
|
5477
|
+
var TWIN2 = /\+3s$/;
|
|
5478
|
+
function foldTwins(rows, placeOf, also = () => "") {
|
|
5479
|
+
const seen = new Set;
|
|
5480
|
+
const out = rows.filter((r) => {
|
|
5481
|
+
const key = `${placeOf(r).replace(TWIN2, "")}\x00${also(r)}`;
|
|
5482
|
+
if (seen.has(key))
|
|
5483
|
+
return false;
|
|
5484
|
+
seen.add(key);
|
|
5485
|
+
return true;
|
|
5486
|
+
});
|
|
5487
|
+
return { rows: out, folded: rows.length - out.length };
|
|
5488
|
+
}
|
|
5489
|
+
var foldedTwinsText = (n) => `${n} “${PLACE_KIND_WORDS.after3s}” ${n === 1 ? "twin" : "twins"} folded into ${n === 1 ? "its place" : "their places"}`;
|
|
5490
|
+
function placeIdShownNameParts(id, o = {}) {
|
|
5431
5491
|
const p = placeParts(id);
|
|
5432
5492
|
if (!p)
|
|
5433
5493
|
return null;
|
|
5434
5494
|
const component = (part) => ({ name: part.name, file: part.file ?? "" });
|
|
5435
|
-
const screen = p.screen.name === "(no screen)" ?
|
|
5495
|
+
const screen = p.screen.name === "(no screen)" ? [] : p.screen.name === "(outside app components)" ? [textPart(screenName(p.screen.file ?? ""))] : componentNameParts(p.screen.name, p.screen.file ?? "");
|
|
5436
5496
|
if (p.kind === "screen" || p.kind === "launch")
|
|
5437
|
-
return screen
|
|
5438
|
-
return
|
|
5497
|
+
return nameParts([...screen, textPart(p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "")]);
|
|
5498
|
+
return placeShownNameParts({ 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 });
|
|
5439
5499
|
}
|
|
5440
5500
|
var isLibraryPlace = (id) => placeParts(id)?.outsideHost != null;
|
|
5441
|
-
function
|
|
5442
|
-
const
|
|
5501
|
+
function dropTail(parts, n) {
|
|
5502
|
+
const out = [];
|
|
5503
|
+
for (const p of [...parts].reverse()) {
|
|
5504
|
+
if (n >= p.text.length) {
|
|
5505
|
+
n -= p.text.length;
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
out.unshift({ ...p, text: p.text.slice(0, p.text.length - n) });
|
|
5509
|
+
n = 0;
|
|
5510
|
+
}
|
|
5511
|
+
return out;
|
|
5512
|
+
}
|
|
5513
|
+
function stemNameParts(stem, kind) {
|
|
5514
|
+
const shown = placeIdShownNameParts(stem);
|
|
5443
5515
|
if (shown !== null)
|
|
5444
|
-
return isLibraryPlace(stem) ? shown : `${KIND_WORD[kind]}:
|
|
5516
|
+
return nameParts(isLibraryPlace(stem) ? shown : [textPart(`${KIND_WORD[kind]}: `), ...shown]);
|
|
5517
|
+
return plainParts(stemText(stem, kind));
|
|
5518
|
+
}
|
|
5519
|
+
function stemText(stem, kind) {
|
|
5445
5520
|
if (kind === "journey")
|
|
5446
5521
|
return PLACE_KIND_WORDS.journey;
|
|
5447
5522
|
if (kind === "other")
|
|
@@ -5457,12 +5532,13 @@ function stemName(stem, kind) {
|
|
|
5457
5532
|
const visit = visitMark ? Number(visitMark[2]) : 1;
|
|
5458
5533
|
return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
|
|
5459
5534
|
}
|
|
5460
|
-
|
|
5535
|
+
var windowName = (id) => namePartsText(windowNameParts(id));
|
|
5536
|
+
function windowNameParts(id) {
|
|
5461
5537
|
if (id === "flow")
|
|
5462
|
-
return PLACE_KIND_WORDS.journey;
|
|
5538
|
+
return plainParts(PLACE_KIND_WORDS.journey);
|
|
5463
5539
|
const ref = placeRef(id);
|
|
5464
|
-
const name =
|
|
5465
|
-
return ref.segment === "after3s" ?
|
|
5540
|
+
const name = stemNameParts(ref.stem, ref.kind);
|
|
5541
|
+
return ref.segment === "after3s" ? nameParts([...name, textPart(` · ${PLACE_KIND_WORDS.after3s}`)]) : name;
|
|
5466
5542
|
}
|
|
5467
5543
|
function placeKindWord(id) {
|
|
5468
5544
|
if (id === "flow")
|
|
@@ -5470,31 +5546,33 @@ function placeKindWord(id) {
|
|
|
5470
5546
|
const { kind } = placeRef(id);
|
|
5471
5547
|
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5472
5548
|
}
|
|
5473
|
-
|
|
5549
|
+
var windowQualified = (id) => namePartsText(windowQualifiedParts(id));
|
|
5550
|
+
function windowQualifiedParts(id) {
|
|
5474
5551
|
const word = placeOpenWord(id);
|
|
5475
|
-
const label =
|
|
5552
|
+
const label = placeLabelParts(id);
|
|
5476
5553
|
if (!word)
|
|
5477
5554
|
return label;
|
|
5478
5555
|
const after = placeRef(id).segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5479
|
-
return
|
|
5556
|
+
return nameParts([...after ? dropTail(label, after.length) : label, textPart(` (${word})${after}`)]);
|
|
5480
5557
|
}
|
|
5481
5558
|
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5482
5559
|
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5483
|
-
|
|
5560
|
+
var placeLabel = (id, screen) => namePartsText(placeLabelParts(id, screen));
|
|
5561
|
+
function placeLabelParts(id, screen) {
|
|
5484
5562
|
if (id === "flow")
|
|
5485
|
-
return PLACE_KIND_WORDS.journey;
|
|
5563
|
+
return plainParts(PLACE_KIND_WORDS.journey);
|
|
5486
5564
|
const ref = placeRef(id);
|
|
5487
5565
|
const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5488
|
-
const shown =
|
|
5566
|
+
const shown = placeIdShownNameParts(ref.stem);
|
|
5489
5567
|
if (shown !== null)
|
|
5490
|
-
return isLibraryPlace(ref.stem) ?
|
|
5568
|
+
return nameParts([...isLibraryPlace(ref.stem) ? [] : [textPart(`${KIND_WORD[ref.kind]} · `)], ...shown, textPart(after)]);
|
|
5491
5569
|
if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
|
|
5492
5570
|
const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
|
|
5493
|
-
return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}
|
|
5571
|
+
return plainParts(`${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`);
|
|
5494
5572
|
}
|
|
5495
5573
|
const name = windowName(ref.stem);
|
|
5496
5574
|
const colon = name.indexOf(": ");
|
|
5497
|
-
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}
|
|
5575
|
+
return plainParts(`${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`);
|
|
5498
5576
|
}
|
|
5499
5577
|
// packages/present/dist/src/placeNames.js
|
|
5500
5578
|
function shortestPaths(files) {
|
|
@@ -5513,6 +5591,28 @@ function shortestPaths(files) {
|
|
|
5513
5591
|
}
|
|
5514
5592
|
return out;
|
|
5515
5593
|
}
|
|
5594
|
+
// packages/present/dist/src/words.js
|
|
5595
|
+
var REASON_WORDS = {
|
|
5596
|
+
"no-baseline": "there are no main runs on this phone and journey yet",
|
|
5597
|
+
"too-few-runs": "there are too few valid runs on a side",
|
|
5598
|
+
"judged-measures-missing": "the judged measures are missing on a side",
|
|
5599
|
+
"measured-differently": "the sides were measured differently",
|
|
5600
|
+
"runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
|
|
5601
|
+
"facts-version-mismatch": "the sides were derived with different versions",
|
|
5602
|
+
"not-judged-on-simulators": "timing is not judged on simulators",
|
|
5603
|
+
"platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
|
|
5604
|
+
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5605
|
+
};
|
|
5606
|
+
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5607
|
+
var AT_MOST = "≤ ";
|
|
5608
|
+
var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
|
|
5609
|
+
var AT_LEAST = "≥ ";
|
|
5610
|
+
var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
|
|
5611
|
+
|
|
5612
|
+
// packages/present/dist/src/shown.js
|
|
5613
|
+
var feltGainText = (ms) => ms && ms > 0 ? `felt gain −${fieldValueText("jank_ms", ms)}` : null;
|
|
5614
|
+
var feltCostText = (ms) => ms && ms > 0 ? `felt cost +${fieldValueText("jank_ms", ms)}` : null;
|
|
5615
|
+
|
|
5516
5616
|
// packages/present/dist/src/stability.js
|
|
5517
5617
|
var UNSTABLE_WORDS = {
|
|
5518
5618
|
short: "main varies too much here",
|
|
@@ -5619,8 +5719,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5619
5719
|
for (const [screen, rows] of screens) {
|
|
5620
5720
|
const line = notInRunRowsText({ screens: rows.filter((p) => p.kind === "screen").length, places: rows.filter((p) => p.kind !== "screen").length }, c.page);
|
|
5621
5721
|
out.push(`${screen ? `${screen}: ` : ""}${line.lead}${line.rest}`);
|
|
5622
|
-
if (rows.length > 3)
|
|
5623
|
-
out.push(` ${foldedText(rows.length, "places", c.page)}`);
|
|
5624
5722
|
if (!short || rows.length <= 3)
|
|
5625
5723
|
out.push(...rows.map((p) => ` ${windowQualified(p.place)}`));
|
|
5626
5724
|
}
|
|
@@ -5630,8 +5728,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5630
5728
|
for (const [place, rows] of removed) {
|
|
5631
5729
|
const line = removedRowsText(rows.length, c.page);
|
|
5632
5730
|
out.push(`${windowQualified(place)}: ${line.lead}${line.rest}`);
|
|
5633
|
-
if (rows.length > 3)
|
|
5634
|
-
out.push(` ${foldedText(rows.length, "components", c.page)}`);
|
|
5635
5731
|
if (!short || rows.length <= 3)
|
|
5636
5732
|
out.push(...rows.map((r) => ` ${r.ref.kind === "breakdown" ? componentReadName({ key: r.ref.key, name: r.ref.name }) : measureWords(r.ref.measure)}`));
|
|
5637
5733
|
}
|
|
@@ -5663,7 +5759,7 @@ var READ_USAGE = {
|
|
|
5663
5759
|
page: "--page must be a whole number from 1; use it only with places or compare.",
|
|
5664
5760
|
all: "--all is for places or compare; do not combine it with --page, --offset or --short.",
|
|
5665
5761
|
detailFlag: " --detail (places: include recorded run detail; also included by --verbose or component paging)",
|
|
5666
|
-
detail: "Include recorded per-run component and window detail.",
|
|
5762
|
+
detail: "Include recorded per-run component and window detail: an opt-in diagnostic that reads the runs' recorded profiles (slower); ranking places never needs it.",
|
|
5667
5763
|
compareTop: "--top-components must be a whole number from 1 to 200 for compare.",
|
|
5668
5764
|
top: "--top-components must be a whole number from 0 to 20, or from 1 to 200 with --page or --all.",
|
|
5669
5765
|
paging: "The API did not advance the component page; stopped without claiming the list is complete.",
|
|
@@ -5673,23 +5769,119 @@ var READ_USAGE = {
|
|
|
5673
5769
|
var componentListsText = (totals, shown) => `Stored component rows: ${shown.rows} of ${totals.rows} compared; ${shown.added} of ${totals.added} ${READ_WORDS.onlyB.toLowerCase()}; ${shown.removed} of ${totals.removed} ${READ_WORDS.onlyA.toLowerCase()}.`;
|
|
5674
5770
|
var notMeasuredText = (side) => `${READ_WORDS.notMeasured} in run ${side.toUpperCase()}`;
|
|
5675
5771
|
|
|
5676
|
-
// packages/present/dist/src/
|
|
5677
|
-
var
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5772
|
+
// packages/present/dist/src/problems.js
|
|
5773
|
+
var FAR_OVER_BUDGET_WORD = "Far over budget";
|
|
5774
|
+
var heaviestWords = (name, value) => [textPart(`${HEAVIEST_WORD}: `), componentPart(name), ...value ? [textPart(` ${value}`)] : []];
|
|
5775
|
+
var HEAVIEST_WORD = "heaviest";
|
|
5776
|
+
var PROBLEM_TAG_WORDS = { new: NEW_IN_PR_CHIP, main: "Also on main", none: NOT_COMPARED_WORD };
|
|
5777
|
+
var PROBLEM_TAG_LINE_WORDS = { new: "new in this PR", main: "also on main", none: "not compared with main" };
|
|
5778
|
+
var problems = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "problem" : "problems"}`;
|
|
5779
|
+
var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5780
|
+
var problemsAtPlaces = (n, at) => `${problems(n)} at ${places(at)}`;
|
|
5781
|
+
function problemCountsText(c, own) {
|
|
5782
|
+
const tags = c.tags;
|
|
5783
|
+
const split = tags ? `: ${[`${tags.new} ${PROBLEM_TAG_LINE_WORDS.new}`, ...["main", "none"].filter((t) => tags[t]).map((t) => `${tags[t]} ${PROBLEM_TAG_LINE_WORDS[t]}`)].join(", ")}` : "";
|
|
5784
|
+
return `${problemsAtPlaces(c.count, c.places)} in ${own} own runs${split}${c.far_count ? `; ${c.far_count} of them ${FAR_OVER_BUDGET_WORD.toLowerCase()}` : ""}`;
|
|
5785
|
+
}
|
|
5786
|
+
var newProblems = (n) => `${n.toLocaleString("en-US")} new ${n === 1 ? "problem" : "problems"}`;
|
|
5787
|
+
var PROBLEMS_BLOCK_WORDS = {
|
|
5788
|
+
branch: {
|
|
5789
|
+
heading: "New problems in this branch",
|
|
5790
|
+
sub: "too slow on their own, whatever main does; they don’t block the merge",
|
|
5791
|
+
seeAll: (n) => `See all ${newProblems(n)}`,
|
|
5792
|
+
alsoLine: (n, at) => `${PROBLEM_TAG_WORDS.main}: ${problemsAtPlaces(n, at)}`,
|
|
5793
|
+
notComparedLine: (n, at) => `${PROBLEM_TAG_WORDS.none}: ${problemsAtPlaces(n, at)}`,
|
|
5794
|
+
choices: { new: PROBLEM_TAG_WORDS.new, also: PROBLEM_TAG_WORDS.main, not_compared: PROBLEM_TAG_WORDS.none },
|
|
5795
|
+
panelTitle: "Problems in this branch",
|
|
5796
|
+
panelNote: "Each one is too slow in this branch’s own runs, whatever main does. Problems never block.",
|
|
5797
|
+
filter: "Which problems"
|
|
5798
|
+
},
|
|
5799
|
+
main: {
|
|
5800
|
+
heading: "New problems in this session",
|
|
5801
|
+
sub: "too slow on their own, whatever earlier sessions did",
|
|
5802
|
+
seeAll: (n) => `See all ${newProblems(n)}`,
|
|
5803
|
+
alsoLine: (n, at) => `Also in earlier sessions: ${problemsAtPlaces(n, at)}`,
|
|
5804
|
+
notComparedLine: (n, at) => `Not compared with earlier sessions: ${problemsAtPlaces(n, at)}`,
|
|
5805
|
+
choices: { new: "New in this session", also: "Also in earlier sessions", not_compared: "Not compared with earlier sessions" },
|
|
5806
|
+
panelTitle: "Problems in this session",
|
|
5807
|
+
panelNote: "Each one is too slow in this session’s own runs, whatever earlier sessions did. Problems never block.",
|
|
5808
|
+
filter: "Which problems"
|
|
5809
|
+
}
|
|
5687
5810
|
};
|
|
5688
|
-
|
|
5689
|
-
var
|
|
5690
|
-
|
|
5691
|
-
var
|
|
5692
|
-
var
|
|
5811
|
+
// packages/present/dist/src/place-tier.js
|
|
5812
|
+
var slowerCountsText = (n, sep = " · ") => [n.worse ? `${n.worse} ${STATE_WORDS.worse.toLowerCase()}` : "", n.slight ? `${n.slight} ${STATE_WORDS.warning.toLowerCase()}` : ""].filter(Boolean).join(sep);
|
|
5813
|
+
// packages/present/dist/src/verdict-section.js
|
|
5814
|
+
var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5815
|
+
var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
|
|
5816
|
+
var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
|
|
5817
|
+
var VERDICT_SECTION_WORDS = {
|
|
5818
|
+
slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
|
|
5819
|
+
blocksMerge: "blocks the merge",
|
|
5820
|
+
faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
|
|
5821
|
+
mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
|
|
5822
|
+
unpublishedSdk: "ran an unpublished SDK build",
|
|
5823
|
+
noneMoved: (total, judged = total) => judged < total ? `No place moved out of its normal range (${judged.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} judged)` : `No place moved out of its normal range (${places2(total)})`,
|
|
5824
|
+
noneMovedMeasured: (measured, same) => same ? `Nothing moved at ${of(same, same + measured)}; ${measured} measured differently` : `Nothing moved; ${measured} ${measured === 1 ? "place was" : "places were"} measured differently`,
|
|
5825
|
+
tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
|
|
5826
|
+
tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
|
|
5827
|
+
tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
|
|
5828
|
+
noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
|
|
5829
|
+
noisy: "Not judged: main’s own runs vary too much at every place here",
|
|
5830
|
+
versionMismatch: "Not compared: the sides were computed with different analysis versions",
|
|
5831
|
+
unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
|
|
5832
|
+
notCompared: (reason) => `Not compared: ${reason}`,
|
|
5833
|
+
notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
|
|
5834
|
+
collecting: (collected, baseline, after) => after ? `Main is collecting a new baseline after ${after}: ${collected} of ${baseline} sessions` : `Main is collecting its baseline: ${collected} of ${baseline} sessions`,
|
|
5835
|
+
doesNotBlock: "doesn’t block",
|
|
5836
|
+
fixes: {
|
|
5837
|
+
branch: {
|
|
5838
|
+
"no-baseline": "Add this journey on this phone to main’s nightly runs.",
|
|
5839
|
+
"too-few-runs": "Run this branch again on this phone.",
|
|
5840
|
+
"judged-measures-missing": "Record the judged measures, then run again.",
|
|
5841
|
+
"runs-unknown": "Run this branch again; a full set of runs decides it.",
|
|
5842
|
+
"facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed.",
|
|
5843
|
+
"platforms-differ": "Add this phone’s platform to main’s nightly runs."
|
|
5844
|
+
},
|
|
5845
|
+
main: {
|
|
5846
|
+
"no-baseline": "Nothing to do: main’s next 3 nightly sessions make a result possible.",
|
|
5847
|
+
"too-few-runs": "Main’s next nightly session decides it.",
|
|
5848
|
+
"runs-unknown": "Main’s next nightly session decides it.",
|
|
5849
|
+
"judged-measures-missing": "Record the judged measures on main; its next nightly session is compared.",
|
|
5850
|
+
"facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed."
|
|
5851
|
+
}
|
|
5852
|
+
},
|
|
5853
|
+
waitForMain: { branch: "Nothing to do on this branch; main’s next nightly session decides it.", main: "Main’s next nightly session decides it." },
|
|
5854
|
+
sides: {
|
|
5855
|
+
branch: { left: "Main", right: "This branch", rightShort: "Branch" },
|
|
5856
|
+
main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
|
|
5857
|
+
runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
|
|
5858
|
+
},
|
|
5859
|
+
branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
|
|
5860
|
+
inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
|
|
5861
|
+
whatChangedLabel: "What changed",
|
|
5862
|
+
moreSlower: (n) => `+${n.toLocaleString("en-US")} more slower`,
|
|
5863
|
+
slowerPanel: (n) => `${places2(n)} slower`,
|
|
5864
|
+
forInformation: "for information",
|
|
5865
|
+
allFilter: "All",
|
|
5866
|
+
filter: {
|
|
5867
|
+
label: "Show places",
|
|
5868
|
+
choice: (tierWord) => upperFirst(tierWord),
|
|
5869
|
+
showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
|
|
5870
|
+
},
|
|
5871
|
+
countsLabel: "Places by result",
|
|
5872
|
+
links: {
|
|
5873
|
+
openChart: "Open chart",
|
|
5874
|
+
seeAll: (n) => `See all ${places2(n)}`,
|
|
5875
|
+
whichPlaces: "Which places",
|
|
5876
|
+
whichChanges: "Which changes",
|
|
5877
|
+
howToFix: "How to fix",
|
|
5878
|
+
whatDiffers: "What differs",
|
|
5879
|
+
whatChanged: "What changed",
|
|
5880
|
+
openSetup: "Open setup",
|
|
5881
|
+
openScreen: "Open screen"
|
|
5882
|
+
}
|
|
5883
|
+
};
|
|
5884
|
+
|
|
5693
5885
|
// packages/present/dist/src/main-history.js
|
|
5694
5886
|
var MAIN_TIER_WORDS = {
|
|
5695
5887
|
same: "In norm",
|
|
@@ -5706,11 +5898,13 @@ var MAIN_TIER_RESULT_WORDS = {
|
|
|
5706
5898
|
worse: STATE_WORDS.worse
|
|
5707
5899
|
};
|
|
5708
5900
|
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5901
|
+
var earlierWords = (sessions, since, config) => `main has ${sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${since ? ` on ${since}` : ""}`;
|
|
5709
5902
|
function mainCantWords(reason, config) {
|
|
5710
5903
|
if (!reason)
|
|
5711
5904
|
return "";
|
|
5712
|
-
return reason.kind === "noisy_main" ? "main’s runs vary too much there" :
|
|
5905
|
+
return reason.kind === "noisy_main" ? "main’s runs vary too much there" : earlierWords(String(reason.earlier_sessions), reason.since_label, config);
|
|
5713
5906
|
}
|
|
5907
|
+
var mainCantRangeWords = (r, config) => earlierWords(`${r.min}–${r.max}`, r.since_label, config);
|
|
5714
5908
|
var waitingSessionsWords = (sessions) => `${sessions} older main ${sessions === 1 ? "session waits" : "sessions wait"} for a reprocess`;
|
|
5715
5909
|
function mainPlaceReasonWords(reason, config) {
|
|
5716
5910
|
switch (reason.kind) {
|
|
@@ -5740,9 +5934,11 @@ var SHORT_ASPECT_WORDS = {
|
|
|
5740
5934
|
driverVersion: "Driver",
|
|
5741
5935
|
journeyVersion: "Journey"
|
|
5742
5936
|
};
|
|
5937
|
+
var cantAt = (places) => `${STATE_WORDS["cant-tell"]} at ${places} place${places === 1 ? "" : "s"}`;
|
|
5743
5938
|
function mainSessionCantWords(places, reason, config) {
|
|
5744
|
-
return `${
|
|
5939
|
+
return `${cantAt(places)}: ${mainCantWords(reason, config)}`;
|
|
5745
5940
|
}
|
|
5941
|
+
var mainSessionCantRangeWords = (places, r, config) => `${cantAt(places)}: ${mainCantRangeWords(r, config)}`;
|
|
5746
5942
|
var KIND_PLURAL = {
|
|
5747
5943
|
screen: "screens",
|
|
5748
5944
|
press: "taps",
|
|
@@ -5767,56 +5963,12 @@ function mainCutWords(v) {
|
|
|
5767
5963
|
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5768
5964
|
}
|
|
5769
5965
|
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5770
|
-
|
|
5771
|
-
var
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
blocksMerge: "blocks the merge",
|
|
5777
|
-
faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
|
|
5778
|
-
mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
|
|
5779
|
-
unpublishedSdk: "ran an unpublished SDK build",
|
|
5780
|
-
noneMoved: (total, judged = total) => judged < total ? `No place moved out of its normal range (${judged.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} judged)` : `No place moved out of its normal range (${places(total)})`,
|
|
5781
|
-
noneMovedMeasured: (measured, same) => same ? `Nothing moved at ${of(same, same + measured)}; ${measured} measured differently` : `Nothing moved; ${measured} ${measured === 1 ? "place was" : "places were"} measured differently`,
|
|
5782
|
-
tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
|
|
5783
|
-
tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
|
|
5784
|
-
tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
|
|
5785
|
-
noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
|
|
5786
|
-
noisy: "Not judged: main’s own runs vary too much at every place here",
|
|
5787
|
-
versionMismatch: "Not compared: the sides were computed with different analysis versions",
|
|
5788
|
-
unconfirmed: (n) => `Can’t confirm ${places(n)}: a run has no value there`,
|
|
5789
|
-
notCompared: (reason) => `Not compared: ${reason}`,
|
|
5790
|
-
notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
|
|
5791
|
-
collecting: (collected, baseline, after) => after ? `Main is collecting a new baseline after ${after}: ${collected} of ${baseline} sessions` : `Main is collecting its baseline: ${collected} of ${baseline} sessions`,
|
|
5792
|
-
doesNotBlock: "doesn’t block",
|
|
5793
|
-
sides: {
|
|
5794
|
-
branch: { left: "Main", right: "This branch", rightShort: "Branch" },
|
|
5795
|
-
main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
|
|
5796
|
-
runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
|
|
5797
|
-
},
|
|
5798
|
-
branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
|
|
5799
|
-
inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
|
|
5800
|
-
whatChangedLabel: "What changed",
|
|
5801
|
-
forInformation: "for information",
|
|
5802
|
-
allFilter: "All",
|
|
5803
|
-
filter: {
|
|
5804
|
-
label: "Show places",
|
|
5805
|
-
choice: (tierWord) => upperFirst(tierWord),
|
|
5806
|
-
showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
|
|
5807
|
-
},
|
|
5808
|
-
countsLabel: "Places by result",
|
|
5809
|
-
links: {
|
|
5810
|
-
openChart: "Open chart",
|
|
5811
|
-
seeAll: (n) => `See all ${places(n)}`,
|
|
5812
|
-
whichPlaces: "Which places",
|
|
5813
|
-
whichChanges: "Which changes",
|
|
5814
|
-
howToFix: "How to fix",
|
|
5815
|
-
whatDiffers: "What differs",
|
|
5816
|
-
whatChanged: "What changed",
|
|
5817
|
-
openSetup: "Open setup",
|
|
5818
|
-
openScreen: "Open screen"
|
|
5819
|
-
}
|
|
5966
|
+
var MEMORY_HELD_WORDS = { head: "Memory", rest: "still held after the journey", text: "Memory still held after the journey" };
|
|
5967
|
+
var MAIN_CHART_TITLES = {
|
|
5968
|
+
renders: "Places vs their normal range",
|
|
5969
|
+
rendersTip: (baseline) => `Share of the journey’s places per session, by result against main’s ${baseline} sessions before it. Dashed: not judged yet, while main collects ${baseline} sessions on the setup (after main’s start or a change).`,
|
|
5970
|
+
memory: `${MEMORY_HELD_WORDS.text} · MB`,
|
|
5971
|
+
memoryTip: "Memory kept by the journey: what it still holds after the clean-up at its end. Line: the median of a session; band: the spread inside it."
|
|
5820
5972
|
};
|
|
5821
5973
|
// packages/present/dist/src/summary-message.js
|
|
5822
5974
|
var SUMMARY_WORDS = {
|
|
@@ -5832,39 +5984,62 @@ var SUMMARY_WORDS = {
|
|
|
5832
5984
|
fewRuns: "few runs",
|
|
5833
5985
|
measuredDifferently: "measured differently",
|
|
5834
5986
|
namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
|
|
5987
|
+
morePlaces: (n) => `+${n} more ${n === 1 ? "place" : "places"} changed`,
|
|
5988
|
+
slowerPlaces: (n, split) => `${n} ${n === 1 ? "place" : "places"} slower${split && slowerCountsText(split) ? `: ${slowerCountsText(split, ", ")}` : ""}`,
|
|
5989
|
+
noPlaceSlower: "no place slower",
|
|
5990
|
+
toCheck: (n) => `${n} ${n === 1 ? "component" : "components"} to check`,
|
|
5991
|
+
compared: {
|
|
5992
|
+
branch: (sessions, days) => `vs main’s last ${sessions}${days}`,
|
|
5993
|
+
main: (sessions, days) => `newest session vs main’s ${sessions} before${days}`,
|
|
5994
|
+
mainSlower: (sessions, days) => `newest session slower than main’s ${sessions} before${days}`
|
|
5995
|
+
},
|
|
5996
|
+
sessions: (n) => `${n} ${n === 1 ? "session" : "sessions"}`,
|
|
5997
|
+
days: (days) => days ? ` (${days})` : "",
|
|
5998
|
+
runs: (runs, each) => each ? `${each} ${each === 1 ? "run" : "runs"} each` : `${runs} ${runs === 1 ? "run" : "runs"}`,
|
|
5999
|
+
unblock: (why, fix) => `${why} → ${fix}`,
|
|
5835
6000
|
link: {
|
|
5836
|
-
|
|
5837
|
-
|
|
6001
|
+
comparison: "See the comparison",
|
|
6002
|
+
session: "See the session",
|
|
5838
6003
|
branch: (branch) => `Open ${branch}`,
|
|
5839
6004
|
runSet: "Open the run set"
|
|
5840
6005
|
}
|
|
5841
6006
|
};
|
|
5842
|
-
var ORDER = [
|
|
6007
|
+
var ORDER = [...STATE_ORDER.slice(0, 3), "not-computed", ...STATE_ORDER.slice(3)];
|
|
5843
6008
|
var rank = (l) => ORDER.indexOf(l.state);
|
|
5844
6009
|
var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
|
|
5845
6010
|
var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
|
|
5846
6011
|
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
5847
|
-
var NAME = { name: 32,
|
|
5848
|
-
var
|
|
6012
|
+
var NAME = { name: 32, button: 30 };
|
|
6013
|
+
var TOP2 = 3;
|
|
6014
|
+
var MAX_LINES = 5;
|
|
5849
6015
|
var upper = (s) => s ? `${s[0].toUpperCase()}${s.slice(1)}` : s;
|
|
5850
6016
|
var fromTo = (a, b) => {
|
|
5851
6017
|
const unit = (v) => / [^\d\s]+$/.exec(v)?.[0];
|
|
5852
6018
|
return unit(a) && unit(a) === unit(b) ? `${a.slice(0, -unit(a).length)} → ${b}` : `${a} → ${b}`;
|
|
5853
6019
|
};
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
const open = (cut.match(/“/g) ?? []).length > (cut.match(/”/g) ?? []).length;
|
|
5862
|
-
return `${cut}…${open ? "”" : ""}`;
|
|
5863
|
-
}
|
|
5864
|
-
var changeText2 = (c, room) => {
|
|
5865
|
-
const what = `${c.measure} ${c.main !== null && c.now !== null ? fromTo(c.main, c.now) : c.change}`;
|
|
5866
|
-
return c.place ? `${clipPlace(c.place, Math.max(NAME.place, room - what.length - 3))} · ${what}` : upper(what);
|
|
6020
|
+
var days = (labels) => {
|
|
6021
|
+
const a = labels[0], b = labels.at(-1);
|
|
6022
|
+
if (!a || !b || a === b)
|
|
6023
|
+
return a ?? "";
|
|
6024
|
+
const [ma] = a.split(" ");
|
|
6025
|
+
const [mb, db] = b.split(" ");
|
|
6026
|
+
return ma === mb ? `${a}–${db}` : `${a}–${b}`;
|
|
5867
6027
|
};
|
|
6028
|
+
var changeLine = (c, tier) => {
|
|
6029
|
+
const values = c.main !== null && c.now !== null ? `${fromTo(c.main, c.now)}${c.change ? ` (${c.change})` : ""}` : c.change;
|
|
6030
|
+
return [c.place ?? upper(c.measure), ...c.place ? [c.measure] : [], values, WORD[c.tier ?? tier]].filter(Boolean).join(" · ");
|
|
6031
|
+
};
|
|
6032
|
+
function tierSplit(l) {
|
|
6033
|
+
const worse = new Set, all = new Set;
|
|
6034
|
+
for (const c of l.changes) {
|
|
6035
|
+
const key = c.key ?? c.place ?? c.measure;
|
|
6036
|
+
all.add(key);
|
|
6037
|
+
if ((c.tier ?? l.state) === "worse")
|
|
6038
|
+
worse.add(key);
|
|
6039
|
+
}
|
|
6040
|
+
const split = { worse: worse.size, slight: all.size - worse.size };
|
|
6041
|
+
return l.state === "worse" && !split.slight || l.state === "warning" && !split.worse ? undefined : split;
|
|
6042
|
+
}
|
|
5868
6043
|
var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
|
|
5869
6044
|
var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
|
|
5870
6045
|
function why(l) {
|
|
@@ -5880,19 +6055,30 @@ function why(l) {
|
|
|
5880
6055
|
return upper(SUMMARY_WORDS.placesOnPage);
|
|
5881
6056
|
return upper(wordReason(l.word) ?? (l.word === NO_BASELINE_WORD ? l.word : REASON_WORDS["judged-measures-missing"]));
|
|
5882
6057
|
}
|
|
6058
|
+
function unblock(l, pr) {
|
|
6059
|
+
const page = pr ? "branch" : "main";
|
|
6060
|
+
const reason = l.namesChanged ? "not-enough-main" : l.reason;
|
|
6061
|
+
if (!reason)
|
|
6062
|
+
return null;
|
|
6063
|
+
const fix = reason === "not-enough-main" ? VERDICT_SECTION_WORDS.waitForMain[page] : VERDICT_SECTION_WORDS.fixes[page][reason];
|
|
6064
|
+
return fix ? lowerFirst(fix.replace(/\.$/, "")) : null;
|
|
6065
|
+
}
|
|
5883
6066
|
var noneMoved = VERDICT_SECTION_WORDS.noneMoved(1).replace(/ \(.*\)$/, "");
|
|
5884
6067
|
function conditions(l, pr) {
|
|
5885
6068
|
const moved = l.state === "worse" || l.state === "warning";
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
6069
|
+
return [
|
|
6070
|
+
pr && l.state === "worse" ? l.blocking ? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "",
|
|
6071
|
+
moved && l.measuredDifferently ? SUMMARY_WORDS.measuredDifferently : "",
|
|
6072
|
+
(moved || l.state === "better") && l.fewRuns ? SUMMARY_WORDS.fewRuns : ""
|
|
6073
|
+
].filter(Boolean);
|
|
6074
|
+
}
|
|
6075
|
+
function compared(l, pr, slower) {
|
|
6076
|
+
const judged = l.state === "worse" || l.state === "warning" || l.state === "better" || l.state === "no-change";
|
|
6077
|
+
if (!judged || !l.main?.sessions)
|
|
6078
|
+
return null;
|
|
6079
|
+
const args = [SUMMARY_WORDS.sessions(l.main.sessions), SUMMARY_WORDS.days(days(l.main.labels))];
|
|
6080
|
+
const what = pr ? SUMMARY_WORDS.compared.branch(...args) : slower ? SUMMARY_WORDS.compared.mainSlower(...args) : SUMMARY_WORDS.compared.main(...args);
|
|
6081
|
+
return `${what} · ${SUMMARY_WORDS.runs(l.main.runs, l.main.each)}`;
|
|
5896
6082
|
}
|
|
5897
6083
|
function summaryMessage(input) {
|
|
5898
6084
|
const l = shownLane(input.lanes);
|
|
@@ -5901,37 +6087,84 @@ function summaryMessage(input) {
|
|
|
5901
6087
|
state: "not-computed",
|
|
5902
6088
|
newestSlower: false,
|
|
5903
6089
|
lead: `${SUMMARY_WORDS.notComputed} · ${clip(input.branch, NAME.name)}`,
|
|
5904
|
-
|
|
6090
|
+
...withLines([upper(SUMMARY_WORDS.notComputedWhy)]),
|
|
5905
6091
|
link: { label: SUMMARY_WORDS.link.branch(clip(input.branch, NAME.button - 5)), url: input.url }
|
|
5906
6092
|
};
|
|
5907
6093
|
}
|
|
5908
6094
|
const journeys = new Set(input.lanes.map((x) => x.journey ?? "-")).size;
|
|
6095
|
+
const phones = new Set(input.lanes.map((x) => x.phone ?? "-")).size;
|
|
5909
6096
|
const single = input.lanes.length === 1;
|
|
5910
|
-
const phone = l.phone ?? "unknown phone";
|
|
5911
|
-
const several = journeys === 1 ? SUMMARY_WORDS.phones(new Set(input.lanes.map((x) => x.phone ?? "-")).size) : SUMMARY_WORDS.journeys(journeys);
|
|
5912
6097
|
const newestSlower = !input.pr && (l.state === "worse" || l.state === "warning");
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
6098
|
+
const lead = [
|
|
6099
|
+
notJudgedWhy(l) ? NOT_JUDGED_WORD : WORD[l.state],
|
|
6100
|
+
...input.app ? [input.app] : [],
|
|
6101
|
+
journeys === 1 ? l.journey ?? "journey" : SUMMARY_WORDS.journeys(journeys),
|
|
6102
|
+
input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
|
|
6103
|
+
phones === 1 ? l.phone ?? "unknown phone" : SUMMARY_WORDS.phones(phones)
|
|
6104
|
+
].join(" · ");
|
|
6105
|
+
const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
|
|
6106
|
+
const link = { label, url: (single ? l.url : null) ?? input.url };
|
|
6107
|
+
if (!input.slack)
|
|
6108
|
+
return { state: l.state, newestSlower, lead, ...withLines(changeLines(l, input.pr, newestSlower)), link };
|
|
6109
|
+
const lines = [];
|
|
6110
|
+
if (l.state === "worse" || l.state === "warning") {
|
|
6111
|
+
const n = l.places || l.changes.length;
|
|
6112
|
+
lines.push(upper([...n ? [SUMMARY_WORDS.slowerPlaces(n, tierSplit(l))] : [], feltCostText(l.feltCostMs)].filter(Boolean).join(" · ") || why(l)));
|
|
6113
|
+
} else if (l.state === "no-change")
|
|
6114
|
+
lines.push(upper(SUMMARY_WORDS.noPlaceSlower));
|
|
6115
|
+
else if (l.state === "cant-tell") {
|
|
6116
|
+
const fix = unblock(l, input.pr);
|
|
6117
|
+
lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
|
|
6118
|
+
} else if (l.state === "better")
|
|
6119
|
+
lines.push([why(l), feltGainText(l.feltGainMs)].filter(Boolean).join(" · "));
|
|
6120
|
+
else
|
|
6121
|
+
lines.push(why(l));
|
|
6122
|
+
if (l.toCheckCount)
|
|
6123
|
+
lines.push(upper(SUMMARY_WORDS.toCheck(l.toCheckCount)));
|
|
6124
|
+
const basis = compared(l, input.pr, false);
|
|
6125
|
+
const extra = conditions(l, input.pr).join(", ");
|
|
6126
|
+
const heat = l.heatedRuns ? SUMMARY_WORDS.heated(l.heatedRuns, l.runs) : null;
|
|
6127
|
+
const context = [basis ? `${basis}${extra ? `; ${extra}` : ""}` : extra, heat].filter(Boolean).join(" · ");
|
|
6128
|
+
if (context)
|
|
6129
|
+
lines.push(basis ? context : upper(context));
|
|
6130
|
+
return { state: l.state, newestSlower, lead, ...withLines(lines), link };
|
|
6131
|
+
}
|
|
6132
|
+
function changeLines(l, pr, newestSlower) {
|
|
6133
|
+
const lines = [];
|
|
6134
|
+
const basis = compared(l, pr, newestSlower);
|
|
6135
|
+
const extra = conditions(l, pr).join(", ");
|
|
6136
|
+
const heat = l.heatedRuns ? upper(SUMMARY_WORDS.heated(l.heatedRuns, l.runs)) : null;
|
|
6137
|
+
const felt = l.state === "worse" || l.state === "warning" ? feltCostText(l.feltCostMs) : l.state === "better" ? feltGainText(l.feltGainMs) : null;
|
|
6138
|
+
const sums = upper([felt, l.toCheckCount ? SUMMARY_WORDS.toCheck(l.toCheckCount) : null].filter(Boolean).join(" · "));
|
|
6139
|
+
const tail = [...sums ? [sums] : [], ...basis || extra ? [basis ? `${basis}${extra ? `; ${extra}` : ""}` : upper(extra)] : [], ...heat ? [heat] : []];
|
|
6140
|
+
if (l.state === "worse" || l.state === "warning" || l.state === "better") {
|
|
6141
|
+
const seen = new Set;
|
|
6142
|
+
const places = l.changes.filter((c) => {
|
|
6143
|
+
const key = c.key ?? c.place ?? c.measure;
|
|
6144
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
6145
|
+
});
|
|
6146
|
+
const room = MAX_LINES - tail.length;
|
|
6147
|
+
const top = places.length > room ? places.slice(0, Math.min(TOP2, room - 1)) : places.slice(0, TOP2);
|
|
6148
|
+
lines.push(...top.map((c) => changeLine(c, l.state)));
|
|
6149
|
+
if (!top.length)
|
|
6150
|
+
lines.push(why(l));
|
|
6151
|
+
const more = l.places - top.length;
|
|
6152
|
+
if (top.length && more > 0)
|
|
6153
|
+
lines.push(SUMMARY_WORDS.morePlaces(more));
|
|
6154
|
+
} else {
|
|
6155
|
+
const fix = l.state === "cant-tell" ? unblock(l, pr) : null;
|
|
6156
|
+
lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
|
|
6157
|
+
}
|
|
6158
|
+
lines.push(...tail);
|
|
6159
|
+
return lines;
|
|
5929
6160
|
}
|
|
6161
|
+
var withLines = (lines) => ({ lines, detail: lines.join(`
|
|
6162
|
+
`) });
|
|
5930
6163
|
function pendingMessage(branch, url) {
|
|
5931
|
-
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.notComputed} · ${clip(branch, NAME.name)}`,
|
|
6164
|
+
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.notComputed} · ${clip(branch, NAME.name)}`, ...withLines([upper(SUMMARY_WORDS.notComputedWhy)]), link: { label: SUMMARY_WORDS.link.runSet, url } };
|
|
5932
6165
|
}
|
|
5933
6166
|
function noResultMessage(branch, reason, url) {
|
|
5934
|
-
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`,
|
|
6167
|
+
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`, ...withLines([reason.replace(/\.$/, "")]), link: url ? { label: SUMMARY_WORDS.link.runSet, url } : null };
|
|
5935
6168
|
}
|
|
5936
6169
|
// packages/present/dist/src/run-set-message.js
|
|
5937
6170
|
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
@@ -6006,6 +6239,8 @@ function laneOf(v, sc, set) {
|
|
|
6006
6239
|
changes: shown.map((i) => ({
|
|
6007
6240
|
place: placeOf(i.step),
|
|
6008
6241
|
measure: measureWord(i),
|
|
6242
|
+
key: i.finding ?? placeRef(i.step ?? "*").stem,
|
|
6243
|
+
tier: i.state === "better" ? "better" : i.grade === "warn" ? "warning" : "worse",
|
|
6009
6244
|
main: i.baseline === null ? null : atMost(fieldValueText(i.metric, i.baseline), i.baseline_is_bound),
|
|
6010
6245
|
now: i.candidate === null ? null : fieldValueText(i.metric, i.candidate),
|
|
6011
6246
|
change: changeWords(i)
|
|
@@ -6017,17 +6252,23 @@ function laneOf(v, sc, set) {
|
|
|
6017
6252
|
heatedRuns: sc.heated_runs ?? 0,
|
|
6018
6253
|
runs: sc.runs.valid + sc.runs.invalid,
|
|
6019
6254
|
namesChanged: sc.context?.namesChanged ?? null,
|
|
6020
|
-
url: linkOf(sc, set)
|
|
6255
|
+
url: linkOf(sc, set),
|
|
6256
|
+
main: sc.main ?? null,
|
|
6257
|
+
reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null,
|
|
6258
|
+
feltCostMs: sc.headline?.felt_cost_ms ?? null,
|
|
6259
|
+
feltGainMs: sc.headline?.felt_gain_ms ?? null,
|
|
6260
|
+
toCheckCount: sc.headline?.to_check_count ?? null
|
|
6021
6261
|
};
|
|
6022
6262
|
}
|
|
6023
6263
|
var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
|
|
6024
6264
|
var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
|
|
6025
6265
|
var isPr = (s) => !!s.verdicts?.scenarios.some((sc) => sc.role === "comparison");
|
|
6026
|
-
var runSetMessage = (s) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url });
|
|
6266
|
+
var runSetMessage = (s, o = {}) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url, app: s.app ?? null, prNumber: s.pull_request?.number ?? null, slack: o.slack });
|
|
6027
6267
|
var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
6028
6268
|
function messageTone(s) {
|
|
6029
6269
|
const lanes = s ? lanesOf(s) : [];
|
|
6030
|
-
|
|
6270
|
+
const pr = !!s && isPr(s);
|
|
6271
|
+
if (lanes.some((l) => l.state === "worse" && (l.blocking || !pr)))
|
|
6031
6272
|
return { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error };
|
|
6032
6273
|
if (lanes.some((l) => l.state === "warning"))
|
|
6033
6274
|
return { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn };
|
|
@@ -6062,25 +6303,50 @@ function resultSlackPayload(s, o = {}) {
|
|
|
6062
6303
|
const moved = lanes.some((l) => l.state === "worse" || l.state === "warning");
|
|
6063
6304
|
const heat = lanes.some((l) => l.heatedRuns > 0);
|
|
6064
6305
|
const improved = lanes.some((l) => l.state === "better") || s.verdicts.state === "better";
|
|
6065
|
-
if (isPr(s)
|
|
6306
|
+
if (!isPr(s) && !moved && !improved && !heat && !o.always)
|
|
6066
6307
|
return null;
|
|
6067
|
-
return slackPayload(runSetMessage(s), messageTone(s));
|
|
6308
|
+
return slackPayload(runSetMessage(s, { slack: true }), messageTone(s));
|
|
6068
6309
|
}
|
|
6069
6310
|
var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
|
|
6070
6311
|
var markdown = (m, mark) => `### ${mark} ${m.lead}
|
|
6071
6312
|
|
|
6072
|
-
${m.
|
|
6313
|
+
${m.lines.join(`
|
|
6314
|
+
`)}
|
|
6073
6315
|
${m.link ? `
|
|
6074
6316
|
[${m.link.label} ›](${m.link.url})
|
|
6075
6317
|
` : ""}`;
|
|
6076
6318
|
var resultMarkdown = (s) => markdown(runSetMessage(s), messageTone(s).mark);
|
|
6077
6319
|
var noResultMarkdown = (branch, reason, url) => markdown(noResultMessage(branch, reason, url), "⚪");
|
|
6078
6320
|
var pendingMarkdown = (branch, url) => markdown(pendingMessage(branch, url), "⚪");
|
|
6321
|
+
// packages/present/dist/src/stale.js
|
|
6322
|
+
function staleNoteText(why) {
|
|
6323
|
+
const newer = why.includes("newer-run-set");
|
|
6324
|
+
const rules = why.some((w) => w === "core-version" || w === "config");
|
|
6325
|
+
if (newer && rules)
|
|
6326
|
+
return "Result from an older run set and older rules.";
|
|
6327
|
+
if (newer)
|
|
6328
|
+
return "Result from an older run set; a newer one has arrived.";
|
|
6329
|
+
if (rules)
|
|
6330
|
+
return "Result from older rules; it is not recomputed yet.";
|
|
6331
|
+
return "Result from older inputs.";
|
|
6332
|
+
}
|
|
6333
|
+
var staleFlagText = (why) => {
|
|
6334
|
+
const note = staleNoteText(why).replace(/\.$/, "");
|
|
6335
|
+
return `${note.charAt(0).toLowerCase()}${note.slice(1)}`;
|
|
6336
|
+
};
|
|
6337
|
+
var STALE_MARK = "stale result";
|
|
6079
6338
|
// apps/agent/src/upload.ts
|
|
6080
6339
|
import { join as join4 } from "node:path";
|
|
6081
6340
|
var MAX_RATE_RETRIES = 5;
|
|
6082
6341
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6342
|
+
var pause = (ms) => sleep(ms);
|
|
6343
|
+
|
|
6083
6344
|
class UploadError extends Error {
|
|
6345
|
+
status;
|
|
6346
|
+
constructor(message, status) {
|
|
6347
|
+
super(message);
|
|
6348
|
+
this.status = status;
|
|
6349
|
+
}
|
|
6084
6350
|
}
|
|
6085
6351
|
async function call(f, base, path, headers, body, method = "POST") {
|
|
6086
6352
|
let res;
|
|
@@ -6092,7 +6358,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
6092
6358
|
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
6093
6359
|
});
|
|
6094
6360
|
} catch (e) {
|
|
6095
|
-
throw new UploadError(`${method} ${path}: ${e.message}
|
|
6361
|
+
throw new UploadError(`${method} ${path}: ${e.message}`, 0);
|
|
6096
6362
|
}
|
|
6097
6363
|
if (res.status !== 429 || attempt >= MAX_RATE_RETRIES)
|
|
6098
6364
|
break;
|
|
@@ -6102,7 +6368,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
6102
6368
|
}
|
|
6103
6369
|
const text = await res.text();
|
|
6104
6370
|
if (res.status < 200 || res.status >= 300)
|
|
6105
|
-
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}
|
|
6371
|
+
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}`, res.status);
|
|
6106
6372
|
try {
|
|
6107
6373
|
return text ? JSON.parse(text) : {};
|
|
6108
6374
|
} catch {
|
|
@@ -6239,13 +6505,35 @@ async function createRunSet(t, body) {
|
|
|
6239
6505
|
throw new UploadError("POST /v1/run-sets: response has no run_set_id (is the URL the KeepSnappy API?)");
|
|
6240
6506
|
return { run_set_id: r.run_set_id, dashboard_url: String(r.dashboard_url ?? "") };
|
|
6241
6507
|
}
|
|
6508
|
+
var UNKNOWN_OUTCOME = new Set([0, 502, 503, 504]);
|
|
6509
|
+
var CONFIRM_READS = 6;
|
|
6510
|
+
var CONFIRM_WAIT_MS = 1e4;
|
|
6242
6511
|
async function completeRunSet(t, id, verdicts) {
|
|
6243
6512
|
const path = `/v1/run-sets/${encodeURIComponent(id)}/complete`;
|
|
6244
|
-
|
|
6513
|
+
let r;
|
|
6514
|
+
try {
|
|
6515
|
+
r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), { verdicts });
|
|
6516
|
+
} catch (e) {
|
|
6517
|
+
const closed = e instanceof UploadError && UNKNOWN_OUTCOME.has(e.status ?? -1) ? await confirmedClose(t, id) : null;
|
|
6518
|
+
if (closed)
|
|
6519
|
+
return closed;
|
|
6520
|
+
throw e;
|
|
6521
|
+
}
|
|
6245
6522
|
if (!r.run_set_id)
|
|
6246
6523
|
throw new UploadError(`POST ${path}: response has no run_set_id`);
|
|
6247
6524
|
return r;
|
|
6248
6525
|
}
|
|
6526
|
+
async function confirmedClose(t, id) {
|
|
6527
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}`;
|
|
6528
|
+
for (let i = 0;i < CONFIRM_READS; i++) {
|
|
6529
|
+
if (i)
|
|
6530
|
+
await pause(CONFIRM_WAIT_MS);
|
|
6531
|
+
const s = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), undefined, "GET").catch(() => null);
|
|
6532
|
+
if (s?.run_set_id && (s.status === "processed" || s.status === "failed"))
|
|
6533
|
+
return s;
|
|
6534
|
+
}
|
|
6535
|
+
return null;
|
|
6536
|
+
}
|
|
6249
6537
|
async function createIngestRun(t, body) {
|
|
6250
6538
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/runs", keyHeader(t), body);
|
|
6251
6539
|
if (!r.run_id || !r.run_token)
|
|
@@ -10188,8 +10476,14 @@ ${outcome.summary}`);
|
|
|
10188
10476
|
const info = { device, ...git.branch ? { branch: git.branch } : {}, ...git.commit ? { commit: git.commit } : {} };
|
|
10189
10477
|
await deps.fs.writeFile(join9(out, RUN_INFO), `${JSON.stringify({ ...info, ...runSet ? { run_set_id: runSet.run_set_id } : {} }, null, 2)}
|
|
10190
10478
|
`);
|
|
10191
|
-
if (!api)
|
|
10479
|
+
if (!api) {
|
|
10480
|
+
if (!runs.some((run) => run.valid)) {
|
|
10481
|
+
const reasons = [...new Set(runs.map((run) => run.invalid_reason).filter(Boolean))];
|
|
10482
|
+
rt.print(`No valid run was recorded: ${runs.length === 0 ? "no runs were attempted" : `${runs.length} ${runs.length === 1 ? "run" : "runs"} invalid${reasons.length ? ` (${reasons.join(", ")})` : ""}`}.`);
|
|
10483
|
+
return EXIT.infra;
|
|
10484
|
+
}
|
|
10192
10485
|
return EXIT.ok;
|
|
10486
|
+
}
|
|
10193
10487
|
const up = await uploadRuns(deps, out, runs, {
|
|
10194
10488
|
ingestUrl: api.url,
|
|
10195
10489
|
key: api.key,
|
|
@@ -10410,7 +10704,7 @@ async function aa(deps, outDir, opts = {}) {
|
|
|
10410
10704
|
}
|
|
10411
10705
|
|
|
10412
10706
|
// apps/agent/src/farm.ts
|
|
10413
|
-
import { dirname as
|
|
10707
|
+
import { dirname as dirname4, join as join13, resolve as resolve6 } from "node:path";
|
|
10414
10708
|
|
|
10415
10709
|
// packages/appium-driver/dist/src/links.js
|
|
10416
10710
|
var ALERT_POLL_MS = 250;
|
|
@@ -10714,7 +11008,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
10714
11008
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
10715
11009
|
import { fileURLToPath } from "node:url";
|
|
10716
11010
|
function driverVersion() {
|
|
10717
|
-
const built = "0.1.0-alpha.
|
|
11011
|
+
const built = "0.1.0-alpha.20";
|
|
10718
11012
|
if (built)
|
|
10719
11013
|
return built;
|
|
10720
11014
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -10769,6 +11063,11 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
|
10769
11063
|
var FLUSH_MARGIN_MS = 30000;
|
|
10770
11064
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10771
11065
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
11066
|
+
// packages/verdict-core/dist/src/version.generated.js
|
|
11067
|
+
var CORE_FINGERPRINT = "1c8a7ed82956dae0";
|
|
11068
|
+
|
|
11069
|
+
// packages/verdict-core/dist/src/version.js
|
|
11070
|
+
var CORE_VERSION = parseInt(CORE_FINGERPRINT.slice(0, 7), 16);
|
|
10772
11071
|
// packages/verdict-core/dist/src/window-count.js
|
|
10773
11072
|
var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
|
|
10774
11073
|
var OFF = new WeakSet;
|
|
@@ -10795,6 +11094,9 @@ var CLAUSES = {
|
|
|
10795
11094
|
commit: { c: 0.8326, k95: 1.7675, k99: 2.8108 },
|
|
10796
11095
|
component: { c: 0.6481, k95: 1.8075, k99: 2.6648 }
|
|
10797
11096
|
}),
|
|
11097
|
+
"T-NOISE-JANK": row("T-NOISE-JANK", "The judged `jank` rule: the no-change noise curve of jank time, as T-WARN-NOISE; at a main median of 0, Slightly worse past zeroWarn and Worse past zeroError.", "ms", ">", "W8 option A, Arek 2026-10-26 [ctl-arek-jank-time]; fitted by the 2026-10-16 method (dedup-nomi, PR 3x3 vs main 3x3, every split) on pack v3 aa-ni (S23 TapIn, 10 sessions, 4200 splits, 19 places with a median above 0, R² -0.27; the 10-16 S23 and iPhone sessions are no longer stored); zeroWarn = the largest d99 at a median of 0 over the A/A pools with an export (farm16 Pixel 7), zeroError = zeroWarn x k99 / k95", true, {
|
|
11098
|
+
jank: { c: 0.9159, k95: 2.5757, k99: 3.2072, zeroWarn: 10.38, zeroError: 12.92 }
|
|
11099
|
+
}),
|
|
10798
11100
|
"T-REL": row("T-REL", "The relative part of both tiers when a rule has no noise curve; the deciding config's tiers cap it at the error ms, so no input reaches it.", "share", ">", "REQ-MET-26, controller 2026-10-16", false, { rel: 0.3 }),
|
|
10799
11101
|
"J-MEMORY-RETAINED": row("J-MEMORY-RETAINED", "Memory the journey kept after the end GC: Slightly worse past max(warn MB, warn % of main), Worse past max(error MB, error % of main), on every device.", "MB", ">", "REQ-MET-4, Arek 2026-10-20 ~08:00, pair B (standing-rulings.md); provisional", true, { warn: { abs: 20, rel: 0.1 }, error: { abs: 150, rel: 0.3 } }),
|
|
10800
11102
|
"E-REACT-COUNTS": row("E-REACT-COUNTS", "Commit and render counts are evidence past max(floor, +50 % of main).", "count", ">", "REQ-MET-13, Arek 2026-10-20 (morning rulings, Layer 2); provisional", true, { commits: { rel: 0.5, abs: 2 }, renders: { rel: 0.5, abs: 10 } }),
|
|
@@ -10802,6 +11104,7 @@ var CLAUSES = {
|
|
|
10802
11104
|
"E-BACKGROUND": row("E-BACKGROUND", "Background places are evidence past max(floor, % of main).", "ms", ">", "REQ-MEAS-32; the number has no ruling", true, { background_commit: FLOOR, background_render: FLOOR, background_component: FLOOR }),
|
|
10803
11105
|
"E-LAUNCH": row("E-LAUNCH", "The launch screen is evidence past max(5 ms, +30 % of main).", "ms", ">", "REQ-VER-15; floor adopted 2026-10-21 from config.ts F-13, [ctl-stab-noold-1625]", true, { launch_commit: FLOOR, launch_render: FLOOR, launch_component: FLOOR }),
|
|
10804
11106
|
"E-STEP": row("E-STEP", "A declared step is evidence past max(5 ms, +30 % of main).", "ms", ">", "REQ-MET-14; floor adopted 2026-10-21 from config.ts F-13, [ctl-stab-noold-1625]", true, { step_commit: FLOOR, step_render: FLOOR, step_component: FLOOR }),
|
|
11107
|
+
"E-COMMIT-MAX": row("E-COMMIT-MAX", "The slowest commit at a judged window is evidence past max(5 ms, +30 % of main) while jank time is judged.", "ms", ">", "W8 option A, Arek 2026-10-26 [ctl-arek-jank-time]: commit_max_ms stays shown as evidence", true, { commit_max: FLOOR }),
|
|
10805
11108
|
"E-TOUCH": row("E-TOUCH", "Touch response is evidence past max(10 ms, +20 % of main).", "ms", ">", 'ruling "touch response is not judged" (REQ-MET-24)', true, { touch: { rel: 0.2, abs: 10 } }),
|
|
10806
11109
|
"E-FREEZE": row("E-FREEZE", "The longest freeze (window, journey) is evidence past max(17 ms, +50 % of main).", "ms", ">", NO_RULING, true, { freeze: { rel: 0.5, abs: 17 }, freeze_journey: { rel: 0.5, abs: 17 } }),
|
|
10807
11110
|
"E-MEMORY-PLACE": row("E-MEMORY-PLACE", "Memory added at a place is evidence past this many MB (screens have their own), on every device.", "MB", ">", 'REQ-MET-4 amendment 2026-10-18 ~21:10 and ~22:20 ("abs 2 MB, screens 10 MB")', true, { memory_added: { abs: 2 }, memory_added_screen: { abs: 10 } }),
|
|
@@ -10857,7 +11160,8 @@ var REACT_MEASURES = [
|
|
|
10857
11160
|
"self_ms_top",
|
|
10858
11161
|
"component_ms",
|
|
10859
11162
|
"dimension:component",
|
|
10860
|
-
"dimension:updater"
|
|
11163
|
+
"dimension:updater",
|
|
11164
|
+
"jank_ms"
|
|
10861
11165
|
];
|
|
10862
11166
|
var WINDOW_MODEL_AFFECTS = [
|
|
10863
11167
|
"commits",
|
|
@@ -10882,7 +11186,8 @@ var WINDOW_MODEL_AFFECTS = [
|
|
|
10882
11186
|
"memory_left_mb",
|
|
10883
11187
|
"retained_mb_per_cycle",
|
|
10884
11188
|
"dimension:component",
|
|
10885
|
-
"dimension:updater"
|
|
11189
|
+
"dimension:updater",
|
|
11190
|
+
"jank_ms"
|
|
10886
11191
|
];
|
|
10887
11192
|
var MEASURES_AT = (kind) => [...Object.keys(MEASURES).filter((id) => MEASURES[id].at.includes(kind)), "dimension:component", "dimension:updater"];
|
|
10888
11193
|
var SPLIT_AFFECTS = [
|
|
@@ -11040,7 +11345,7 @@ var DRIVER_RELEASES = [
|
|
|
11040
11345
|
{ affects: LOADED_MEMORY }
|
|
11041
11346
|
],
|
|
11042
11347
|
uncalibrated: false,
|
|
11043
|
-
source: "Drag end points move above the platform bottom system-gesture zone (fix/driver-gesture-zone); start points unchanged; on iOS a shortened drag keeps its duration (slower); on Android it keeps its speed (ends sooner). A drag's end, and the duration of a step whose scroll steps contain such drags, can change. W1 (#306): farm sessions record no video or device logs by default (`--capture off`), so the phone carries less load and every memory measure (mem.*, memory_left_mb, sampler.memory.*, ri.memory.*) is measured differently from capture-on alpha.16 sessions (Elastyczność 2026-10-26 15:45 B1-DIFF; controller Stab 4). The declaration only separates alpha.16 from alpha.17: two alpha.17 sessions with capture on and off
|
|
11348
|
+
source: "Drag end points move above the platform bottom system-gesture zone (fix/driver-gesture-zone); start points unchanged; on iOS a shortened drag keeps its duration (slower); on Android it keeps its speed (ends sooner). A drag's end, and the duration of a step whose scroll steps contain such drags, can change. W1 (#306): farm sessions record no video or device logs by default (`--capture off`), so the phone carries less load and every memory measure (mem.*, memory_left_mb, sampler.memory.*, ri.memory.*) is measured differently from capture-on alpha.16 sessions (Elastyczność 2026-10-26 15:45 B1-DIFF; controller Stab 4). The declaration only separates alpha.16 from alpha.17: two alpha.17 sessions with capture on and off are told apart by the `capture` measured-with aspect (run setup.capture, #344, #426). Screens and taps stay compared (REQ-FARM-35)."
|
|
11044
11349
|
},
|
|
11045
11350
|
{
|
|
11046
11351
|
version: "0.1.0-alpha.18",
|
|
@@ -11048,8 +11353,31 @@ var DRIVER_RELEASES = [
|
|
|
11048
11353
|
declares: [{ affects: DRAG_TIME_AND_CPU, places: ["drag"], platforms: ["ios"] }],
|
|
11049
11354
|
uncalibrated: false,
|
|
11050
11355
|
source: "R-SCROLL-BUILD (#355 one scroll primitive and the counted gesture retry, #356 the scroll fault matrix). Android declares nothing: on a phone that takes every gesture the driver sends the same requests, bodies and waits as alpha.17 (the whole command stream is pinned). iOS: each checked drag (every to-top drag of flingUntilShown/passChecked and the first drag of each scrollToEnd pass) gains two frame reads of the held row, one right before and one right after it, so a drag window's time and CPU can change; its React render, commits and memory stay compared. A phone that drops a gesture (BrowserStack Galaxy S23, S25 Ultra) gets that gesture once more, as a counted harness defect (REQ-FARM-35)."
|
|
11356
|
+
},
|
|
11357
|
+
{
|
|
11358
|
+
version: "0.1.0-alpha.19",
|
|
11359
|
+
previous: "0.1.0-alpha.18",
|
|
11360
|
+
declares: [
|
|
11361
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"], platforms: ["ios"] },
|
|
11362
|
+
{ affects: MEASURES_AT("step"), places: ["step"], platforms: ["ios"] }
|
|
11363
|
+
],
|
|
11364
|
+
uncalibrated: false,
|
|
11365
|
+
source: "#393 (fix/keepsnappy-clamped-row-drag-travel, appium-driver CHANGELOG 2026-10-27): on iOS a row-to-top drag whose end safeBottomY clamps (chat history on every iPhone with a home indicator) starts 33 pt higher, so it travels 33 pt further in the same 400 ms (a faster release, a longer coast), with one list lookup and two frame reads right before it. Every measure of that drag window can move, React work included (more rows scroll by), and so can the step whose scroll steps contain it. Android and unclamped drags send the same requests as alpha.18 (pinned): declares nothing there (REQ-FARM-35)."
|
|
11366
|
+
},
|
|
11367
|
+
{
|
|
11368
|
+
version: "0.1.0-alpha.20",
|
|
11369
|
+
previous: "0.1.0-alpha.19",
|
|
11370
|
+
declares: [
|
|
11371
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
11372
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
11373
|
+
{ affects: MEASURES_AT("journey"), places: ["journey"] }
|
|
11374
|
+
],
|
|
11375
|
+
uncalibrated: false,
|
|
11376
|
+
source: '#417 (plan 84 step 2.1, D1, appium-driver CHANGELOG "release alpha.20", 2026-10-27): one gesture definition for every list and screen scroll on both platforms, one touch held 150 ms over 60 % of the list frame (at least 250 dp or pt). Android list and screen flings 12 000 / 3000 dp/s -> that contact (a shorter coast, about 1000-1600 dp in place of about 7000, so fewer momentum frames and possibly one more gesture per page pass); iOS list drags 75 % (end) / 65 % (top) -> 60 %, iOS drawn-row (inverted list) drags 400 -> 150 ms; touches reaching a system zone move whole. Every other request, body and wait is pinned unchanged against alpha.19. The CHANGELOG states "changes Android and iOS list/scroll places; compare only within alpha.20 on both platforms", so both platforms are declared at drag, step and journey places (REQ-FARM-35): the journey holds the whole-run measures the judge reads (memory kept, memory at launch, end and peak, whole-run CPU, the longest freeze), and a shorter coast and one more gesture per page pass move them too.'
|
|
11051
11377
|
}
|
|
11052
11378
|
];
|
|
11379
|
+
var JOURNEY_RELEASES = {};
|
|
11380
|
+
var CAPTURE_MEASURED_WITH = { aspect: "capture", affects: LOADED_MEMORY, marks: true };
|
|
11053
11381
|
var UNSTABLE_NOISY = {
|
|
11054
11382
|
reason: "noisy",
|
|
11055
11383
|
spread: "sessions",
|
|
@@ -11063,7 +11391,6 @@ var UNSTABLE_NOISY = {
|
|
|
11063
11391
|
uncalibrated: false,
|
|
11064
11392
|
source: 'Arek 2026-10-18 ~12:00, ~12:20 (noisy on main = unstable, main only, per comparison, hysteresis), 12:50 (within-session spread only: a regression that lands on main moves whole sessions and must stay judged), 13:20 ("zrób na razie wg rekomendacji"; threshold provisional). ' + "Ratio = z99 x the predicted no-change difference SD from the pooled within-session MAD of main (Var(median) = (pi/2) sigma^2 / n) / the smallest warning step at main's median. " + "Replay (noisy/REPORT.md, derivation 22, 1 PR vs 3 main): S23 night 2 -> 1 false worse z 60 (MAD rule 0), sim 0, NI pool 95 -> 64 warnings z 840 (MAD 92); 3 vs 3 0 everywhere; flagged per split S23 17.8, NI 4.9, sim 0.7; judged per split S23 64.5 -> 46.8; flips S23 8 (MAD 21); planted main steps flagged noisy 0 (MAD 5-29 %, with the between-session part 56-99 %). " + "Curve: kept for the judged-rule lookup; the ratio uses the warning step"
|
|
11065
11393
|
};
|
|
11066
|
-
|
|
11067
11394
|
class ConfigError2 extends Error {
|
|
11068
11395
|
problems;
|
|
11069
11396
|
constructor(problems) {
|
|
@@ -11075,7 +11402,7 @@ class ConfigError2 extends Error {
|
|
|
11075
11402
|
var noRow = (id) => {
|
|
11076
11403
|
throw new ConfigError2([`${id}: no row in the clause table`]);
|
|
11077
11404
|
};
|
|
11078
|
-
var NOISE_FITS = CLAUSES["T-WARN-NOISE"].value;
|
|
11405
|
+
var NOISE_FITS = { ...CLAUSES["T-WARN-NOISE"].value, ...CLAUSES["T-NOISE-JANK"].value };
|
|
11079
11406
|
var TIER_REL = 0.3;
|
|
11080
11407
|
var TIER_UPDATE_MS = { warnMs: 5, errorMs: 15 };
|
|
11081
11408
|
var TIER_WORK_MS = { warnMs: 10, errorMs: 30 };
|
|
@@ -11107,17 +11434,16 @@ function withClauseValues(base) {
|
|
|
11107
11434
|
throw new ConfigError2(["better: missing from base config"]);
|
|
11108
11435
|
const warnMs = CLAUSES["T-WARN-MS"].value;
|
|
11109
11436
|
const errorMs = CLAUSES["TIER-ERROR"].value;
|
|
11110
|
-
const noise = CLAUSES["T-WARN-NOISE"];
|
|
11111
11437
|
const memory = CLAUSES["J-MEMORY-RETAINED"];
|
|
11112
11438
|
const judgedByNoise = (r) => {
|
|
11113
11439
|
const fit = NOISE_FITS[r.id] ?? noRow(r.id);
|
|
11114
|
-
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95 };
|
|
11440
|
+
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95, ...fit.zeroWarn !== undefined ? { atZero: fit.zeroWarn } : {} };
|
|
11115
11441
|
return {
|
|
11116
11442
|
...r,
|
|
11117
11443
|
threshold: [{ abs: warnMs.update, noise: n }],
|
|
11118
11444
|
thresholdByWork: { entry: [{ abs: warnMs.work, noise: n }], mount: [{ abs: warnMs.work, noise: n }] },
|
|
11119
|
-
errorNoise: {
|
|
11120
|
-
uncalibrated:
|
|
11445
|
+
errorNoise: { a: 0, b: fit.c, power: 0.5, lift: fit.k99, ...fit.zeroError !== undefined ? { atZero: fit.zeroError } : {} },
|
|
11446
|
+
uncalibrated: (r.id in CLAUSES["T-NOISE-JANK"].value ? CLAUSES["T-NOISE-JANK"] : CLAUSES["T-WARN-NOISE"]).uncalibrated
|
|
11121
11447
|
};
|
|
11122
11448
|
};
|
|
11123
11449
|
const evidence = (r) => {
|
|
@@ -11125,11 +11451,18 @@ function withClauseValues(base) {
|
|
|
11125
11451
|
const at = r.thresholdAt && Object.fromEntries(Object.keys(r.thresholdAt).map((kind) => [kind, [part((EVIDENCE_BARS.get(`${r.id}_${kind}`) ?? noRow(`${r.id}_${kind}`)).bar)]]));
|
|
11126
11452
|
return { ...r, threshold: [part(e.bar)], ...at ? { thresholdAt: at } : {}, uncalibrated: e.uncalibrated };
|
|
11127
11453
|
};
|
|
11128
|
-
const
|
|
11454
|
+
const valued = (r) => {
|
|
11129
11455
|
if (r.id === "memory_retained")
|
|
11130
11456
|
return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
|
|
11131
|
-
|
|
11132
|
-
|
|
11457
|
+
if (r.role === "judged")
|
|
11458
|
+
return judgedByNoise(r);
|
|
11459
|
+
if (r.id in NOISE_FITS) {
|
|
11460
|
+
const { errorNoise: _error, ...shown } = judgedByNoise(r);
|
|
11461
|
+
return shown;
|
|
11462
|
+
}
|
|
11463
|
+
return evidence(r);
|
|
11464
|
+
};
|
|
11465
|
+
const rules = base.rules.map(valued);
|
|
11133
11466
|
const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
|
|
11134
11467
|
const entry = CLAUSES["P-HEAVY-ENTRY"];
|
|
11135
11468
|
const unstable = CLAUSES["U-NOISY"];
|
|
@@ -11171,12 +11504,38 @@ function withClauseValues(base) {
|
|
|
11171
11504
|
minMainRuns: unstable.value.minMainRuns,
|
|
11172
11505
|
minSessions: unstable.value.minSessions,
|
|
11173
11506
|
uncalibrated: unstable.uncalibrated,
|
|
11174
|
-
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c]))
|
|
11507
|
+
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c])),
|
|
11508
|
+
...u.reads ? { reads: u.reads.map(valued) } : {}
|
|
11175
11509
|
}))
|
|
11176
11510
|
};
|
|
11177
11511
|
}
|
|
11512
|
+
var COMMIT_MAX_JUDGED = noiseJudged("commit", { measure: "commit_max_ms" });
|
|
11513
|
+
var RENDER_JUDGED = noiseJudged("render", { measure: "render_ms" });
|
|
11514
|
+
var COMPONENT_JUDGED = noiseJudged("component", { dimension: "component", measure: "self_ms" });
|
|
11515
|
+
var MEMORY_RETAINED = {
|
|
11516
|
+
id: "memory_retained",
|
|
11517
|
+
role: "judged",
|
|
11518
|
+
on: { measure: "mem.retained_growth_mb" },
|
|
11519
|
+
places: JOURNEY,
|
|
11520
|
+
devices: ANY_DEVICE,
|
|
11521
|
+
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11522
|
+
uncalibrated: true,
|
|
11523
|
+
source: MEMORY_PAIR_SOURCE
|
|
11524
|
+
};
|
|
11525
|
+
var FELT_SOURCE = 'Arek 2026-10-27 ~08:00 ("Ok", [ctl-arek-felt-cost], amends decision 0005 / Q-5): component self time explains, never decides; every component past its noise is listed "to check", also on a green verdict';
|
|
11526
|
+
var JANK_SOURCE = `W8 option A, Arek 2026-10-26 ("Opcja A jest git", [ctl-arek-jank-time]): jank time (the window's commit times past one 60 Hz frame, added up) is judged in place of the slowest commit, so one spike no longer decides and jitter below a frame counts 0; curve T-NOISE-JANK; deciding since simple-4.26 after the pack v3 replay (A/A Slightly worse 58 -> 46 of 840 on aa-ni, doses 54 of 54)`;
|
|
11527
|
+
var NOISY_WIDEN = {
|
|
11528
|
+
noisyFrom: 1,
|
|
11529
|
+
lineMultiplier: 1,
|
|
11530
|
+
sd: "aa-loo",
|
|
11531
|
+
excludeAbove: null,
|
|
11532
|
+
excludeReturnBelow: null,
|
|
11533
|
+
excludeAboveMs: null,
|
|
11534
|
+
uncalibrated: false,
|
|
11535
|
+
source: `Arek 2026-10-27 ~19:45 [ctl-arek-noisy-widen]: noisy places judged with a widened line, tunable down to the old exclusion (excludeAbove 1.0 / 0.75 with the within-session spread); amendment ~19:40 ("Szum z 3 sesji main powinniśmy liczyć tak samo jak A/A"): one main session against the others, between-session shift included, plus the PR's run-to-run spread; ctl 19:45: ×1, k never raised to pass; start position: never left out (ctl 22:20, the Pixel 7 no longer counts). Pack v4 replay against simple-4.27 without the Pixel 7: A/A false Worse 1 / 1 of 1070 splits, doses caught Reference night 166 / 166, Reference PR 93 / 93, farm iPhone ms 4863 -> 5012 of 6580 (Worse 3107 -> 3167), places left out 9.8 % -> 0 %. Footnote Pixel 7 (21 splits): A/A Worse 0 -> 4`
|
|
11536
|
+
};
|
|
11178
11537
|
var SHAPE = {
|
|
11179
|
-
id: "simple-4.
|
|
11538
|
+
id: "simple-4.28+noise95-99",
|
|
11180
11539
|
minRuns: 2,
|
|
11181
11540
|
baseline: { sessions: 3, ...GUESS("Q4 / REQ-VER-33: main's newest 3 sessions against one PR session (the quieter setting of the plan 58 simulation)") },
|
|
11182
11541
|
repeats: { kind: "every-pr-beyond-every-main" },
|
|
@@ -11198,13 +11557,15 @@ var SHAPE = {
|
|
|
11198
11557
|
uncalibrated: false,
|
|
11199
11558
|
source: 'Arek 2026-10-16 ("Tak, to też ustalaliśmy"): the SDK, the driver and the journey are the instrument (REQ-VER-35); a release declares the measures it affects (REQ-VER-34); a forced GC before each step is a different instrument (REQ-VER-13, docs/plans/gc-before-action.md)',
|
|
11200
11559
|
driverReleases: DRIVER_RELEASES,
|
|
11560
|
+
...Object.keys(JOURNEY_RELEASES).length ? { journeyReleases: JOURNEY_RELEASES } : {},
|
|
11201
11561
|
unknownDiffers: ["journeyVersion"]
|
|
11202
11562
|
},
|
|
11203
11563
|
calibrated: { platforms: ["ios", "android"], uncalibrated: false, source: 'REQ-FARM-26: Android moved in by Arek ("Ok", 2026-10-17) after the S23 A/A, 5 sessions × 3 runs: 0 z 25 false worse under the deciding rule (stats-android/aa/aa-report.md)' },
|
|
11204
11564
|
rules: [
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11565
|
+
{ ...COMMIT_MAX_JUDGED, id: "jank", on: { measure: "jank_ms" }, source: JANK_SOURCE },
|
|
11566
|
+
evidenceRule("commit_max", { measure: "commit_max_ms" }, JUDGED_WINDOWS, [], "W8 option A: the slowest commit is shown, jank time is judged", { devices: ANY_DEVICE }),
|
|
11567
|
+
RENDER_JUDGED,
|
|
11568
|
+
{ ...COMPONENT_JUDGED, role: "evidence", source: FELT_SOURCE },
|
|
11208
11569
|
{ ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11209
11570
|
{ ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11210
11571
|
evidenceRule("render_upper", { measure: "render_ms_upper" }, WINDOWS, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "REQ-MEAS-32 timers, option A (Arek 2026-10-17): judged plus timer-carried render time is evidence with the render threshold; render_ms decides"),
|
|
@@ -11230,16 +11591,7 @@ var SHAPE = {
|
|
|
11230
11591
|
evidenceRule("gc", { measure: "gc_ms" }, SHOWN2, [{ rel: 0.5, abs: 2 }], "v5.3 warn level"),
|
|
11231
11592
|
evidenceRule("requests", { measure: "net_requests" }, SHOWN2, [{ rel: 0.5, abs: 1 }], "v5.3 warn level"),
|
|
11232
11593
|
evidenceRule("js_cpu", { measure: "js_cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 5 }], "v5.3 warn level"),
|
|
11233
|
-
|
|
11234
|
-
id: "memory_retained",
|
|
11235
|
-
role: "judged",
|
|
11236
|
-
on: { measure: "mem.retained_growth_mb" },
|
|
11237
|
-
places: JOURNEY,
|
|
11238
|
-
devices: ANY_DEVICE,
|
|
11239
|
-
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11240
|
-
uncalibrated: true,
|
|
11241
|
-
source: MEMORY_PAIR_SOURCE
|
|
11242
|
-
}
|
|
11594
|
+
MEMORY_RETAINED
|
|
11243
11595
|
],
|
|
11244
11596
|
problems: [
|
|
11245
11597
|
{
|
|
@@ -11281,13 +11633,20 @@ var SHAPE = {
|
|
|
11281
11633
|
uncalibrated: true,
|
|
11282
11634
|
source: "ruling d: entry and in-screen mount about 100 ms; an update one frame (16.7 ms on every screen, Arek 2026-10-16)"
|
|
11283
11635
|
},
|
|
11636
|
+
feltCost: {
|
|
11637
|
+
reads: ["jank", "render"],
|
|
11638
|
+
uncalibrated: false,
|
|
11639
|
+
source: 'Arek 2026-10-27 ~08:00 ("Ok", [ctl-arek-felt-cost]) after giga2-scratch/analysis/2026-10-27-verdict-scale.md option C; B4 replay on the jank pack v3 (676d7453a39c) against simple-4.26: A/A Worse / Slightly worse over 16 pools 2 / 68 -> 1 / 34, doses 52 of 54 (in-screen-mount 1.1x and slow-commit 0.5x lose one case each), golden 10 of 10, corpus 0 moves'
|
|
11640
|
+
},
|
|
11284
11641
|
freeze: { on: "sampler_longest_gap_ms", uncalibrated: false, source: 'evidence only: the longest freeze covers the whole window, not the transition; "no dropped frames during the transition" (ruling d) needs per-place frame data that is not available' },
|
|
11285
11642
|
notJudgedLabels: ["launch"],
|
|
11286
11643
|
unstable: [{
|
|
11287
11644
|
...UNSTABLE_NOISY,
|
|
11288
11645
|
step: "own",
|
|
11289
|
-
|
|
11646
|
+
reads: [COMMIT_MAX_JUDGED, RENDER_JUDGED, COMPONENT_JUDGED, MEMORY_RETAINED],
|
|
11647
|
+
source: `${UNSTABLE_NOISY.source}; Arek 2026-10-26 ("A wygląda kusząco. Oddaj do impl.", ctl-arek-met29-own-step): the step is the place's own work kind's (entry and in-screen mount 10 ms, update 5 ms), the smallest only when the kind is unknown; ctl-arek-jank-time: noisiness still reads the slowest commit, so a place is unstable exactly when it was before jank time was judged`
|
|
11290
11648
|
}],
|
|
11649
|
+
noisy: NOISY_WIDEN,
|
|
11291
11650
|
judgeFrom: { mainSessions: 3, uncalibrated: false, source: 'Arek 2026-10-18 (REQ-VER-41): a phone is judged only when main has at least 3 sessions on that phone and journey; with fewer the result is "not enough main yet", not judged, not blocking, values shown. Replay (agents/release/replay, d22): 1 PR vs 2 main on the S23 night pool gave 13 z 60 false worse+warning, 1 vs 3 gave 0' },
|
|
11292
11651
|
measuredWith: [
|
|
11293
11652
|
{ aspect: "renderer", affects: REACT_MEASURES },
|
|
@@ -11296,7 +11655,8 @@ var SHAPE = {
|
|
|
11296
11655
|
{ aspect: "screenWindowModel", affects: WINDOW_MODEL_AFFECTS, places: ["screen"], marks: true },
|
|
11297
11656
|
{ aspect: "cpuSource", affects: ["sampler.cpu_ms"] },
|
|
11298
11657
|
{ aspect: "causeSplit", affects: REACT_MEASURES, places: REACT_PLACES, marks: true },
|
|
11299
|
-
{ aspect: "endGc", affects: MEMORY_MEASURES }
|
|
11658
|
+
{ aspect: "endGc", affects: MEMORY_MEASURES },
|
|
11659
|
+
CAPTURE_MEASURED_WITH
|
|
11300
11660
|
],
|
|
11301
11661
|
tiers: {
|
|
11302
11662
|
rel: TIER_REL,
|
|
@@ -11320,15 +11680,6 @@ var SHAPE = {
|
|
|
11320
11680
|
}
|
|
11321
11681
|
};
|
|
11322
11682
|
var CONFIG = withClauseValues(SHAPE);
|
|
11323
|
-
var P2A_SLIGHT = withClauseValues({
|
|
11324
|
-
...CONFIG,
|
|
11325
|
-
id: `${CONFIG.id}+p2a-sw`,
|
|
11326
|
-
unstable: CONFIG.unstable.map((u) => ({
|
|
11327
|
-
...u,
|
|
11328
|
-
judgeAt: { line: "error", tier: "warning" },
|
|
11329
|
-
source: `${u.source}; P2a candidate (plan 81, Arek 2026-10-23 ~15:45): judged only past the error line, as Slightly worse, never blocking`
|
|
11330
|
-
}))
|
|
11331
|
-
});
|
|
11332
11683
|
var P4_EVEN = {
|
|
11333
11684
|
...CONFIG,
|
|
11334
11685
|
id: `${CONFIG.id}+p4-even`,
|
|
@@ -11339,6 +11690,51 @@ var P3_COMMIT = withClauseValues({
|
|
|
11339
11690
|
id: `${CONFIG.id}+p3-commit`,
|
|
11340
11691
|
commitGrouping: { uncalibrated: true, source: "plan 81 P3; Arek 2026-10-26 [ctl-arek-p81-p3p4]: two windows that share one proven commit (the same slowest commit in every PR run) may be one finding; never merged without it" }
|
|
11341
11692
|
});
|
|
11693
|
+
// apps/agent/src/flow-version.ts
|
|
11694
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
11695
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
11696
|
+
var REFERENCE = /^\s*(?:-\s*)?(?:runFlow|runScript|file)\s*:\s*['"]?([^'"\s#{}$]+\.(?:ya?ml|js))['"]?\s*$/gm;
|
|
11697
|
+
var commonDir = (paths) => {
|
|
11698
|
+
let dir = dirname3(paths[0]);
|
|
11699
|
+
while (!paths.every((p) => p === dir || p.startsWith(dir.endsWith(sep2) ? dir : dir + sep2)))
|
|
11700
|
+
dir = dirname3(dir);
|
|
11701
|
+
return dir;
|
|
11702
|
+
};
|
|
11703
|
+
async function flowsVersion(files, read, skip = []) {
|
|
11704
|
+
if (files.length === 0)
|
|
11705
|
+
return null;
|
|
11706
|
+
const roots = files.map((f) => resolve5(f));
|
|
11707
|
+
const skipped = new Set(skip.map((s) => resolve5(s)));
|
|
11708
|
+
const seen = new Map;
|
|
11709
|
+
const visit = async (file, root) => {
|
|
11710
|
+
if (seen.has(file) || skipped.has(file))
|
|
11711
|
+
return true;
|
|
11712
|
+
const text = await read(file).then((t) => t.replace(/\r\n?/g, `
|
|
11713
|
+
`), () => null);
|
|
11714
|
+
if (text === null)
|
|
11715
|
+
return !root;
|
|
11716
|
+
seen.set(file, text);
|
|
11717
|
+
for (const m of text.matchAll(REFERENCE))
|
|
11718
|
+
await visit(resolve5(dirname3(file), m[1]), false);
|
|
11719
|
+
return true;
|
|
11720
|
+
};
|
|
11721
|
+
for (const r of roots)
|
|
11722
|
+
if (!await visit(r, true))
|
|
11723
|
+
return null;
|
|
11724
|
+
const base = commonDir(roots);
|
|
11725
|
+
const keyed = [...seen].map(([file, text]) => [relative3(base, file).split(sep2).join("/"), text]);
|
|
11726
|
+
const hash = createHash2("sha256");
|
|
11727
|
+
for (const [key, text] of keyed.sort((a, b) => a[0] < b[0] ? -1 : 1))
|
|
11728
|
+
hash.update(key).update("\x00").update(text).update("\x00");
|
|
11729
|
+
return hash.digest("hex").slice(0, 12);
|
|
11730
|
+
}
|
|
11731
|
+
async function maestroCliVersion(deps) {
|
|
11732
|
+
const r = await deps.spawner.run("maestro", ["--version"], { timeoutMs: 30000, killAfterMs: 5000 });
|
|
11733
|
+
if (r.code !== 0)
|
|
11734
|
+
return;
|
|
11735
|
+
return /^\s*v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/m.exec(r.stdout)?.[1];
|
|
11736
|
+
}
|
|
11737
|
+
|
|
11342
11738
|
// apps/agent/src/device-model.ts
|
|
11343
11739
|
import { tmpdir } from "node:os";
|
|
11344
11740
|
import { join as join12 } from "node:path";
|
|
@@ -11481,6 +11877,19 @@ var maestroLinkFields = (sdkArgs = "") => ({ keepsnappyLinkArgs: linkArgs(sdkArg
|
|
|
11481
11877
|
|
|
11482
11878
|
// apps/agent/src/farm.ts
|
|
11483
11879
|
var STATE_VERSION = 1;
|
|
11880
|
+
function plantOf(raw) {
|
|
11881
|
+
let v;
|
|
11882
|
+
try {
|
|
11883
|
+
v = JSON.parse(raw);
|
|
11884
|
+
} catch {
|
|
11885
|
+
return null;
|
|
11886
|
+
}
|
|
11887
|
+
const ok = (p) => {
|
|
11888
|
+
const x = p;
|
|
11889
|
+
return !!x && typeof x.place === "string" && !!x.place && typeof x.kind === "string" && !!x.kind && typeof x.factor === "number" && Number.isFinite(x.factor) && x.factor > 0 && (x.unit === "budget" || x.unit === "MB");
|
|
11890
|
+
};
|
|
11891
|
+
return Array.isArray(v) && v.length > 0 && v.length <= 200 && v.every(ok) ? v : null;
|
|
11892
|
+
}
|
|
11484
11893
|
var COMPLETION_MARKER = "run:complete";
|
|
11485
11894
|
var SETUP_FLAGS = [
|
|
11486
11895
|
["driver-version", "driver_version"],
|
|
@@ -11489,19 +11898,26 @@ var SETUP_FLAGS = [
|
|
|
11489
11898
|
["journey-version", "journey_version"],
|
|
11490
11899
|
["flag-set", "flag_set"]
|
|
11491
11900
|
];
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
|
|
11901
|
+
var LOCAL_PROVIDERS = ["local", "usb", "customer", "simulator", "emulator"];
|
|
11902
|
+
var COMMIT_ID = /^[0-9a-f]{40}$/i;
|
|
11903
|
+
function instrumentFlags(f, maestro = {}) {
|
|
11904
|
+
const given = f["journey-version"]?.trim();
|
|
11905
|
+
const named = given && !COMMIT_ID.test(given) ? given : undefined;
|
|
11906
|
+
if (!f["journey-module"] && f.driver !== "appium") {
|
|
11907
|
+
const driver = f["driver-version"] ?? maestro.maestroVersion;
|
|
11908
|
+
const journey = maestro.flowsVersion ?? named;
|
|
11909
|
+
return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
|
|
11910
|
+
}
|
|
11495
11911
|
let hash;
|
|
11496
11912
|
if (f["journey-module"]) {
|
|
11497
11913
|
try {
|
|
11498
|
-
hash = journeyVersion(
|
|
11914
|
+
hash = journeyVersion(resolve6(f["journey-module"]));
|
|
11499
11915
|
} catch {
|
|
11500
11916
|
hash = undefined;
|
|
11501
11917
|
}
|
|
11502
11918
|
}
|
|
11503
11919
|
const driver = f["driver-version"] ?? driverVersion() ?? undefined;
|
|
11504
|
-
const journey =
|
|
11920
|
+
const journey = hash ?? named;
|
|
11505
11921
|
return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
|
|
11506
11922
|
}
|
|
11507
11923
|
var TOKENS_STUB = "// Replaced by `keepsnappy farm open` for one upload. Never commit real tokens.\noutput.keepsnappyTokens = {};\n";
|
|
@@ -11562,7 +11978,8 @@ async function farmJourneys(deps, f, rt, journeys) {
|
|
|
11562
11978
|
if (!byName.has(j))
|
|
11563
11979
|
rt.print(`journey ${j}: no flow file named ${j}.yaml in --config/--flows; sent without a title`);
|
|
11564
11980
|
}
|
|
11565
|
-
|
|
11981
|
+
const info = await journeysFor(journeys.map((j) => ({ name: j, ...byName.get(j) ?? {} })), { readFile: (p) => deps.fs.readFile(p), root: files.length ? await repoRoot(deps) : null });
|
|
11982
|
+
return { journeys: info, files: files.map((x) => x.file) };
|
|
11566
11983
|
}
|
|
11567
11984
|
async function farmOpen(deps, f, rt) {
|
|
11568
11985
|
const problems = [];
|
|
@@ -11579,7 +11996,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
11579
11996
|
provider = "simulator";
|
|
11580
11997
|
model = model ? withSimulatorWord(model) : simulator;
|
|
11581
11998
|
}
|
|
11582
|
-
if (!model &&
|
|
11999
|
+
if (!model && LOCAL_PROVIDERS.includes(provider)) {
|
|
11583
12000
|
model = await detectDeviceModel(deps, { platform: f.platform, deviceId: f["device-id"]?.trim() }) ?? undefined;
|
|
11584
12001
|
if (model)
|
|
11585
12002
|
rt.print(`device model read from the device: ${model}`);
|
|
@@ -11606,18 +12023,29 @@ async function farmOpen(deps, f, rt) {
|
|
|
11606
12023
|
problems.push("--tokens-js <path> is required (the file the suite overlay puts at flows/keepsnappy/tokens.js)");
|
|
11607
12024
|
if (!f.state)
|
|
11608
12025
|
problems.push("--state <path> is required (read by keepsnappy farm close)");
|
|
12026
|
+
const plant = f.plant === undefined ? undefined : plantOf(f.plant);
|
|
12027
|
+
if (plant === null)
|
|
12028
|
+
problems.push('--plant must be a JSON list of { place, kind, factor > 0, unit: "budget" | "MB" }');
|
|
11609
12029
|
if (problems.length)
|
|
11610
12030
|
throw new ConfigError(problems);
|
|
11611
|
-
const statePath =
|
|
11612
|
-
const tokensPath =
|
|
12031
|
+
const statePath = resolve6(f.state);
|
|
12032
|
+
const tokensPath = resolve6(f["tokens-js"]);
|
|
11613
12033
|
if (await deps.fs.exists(statePath)) {
|
|
11614
12034
|
throw new ConfigError([`${statePath} already exists: close that run set first (keepsnappy farm close --state ${f.state}) or remove the file`]);
|
|
11615
12035
|
}
|
|
11616
12036
|
const api = apiTarget(url, f, rt);
|
|
11617
12037
|
const now = rt.now ?? Date.now;
|
|
11618
12038
|
const device = { model, os: f["device-os"] ?? null, provider };
|
|
11619
|
-
const journeyInfo = await farmJourneys(deps, f, rt, journeys);
|
|
11620
|
-
const
|
|
12039
|
+
const { journeys: journeyInfo, files: flowFiles } = await farmJourneys(deps, f, rt, journeys);
|
|
12040
|
+
const { "driver-version": _driver, "journey-version": given, ...rest } = f;
|
|
12041
|
+
const maestro = !f["journey-module"] && f.driver !== "appium";
|
|
12042
|
+
const flows = maestro ? await flowsVersion(flowFiles, (p) => deps.fs.readFile(p), [tokensPath]) : null;
|
|
12043
|
+
const asked = maestro && !f["driver-version"] && LOCAL_PROVIDERS.includes(provider) ? await maestroCliVersion(deps) : undefined;
|
|
12044
|
+
const instrument = instrumentFlags(f, { ...flows ? { flowsVersion: flows } : {}, ...asked ? { maestroVersion: asked } : {} });
|
|
12045
|
+
if (given?.trim() && given.trim() !== instrument["journey-version"]) {
|
|
12046
|
+
rt.print(`--journey-version ${given.trim()} ignored: ${instrument["journey-version"] ? `the journey version is the hash of what runs, ${instrument["journey-version"]}` : "a commit id names the checkout, not the journey; the run records no journey version"}`);
|
|
12047
|
+
}
|
|
12048
|
+
const withInstrument = { ...rest, ...instrument };
|
|
11621
12049
|
const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
|
|
11622
12050
|
const base = await baseCommitFor(deps, rt.env, { explicit: f["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
|
|
11623
12051
|
const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
|
|
@@ -11634,7 +12062,13 @@ async function farmOpen(deps, f, rt) {
|
|
|
11634
12062
|
journeys: journeyInfo,
|
|
11635
12063
|
repetitions: repeat,
|
|
11636
12064
|
source: "farm",
|
|
11637
|
-
...f["completion-marker"] || Object.keys(setup).length
|
|
12065
|
+
...f["completion-marker"] || Object.keys(setup).length || plant ? {
|
|
12066
|
+
meta: {
|
|
12067
|
+
...f["completion-marker"] ? { completion_marker: COMPLETION_MARKER } : {},
|
|
12068
|
+
...Object.keys(setup).length ? { setup } : {},
|
|
12069
|
+
...plant ? { plant } : {}
|
|
12070
|
+
}
|
|
12071
|
+
} : {}
|
|
11638
12072
|
}).catch(infra2(`cannot open a run set at ${api.url}`));
|
|
11639
12073
|
const prefix = `${git.commit ? git.commit.slice(0, 12) : "farm"}-${rs.run_set_id.slice(0, 8)}`;
|
|
11640
12074
|
const runs = [];
|
|
@@ -11665,7 +12099,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
11665
12099
|
if (rt.env.GITHUB_ACTIONS === "true")
|
|
11666
12100
|
for (const t of Object.values(tokens).flat())
|
|
11667
12101
|
rt.print(`::add-mask::${t}`);
|
|
11668
|
-
await deps.fs.mkdir(
|
|
12102
|
+
await deps.fs.mkdir(dirname4(tokensPath));
|
|
11669
12103
|
await deps.fs.writeFile(tokensPath, tokensJs(api.url, rs.run_set_id, tokens, maestroLinkFields(f["sdk-args"] ?? "")));
|
|
11670
12104
|
const state = {
|
|
11671
12105
|
version: STATE_VERSION,
|
|
@@ -11685,7 +12119,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
11685
12119
|
closed_at: null,
|
|
11686
12120
|
result: null
|
|
11687
12121
|
};
|
|
11688
|
-
await deps.fs.mkdir(
|
|
12122
|
+
await deps.fs.mkdir(dirname4(statePath));
|
|
11689
12123
|
await deps.fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}
|
|
11690
12124
|
`);
|
|
11691
12125
|
await githubOutputs(deps, rt, {
|
|
@@ -11791,7 +12225,7 @@ async function farmClose(deps, f, rt) {
|
|
|
11791
12225
|
throw new ConfigError(["--state <path> is required (written by keepsnappy farm open)"]);
|
|
11792
12226
|
if (f["fail-on-worse"] && !f.wait)
|
|
11793
12227
|
throw new ConfigError(["--fail-on-worse needs --wait"]);
|
|
11794
|
-
const statePath =
|
|
12228
|
+
const statePath = resolve6(f.state);
|
|
11795
12229
|
let state;
|
|
11796
12230
|
try {
|
|
11797
12231
|
state = JSON.parse(await deps.fs.readFile(statePath));
|
|
@@ -11818,10 +12252,10 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11818
12252
|
const api = apiTarget(f.api ?? f["ingest-url"] ?? state.api, f, rt);
|
|
11819
12253
|
const now = rt.now ?? Date.now;
|
|
11820
12254
|
const sleep = rt.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
11821
|
-
const out =
|
|
12255
|
+
const out = resolve6(f.out ?? dirname4(statePath));
|
|
11822
12256
|
let bsText = null;
|
|
11823
12257
|
if (f["bs-build"])
|
|
11824
|
-
bsText = await deps.fs.readFile(
|
|
12258
|
+
bsText = await deps.fs.readFile(resolve6(f["bs-build"])).catch(() => null);
|
|
11825
12259
|
const session = sessionOutcome(bsText);
|
|
11826
12260
|
const where = `${state.branch}${state.commit ? ` @ ${state.commit.slice(0, 7)}` : ""}`;
|
|
11827
12261
|
rt.print(`KeepSnappy run set ${state.run_set_id} (${where}): closing`);
|
|
@@ -11906,7 +12340,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11906
12340
|
// apps/agent/src/farm-run.ts
|
|
11907
12341
|
import { existsSync as existsSync2 } from "node:fs";
|
|
11908
12342
|
import { createRequire } from "node:module";
|
|
11909
|
-
import { dirname as
|
|
12343
|
+
import { dirname as dirname5, join as join16, resolve as resolve7 } from "node:path";
|
|
11910
12344
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11911
12345
|
|
|
11912
12346
|
// apps/agent/src/app-source.ts
|
|
@@ -12179,10 +12613,10 @@ ${r.stderr}`;
|
|
|
12179
12613
|
// apps/agent/src/farm-run.ts
|
|
12180
12614
|
function appiumDriverEntry(o = {}) {
|
|
12181
12615
|
const exists = o.exists ?? existsSync2;
|
|
12182
|
-
const bundled = join16(o.selfDir ??
|
|
12616
|
+
const bundled = join16(o.selfDir ?? dirname5(fileURLToPath2(import.meta.url)), "appium-driver.js");
|
|
12183
12617
|
if (exists(bundled))
|
|
12184
12618
|
return bundled;
|
|
12185
|
-
const pkgDir =
|
|
12619
|
+
const pkgDir = dirname5(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
|
|
12186
12620
|
const built = join16(pkgDir, "dist", "src", "cli.js");
|
|
12187
12621
|
return exists(built) ? built : join16(pkgDir, "src", "cli.ts");
|
|
12188
12622
|
}
|
|
@@ -12281,7 +12715,7 @@ function planFarmRun(f) {
|
|
|
12281
12715
|
ports,
|
|
12282
12716
|
env,
|
|
12283
12717
|
driverArgs: f["driver-arg"] ?? [],
|
|
12284
|
-
out:
|
|
12718
|
+
out: resolve7(f.out ?? "farm-run"),
|
|
12285
12719
|
timeoutMinutes,
|
|
12286
12720
|
killAfterSeconds,
|
|
12287
12721
|
pollSeconds
|
|
@@ -12330,7 +12764,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
12330
12764
|
device: p.device,
|
|
12331
12765
|
app: ctx.app,
|
|
12332
12766
|
...f["custom-id"] ? { customId: f["custom-id"] } : {},
|
|
12333
|
-
suiteDir:
|
|
12767
|
+
suiteDir: resolve7(f["suite-dir"]),
|
|
12334
12768
|
tokensPath: f["tokens-path"] ?? "flows/keepsnappy/tokens.js",
|
|
12335
12769
|
tokensJs: ctx.tokensJs,
|
|
12336
12770
|
execute: f.execute,
|
|
@@ -12356,7 +12790,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
12356
12790
|
throw new Error("no Appium driver entry: pass --driver-script");
|
|
12357
12791
|
const args = [
|
|
12358
12792
|
entry,
|
|
12359
|
-
...f["journey-module"] ? ["--journey-module",
|
|
12793
|
+
...f["journey-module"] ? ["--journey-module", resolve7(f["journey-module"])] : [],
|
|
12360
12794
|
"--platform",
|
|
12361
12795
|
p.platform,
|
|
12362
12796
|
"--device",
|
|
@@ -12433,7 +12867,8 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
12433
12867
|
driver: p.driver,
|
|
12434
12868
|
"tokens-js": tokensJs,
|
|
12435
12869
|
state: statePath,
|
|
12436
|
-
...commit ? { commit } : {}
|
|
12870
|
+
...commit ? { commit } : {},
|
|
12871
|
+
...p.driver !== "appium" && !f["driver-version"] && f["maestro-version"] ? { "driver-version": f["maestro-version"] } : {}
|
|
12437
12872
|
}, rt);
|
|
12438
12873
|
if (openCode !== EXIT.ok)
|
|
12439
12874
|
return openCode;
|
|
@@ -12476,15 +12911,15 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
12476
12911
|
}
|
|
12477
12912
|
|
|
12478
12913
|
// apps/agent/src/sourcemaps.ts
|
|
12479
|
-
import { createHash as
|
|
12914
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
12480
12915
|
import { access, readFile as readFile4, readdir as readdir2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
12481
|
-
import { basename as basename3, join as join17, resolve as
|
|
12916
|
+
import { basename as basename3, join as join17, resolve as resolve8 } from "node:path";
|
|
12482
12917
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
12483
12918
|
import { gzipSync } from "node:zlib";
|
|
12484
12919
|
var MAX_COMPLETE_ROUNDS = 20;
|
|
12485
12920
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
12486
12921
|
async function prepareComponents(projectRoot, options = {}) {
|
|
12487
|
-
const root =
|
|
12922
|
+
const root = resolve8(projectRoot);
|
|
12488
12923
|
const directory = join17(root, ".keepsnappy", "components");
|
|
12489
12924
|
const configurations = await readdir2(directory, { withFileTypes: true }).catch((error) => {
|
|
12490
12925
|
if (error.code === "ENOENT")
|
|
@@ -12525,7 +12960,7 @@ async function prepareComponents(projectRoot, options = {}) {
|
|
|
12525
12960
|
throw new ConfigError([`invalid KeepSnappy component fragment ${file}`]);
|
|
12526
12961
|
if (!hashes.has(fragment.source)) {
|
|
12527
12962
|
try {
|
|
12528
|
-
hashes.set(fragment.source,
|
|
12963
|
+
hashes.set(fragment.source, createHash3("sha256").update(await readFile4(resolve8(root, fragment.source))).digest("hex"));
|
|
12529
12964
|
} catch (error) {
|
|
12530
12965
|
if (error.code !== "ENOENT")
|
|
12531
12966
|
throw error;
|
|
@@ -12550,7 +12985,7 @@ async function prepareComponents(projectRoot, options = {}) {
|
|
|
12550
12985
|
await rm(temporary, { force: true });
|
|
12551
12986
|
}
|
|
12552
12987
|
const gzip = gzipSync(text);
|
|
12553
|
-
return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256:
|
|
12988
|
+
return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash3("sha256").update(gzip).digest("hex") };
|
|
12554
12989
|
}
|
|
12555
12990
|
function stripSourcesContent(map) {
|
|
12556
12991
|
if (!isObject(map))
|
|
@@ -12604,7 +13039,7 @@ async function uploadSourcemaps(opts) {
|
|
|
12604
13039
|
const { map, stripped } = stripSourcesContent(parsed);
|
|
12605
13040
|
const text = Buffer.from(JSON.stringify(map));
|
|
12606
13041
|
const gz = gzipSync(text);
|
|
12607
|
-
const sha256 =
|
|
13042
|
+
const sha256 = createHash3("sha256").update(gz).digest("hex");
|
|
12608
13043
|
const name = basename3(file);
|
|
12609
13044
|
const announced = await call(f, opts.url, `/v1/builds/${encodeURIComponent(opts.build)}/sourcemaps`, headers, {
|
|
12610
13045
|
platform: opts.platform,
|
|
@@ -12743,6 +13178,7 @@ import { parseArgs as parseArgs3 } from "node:util";
|
|
|
12743
13178
|
var SHORT_SESSIONS = 3;
|
|
12744
13179
|
var SHORT_POINTS = 5;
|
|
12745
13180
|
var SHORT_PLACES = 5;
|
|
13181
|
+
var SHORT_COMPARE_MEASURES = 3;
|
|
12746
13182
|
var SHORT_FORM_NOTES = {
|
|
12747
13183
|
list_findings: "the how-sure lines, the place lines of findings that are not Worse and the lanes that were judged",
|
|
12748
13184
|
get_finding: "the why, seen and layer lines, the component lines, the memory evidence lines other than the judged measure and the prompt",
|
|
@@ -12752,7 +13188,8 @@ var SHORT_FORM_NOTES = {
|
|
|
12752
13188
|
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
12753
13189
|
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
12754
13190
|
pr_status: "the per-journey dashboard links and the heat note",
|
|
12755
|
-
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}
|
|
13191
|
+
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}`,
|
|
13192
|
+
compare_runs: `the component lines and the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`
|
|
12756
13193
|
};
|
|
12757
13194
|
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
12758
13195
|
|
|
@@ -12855,10 +13292,11 @@ function measuredLines(l, r, opts) {
|
|
|
12855
13292
|
}
|
|
12856
13293
|
const lane = (x) => {
|
|
12857
13294
|
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}` : "";
|
|
12858
|
-
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)}` : ""}`;
|
|
13295
|
+
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${feltWords(x)}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
|
|
12859
13296
|
};
|
|
12860
13297
|
if (m.lanes.length === 1) {
|
|
12861
13298
|
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
13299
|
+
r.both(...(n.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12862
13300
|
r.full(...(n.session_speed ?? []).map((line) => ` ${line}`));
|
|
12863
13301
|
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
12864
13302
|
return [];
|
|
@@ -12866,9 +13304,10 @@ function measuredLines(l, r, opts) {
|
|
|
12866
13304
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
12867
13305
|
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
12868
13306
|
for (const x of shown) {
|
|
12869
|
-
const keep = !x.judged || !!x.context;
|
|
13307
|
+
const keep = !x.judged || !!x.context || !!x.comparison_notes;
|
|
12870
13308
|
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12871
13309
|
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
13310
|
+
r.add(keep, ...(x.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12872
13311
|
r.full(...(x.session_speed ?? []).map((line) => ` ${line}`));
|
|
12873
13312
|
}
|
|
12874
13313
|
if (m.lanes.length > shown.length)
|
|
@@ -12879,6 +13318,7 @@ function measuredLines(l, r, opts) {
|
|
|
12879
13318
|
...m.lanes.length > shown.length ? [plural2(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []
|
|
12880
13319
|
];
|
|
12881
13320
|
}
|
|
13321
|
+
var feltWords = (x) => `${x.felt_cost ? ` · ${x.felt_cost}` : ""}${x.to_check ? `; ${SUMMARY_WORDS.toCheck(x.to_check)}` : ""}`;
|
|
12882
13322
|
function countsLine2(l) {
|
|
12883
13323
|
const c = l.counts;
|
|
12884
13324
|
const parts = [
|
|
@@ -12932,10 +13372,11 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12932
13372
|
if (s.lanes.length)
|
|
12933
13373
|
r.both("");
|
|
12934
13374
|
for (const l of s.lanes) {
|
|
12935
|
-
const shown = l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)
|
|
13375
|
+
const shown = `${l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)}${l.stale ? ` (${STALE_FLAG})` : ""}`;
|
|
12936
13376
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
12937
|
-
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown} · ${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)}` : ""}`);
|
|
13377
|
+
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown}${feltWords(l)} · ${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)}` : ""}`);
|
|
12938
13378
|
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
13379
|
+
r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12939
13380
|
r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
|
|
12940
13381
|
for (const f of l.worst)
|
|
12941
13382
|
r.both(` ${f.id} ${f.headline}`);
|
|
@@ -12945,6 +13386,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12945
13386
|
r.full(` ${l.link}`);
|
|
12946
13387
|
}
|
|
12947
13388
|
const links = s.lanes.filter((l) => l.link).length;
|
|
13389
|
+
const staleLanes = s.problems?.stale_lanes ?? 0;
|
|
12948
13390
|
const heated = s.counts.heat_runs ?? 0;
|
|
12949
13391
|
const more = [
|
|
12950
13392
|
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (information, never blocks)` : ""],
|
|
@@ -12953,6 +13395,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12953
13395
|
s.not_judged_findings ? `${plural2(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` : ""
|
|
12954
13396
|
],
|
|
12955
13397
|
[true, statusProblemsLine(s)],
|
|
13398
|
+
[true, staleLanes ? `${staleWords(staleLanes)}: ${staleLanes === 1 ? "its word and problems are" : "their words and problems are"} of older rules` : ""],
|
|
12956
13399
|
[true, s.problems?.unread_lanes ? `${unreadWords(s.problems.unread_lanes)} for problems: ${staleResult(s.problems.unread_lanes)}` : ""],
|
|
12957
13400
|
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
12958
13401
|
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
@@ -12968,9 +13411,11 @@ function statusProblemsLine(s) {
|
|
|
12968
13411
|
const p = s.problems;
|
|
12969
13412
|
if (!p?.count)
|
|
12970
13413
|
return "";
|
|
12971
|
-
return `${
|
|
13414
|
+
return `${problemCountsText(p, "this branch’s")}. Not a regression, never blocks: list_problems, or keepsnappy problems --branch ${s.branch}`;
|
|
12972
13415
|
}
|
|
12973
13416
|
var unreadWords = (n) => `${n} journey × phone not read`;
|
|
13417
|
+
var STALE_FLAG = staleFlagText(["config"]);
|
|
13418
|
+
var staleWords = (n) => `${n} journey × phone from a stale result (${STALE_FLAG})`;
|
|
12974
13419
|
var staleResult = (n) => `${n === 1 ? "its stored result is" : "their stored results are"} not current`;
|
|
12975
13420
|
function formatProblemList(l, opts = {}) {
|
|
12976
13421
|
const r = new Rows;
|
|
@@ -12987,7 +13432,7 @@ function formatProblemList(l, opts = {}) {
|
|
|
12987
13432
|
return r.render(opts.form);
|
|
12988
13433
|
}
|
|
12989
13434
|
if (c.count)
|
|
12990
|
-
r.both(`${b}: ${
|
|
13435
|
+
r.both(`${b}: ${problemCountsText(c, b === l.baseline_branch ? "main’s" : "this branch’s")}`);
|
|
12991
13436
|
else if (read)
|
|
12992
13437
|
r.both(`${b}: no problem over budget in the newest run set (${read} journey × phone read)`);
|
|
12993
13438
|
else
|
|
@@ -12995,13 +13440,15 @@ function formatProblemList(l, opts = {}) {
|
|
|
12995
13440
|
r.both(l.note);
|
|
12996
13441
|
if (read && unread)
|
|
12997
13442
|
r.both(`${unreadWords(unread)}: ${staleResult(unread)}, so nothing is known about ${unread === 1 ? "its" : "their"} problems.`);
|
|
13443
|
+
if (c.stale_lanes)
|
|
13444
|
+
r.both(`${staleWords(c.stale_lanes)}: ${c.stale_lanes === 1 ? "its" : "their"} problems are listed, each marked “${STALE_MARK}”.`);
|
|
12998
13445
|
l.problems.forEach((p, i) => {
|
|
12999
13446
|
const keep = i < SHORT_PLACES;
|
|
13000
|
-
r.add(keep, "", `${l.offset + i + 1}. ${p.
|
|
13001
|
-
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}`);
|
|
13447
|
+
r.add(keep, "", `${l.offset + i + 1}. ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}: ${p.text}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
|
|
13448
|
+
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}${p.stale ? `, ${STALE_MARK}` : ""}`);
|
|
13002
13449
|
const top = p.top_components[0];
|
|
13003
13450
|
const topName = componentReadNames(p.top_components)[0];
|
|
13004
|
-
const detail = [p.work, top ?
|
|
13451
|
+
const detail = [p.work, top ? `${namePartsText(heaviestWords(`${topName}${opts.verbose && top.key ? ` (${top.key})` : ""}`, top.self_ms === null ? null : top.self_text))}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
|
|
13005
13452
|
if (detail.length)
|
|
13006
13453
|
r.full(` ${detail.join("; ")}`);
|
|
13007
13454
|
if (p.url)
|
|
@@ -13021,56 +13468,59 @@ function formatProblemList(l, opts = {}) {
|
|
|
13021
13468
|
}
|
|
13022
13469
|
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";
|
|
13023
13470
|
function formatCompare(r, opts = {}) {
|
|
13024
|
-
const
|
|
13025
|
-
|
|
13026
|
-
`Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`,
|
|
13027
|
-
`${r.same_journey ? "Same journey" : "Different journeys"}, ${r.same_phone ? "same phone" : "different phones"}. ${r.note}`
|
|
13028
|
-
];
|
|
13471
|
+
const rows = new Rows;
|
|
13472
|
+
rows.both(`Run A: ${r.a.external_id} on ${r.a.branch ?? "?"}, ${commitText(r.a.commit ? { sha: r.a.commit_sha, ...r.a.commit } : { sha: r.a.commit_sha, title: null, author: null })}`, `Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`, `${r.same_journey ? "Same journey" : "Different journeys"}, ${r.same_phone ? "same phone" : "different phones"}. ${r.note}`);
|
|
13029
13473
|
if (r.setup_differs.length)
|
|
13030
|
-
|
|
13474
|
+
rows.both(`Measured differently: ${r.setup_differs.map((d) => `${d.field} ${JSON.stringify(d.a)} → ${JSON.stringify(d.b)}`).join(", ")}`);
|
|
13031
13475
|
for (const w of r.windows) {
|
|
13032
13476
|
const ms = w.metrics.filter((m) => m.a !== null || m.b !== null);
|
|
13033
13477
|
const c = w.component_page ?? w.components;
|
|
13034
13478
|
if (!ms.length && !c)
|
|
13035
13479
|
continue;
|
|
13036
|
-
|
|
13480
|
+
rows.both("", `${w.window_name}${opts.verbose ? ` (${w.window})` : ""}`);
|
|
13037
13481
|
const singles = [...c?.added ?? [], ...c?.removed ?? [], ...w.component_page?.uncompared?.a ?? [], ...w.component_page?.uncompared?.b ?? []];
|
|
13038
13482
|
const components = [...c?.rows ?? [], ...singles];
|
|
13039
13483
|
const names = componentReadNames(components.map((row) => ({ key: row.key, name: row.component })));
|
|
13040
13484
|
const shown = new Map(components.map((row, i) => [row.key, names[i]]));
|
|
13041
13485
|
for (const row of c?.rows ?? [])
|
|
13042
|
-
|
|
13043
|
-
for (const [label,
|
|
13486
|
+
rows.full(` ${shown.get(row.key)}: ${row.self_ms_a === null ? "–" : exactComponentMs(row.self_ms_a)} → ${row.self_ms_b === null ? "–" : exactComponentMs(row.self_ms_b)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13487
|
+
for (const [label, list] of [
|
|
13044
13488
|
[READ_WORDS.onlyB, c?.added ?? []],
|
|
13045
13489
|
[READ_WORDS.onlyA, c?.removed ?? []]
|
|
13046
13490
|
]) {
|
|
13047
|
-
for (const row of
|
|
13048
|
-
|
|
13491
|
+
for (const row of list)
|
|
13492
|
+
rows.full(` ${shown.get(row.key)}: ${label}; ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13049
13493
|
}
|
|
13050
13494
|
const page = w.component_page;
|
|
13051
13495
|
if (page) {
|
|
13052
13496
|
for (const side of ["a", "b"]) {
|
|
13053
13497
|
if (!page.measured[side])
|
|
13054
|
-
|
|
13498
|
+
rows.full(` ${notMeasuredText(side)}`);
|
|
13055
13499
|
for (const row of page.uncompared?.[side] ?? [])
|
|
13056
|
-
|
|
13500
|
+
rows.full(` ${shown.get(row.key)}: ${side === "a" ? READ_WORDS.runA : READ_WORDS.runB} ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13057
13501
|
}
|
|
13058
|
-
|
|
13502
|
+
rows.full(` ${componentPageText(page.page, components.length)}`);
|
|
13059
13503
|
} else if (c?.totals)
|
|
13060
|
-
|
|
13504
|
+
rows.full(` ${componentListsText(c.totals, { rows: c.rows.length, added: c.added?.length ?? 0, removed: c.removed?.length ?? 0 })}`);
|
|
13061
13505
|
if (c?.omissions)
|
|
13062
13506
|
for (const side of ["a", "b"]) {
|
|
13063
13507
|
if (page && !page.measured[side])
|
|
13064
13508
|
continue;
|
|
13065
13509
|
const o = c.omissions[side];
|
|
13066
13510
|
if (o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0)
|
|
13067
|
-
|
|
13511
|
+
rows.full(` ${omissionText({ run_id: side === "a" ? READ_WORDS.runA : READ_WORDS.runB, ...o })}`);
|
|
13068
13512
|
}
|
|
13513
|
+
const top = new Set(ms.map((m) => ({ m, by: timeMove(m.metric, m.a, m.b) })).filter((x) => x.by !== null && x.by > 0).sort((x, y) => y.by - x.by).slice(0, SHORT_COMPARE_MEASURES).map((x) => x.m));
|
|
13069
13514
|
for (const m of ms)
|
|
13070
|
-
|
|
13515
|
+
rows.add(top.has(m), ` ${m.label.padEnd(28)}${m.a_text.padStart(10)} → ${m.b_text}`);
|
|
13071
13516
|
}
|
|
13072
|
-
|
|
13073
|
-
|
|
13517
|
+
const p = r.windows_page;
|
|
13518
|
+
if (p) {
|
|
13519
|
+
const order = p.order === "moved" ? ", the ones whose time moved most first" : "";
|
|
13520
|
+
const shownText = r.windows.length ? `Showing places ${p.offset + 1}–${p.offset + r.windows.length} of ${p.total}${order}` : `Showing none of ${p.total} places`;
|
|
13521
|
+
rows.both("", p.next_offset !== null ? `${shownText}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).` : `${shownText}.`);
|
|
13522
|
+
}
|
|
13523
|
+
return rows.render(opts.form, ["the component lines", `the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`]);
|
|
13074
13524
|
}
|
|
13075
13525
|
function formatHistory(h, opts = {}) {
|
|
13076
13526
|
const r = new Rows;
|
|
@@ -13102,8 +13552,8 @@ function formatHistory(h, opts = {}) {
|
|
|
13102
13552
|
}
|
|
13103
13553
|
function formatCommits(c) {
|
|
13104
13554
|
return [
|
|
13105
|
-
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) on ${c.branch}, oldest first:`,
|
|
13106
|
-
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
13555
|
+
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) ${c.from_branch ? `from ${c.from_branch} to ${c.branch}` : `on ${c.branch}`}, oldest first:`,
|
|
13556
|
+
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${c.from_branch && x.branch ? ` on ${x.branch}` : ""}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
13107
13557
|
c.note
|
|
13108
13558
|
].join(`
|
|
13109
13559
|
`);
|
|
@@ -13139,7 +13589,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
13139
13589
|
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);
|
|
13140
13590
|
const by = p.filters.measure;
|
|
13141
13591
|
const byLabel = p.places.flatMap((x) => x.measures).find((m) => m.measure === by)?.label ?? by;
|
|
13142
|
-
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` : ""}.`, "");
|
|
13592
|
+
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` : ""}${p.folded_twins ? `; ${foldedTwinsText(p.folded_twins)}` : ""}.`, "");
|
|
13143
13593
|
let componentLines = 0;
|
|
13144
13594
|
p.places.forEach((place, i) => {
|
|
13145
13595
|
const m = place.measures.find((x) => x.measure === by);
|
|
@@ -13212,7 +13662,7 @@ function formatMainSessions(h, opts = {}) {
|
|
|
13212
13662
|
if (!s.places)
|
|
13213
13663
|
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
13214
13664
|
else
|
|
13215
|
-
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)}` : ""}`);
|
|
13665
|
+
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)}` : s.cant_range ? `; ${mainSessionCantRangeWords(s.places.cant, s.cant_range, baseline)}` : ""}`);
|
|
13216
13666
|
if (s.waiting_sessions)
|
|
13217
13667
|
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
13218
13668
|
if (s.memory)
|
|
@@ -13254,9 +13704,13 @@ function formatMainSessionPlaces(p, opts = {}) {
|
|
|
13254
13704
|
return r.both(`${MAIN_SUMMARY_PENDING}${p.summary_reason ? ` (${p.summary_reason})` : ""}`).render(opts.form);
|
|
13255
13705
|
const baselineSessions = p.baseline_sessions;
|
|
13256
13706
|
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13707
|
+
const kept = new Set(foldTwins([...p.places].sort((a, b) => MAIN_TIER_RANK[a.tier] - MAIN_TIER_RANK[b.tier]), (x) => x.place, (x) => x.ref).rows);
|
|
13708
|
+
const places = p.places.filter((x) => kept.has(x));
|
|
13709
|
+
for (const x of places)
|
|
13710
|
+
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, places.filter((row) => row.place === x.place).map((row) => row.ref))}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}${x.reason && mainUnstableWords(x.reason) ? `. ${mainUnstableWords(x.reason)}` : ""}`);
|
|
13711
|
+
if (places.length < p.places.length)
|
|
13712
|
+
r.both(` ${foldedTwinsText(p.places.length - places.length)}`);
|
|
13713
|
+
const inNorm = places.filter((x) => x.tier === "same").length;
|
|
13260
13714
|
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
13261
13715
|
}
|
|
13262
13716
|
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
@@ -13295,6 +13749,12 @@ function renamedLines(renamed, places) {
|
|
|
13295
13749
|
...component ? [` ${componentNotJoinedText(renamed.sdk)}`] : []
|
|
13296
13750
|
];
|
|
13297
13751
|
}
|
|
13752
|
+
function storedValueText(ref, v, unit) {
|
|
13753
|
+
const rest = ref.slice(2);
|
|
13754
|
+
const field = ref.startsWith("b:") ? `${rest.slice(0, rest.lastIndexOf(":"))}.${rest.slice(rest.lastIndexOf(":") + 1)}` : rest;
|
|
13755
|
+
const text = fieldValueText(field, v);
|
|
13756
|
+
return unit && !/[^\d.,\s-]/.test(text) ? `${text} ${unit}` : text;
|
|
13757
|
+
}
|
|
13298
13758
|
function formatPlaceTrend(t, opts = {}) {
|
|
13299
13759
|
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
13300
13760
|
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
@@ -13315,10 +13775,10 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
13315
13775
|
const value = place.sessions.find((s) => s.run_set_id === session.run_set_id);
|
|
13316
13776
|
if (!value)
|
|
13317
13777
|
continue;
|
|
13318
|
-
const
|
|
13778
|
+
const value_ = (v) => storedValueText(place.ref, v, place.unit);
|
|
13319
13779
|
if (!k)
|
|
13320
13780
|
olderSessions.add(session.run_set_id);
|
|
13321
|
-
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${value.window ?? place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}
|
|
13781
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${value.window ?? place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value_(value.median)}, MAD ${value_(value.mad)}, n=${value.n}`);
|
|
13322
13782
|
}
|
|
13323
13783
|
}
|
|
13324
13784
|
if (!t.sessions.length)
|
|
@@ -13375,6 +13835,8 @@ var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
|
13375
13835
|
var listProblems = (t, f = {}) => get(t, "v1/problems", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, limit: f.limit, offset: f.offset || undefined });
|
|
13376
13836
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
13377
13837
|
var compareRuns = (t, a, b, page = {}) => get(t, "v1/compare", { a, b, ...page });
|
|
13838
|
+
var SHORT_COMPARE_PLACES = 10;
|
|
13839
|
+
var compareQuery = (short, page) => short ? { order: "moved", limit: SHORT_COMPARE_PLACES, ...page } : page;
|
|
13378
13840
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
13379
13841
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
13380
13842
|
var layers = (t) => get(t, "v1/layers");
|
|
@@ -13409,7 +13871,7 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
13409
13871
|
problems --branch NAME | --main [--journey KEY] [--phone MODEL] [--limit N] [--offset N] [--short] [--json]
|
|
13410
13872
|
(the single problems "Over budget" / "Far over budget" in the branch's own runs, whatever main does: not a regression, never blocking)
|
|
13411
13873
|
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
13412
|
-
compare <runA> <runB> [--json]
|
|
13874
|
+
compare <runA> <runB> [--short] [--limit N] [--offset N] [--json] (run ids or external ids; one run against one, no statistics; --short: the places whose time moved most)
|
|
13413
13875
|
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
13414
13876
|
commits <from> [<to>] [--branch NAME] [--json] (measured commits between two run sets or commit hashes)
|
|
13415
13877
|
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]
|
|
@@ -13425,7 +13887,7 @@ ${READ_USAGE.detailFlag}
|
|
|
13425
13887
|
--short (findings, status, problems, finding, history, places, main, trend: what decides, without the evidence detail; the same as the MCP tools' form short; --json is unchanged)
|
|
13426
13888
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
13427
13889
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
13428
|
-
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "history", "places", "main", "trend"]);
|
|
13890
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "places", "main", "trend"]);
|
|
13429
13891
|
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
13430
13892
|
async function findingsCommand(cmd, argv, rt) {
|
|
13431
13893
|
const args = (() => {
|
|
@@ -13564,7 +14026,8 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
13564
14026
|
const size = o["top-components"] === undefined ? 12 : Number(o["top-components"]);
|
|
13565
14027
|
if (!Number.isInteger(size) || size < 1 || size > 200)
|
|
13566
14028
|
throw new UsageError([READ_USAGE.compareTop]);
|
|
13567
|
-
const
|
|
14029
|
+
const components = page || o.all || o["top-components"] !== undefined ? { component_offset: ((page ?? 1) - 1) * size, component_limit: size } : {};
|
|
14030
|
+
const query = compareQuery(o.short === true, { ...components, ...limit ? { limit } : {}, ...offset !== undefined ? { offset } : {} });
|
|
13568
14031
|
const compared = await compareRuns(t, p[0], p[1], query);
|
|
13569
14032
|
return out(o.all ? await allCompare(t, compared, size) : compared, formatCompare);
|
|
13570
14033
|
}
|
|
@@ -13728,6 +14191,7 @@ var USAGE = `keepsnappy <command>
|
|
|
13728
14191
|
[--config keepsnappy.yml] [--flows <files|dirs|globs,...>] (journey titles: keepsnappy.yml flows.paths, else the flow's name: / top comment)
|
|
13729
14192
|
[--completion-marker] (the flow posts step run:complete at the end of each iteration; runs without it are invalid)
|
|
13730
14193
|
[--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)
|
|
14194
|
+
[--plant JSON] (what the build plants against main, [{ place, kind, factor, unit: budget|MB }]: the run set's meta.plant)
|
|
13731
14195
|
[--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
|
|
13732
14196
|
farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
|
|
13733
14197
|
(--fail-on-worse as for run: with no verdict computed on the run set it checks nothing)
|
|
@@ -13823,6 +14287,7 @@ async function main2(argv) {
|
|
|
13823
14287
|
"harness-version": { type: "string" },
|
|
13824
14288
|
"journey-version": { type: "string" },
|
|
13825
14289
|
"flag-set": { type: "string" },
|
|
14290
|
+
plant: { type: "string" },
|
|
13826
14291
|
app: { type: "string" },
|
|
13827
14292
|
driver: { type: "string" },
|
|
13828
14293
|
"tunnel-for": { type: "string", multiple: true },
|
|
@@ -13873,9 +14338,9 @@ async function main2(argv) {
|
|
|
13873
14338
|
if (platform === "ios-device" && !o.device)
|
|
13874
14339
|
throw new ConfigError(["--device <udid> is required for ios-device"]);
|
|
13875
14340
|
const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
|
|
13876
|
-
const out =
|
|
14341
|
+
const out = resolve9(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
13877
14342
|
const udid = o.device ?? "booted";
|
|
13878
|
-
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid,
|
|
14343
|
+
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve9(out, ".scratch"));
|
|
13879
14344
|
const installer = platform === "ios-sim" ? new IosSimulatorInstaller(deps, udid) : new IosDeviceInstaller(deps, udid);
|
|
13880
14345
|
const bundleId = await bundleIdOf(deps, arms[0]?.target, o["bundle-id"]);
|
|
13881
14346
|
let markers;
|
|
@@ -13894,7 +14359,7 @@ async function main2(argv) {
|
|
|
13894
14359
|
outDir: out,
|
|
13895
14360
|
testCommand: positionals,
|
|
13896
14361
|
bundleId,
|
|
13897
|
-
...o["ri-capture"] ? { ri: new FileRiSource(deps,
|
|
14362
|
+
...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve9(o["ri-capture"])) } : {},
|
|
13898
14363
|
...o["mem-window"] ? { memWindow: o["mem-window"] } : {},
|
|
13899
14364
|
...o.timeout ? { testTimeoutMs: Number(o.timeout) * 1000 } : {}
|
|
13900
14365
|
});
|
|
@@ -13906,9 +14371,9 @@ async function main2(argv) {
|
|
|
13906
14371
|
const dir = positionals[0];
|
|
13907
14372
|
if (!dir)
|
|
13908
14373
|
throw new ConfigError(["extract needs <out-dir>"]);
|
|
13909
|
-
const ri = o["ri-capture"] ? new FileRiSource(deps,
|
|
13910
|
-
const rows = await extract(deps,
|
|
13911
|
-
console.log(`${rows.length} rows -> ${
|
|
14374
|
+
const ri = o["ri-capture"] ? new FileRiSource(deps, resolve9(o["ri-capture"])) : undefined;
|
|
14375
|
+
const rows = await extract(deps, resolve9(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
|
|
14376
|
+
console.log(`${rows.length} rows -> ${resolve9(dir, "metrics.csv")}`);
|
|
13912
14377
|
return 0;
|
|
13913
14378
|
}
|
|
13914
14379
|
if (cmd === "aa") {
|
|
@@ -13916,7 +14381,7 @@ async function main2(argv) {
|
|
|
13916
14381
|
if (!dir)
|
|
13917
14382
|
throw new ConfigError(["aa needs <out-dir>"]);
|
|
13918
14383
|
const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
|
|
13919
|
-
console.log((await aa(deps,
|
|
14384
|
+
console.log((await aa(deps, resolve9(dir), { config })).table);
|
|
13920
14385
|
return 0;
|
|
13921
14386
|
}
|
|
13922
14387
|
console.error(USAGE);
|