@keepsnappy/agent 0.1.0-alpha.17 → 0.1.0-alpha.19
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 +6 -6
- package/dist/appium-driver.js +30 -20
- package/dist/keepsnappy.js +1538 -459
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -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",
|
|
@@ -2198,6 +2208,7 @@ function isComponentConfiguration(value) {
|
|
|
2198
2208
|
}
|
|
2199
2209
|
|
|
2200
2210
|
// packages/facts/dist/src/places.js
|
|
2211
|
+
var COMMIT_IDENTITY_KINDS = ["screen", "press", "drag", "other"];
|
|
2201
2212
|
var placeWithoutVisit = (id) => placeRef(id).stem.replace(/#(\d+|repeat)$/, "");
|
|
2202
2213
|
var AFTER_3S = "+3s";
|
|
2203
2214
|
var kindOf = (stem) => {
|
|
@@ -2323,7 +2334,7 @@ function withLaunchNames(windows) {
|
|
|
2323
2334
|
}
|
|
2324
2335
|
|
|
2325
2336
|
// packages/derive/dist/src/model.js
|
|
2326
|
-
var DERIVATION_VERSION =
|
|
2337
|
+
var DERIVATION_VERSION = 28;
|
|
2327
2338
|
// packages/stats/dist/src/core/rng.js
|
|
2328
2339
|
function splitmix32(seed) {
|
|
2329
2340
|
let s = seed >>> 0;
|
|
@@ -3180,6 +3191,44 @@ function estimateClockOffset(markers, windows, byOrder = false) {
|
|
|
3180
3191
|
const negatives = gaps.filter((g) => g < 0);
|
|
3181
3192
|
return negatives.length * 2 > starts.length ? Math.min(...negatives) : 0;
|
|
3182
3193
|
}
|
|
3194
|
+
var ulp = (x) => x === 0 ? Number.MIN_VALUE : 2 ** (Math.floor(Math.log2(Math.abs(x))) - 52);
|
|
3195
|
+
function fitClockOffset(markers, windows, offset, errorMs) {
|
|
3196
|
+
if (!(errorMs > 0))
|
|
3197
|
+
return offset;
|
|
3198
|
+
const inputs = windows.map(inputWall).filter((x) => x !== undefined).sort((a, b) => a - b);
|
|
3199
|
+
const ss = spans(markers, 0).sort((a, b) => a.markerStart - b.markerStart);
|
|
3200
|
+
const score = (o) => {
|
|
3201
|
+
let started = 0;
|
|
3202
|
+
let inside = 0;
|
|
3203
|
+
for (const [i, s] of ss.entries()) {
|
|
3204
|
+
const next = ss[i + 1]?.markerStart;
|
|
3205
|
+
const t = inputs.find((x) => x >= s.markerStart + o);
|
|
3206
|
+
if (t === undefined || next !== undefined && t >= next + o)
|
|
3207
|
+
continue;
|
|
3208
|
+
started += 1;
|
|
3209
|
+
const end = Math.min(s.markerEnd ?? Infinity, next ?? Infinity) + o;
|
|
3210
|
+
inside += inputs.filter((x) => x >= s.markerStart + o && x < end).length;
|
|
3211
|
+
}
|
|
3212
|
+
return [started, inside];
|
|
3213
|
+
};
|
|
3214
|
+
let best = offset;
|
|
3215
|
+
let top = score(offset);
|
|
3216
|
+
for (const [i, s] of ss.entries()) {
|
|
3217
|
+
const prevEnd = ss[i - 1]?.markerEnd;
|
|
3218
|
+
for (const t of inputs) {
|
|
3219
|
+
if (prevEnd !== undefined && t >= prevEnd + offset && t - (prevEnd + offset) <= s.markerStart + offset - t)
|
|
3220
|
+
continue;
|
|
3221
|
+
const o = t - s.markerStart - ulp(t);
|
|
3222
|
+
if (o >= offset || o < offset - errorMs)
|
|
3223
|
+
continue;
|
|
3224
|
+
const sc = score(o);
|
|
3225
|
+
const better = sc[0] > top[0] || sc[0] === top[0] && (sc[1] > top[1] || sc[1] === top[1] && o > best);
|
|
3226
|
+
if (better)
|
|
3227
|
+
[best, top] = [o, sc];
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
return best;
|
|
3231
|
+
}
|
|
3183
3232
|
function spans(markers, offset) {
|
|
3184
3233
|
const sorted = [...markers].sort((a, b) => a.t_host - b.t_host);
|
|
3185
3234
|
const out = [];
|
|
@@ -3206,7 +3255,8 @@ function ownWindowEnd(windows, t) {
|
|
|
3206
3255
|
}
|
|
3207
3256
|
function alignSteps(markers, windows, opts = {}) {
|
|
3208
3257
|
const byOrder = opts.byOrder === true;
|
|
3209
|
-
const
|
|
3258
|
+
const given = opts.clockOffsetMs;
|
|
3259
|
+
const offset = given === undefined ? estimateClockOffset(markers, windows, byOrder) : byOrder && opts.clockErrorMs ? fitClockOffset(markers, windows, given, opts.clockErrorMs) : given;
|
|
3210
3260
|
const ss = spans(markers, offset).sort((a, b) => a.markerStart - b.markerStart);
|
|
3211
3261
|
const inputs = windows.map(inputWall).filter((x) => x !== undefined).sort((a, b) => a - b);
|
|
3212
3262
|
const starts = ss.map((s, i) => {
|
|
@@ -3256,7 +3306,7 @@ function clusters(windows) {
|
|
|
3256
3306
|
}
|
|
3257
3307
|
return out;
|
|
3258
3308
|
}
|
|
3259
|
-
function
|
|
3309
|
+
function processSegments(windows, version) {
|
|
3260
3310
|
const order = windows.map((w, i) => ({ w, i })).sort((a, b) => a.w.endWall - b.w.endWall || a.i - b.i);
|
|
3261
3311
|
const segment = new Map;
|
|
3262
3312
|
let seg = 0;
|
|
@@ -3274,24 +3324,35 @@ function onAppClock(windows, version) {
|
|
|
3274
3324
|
prevId = id;
|
|
3275
3325
|
segment.set(w, seg);
|
|
3276
3326
|
}
|
|
3327
|
+
return segment;
|
|
3328
|
+
}
|
|
3329
|
+
function onAppClock(windows, version) {
|
|
3330
|
+
const process2 = processSegments(windows, version);
|
|
3331
|
+
const step = (w) => version >= 28 ? w.clockStep : undefined;
|
|
3277
3332
|
const offset = new Map;
|
|
3278
|
-
for (const [w,
|
|
3333
|
+
for (const [w, seg] of process2) {
|
|
3279
3334
|
const t1 = num(w.raw.t1);
|
|
3280
3335
|
if (t1 === undefined)
|
|
3281
3336
|
continue;
|
|
3282
|
-
const
|
|
3283
|
-
if (
|
|
3284
|
-
|
|
3337
|
+
const st = step(w);
|
|
3338
|
+
if (st && w.endWall - t1 < st.floor)
|
|
3339
|
+
continue;
|
|
3340
|
+
const o = w.endWall - t1 - (st?.end ?? 0);
|
|
3341
|
+
if (!offset.has(seg) || o < offset.get(seg))
|
|
3342
|
+
offset.set(seg, o);
|
|
3285
3343
|
}
|
|
3286
3344
|
return windows.map((w) => {
|
|
3287
|
-
const
|
|
3288
|
-
const o = s === undefined ? undefined : offset.get(s);
|
|
3289
|
-
const t0 = num(w.raw.t0);
|
|
3345
|
+
const seg = process2.get(w);
|
|
3290
3346
|
const t1 = num(w.raw.t1);
|
|
3347
|
+
const own = seg === undefined || t1 === undefined ? undefined : w.endWall - t1 - (step(w)?.end ?? 0);
|
|
3348
|
+
const o = seg === undefined ? undefined : offset.get(seg) ?? own;
|
|
3349
|
+
const t0 = num(w.raw.t0);
|
|
3291
3350
|
if (o === undefined || t0 === undefined || t1 === undefined)
|
|
3292
3351
|
return w;
|
|
3352
|
+
const st = step(w);
|
|
3353
|
+
const [atStart, atEnd] = [st?.start ?? 0, st?.end ?? 0];
|
|
3293
3354
|
const touch = version >= 23 ? num(obj2(w.raw.interaction).touchStartT) : undefined;
|
|
3294
|
-
return { ...w, startWall: t0 + o, endWall: t1 + o, ...touch === undefined ? {} : { touchWall: touch + o } };
|
|
3355
|
+
return { ...w, startWall: t0 + o + atStart, endWall: t1 + o + atEnd, ...touch === undefined ? {} : { touchWall: touch + o + atStart } };
|
|
3295
3356
|
});
|
|
3296
3357
|
}
|
|
3297
3358
|
|
|
@@ -3683,7 +3744,7 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3683
3744
|
const kind = placeRef(key).kind;
|
|
3684
3745
|
const active = occ.filter((o) => !o.idle);
|
|
3685
3746
|
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);
|
|
3686
|
-
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) &&
|
|
3747
|
+
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && COMMIT_IDENTITY_KINDS.includes(kind) && selected?.slowestCommit;
|
|
3687
3748
|
const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
|
|
3688
3749
|
profiles.push({ ...profile, ...identity ? { slowestCommit: identity } : {}, ...openedBy ? { openedBy } : {} });
|
|
3689
3750
|
}
|
|
@@ -3829,7 +3890,7 @@ function withActionWindows(windows, steps = [], version) {
|
|
|
3829
3890
|
}
|
|
3830
3891
|
const durMs = w.raw.endReason === "timeout" ? undefined : num3(w.raw.durMs);
|
|
3831
3892
|
const raw = { ...rest, name: w.name, kind: w.raw.kind, t0: w.raw.t0, t1: w.raw.t1, ...durMs === undefined ? {} : { durMs }, endReason: a.endReason, sdkEndReason: w.raw.endReason, actionSpanMs: a.spanMs, actionUncoveredMs: a.uncoveredMs };
|
|
3832
|
-
return { name: w.name, startWall: w.startWall, endWall: w.endWall, raw };
|
|
3893
|
+
return { name: w.name, startWall: w.startWall, endWall: w.endWall, raw, ...w.clockStep === undefined ? {} : { clockStep: w.clockStep } };
|
|
3833
3894
|
});
|
|
3834
3895
|
}
|
|
3835
3896
|
function actionMetrics(windows, steps = [], version) {
|
|
@@ -4942,7 +5003,7 @@ function keyFirstOpens(windows) {
|
|
|
4942
5003
|
}
|
|
4943
5004
|
|
|
4944
5005
|
// packages/derive/dist/src/derive.js
|
|
4945
|
-
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27];
|
|
5006
|
+
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28];
|
|
4946
5007
|
var windowsForVersion = (windows, version = DERIVATION_VERSION) => version >= 22 ? keyFirstOpens(windows) : windows;
|
|
4947
5008
|
function stepWindowsFor(windows, version = DERIVATION_VERSION) {
|
|
4948
5009
|
return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version);
|
|
@@ -4978,7 +5039,7 @@ function profileOptions(windows, version, processWindows = windows) {
|
|
|
4978
5039
|
return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, openedBy };
|
|
4979
5040
|
}
|
|
4980
5041
|
function deriveRun(input) {
|
|
4981
|
-
const { run, markers = [], ri, clockOffsetMs, samples = [], memWindow } = input;
|
|
5042
|
+
const { run, markers = [], ri, clockOffsetMs, clockErrorMs, samples = [], memWindow } = input;
|
|
4982
5043
|
const version = input.derivationVersion ?? DERIVATION_VERSION;
|
|
4983
5044
|
if (!DERIVATION_VERSIONS.includes(version))
|
|
4984
5045
|
throw new Error(`unknown derivation version ${String(version)}`);
|
|
@@ -4987,7 +5048,7 @@ function deriveRun(input) {
|
|
|
4987
5048
|
const cpu = input.cpu && renamed.size ? new Map([...input.cpu].map(([k, v]) => [renamed.get(k) ?? k, v])) : input.cpu;
|
|
4988
5049
|
const background = version >= 16 ? named.filter(isBackgroundWindow) : [];
|
|
4989
5050
|
const sdkWindows = stepWindowsFor(input.windows, version);
|
|
4990
|
-
const off = clockOffsetMs === undefined ? {} : { clockOffsetMs };
|
|
5051
|
+
const off = { ...clockOffsetMs === undefined ? {} : { clockOffsetMs }, ...version >= 28 && clockErrorMs !== undefined ? { clockErrorMs } : {} };
|
|
4991
5052
|
const preSteps = version >= 11 && markers.length ? alignRunSteps(markers, sdkWindows, off, version) : [];
|
|
4992
5053
|
const windows = version >= 11 ? withActionWindows(sdkWindows, preSteps, version) : sdkWindows;
|
|
4993
5054
|
const steps = version >= 11 ? preSteps : alignRunSteps(markers, windows, clockOffsetMs === undefined ? {} : { clockOffsetMs }, version);
|
|
@@ -5150,102 +5211,9 @@ var markerEnv = (url, runId) => ({
|
|
|
5150
5211
|
MAESTRO_KEEPSNAPPY_RUN_ID: runId
|
|
5151
5212
|
});
|
|
5152
5213
|
|
|
5153
|
-
// apps/agent/src/upload.ts
|
|
5154
|
-
import { join as join4 } from "node:path";
|
|
5155
|
-
var MAX_RATE_RETRIES = 5;
|
|
5156
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5157
|
-
class UploadError extends Error {
|
|
5158
|
-
}
|
|
5159
|
-
async function call(f, base, path, headers, body, method = "POST") {
|
|
5160
|
-
let res;
|
|
5161
|
-
for (let attempt = 0;; attempt++) {
|
|
5162
|
-
try {
|
|
5163
|
-
res = await f(`${base.replace(/\/+$/, "")}${path}`, {
|
|
5164
|
-
method,
|
|
5165
|
-
headers: method === "POST" ? { "content-type": "application/json", ...headers } : headers,
|
|
5166
|
-
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
5167
|
-
});
|
|
5168
|
-
} catch (e) {
|
|
5169
|
-
throw new UploadError(`${method} ${path}: ${e.message}`);
|
|
5170
|
-
}
|
|
5171
|
-
if (res.status !== 429 || attempt >= MAX_RATE_RETRIES)
|
|
5172
|
-
break;
|
|
5173
|
-
const wait = Number(res.headers.get("retry-after") ?? "1");
|
|
5174
|
-
await res.text().catch(() => "");
|
|
5175
|
-
await sleep(Math.min(Math.max(Number.isFinite(wait) ? wait : 1, 1), 60) * 1000);
|
|
5176
|
-
}
|
|
5177
|
-
const text = await res.text();
|
|
5178
|
-
if (res.status < 200 || res.status >= 300)
|
|
5179
|
-
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}`);
|
|
5180
|
-
try {
|
|
5181
|
-
return text ? JSON.parse(text) : {};
|
|
5182
|
-
} catch {
|
|
5183
|
-
throw new UploadError(`${method} ${path} -> ${res.status}, the answer is not JSON (is the URL the KeepSnappy API?): ${text.slice(0, 200)}`);
|
|
5184
|
-
}
|
|
5185
|
-
}
|
|
5186
|
-
async function uploadRuns(deps, outDir, runs, opts) {
|
|
5187
|
-
const f = opts.fetch ?? fetch;
|
|
5188
|
-
const size = opts.batchSize ?? 200;
|
|
5189
|
-
const skipped = [];
|
|
5190
|
-
let uploaded = 0;
|
|
5191
|
-
for (const r of runs) {
|
|
5192
|
-
if (!r.valid) {
|
|
5193
|
-
skipped.push({ run_id: r.run_id, why: r.invalid_reason || "invalid" });
|
|
5194
|
-
continue;
|
|
5195
|
-
}
|
|
5196
|
-
const log = await deps.fs.readFile(join4(outDir, r.run_id, "keepsnappy.log"));
|
|
5197
|
-
const windows = windowsInRun(parseWindows(log), r.t0, r.t1);
|
|
5198
|
-
if (windows.length === 0) {
|
|
5199
|
-
skipped.push({ run_id: r.run_id, why: "no-windows" });
|
|
5200
|
-
continue;
|
|
5201
|
-
}
|
|
5202
|
-
const externalId = `${opts.externalPrefix}-${r.run_id}`;
|
|
5203
|
-
const created = await call(f, opts.ingestUrl, "/v1/runs", { "x-keepsnappy-key": opts.key }, {
|
|
5204
|
-
external_id: externalId,
|
|
5205
|
-
started_at: r.t0,
|
|
5206
|
-
ended_at: r.t1,
|
|
5207
|
-
device: { ...opts.device?.model ? { model: opts.device.model } : {}, ...opts.device?.os ? { os: opts.device.os } : {}, ...opts.device?.id ? { id: opts.device.id } : {}, exclusive: true },
|
|
5208
|
-
meta: {
|
|
5209
|
-
arm: r.arm,
|
|
5210
|
-
driver: opts.driver ?? "keepsnappy-run",
|
|
5211
|
-
...opts.meta,
|
|
5212
|
-
...r.scenario ? { scenario: r.scenario } : {},
|
|
5213
|
-
...opts.runSetId ? { run_set_id: opts.runSetId } : {}
|
|
5214
|
-
}
|
|
5215
|
-
});
|
|
5216
|
-
const runId = String(created.run_id ?? "");
|
|
5217
|
-
const token = String(created.run_token ?? "");
|
|
5218
|
-
if (!runId || !token)
|
|
5219
|
-
throw new UploadError("POST /v1/runs: response has no run_id/run_token");
|
|
5220
|
-
const items = windows.map((w) => `${w.endWall} KEEPSNAPPY window ${JSON.stringify(w.raw)}`);
|
|
5221
|
-
for (let i = 0;i * size < items.length; i++) {
|
|
5222
|
-
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
5223
|
-
batch_id: `${externalId}-${String(i + 1).padStart(4, "0")}`,
|
|
5224
|
-
items: items.slice(i * size, (i + 1) * size)
|
|
5225
|
-
});
|
|
5226
|
-
}
|
|
5227
|
-
const samples = samplesInRun(parseSamples(log), r.t0, r.t1).map((x) => `${x.wall} KEEPSNAPPY sample ${JSON.stringify(x.raw)}`);
|
|
5228
|
-
for (let i = 0;i * size < samples.length; i++) {
|
|
5229
|
-
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
5230
|
-
batch_id: `${externalId}-s${String(i + 1).padStart(4, "0")}`,
|
|
5231
|
-
items: samples.slice(i * size, (i + 1) * size)
|
|
5232
|
-
});
|
|
5233
|
-
}
|
|
5234
|
-
let markers = [];
|
|
5235
|
-
try {
|
|
5236
|
-
markers = parseMarkersNdjson(await deps.fs.readFile(join4(outDir, r.run_id, "markers.ndjson")));
|
|
5237
|
-
} catch {}
|
|
5238
|
-
for (const m of markers) {
|
|
5239
|
-
await call(f, opts.ingestUrl, "/v1/markers", { authorization: `Bearer ${token}` }, { step: m.step, phase: m.phase, t_host: m.t_host });
|
|
5240
|
-
}
|
|
5241
|
-
await call(f, opts.ingestUrl, `/v1/runs/${runId}/finish`, { "x-keepsnappy-key": opts.key }, {});
|
|
5242
|
-
uploaded++;
|
|
5243
|
-
}
|
|
5244
|
-
return { uploaded, skipped };
|
|
5245
|
-
}
|
|
5246
|
-
|
|
5247
5214
|
// packages/present/dist/src/capital.js
|
|
5248
5215
|
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5216
|
+
var lowerFirst = (s) => s ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
5249
5217
|
|
|
5250
5218
|
// packages/present/dist/src/placeWords.js
|
|
5251
5219
|
var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
|
|
@@ -5285,7 +5253,13 @@ var STATE_WORDS = {
|
|
|
5285
5253
|
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5286
5254
|
var isWorseState = (state) => state === "worse";
|
|
5287
5255
|
var NOT_BLOCKING = "not blocking";
|
|
5288
|
-
var badgedWord = (word, blocking) =>
|
|
5256
|
+
var badgedWord = (word, blocking) => `${word}${notBlockingBadge(word, blocking)}`;
|
|
5257
|
+
var notBlockingBadge = (word, blocking) => word === STATE_WORDS.worse && blocking === false ? ` (${NOT_BLOCKING})` : "";
|
|
5258
|
+
var BLOCKED_WORD = "Blocked";
|
|
5259
|
+
var NOT_COMPUTED_WORD = "Verdict not computed yet (computed on demand)";
|
|
5260
|
+
var NO_BASELINE_WORD = "Waiting for main";
|
|
5261
|
+
var NOT_COMPARED_WORD = "Not compared with main";
|
|
5262
|
+
var NOT_JUDGED_WORD = "Not judged";
|
|
5289
5263
|
|
|
5290
5264
|
// packages/present/dist/src/values.js
|
|
5291
5265
|
var NO_VALUE = "–";
|
|
@@ -5314,17 +5288,23 @@ function roundTo(v, decimals) {
|
|
|
5314
5288
|
return Number(magnitude.toFixed(decimals)) * Math.sign(v);
|
|
5315
5289
|
return Number(`${Math.round(scaled)}e-${decimals}`) * Math.sign(v);
|
|
5316
5290
|
}
|
|
5291
|
+
function written(unit, decimals, v) {
|
|
5292
|
+
if (unit === "ms" && Math.abs(v) >= LONG_MS && Math.abs(v) < 1e9)
|
|
5293
|
+
return { n: roundTo(v / 1000, 1) + 0, seconds: true };
|
|
5294
|
+
return { n: roundTo(v * shownUnit(unit).scale, decimals) + 0, seconds: false };
|
|
5295
|
+
}
|
|
5317
5296
|
function format(unit, decimals, v) {
|
|
5318
5297
|
if (v === null || v === undefined || !Number.isFinite(v))
|
|
5319
5298
|
return NO_VALUE;
|
|
5299
|
+
const { n, seconds } = written(unit, decimals, v);
|
|
5300
|
+
if (seconds)
|
|
5301
|
+
return `${String(n)} s`;
|
|
5320
5302
|
const shown = shownUnit(unit);
|
|
5321
|
-
|
|
5322
|
-
return `${String(roundTo(v / 1000, 1) + 0)} s`;
|
|
5323
|
-
const text = String(roundTo(v * shown.scale, decimals) + 0);
|
|
5324
|
-
return shown.unit ? `${text} ${shown.unit}` : text;
|
|
5303
|
+
return shown.unit ? `${String(n)} ${shown.unit}` : String(n);
|
|
5325
5304
|
}
|
|
5305
|
+
var defOf = (measure) => isMeasureId(measure) ? MEASURES[measure] : BREAKDOWN_MEASURES[measure];
|
|
5326
5306
|
function valueText(measure, v) {
|
|
5327
|
-
const def =
|
|
5307
|
+
const def = defOf(measure);
|
|
5328
5308
|
return format(def.unit, def.decimals, v);
|
|
5329
5309
|
}
|
|
5330
5310
|
var KEY_SEPARATOR = ":";
|
|
@@ -5342,6 +5322,13 @@ function fieldValueText(field, v) {
|
|
|
5342
5322
|
const def = fieldDef(field.split(KEY_SEPARATOR)[0]);
|
|
5343
5323
|
return format(def?.unit, def?.decimals ?? FALLBACK_DECIMALS, v);
|
|
5344
5324
|
}
|
|
5325
|
+
function timeMove(field, a, b) {
|
|
5326
|
+
if (a === null || b === null || !(a > 0))
|
|
5327
|
+
return null;
|
|
5328
|
+
const breakdown = breakdownMeasure(field);
|
|
5329
|
+
const unit = breakdown ? BREAKDOWN_MEASURES[breakdown].unit : fieldDef(field.split(KEY_SEPARATOR)[0])?.unit;
|
|
5330
|
+
return unit === "ms" ? Math.abs(b - a) / a : null;
|
|
5331
|
+
}
|
|
5345
5332
|
|
|
5346
5333
|
// packages/present/dist/src/rowWords.js
|
|
5347
5334
|
var GONE_TAILS = {
|
|
@@ -5370,6 +5357,14 @@ function notInRunRowsText(c, page) {
|
|
|
5370
5357
|
const them = c.screens + c.places === 1 ? "it" : "them";
|
|
5371
5358
|
return { lead: `${what} ${GONE_TAILS[page].notInRun}:`, rest: ` the ${page === "journey" ? "newest session" : "journey"} did not reach ${them}; not counted.` };
|
|
5372
5359
|
}
|
|
5360
|
+
var sdkTag = (version) => version.replace(/^\d+\.\d+\.\d+-/, "");
|
|
5361
|
+
var placeChartMarker = (tag) => `Place names changed with SDK ${tag}`;
|
|
5362
|
+
var placeNamesChangedOmitted = (tag) => `the line where place names changed (SDK ${tag})`;
|
|
5363
|
+
var componentChartMarker = (tag) => `SDK ${tag}: names changed, library time now counted`;
|
|
5364
|
+
var ownTimeOnlyBefore = (version) => `own time only, before SDK ${version}`;
|
|
5365
|
+
var placeWasText = (oldKey) => `before it this place was ${oldKey}`;
|
|
5366
|
+
var PLACE_NOT_JOINED_TEXT = "no recorded old name, its history starts here";
|
|
5367
|
+
var componentNotJoinedText = (sdk) => `${componentChartMarker(sdk)}; values before it were ${ownTimeOnlyBefore(sdk)}, not joined`;
|
|
5373
5368
|
var OUTSIDE_APP_NAME = "Outside app components";
|
|
5374
5369
|
var OTHER_RENDER_WORK_NAME = "Other render work";
|
|
5375
5370
|
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
@@ -5388,9 +5383,6 @@ function leadBody(subject, measure, main, pr) {
|
|
|
5388
5383
|
var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_STATE[tier]]}: ${leadBody(subject, measure, main, pr)}`;
|
|
5389
5384
|
var FOR_INFORMATION = "For information";
|
|
5390
5385
|
var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
|
|
5391
|
-
function foldedText(n, rows, page) {
|
|
5392
|
-
return `${n} ${GONE_TAILS[page][rows === "components" ? "removed" : "notInRun"]}`;
|
|
5393
|
-
}
|
|
5394
5386
|
function placeShownName(p, o = {}) {
|
|
5395
5387
|
const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
|
|
5396
5388
|
const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
|
|
@@ -5451,6 +5443,19 @@ var placeOpenWord = (id) => {
|
|
|
5451
5443
|
const open = placeOpen(id);
|
|
5452
5444
|
return open ? OPEN_WORDS[open] : null;
|
|
5453
5445
|
};
|
|
5446
|
+
var TWIN2 = /\+3s$/;
|
|
5447
|
+
function foldTwins(rows, placeOf, also = () => "") {
|
|
5448
|
+
const seen = new Set;
|
|
5449
|
+
const out = rows.filter((r) => {
|
|
5450
|
+
const key = `${placeOf(r).replace(TWIN2, "")}\x00${also(r)}`;
|
|
5451
|
+
if (seen.has(key))
|
|
5452
|
+
return false;
|
|
5453
|
+
seen.add(key);
|
|
5454
|
+
return true;
|
|
5455
|
+
});
|
|
5456
|
+
return { rows: out, folded: rows.length - out.length };
|
|
5457
|
+
}
|
|
5458
|
+
var foldedTwinsText = (n) => `${n} “${PLACE_KIND_WORDS.after3s}” ${n === 1 ? "twin" : "twins"} folded into ${n === 1 ? "its place" : "their places"}`;
|
|
5454
5459
|
function placeIdShownName(id, o = {}) {
|
|
5455
5460
|
const p = placeParts(id);
|
|
5456
5461
|
if (!p)
|
|
@@ -5537,6 +5542,42 @@ function shortestPaths(files) {
|
|
|
5537
5542
|
}
|
|
5538
5543
|
return out;
|
|
5539
5544
|
}
|
|
5545
|
+
// packages/present/dist/src/problems.js
|
|
5546
|
+
var FAR_OVER_BUDGET_WORD = "Far over budget";
|
|
5547
|
+
var PROBLEM_TAG_WORDS = { new: NEW_IN_PR_CHIP, main: "Also on main", none: NOT_COMPARED_WORD };
|
|
5548
|
+
var PROBLEM_TAG_LINE_WORDS = { new: "new in this PR", main: "also on main", none: "not compared with main" };
|
|
5549
|
+
var problems = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "problem" : "problems"}`;
|
|
5550
|
+
var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5551
|
+
function problemCountsText(c, own) {
|
|
5552
|
+
const tags = c.tags;
|
|
5553
|
+
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(", ")}` : "";
|
|
5554
|
+
return `${problems(c.count)} at ${places(c.places)} in ${own} own runs${split}${c.far_count ? `; ${c.far_count} of them ${FAR_OVER_BUDGET_WORD.toLowerCase()}` : ""}`;
|
|
5555
|
+
}
|
|
5556
|
+
var newProblems = (n) => `${n.toLocaleString("en-US")} new ${n === 1 ? "problem" : "problems"}`;
|
|
5557
|
+
var PROBLEMS_BLOCK_WORDS = {
|
|
5558
|
+
branch: {
|
|
5559
|
+
heading: "New problems in this branch",
|
|
5560
|
+
sub: "too slow on their own, whatever main does; they don’t block the merge",
|
|
5561
|
+
seeAll: (n) => `See all ${newProblems(n)}`,
|
|
5562
|
+
alsoLine: (n) => `${problems(n)} also on main`,
|
|
5563
|
+
notComparedLine: (n) => `${problems(n)} not compared with main`,
|
|
5564
|
+
choices: { new: PROBLEM_TAG_WORDS.new, also: PROBLEM_TAG_WORDS.main, not_compared: PROBLEM_TAG_WORDS.none },
|
|
5565
|
+
panelTitle: "Problems in this branch",
|
|
5566
|
+
panelNote: "Each one is too slow in this branch’s own runs, whatever main does. Problems never block.",
|
|
5567
|
+
filter: "Which problems"
|
|
5568
|
+
},
|
|
5569
|
+
main: {
|
|
5570
|
+
heading: "New problems in this session",
|
|
5571
|
+
sub: "too slow on their own, whatever earlier sessions did",
|
|
5572
|
+
seeAll: (n) => `See all ${newProblems(n)}`,
|
|
5573
|
+
alsoLine: (n) => `${problems(n)} also in earlier sessions`,
|
|
5574
|
+
notComparedLine: (n) => `${problems(n)} not compared with earlier sessions`,
|
|
5575
|
+
choices: { new: "New in this session", also: "Also in earlier sessions", not_compared: "Not compared with earlier sessions" },
|
|
5576
|
+
panelTitle: "Problems in this session",
|
|
5577
|
+
panelNote: "Each one is too slow in this session’s own runs, whatever earlier sessions did. Problems never block.",
|
|
5578
|
+
filter: "Which problems"
|
|
5579
|
+
}
|
|
5580
|
+
};
|
|
5540
5581
|
// packages/present/dist/src/stability.js
|
|
5541
5582
|
var UNSTABLE_WORDS = {
|
|
5542
5583
|
short: "main varies too much here",
|
|
@@ -5544,8 +5585,34 @@ var UNSTABLE_WORDS = {
|
|
|
5544
5585
|
line: "Main varies too much here to tell",
|
|
5545
5586
|
ratioTip: "1.0 = main’s run-to-run noise alone would pass the warning step about once in 100 comparisons.",
|
|
5546
5587
|
note: "This place varies a lot between runs. Worth checking why: it may be in the code.",
|
|
5547
|
-
what: "Main’s own runs vary too much here to tell, so a comparison there would mostly measure noise. It is shown with its values, never judged, never blocks and is never counted. It is judged again once main settles."
|
|
5588
|
+
what: "Main’s own runs vary too much here to tell, so a comparison there would mostly measure noise. It is shown with its values, never judged, never blocks and is never counted. It is judged again once main settles.",
|
|
5589
|
+
hint: {
|
|
5590
|
+
work: "The work differs between runs or sessions ({count} {min}–{max}): likely the code or the test steps.",
|
|
5591
|
+
speed: "The same work ran at different speeds ({count} {n} in every run): likely the phone or the session.",
|
|
5592
|
+
unclear: "The stored facts don’t show whether the work or the speed varies."
|
|
5593
|
+
}
|
|
5548
5594
|
};
|
|
5595
|
+
function breakdownParts(ref) {
|
|
5596
|
+
const [kind, ...rest] = ref.split(":");
|
|
5597
|
+
return kind === "b" && rest.length >= 3 ? { key: rest.slice(1, -1).join(":"), measure: rest[rest.length - 1] } : null;
|
|
5598
|
+
}
|
|
5599
|
+
var countText = (n) => String(Math.round(n * 10) / 10);
|
|
5600
|
+
function countName(count, ref) {
|
|
5601
|
+
if (count !== "component.renders" && count !== "component.mounts")
|
|
5602
|
+
return MEASURES[count].short;
|
|
5603
|
+
const key = (ref ? breakdownParts(ref)?.key : undefined) ?? "component";
|
|
5604
|
+
return `${key} ${BREAKDOWN_MEASURES[count === "component.renders" ? "renders" : "mounts"].label}`;
|
|
5605
|
+
}
|
|
5606
|
+
function unstableHintLine(hint, ref) {
|
|
5607
|
+
if (hint.kind === "unclear" || hint.count === null || hint.range === null)
|
|
5608
|
+
return UNSTABLE_WORDS.hint.unclear;
|
|
5609
|
+
const count = countName(hint.count, ref);
|
|
5610
|
+
if (hint.kind === "work")
|
|
5611
|
+
return UNSTABLE_WORDS.hint.work.replace("{count}", count).replace("{min}", countText(hint.range[0])).replace("{max}", countText(hint.range[1]));
|
|
5612
|
+
return hint.median === null ? UNSTABLE_WORDS.hint.unclear : UNSTABLE_WORDS.hint.speed.replace("{count}", count).replace("{n}", countText(hint.median));
|
|
5613
|
+
}
|
|
5614
|
+
var unstableNoteWords = (u) => u.hint ? `${UNSTABLE_WORDS.note} ${unstableHintLine(u.hint, u.stabilityRef)}` : UNSTABLE_WORDS.note;
|
|
5615
|
+
var mainUnstableWords = (reason) => reason.kind === "noisy_main" ? unstableNoteWords(reason) : "";
|
|
5549
5616
|
|
|
5550
5617
|
// packages/present/dist/src/present.js
|
|
5551
5618
|
var ASPECT_WORDS = {
|
|
@@ -5617,8 +5684,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5617
5684
|
for (const [screen, rows] of screens) {
|
|
5618
5685
|
const line = notInRunRowsText({ screens: rows.filter((p) => p.kind === "screen").length, places: rows.filter((p) => p.kind !== "screen").length }, c.page);
|
|
5619
5686
|
out.push(`${screen ? `${screen}: ` : ""}${line.lead}${line.rest}`);
|
|
5620
|
-
if (rows.length > 3)
|
|
5621
|
-
out.push(` ${foldedText(rows.length, "places", c.page)}`);
|
|
5622
5687
|
if (!short || rows.length <= 3)
|
|
5623
5688
|
out.push(...rows.map((p) => ` ${windowQualified(p.place)}`));
|
|
5624
5689
|
}
|
|
@@ -5628,8 +5693,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5628
5693
|
for (const [place, rows] of removed) {
|
|
5629
5694
|
const line = removedRowsText(rows.length, c.page);
|
|
5630
5695
|
out.push(`${windowQualified(place)}: ${line.lead}${line.rest}`);
|
|
5631
|
-
if (rows.length > 3)
|
|
5632
|
-
out.push(` ${foldedText(rows.length, "components", c.page)}`);
|
|
5633
5696
|
if (!short || rows.length <= 3)
|
|
5634
5697
|
out.push(...rows.map((r) => ` ${r.ref.kind === "breakdown" ? componentReadName({ key: r.ref.key, name: r.ref.name }) : measureWords(r.ref.measure)}`));
|
|
5635
5698
|
}
|
|
@@ -5684,6 +5747,10 @@ var REASON_WORDS = {
|
|
|
5684
5747
|
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5685
5748
|
};
|
|
5686
5749
|
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5750
|
+
var AT_MOST = "≤ ";
|
|
5751
|
+
var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
|
|
5752
|
+
var AT_LEAST = "≥ ";
|
|
5753
|
+
var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
|
|
5687
5754
|
// packages/present/dist/src/main-history.js
|
|
5688
5755
|
var MAIN_TIER_WORDS = {
|
|
5689
5756
|
same: "In norm",
|
|
@@ -5700,11 +5767,13 @@ var MAIN_TIER_RESULT_WORDS = {
|
|
|
5700
5767
|
worse: STATE_WORDS.worse
|
|
5701
5768
|
};
|
|
5702
5769
|
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5770
|
+
var earlierWords = (sessions, since, config) => `main has ${sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${since ? ` on ${since}` : ""}`;
|
|
5703
5771
|
function mainCantWords(reason, config) {
|
|
5704
5772
|
if (!reason)
|
|
5705
5773
|
return "";
|
|
5706
|
-
return reason.kind === "noisy_main" ? "main’s runs vary too much there" :
|
|
5774
|
+
return reason.kind === "noisy_main" ? "main’s runs vary too much there" : earlierWords(String(reason.earlier_sessions), reason.since_label, config);
|
|
5707
5775
|
}
|
|
5776
|
+
var mainCantRangeWords = (r, config) => earlierWords(`${r.min}–${r.max}`, r.since_label, config);
|
|
5708
5777
|
var waitingSessionsWords = (sessions) => `${sessions} older main ${sessions === 1 ? "session waits" : "sessions wait"} for a reprocess`;
|
|
5709
5778
|
function mainPlaceReasonWords(reason, config) {
|
|
5710
5779
|
switch (reason.kind) {
|
|
@@ -5734,9 +5803,11 @@ var SHORT_ASPECT_WORDS = {
|
|
|
5734
5803
|
driverVersion: "Driver",
|
|
5735
5804
|
journeyVersion: "Journey"
|
|
5736
5805
|
};
|
|
5806
|
+
var cantAt = (places) => `${STATE_WORDS["cant-tell"]} at ${places} place${places === 1 ? "" : "s"}`;
|
|
5737
5807
|
function mainSessionCantWords(places, reason, config) {
|
|
5738
|
-
return `${
|
|
5808
|
+
return `${cantAt(places)}: ${mainCantWords(reason, config)}`;
|
|
5739
5809
|
}
|
|
5810
|
+
var mainSessionCantRangeWords = (places, r, config) => `${cantAt(places)}: ${mainCantRangeWords(r, config)}`;
|
|
5740
5811
|
var KIND_PLURAL = {
|
|
5741
5812
|
screen: "screens",
|
|
5742
5813
|
press: "taps",
|
|
@@ -5761,6 +5832,495 @@ function mainCutWords(v) {
|
|
|
5761
5832
|
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5762
5833
|
}
|
|
5763
5834
|
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5835
|
+
// packages/present/dist/src/verdict-section.js
|
|
5836
|
+
var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5837
|
+
var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
|
|
5838
|
+
var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
|
|
5839
|
+
var VERDICT_SECTION_WORDS = {
|
|
5840
|
+
slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
|
|
5841
|
+
blocksMerge: "blocks the merge",
|
|
5842
|
+
faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
|
|
5843
|
+
mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
|
|
5844
|
+
unpublishedSdk: "ran an unpublished SDK build",
|
|
5845
|
+
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)})`,
|
|
5846
|
+
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`,
|
|
5847
|
+
tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
|
|
5848
|
+
tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
|
|
5849
|
+
tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
|
|
5850
|
+
noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
|
|
5851
|
+
noisy: "Not judged: main’s own runs vary too much at every place here",
|
|
5852
|
+
versionMismatch: "Not compared: the sides were computed with different analysis versions",
|
|
5853
|
+
unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
|
|
5854
|
+
notCompared: (reason) => `Not compared: ${reason}`,
|
|
5855
|
+
notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
|
|
5856
|
+
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`,
|
|
5857
|
+
doesNotBlock: "doesn’t block",
|
|
5858
|
+
fixes: {
|
|
5859
|
+
branch: {
|
|
5860
|
+
"no-baseline": "Add this journey on this phone to main’s nightly runs.",
|
|
5861
|
+
"too-few-runs": "Run this branch again on this phone.",
|
|
5862
|
+
"judged-measures-missing": "Record the judged measures, then run again.",
|
|
5863
|
+
"runs-unknown": "Run this branch again; a full set of runs decides it.",
|
|
5864
|
+
"facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed.",
|
|
5865
|
+
"platforms-differ": "Add this phone’s platform to main’s nightly runs."
|
|
5866
|
+
},
|
|
5867
|
+
main: {
|
|
5868
|
+
"no-baseline": "Nothing to do: main’s next 3 nightly sessions make a result possible.",
|
|
5869
|
+
"too-few-runs": "Main’s next nightly session decides it.",
|
|
5870
|
+
"runs-unknown": "Main’s next nightly session decides it.",
|
|
5871
|
+
"judged-measures-missing": "Record the judged measures on main; its next nightly session is compared.",
|
|
5872
|
+
"facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed."
|
|
5873
|
+
}
|
|
5874
|
+
},
|
|
5875
|
+
waitForMain: { branch: "Nothing to do on this branch; main’s next nightly session decides it.", main: "Main’s next nightly session decides it." },
|
|
5876
|
+
sides: {
|
|
5877
|
+
branch: { left: "Main", right: "This branch", rightShort: "Branch" },
|
|
5878
|
+
main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
|
|
5879
|
+
runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
|
|
5880
|
+
},
|
|
5881
|
+
branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
|
|
5882
|
+
inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
|
|
5883
|
+
whatChangedLabel: "What changed",
|
|
5884
|
+
forInformation: "for information",
|
|
5885
|
+
allFilter: "All",
|
|
5886
|
+
filter: {
|
|
5887
|
+
label: "Show places",
|
|
5888
|
+
choice: (tierWord) => upperFirst(tierWord),
|
|
5889
|
+
showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
|
|
5890
|
+
},
|
|
5891
|
+
countsLabel: "Places by result",
|
|
5892
|
+
links: {
|
|
5893
|
+
openChart: "Open chart",
|
|
5894
|
+
seeAll: (n) => `See all ${places2(n)}`,
|
|
5895
|
+
whichPlaces: "Which places",
|
|
5896
|
+
whichChanges: "Which changes",
|
|
5897
|
+
howToFix: "How to fix",
|
|
5898
|
+
whatDiffers: "What differs",
|
|
5899
|
+
whatChanged: "What changed",
|
|
5900
|
+
openSetup: "Open setup",
|
|
5901
|
+
openScreen: "Open screen"
|
|
5902
|
+
}
|
|
5903
|
+
};
|
|
5904
|
+
// packages/present/dist/src/summary-message.js
|
|
5905
|
+
var SUMMARY_WORDS = {
|
|
5906
|
+
notComputed: "Not judged yet",
|
|
5907
|
+
notComputedWhy: "waiting for the judgement",
|
|
5908
|
+
placesOnPage: "the places are on the page",
|
|
5909
|
+
noResult: "No result",
|
|
5910
|
+
main: "Main",
|
|
5911
|
+
newestSlower: "newest session slower",
|
|
5912
|
+
journeys: (n) => `${n} journeys`,
|
|
5913
|
+
phones: (n) => `${n} phones`,
|
|
5914
|
+
heated: (n, of) => `${INFO_WORDS.heat.toLowerCase()} in ${n} of ${of} runs`,
|
|
5915
|
+
fewRuns: "few runs",
|
|
5916
|
+
measuredDifferently: "measured differently",
|
|
5917
|
+
namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
|
|
5918
|
+
morePlaces: (n) => `+${n} more ${n === 1 ? "place" : "places"} changed`,
|
|
5919
|
+
compared: {
|
|
5920
|
+
branch: (sessions, days) => `vs main’s last ${sessions}${days}`,
|
|
5921
|
+
main: (sessions, days) => `newest session vs main’s ${sessions} before${days}`,
|
|
5922
|
+
mainSlower: (sessions, days) => `newest session slower than main’s ${sessions} before${days}`
|
|
5923
|
+
},
|
|
5924
|
+
sessions: (n) => `${n} ${n === 1 ? "session" : "sessions"}`,
|
|
5925
|
+
days: (days) => days ? ` (${days})` : "",
|
|
5926
|
+
runs: (runs, each) => each ? `${each} ${each === 1 ? "run" : "runs"} each` : `${runs} ${runs === 1 ? "run" : "runs"}`,
|
|
5927
|
+
unblock: (why, fix) => `${why} → ${fix}`,
|
|
5928
|
+
link: {
|
|
5929
|
+
comparison: "See the comparison",
|
|
5930
|
+
session: "See the session",
|
|
5931
|
+
branch: (branch) => `Open ${branch}`,
|
|
5932
|
+
runSet: "Open the run set"
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
var ORDER = ["worse", "warning", "cant-tell", "not-computed", "better", "no-change"];
|
|
5936
|
+
var rank = (l) => ORDER.indexOf(l.state);
|
|
5937
|
+
var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
|
|
5938
|
+
var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
|
|
5939
|
+
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
5940
|
+
var NAME = { name: 32, button: 30 };
|
|
5941
|
+
var TOP2 = 3;
|
|
5942
|
+
var MAX_LINES = 5;
|
|
5943
|
+
var upper = (s) => s ? `${s[0].toUpperCase()}${s.slice(1)}` : s;
|
|
5944
|
+
var fromTo = (a, b) => {
|
|
5945
|
+
const unit = (v) => / [^\d\s]+$/.exec(v)?.[0];
|
|
5946
|
+
return unit(a) && unit(a) === unit(b) ? `${a.slice(0, -unit(a).length)} → ${b}` : `${a} → ${b}`;
|
|
5947
|
+
};
|
|
5948
|
+
var days = (labels) => {
|
|
5949
|
+
const a = labels[0], b = labels.at(-1);
|
|
5950
|
+
if (!a || !b || a === b)
|
|
5951
|
+
return a ?? "";
|
|
5952
|
+
const [ma] = a.split(" ");
|
|
5953
|
+
const [mb, db] = b.split(" ");
|
|
5954
|
+
return ma === mb ? `${a}–${db}` : `${a}–${b}`;
|
|
5955
|
+
};
|
|
5956
|
+
var changeLine = (c, tier) => {
|
|
5957
|
+
const values = c.main !== null && c.now !== null ? `${fromTo(c.main, c.now)}${c.change ? ` (${c.change})` : ""}` : c.change;
|
|
5958
|
+
return [c.place ?? upper(c.measure), ...c.place ? [c.measure] : [], values, WORD[c.tier ?? tier]].filter(Boolean).join(" · ");
|
|
5959
|
+
};
|
|
5960
|
+
var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
|
|
5961
|
+
var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
|
|
5962
|
+
function why(l) {
|
|
5963
|
+
if (l.state === "not-computed")
|
|
5964
|
+
return notJudgedWhy(l) ?? upper(SUMMARY_WORDS.notComputedWhy);
|
|
5965
|
+
if (l.state === "cant-tell" && l.namesChanged)
|
|
5966
|
+
return SUMMARY_WORDS.namesChanged(l.namesChanged.sdk, l.namesChanged.mainSessions, l.namesChanged.needed);
|
|
5967
|
+
if (l.answer)
|
|
5968
|
+
return l.answer;
|
|
5969
|
+
if (l.state === "no-change")
|
|
5970
|
+
return l.places ? VERDICT_SECTION_WORDS.noneMoved(l.places) : noneMoved;
|
|
5971
|
+
if (l.state !== "cant-tell")
|
|
5972
|
+
return upper(SUMMARY_WORDS.placesOnPage);
|
|
5973
|
+
return upper(wordReason(l.word) ?? (l.word === NO_BASELINE_WORD ? l.word : REASON_WORDS["judged-measures-missing"]));
|
|
5974
|
+
}
|
|
5975
|
+
function unblock(l, pr) {
|
|
5976
|
+
const page = pr ? "branch" : "main";
|
|
5977
|
+
const reason = l.namesChanged ? "not-enough-main" : l.reason;
|
|
5978
|
+
if (!reason)
|
|
5979
|
+
return null;
|
|
5980
|
+
const fix = reason === "not-enough-main" ? VERDICT_SECTION_WORDS.waitForMain[page] : VERDICT_SECTION_WORDS.fixes[page][reason];
|
|
5981
|
+
return fix ? lowerFirst(fix.replace(/\.$/, "")) : null;
|
|
5982
|
+
}
|
|
5983
|
+
var noneMoved = VERDICT_SECTION_WORDS.noneMoved(1).replace(/ \(.*\)$/, "");
|
|
5984
|
+
function conditions(l, pr) {
|
|
5985
|
+
const moved = l.state === "worse" || l.state === "warning";
|
|
5986
|
+
return [
|
|
5987
|
+
pr && l.state === "worse" ? l.blocking ? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "",
|
|
5988
|
+
moved && l.measuredDifferently ? SUMMARY_WORDS.measuredDifferently : "",
|
|
5989
|
+
(moved || l.state === "better") && l.fewRuns ? SUMMARY_WORDS.fewRuns : ""
|
|
5990
|
+
].filter(Boolean);
|
|
5991
|
+
}
|
|
5992
|
+
function compared(l, pr, slower) {
|
|
5993
|
+
const judged = l.state === "worse" || l.state === "warning" || l.state === "better" || l.state === "no-change";
|
|
5994
|
+
if (!judged || !l.main?.sessions)
|
|
5995
|
+
return null;
|
|
5996
|
+
const args = [SUMMARY_WORDS.sessions(l.main.sessions), SUMMARY_WORDS.days(days(l.main.labels))];
|
|
5997
|
+
const what = pr ? SUMMARY_WORDS.compared.branch(...args) : slower ? SUMMARY_WORDS.compared.mainSlower(...args) : SUMMARY_WORDS.compared.main(...args);
|
|
5998
|
+
return `${what} · ${SUMMARY_WORDS.runs(l.main.runs, l.main.each)}`;
|
|
5999
|
+
}
|
|
6000
|
+
function summaryMessage(input) {
|
|
6001
|
+
const l = shownLane(input.lanes);
|
|
6002
|
+
if (!l) {
|
|
6003
|
+
return {
|
|
6004
|
+
state: "not-computed",
|
|
6005
|
+
newestSlower: false,
|
|
6006
|
+
lead: `${SUMMARY_WORDS.notComputed} · ${clip(input.branch, NAME.name)}`,
|
|
6007
|
+
...withLines([upper(SUMMARY_WORDS.notComputedWhy)]),
|
|
6008
|
+
link: { label: SUMMARY_WORDS.link.branch(clip(input.branch, NAME.button - 5)), url: input.url }
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
const journeys = new Set(input.lanes.map((x) => x.journey ?? "-")).size;
|
|
6012
|
+
const phones = new Set(input.lanes.map((x) => x.phone ?? "-")).size;
|
|
6013
|
+
const single = input.lanes.length === 1;
|
|
6014
|
+
const newestSlower = !input.pr && (l.state === "worse" || l.state === "warning");
|
|
6015
|
+
const lead = [
|
|
6016
|
+
notJudgedWhy(l) ? NOT_JUDGED_WORD : WORD[l.state],
|
|
6017
|
+
...input.app ? [input.app] : [],
|
|
6018
|
+
journeys === 1 ? l.journey ?? "journey" : SUMMARY_WORDS.journeys(journeys),
|
|
6019
|
+
input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
|
|
6020
|
+
phones === 1 ? l.phone ?? "unknown phone" : SUMMARY_WORDS.phones(phones)
|
|
6021
|
+
].join(" · ");
|
|
6022
|
+
const lines = [];
|
|
6023
|
+
const basis = compared(l, input.pr, newestSlower);
|
|
6024
|
+
const extra = conditions(l, input.pr).join(", ");
|
|
6025
|
+
const heat = l.heatedRuns ? upper(SUMMARY_WORDS.heated(l.heatedRuns, l.runs)) : null;
|
|
6026
|
+
const tail = [...basis || extra ? [basis ? `${basis}${extra ? `; ${extra}` : ""}` : upper(extra)] : [], ...heat ? [heat] : []];
|
|
6027
|
+
if (l.state === "worse" || l.state === "warning" || l.state === "better") {
|
|
6028
|
+
const seen = new Set;
|
|
6029
|
+
const places = l.changes.filter((c) => {
|
|
6030
|
+
const key = c.key ?? c.place ?? c.measure;
|
|
6031
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
6032
|
+
});
|
|
6033
|
+
const room = MAX_LINES - tail.length;
|
|
6034
|
+
const top = places.length > room ? places.slice(0, Math.min(TOP2, room - 1)) : places.slice(0, TOP2);
|
|
6035
|
+
lines.push(...top.map((c) => changeLine(c, l.state)));
|
|
6036
|
+
if (!top.length)
|
|
6037
|
+
lines.push(why(l));
|
|
6038
|
+
const more = l.places - top.length;
|
|
6039
|
+
if (top.length && more > 0)
|
|
6040
|
+
lines.push(SUMMARY_WORDS.morePlaces(more));
|
|
6041
|
+
} else {
|
|
6042
|
+
const fix = l.state === "cant-tell" ? unblock(l, input.pr) : null;
|
|
6043
|
+
lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
|
|
6044
|
+
}
|
|
6045
|
+
lines.push(...tail);
|
|
6046
|
+
const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
|
|
6047
|
+
return { state: l.state, newestSlower, lead, ...withLines(lines), link: { label, url: (single ? l.url : null) ?? input.url } };
|
|
6048
|
+
}
|
|
6049
|
+
var withLines = (lines) => ({ lines, detail: lines.join(`
|
|
6050
|
+
`) });
|
|
6051
|
+
function pendingMessage(branch, url) {
|
|
6052
|
+
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 } };
|
|
6053
|
+
}
|
|
6054
|
+
function noResultMessage(branch, reason, url) {
|
|
6055
|
+
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 };
|
|
6056
|
+
}
|
|
6057
|
+
// packages/present/dist/src/run-set-message.js
|
|
6058
|
+
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
6059
|
+
function movedItems(v, sc) {
|
|
6060
|
+
const flat = v.items.filter((i) => i.scenario === sc.scenario && (!sc.device || !i.device_id || i.device_id === sc.device.id));
|
|
6061
|
+
const h = sc.headline;
|
|
6062
|
+
if (h?.errors === undefined)
|
|
6063
|
+
return flat.filter((i) => i.state === "worse" || i.state === "better");
|
|
6064
|
+
const graded = [...h.errors, ...h.warnings ?? []].map((g) => {
|
|
6065
|
+
const same = flat.find((i) => i.step === g.window && i.metric === g.metric);
|
|
6066
|
+
return {
|
|
6067
|
+
...same ?? { kind: "change", low_data: false, low_data_reason: null },
|
|
6068
|
+
scenario: sc.scenario,
|
|
6069
|
+
step: g.window,
|
|
6070
|
+
metric: g.metric,
|
|
6071
|
+
state: "worse",
|
|
6072
|
+
grade: g.grade,
|
|
6073
|
+
baseline: g.baseline ?? same?.baseline ?? null,
|
|
6074
|
+
...(g.baseline != null ? g.baseline_is_bound : same?.baseline_is_bound) ? { baseline_is_bound: true } : {},
|
|
6075
|
+
...(g.baseline != null ? g.change_is_bound : same?.change_is_bound) ? { change_is_bound: true } : {},
|
|
6076
|
+
candidate: g.candidate ?? same?.candidate ?? null,
|
|
6077
|
+
change_text: g.change,
|
|
6078
|
+
...g.finding ? { finding: g.finding } : {},
|
|
6079
|
+
...g.label ? { label_text: midLine(g.label, g.metric) } : {},
|
|
6080
|
+
...sc.device ? { device_id: sc.device.id } : {}
|
|
6081
|
+
};
|
|
6082
|
+
});
|
|
6083
|
+
return [...graded, ...flat.filter((i) => i.state === "better" && (i.judged ?? "outcome") === "outcome")];
|
|
6084
|
+
}
|
|
6085
|
+
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
6086
|
+
var linkOf = (x, set) => x?.share_url || set?.share_url || x?.dashboard_url || set?.dashboard_url || null;
|
|
6087
|
+
var measureWord = (i) => {
|
|
6088
|
+
if (i.label_text)
|
|
6089
|
+
return i.label_text;
|
|
6090
|
+
if (i.metric === "dur_ms" && placeKindWord(i.step ?? "") !== "Tap")
|
|
6091
|
+
return "length";
|
|
6092
|
+
return measureDef(i.metric)?.short ?? i.metric.replace(/_/g, " ");
|
|
6093
|
+
};
|
|
6094
|
+
var changeWords = (i) => {
|
|
6095
|
+
if (i.change_text)
|
|
6096
|
+
return i.change_text.replace(/ \(.*\)$/, "");
|
|
6097
|
+
const { baseline: b, candidate: c } = i;
|
|
6098
|
+
if (b === null || c === null)
|
|
6099
|
+
return "";
|
|
6100
|
+
if (b === 0)
|
|
6101
|
+
return atLeast(`${c - b >= 0 ? "+" : ""}${fieldValueText(i.metric, c - b)}`, i.change_is_bound);
|
|
6102
|
+
const p = Math.round((c / b - 1) * 100);
|
|
6103
|
+
return atLeast(`${p >= 0 ? "+" : ""}${p} %`, i.change_is_bound);
|
|
6104
|
+
};
|
|
6105
|
+
var placeOf = (step) => !step || step === "flow" || step === "*" ? null : placeLabel(step);
|
|
6106
|
+
function stateOf(sc, errors) {
|
|
6107
|
+
if (sc.state === null)
|
|
6108
|
+
return sc.word === NO_BASELINE_WORD ? "cant-tell" : "not-computed";
|
|
6109
|
+
if (sc.state === "inconclusive")
|
|
6110
|
+
return "cant-tell";
|
|
6111
|
+
if (sc.state === "warn")
|
|
6112
|
+
return errors || sc.word?.startsWith(STATE_WORDS.worse) ? "worse" : "warning";
|
|
6113
|
+
return sc.state;
|
|
6114
|
+
}
|
|
6115
|
+
function laneOf(v, sc, set) {
|
|
6116
|
+
const moved = sc.state === "inconclusive" ? [] : movedItems(v, sc);
|
|
6117
|
+
const worse = moved.filter((i) => i.state === "worse").sort((a, b) => Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
6118
|
+
const errors = worse.filter((i) => i.grade !== "warn");
|
|
6119
|
+
const state = stateOf(sc, errors.length);
|
|
6120
|
+
const shown = state === "better" ? moved.filter((i) => i.state === "better") : worse;
|
|
6121
|
+
return {
|
|
6122
|
+
journey: sc.scenario,
|
|
6123
|
+
phone: sc.device?.model ?? null,
|
|
6124
|
+
state,
|
|
6125
|
+
answer: sc.headline?.answer ?? null,
|
|
6126
|
+
word: sc.word ?? null,
|
|
6127
|
+
changes: shown.map((i) => ({
|
|
6128
|
+
place: placeOf(i.step),
|
|
6129
|
+
measure: measureWord(i),
|
|
6130
|
+
key: i.finding ?? placeRef(i.step ?? "*").stem,
|
|
6131
|
+
tier: i.state === "better" ? "better" : i.grade === "warn" ? "warning" : "worse",
|
|
6132
|
+
main: i.baseline === null ? null : atMost(fieldValueText(i.metric, i.baseline), i.baseline_is_bound),
|
|
6133
|
+
now: i.candidate === null ? null : fieldValueText(i.metric, i.candidate),
|
|
6134
|
+
change: changeWords(i)
|
|
6135
|
+
})),
|
|
6136
|
+
places: placeCount(shown),
|
|
6137
|
+
blocking: sc.headline?.blocking ?? v.blocking ?? v.headline?.blocking ?? true,
|
|
6138
|
+
measuredDifferently: !!sc.headline?.measured_differently?.length,
|
|
6139
|
+
fewRuns: sc.low_data,
|
|
6140
|
+
heatedRuns: sc.heated_runs ?? 0,
|
|
6141
|
+
runs: sc.runs.valid + sc.runs.invalid,
|
|
6142
|
+
namesChanged: sc.context?.namesChanged ?? null,
|
|
6143
|
+
url: linkOf(sc, set),
|
|
6144
|
+
main: sc.main ?? null,
|
|
6145
|
+
reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null
|
|
6146
|
+
};
|
|
6147
|
+
}
|
|
6148
|
+
var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
|
|
6149
|
+
var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
|
|
6150
|
+
var isPr = (s) => !!s.verdicts?.scenarios.some((sc) => sc.role === "comparison");
|
|
6151
|
+
var runSetMessage = (s) => 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 });
|
|
6152
|
+
var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
6153
|
+
function messageTone(s) {
|
|
6154
|
+
const lanes = s ? lanesOf(s) : [];
|
|
6155
|
+
const pr = !!s && isPr(s);
|
|
6156
|
+
if (lanes.some((l) => l.state === "worse" && (l.blocking || !pr)))
|
|
6157
|
+
return { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error };
|
|
6158
|
+
if (lanes.some((l) => l.state === "warning"))
|
|
6159
|
+
return { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn };
|
|
6160
|
+
switch (shownLane(lanes)?.state) {
|
|
6161
|
+
case "better":
|
|
6162
|
+
return { mark: "\uD83D\uDFE2", color: MESSAGE_COLORS.better };
|
|
6163
|
+
case "cant-tell":
|
|
6164
|
+
return { mark: "❔", color: MESSAGE_COLORS.none };
|
|
6165
|
+
default:
|
|
6166
|
+
return { mark: "⚪", color: MESSAGE_COLORS.none };
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6170
|
+
function slackPayload(m, tone) {
|
|
6171
|
+
const lead = `${tone.mark} ${esc(m.lead)}`;
|
|
6172
|
+
return {
|
|
6173
|
+
attachments: [{
|
|
6174
|
+
color: tone.color,
|
|
6175
|
+
fallback: lead,
|
|
6176
|
+
blocks: [
|
|
6177
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${lead}*
|
|
6178
|
+
${esc(m.detail)}` } },
|
|
6179
|
+
...m.link ? [{ type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: m.link.label }, url: m.link.url }] }] : []
|
|
6180
|
+
]
|
|
6181
|
+
}]
|
|
6182
|
+
};
|
|
6183
|
+
}
|
|
6184
|
+
function resultSlackPayload(s, o = {}) {
|
|
6185
|
+
if (!s.verdicts)
|
|
6186
|
+
return null;
|
|
6187
|
+
const lanes = lanesOf(s);
|
|
6188
|
+
const moved = lanes.some((l) => l.state === "worse" || l.state === "warning");
|
|
6189
|
+
const heat = lanes.some((l) => l.heatedRuns > 0);
|
|
6190
|
+
const improved = lanes.some((l) => l.state === "better") || s.verdicts.state === "better";
|
|
6191
|
+
if (isPr(s) ? !moved && !heat : !moved && !improved && !heat && !o.always)
|
|
6192
|
+
return null;
|
|
6193
|
+
return slackPayload(runSetMessage(s), messageTone(s));
|
|
6194
|
+
}
|
|
6195
|
+
var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
|
|
6196
|
+
var markdown = (m, mark) => `### ${mark} ${m.lead}
|
|
6197
|
+
|
|
6198
|
+
${m.lines.join(`
|
|
6199
|
+
`)}
|
|
6200
|
+
${m.link ? `
|
|
6201
|
+
[${m.link.label} ›](${m.link.url})
|
|
6202
|
+
` : ""}`;
|
|
6203
|
+
var resultMarkdown = (s) => markdown(runSetMessage(s), messageTone(s).mark);
|
|
6204
|
+
var noResultMarkdown = (branch, reason, url) => markdown(noResultMessage(branch, reason, url), "⚪");
|
|
6205
|
+
var pendingMarkdown = (branch, url) => markdown(pendingMessage(branch, url), "⚪");
|
|
6206
|
+
// packages/present/dist/src/stale.js
|
|
6207
|
+
function staleNoteText(why) {
|
|
6208
|
+
const newer = why.includes("newer-run-set");
|
|
6209
|
+
const rules = why.some((w) => w === "core-version" || w === "config");
|
|
6210
|
+
if (newer && rules)
|
|
6211
|
+
return "Result from an older run set and older rules.";
|
|
6212
|
+
if (newer)
|
|
6213
|
+
return "Result from an older run set; a newer one has arrived.";
|
|
6214
|
+
if (rules)
|
|
6215
|
+
return "Result from older rules; it is not recomputed yet.";
|
|
6216
|
+
return "Result from older inputs.";
|
|
6217
|
+
}
|
|
6218
|
+
var staleFlagText = (why) => {
|
|
6219
|
+
const note = staleNoteText(why).replace(/\.$/, "");
|
|
6220
|
+
return `${note.charAt(0).toLowerCase()}${note.slice(1)}`;
|
|
6221
|
+
};
|
|
6222
|
+
var STALE_MARK = "stale result";
|
|
6223
|
+
// apps/agent/src/upload.ts
|
|
6224
|
+
import { join as join4 } from "node:path";
|
|
6225
|
+
var MAX_RATE_RETRIES = 5;
|
|
6226
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6227
|
+
var pause = (ms) => sleep(ms);
|
|
6228
|
+
|
|
6229
|
+
class UploadError extends Error {
|
|
6230
|
+
status;
|
|
6231
|
+
constructor(message, status) {
|
|
6232
|
+
super(message);
|
|
6233
|
+
this.status = status;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
async function call(f, base, path, headers, body, method = "POST") {
|
|
6237
|
+
let res;
|
|
6238
|
+
for (let attempt = 0;; attempt++) {
|
|
6239
|
+
try {
|
|
6240
|
+
res = await f(`${base.replace(/\/+$/, "")}${path}`, {
|
|
6241
|
+
method,
|
|
6242
|
+
headers: method === "POST" ? { "content-type": "application/json", ...headers } : headers,
|
|
6243
|
+
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
6244
|
+
});
|
|
6245
|
+
} catch (e) {
|
|
6246
|
+
throw new UploadError(`${method} ${path}: ${e.message}`, 0);
|
|
6247
|
+
}
|
|
6248
|
+
if (res.status !== 429 || attempt >= MAX_RATE_RETRIES)
|
|
6249
|
+
break;
|
|
6250
|
+
const wait = Number(res.headers.get("retry-after") ?? "1");
|
|
6251
|
+
await res.text().catch(() => "");
|
|
6252
|
+
await sleep(Math.min(Math.max(Number.isFinite(wait) ? wait : 1, 1), 60) * 1000);
|
|
6253
|
+
}
|
|
6254
|
+
const text = await res.text();
|
|
6255
|
+
if (res.status < 200 || res.status >= 300)
|
|
6256
|
+
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}`, res.status);
|
|
6257
|
+
try {
|
|
6258
|
+
return text ? JSON.parse(text) : {};
|
|
6259
|
+
} catch {
|
|
6260
|
+
throw new UploadError(`${method} ${path} -> ${res.status}, the answer is not JSON (is the URL the KeepSnappy API?): ${text.slice(0, 200)}`);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
async function uploadRuns(deps, outDir, runs, opts) {
|
|
6264
|
+
const f = opts.fetch ?? fetch;
|
|
6265
|
+
const size = opts.batchSize ?? 200;
|
|
6266
|
+
const skipped = [];
|
|
6267
|
+
let uploaded = 0;
|
|
6268
|
+
for (const r of runs) {
|
|
6269
|
+
if (!r.valid) {
|
|
6270
|
+
skipped.push({ run_id: r.run_id, why: r.invalid_reason || "invalid" });
|
|
6271
|
+
continue;
|
|
6272
|
+
}
|
|
6273
|
+
const log = await deps.fs.readFile(join4(outDir, r.run_id, "keepsnappy.log"));
|
|
6274
|
+
const windows = windowsInRun(parseWindows(log), r.t0, r.t1);
|
|
6275
|
+
if (windows.length === 0) {
|
|
6276
|
+
skipped.push({ run_id: r.run_id, why: "no-windows" });
|
|
6277
|
+
continue;
|
|
6278
|
+
}
|
|
6279
|
+
const externalId = `${opts.externalPrefix}-${r.run_id}`;
|
|
6280
|
+
const created = await call(f, opts.ingestUrl, "/v1/runs", { "x-keepsnappy-key": opts.key }, {
|
|
6281
|
+
external_id: externalId,
|
|
6282
|
+
started_at: r.t0,
|
|
6283
|
+
ended_at: r.t1,
|
|
6284
|
+
device: { ...opts.device?.model ? { model: opts.device.model } : {}, ...opts.device?.os ? { os: opts.device.os } : {}, ...opts.device?.id ? { id: opts.device.id } : {}, exclusive: true },
|
|
6285
|
+
meta: {
|
|
6286
|
+
arm: r.arm,
|
|
6287
|
+
driver: opts.driver ?? "keepsnappy-run",
|
|
6288
|
+
...opts.meta,
|
|
6289
|
+
...r.scenario ? { scenario: r.scenario } : {},
|
|
6290
|
+
...opts.runSetId ? { run_set_id: opts.runSetId } : {}
|
|
6291
|
+
}
|
|
6292
|
+
});
|
|
6293
|
+
const runId = String(created.run_id ?? "");
|
|
6294
|
+
const token = String(created.run_token ?? "");
|
|
6295
|
+
if (!runId || !token)
|
|
6296
|
+
throw new UploadError("POST /v1/runs: response has no run_id/run_token");
|
|
6297
|
+
const items = windows.map((w) => `${w.endWall} KEEPSNAPPY window ${JSON.stringify(w.raw)}`);
|
|
6298
|
+
for (let i = 0;i * size < items.length; i++) {
|
|
6299
|
+
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
6300
|
+
batch_id: `${externalId}-${String(i + 1).padStart(4, "0")}`,
|
|
6301
|
+
items: items.slice(i * size, (i + 1) * size)
|
|
6302
|
+
});
|
|
6303
|
+
}
|
|
6304
|
+
const samples = samplesInRun(parseSamples(log), r.t0, r.t1).map((x) => `${x.wall} KEEPSNAPPY sample ${JSON.stringify(x.raw)}`);
|
|
6305
|
+
for (let i = 0;i * size < samples.length; i++) {
|
|
6306
|
+
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
6307
|
+
batch_id: `${externalId}-s${String(i + 1).padStart(4, "0")}`,
|
|
6308
|
+
items: samples.slice(i * size, (i + 1) * size)
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
let markers = [];
|
|
6312
|
+
try {
|
|
6313
|
+
markers = parseMarkersNdjson(await deps.fs.readFile(join4(outDir, r.run_id, "markers.ndjson")));
|
|
6314
|
+
} catch {}
|
|
6315
|
+
for (const m of markers) {
|
|
6316
|
+
await call(f, opts.ingestUrl, "/v1/markers", { authorization: `Bearer ${token}` }, { step: m.step, phase: m.phase, t_host: m.t_host });
|
|
6317
|
+
}
|
|
6318
|
+
await call(f, opts.ingestUrl, `/v1/runs/${runId}/finish`, { "x-keepsnappy-key": opts.key }, {});
|
|
6319
|
+
uploaded++;
|
|
6320
|
+
}
|
|
6321
|
+
return { uploaded, skipped };
|
|
6322
|
+
}
|
|
6323
|
+
|
|
5764
6324
|
// apps/agent/src/summary.ts
|
|
5765
6325
|
var fieldWord = (field) => measureDef(field)?.short ?? field;
|
|
5766
6326
|
function plainMetric(metric) {
|
|
@@ -5799,7 +6359,6 @@ function summarize(input) {
|
|
|
5799
6359
|
|
|
5800
6360
|
// apps/agent/src/remote.ts
|
|
5801
6361
|
var isBlocking = (s) => !!s?.verdicts && (s.verdicts.blocking ?? s.verdicts.state === "worse");
|
|
5802
|
-
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
5803
6362
|
function heatNote(sc) {
|
|
5804
6363
|
const n = sc.heated_runs ?? 0;
|
|
5805
6364
|
if (!n)
|
|
@@ -5807,30 +6366,6 @@ function heatNote(sc) {
|
|
|
5807
6366
|
const phone = sc.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
5808
6367
|
return `${INFO_WORDS.heat} in ${n} of ${sc.runs.valid + sc.runs.invalid} runs · ${sc.scenario ?? "journey"} · ${phone}`;
|
|
5809
6368
|
}
|
|
5810
|
-
function movedItems(v, sc) {
|
|
5811
|
-
const flat = v.items.filter((i) => i.scenario === sc.scenario && (!sc.device || !i.device_id || i.device_id === sc.device.id));
|
|
5812
|
-
const h = sc.headline;
|
|
5813
|
-
if (h?.errors === undefined)
|
|
5814
|
-
return flat.filter((i) => i.state === "worse" || i.state === "better");
|
|
5815
|
-
const graded = [...h.errors, ...h.warnings ?? []].map((g) => {
|
|
5816
|
-
const same = flat.find((i) => i.step === g.window && i.metric === g.metric);
|
|
5817
|
-
return {
|
|
5818
|
-
...same ?? { kind: "change", low_data: false, low_data_reason: null },
|
|
5819
|
-
scenario: sc.scenario,
|
|
5820
|
-
step: g.window,
|
|
5821
|
-
metric: g.metric,
|
|
5822
|
-
state: "worse",
|
|
5823
|
-
grade: g.grade,
|
|
5824
|
-
baseline: g.baseline ?? same?.baseline ?? null,
|
|
5825
|
-
candidate: g.candidate ?? same?.candidate ?? null,
|
|
5826
|
-
change_text: g.change,
|
|
5827
|
-
...g.finding ? { finding: g.finding } : {},
|
|
5828
|
-
...g.label ? { label_text: midLine(g.label, g.metric) } : {},
|
|
5829
|
-
...sc.device ? { device_id: sc.device.id } : {}
|
|
5830
|
-
};
|
|
5831
|
-
});
|
|
5832
|
-
return [...graded, ...flat.filter((i) => i.state === "better" && (i.judged ?? "outcome") === "outcome")];
|
|
5833
|
-
}
|
|
5834
6369
|
var placeOfRow = (g) => g.finding ?? placeRef(g.window).stem;
|
|
5835
6370
|
function movedCounts(sc) {
|
|
5836
6371
|
const h = sc.headline;
|
|
@@ -5840,7 +6375,6 @@ function movedCounts(sc) {
|
|
|
5840
6375
|
const slightly = new Set((h.warnings ?? []).map(placeOfRow).filter((p) => !worse.has(p)));
|
|
5841
6376
|
return { worse: worse.size, warnings: slightly.size, better: h.counts.better, noChange: h.counts.no_change, inconclusive: h.counts.inconclusive };
|
|
5842
6377
|
}
|
|
5843
|
-
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
5844
6378
|
var addCounts = (xs) => xs.reduce((a, c) => ({ worse: a.worse + c.worse, warnings: a.warnings + c.warnings, better: a.better + c.better, noChange: a.noChange + c.noChange, inconclusive: a.inconclusive + c.inconclusive }), { worse: 0, warnings: 0, better: 0, noChange: 0, inconclusive: 0 });
|
|
5845
6379
|
var setupLine = (h) => {
|
|
5846
6380
|
const parts = [
|
|
@@ -5856,13 +6390,35 @@ async function createRunSet(t, body) {
|
|
|
5856
6390
|
throw new UploadError("POST /v1/run-sets: response has no run_set_id (is the URL the KeepSnappy API?)");
|
|
5857
6391
|
return { run_set_id: r.run_set_id, dashboard_url: String(r.dashboard_url ?? "") };
|
|
5858
6392
|
}
|
|
6393
|
+
var UNKNOWN_OUTCOME = new Set([0, 502, 503, 504]);
|
|
6394
|
+
var CONFIRM_READS = 6;
|
|
6395
|
+
var CONFIRM_WAIT_MS = 1e4;
|
|
5859
6396
|
async function completeRunSet(t, id, verdicts) {
|
|
5860
6397
|
const path = `/v1/run-sets/${encodeURIComponent(id)}/complete`;
|
|
5861
|
-
|
|
6398
|
+
let r;
|
|
6399
|
+
try {
|
|
6400
|
+
r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), { verdicts });
|
|
6401
|
+
} catch (e) {
|
|
6402
|
+
const closed = e instanceof UploadError && UNKNOWN_OUTCOME.has(e.status ?? -1) ? await confirmedClose(t, id) : null;
|
|
6403
|
+
if (closed)
|
|
6404
|
+
return closed;
|
|
6405
|
+
throw e;
|
|
6406
|
+
}
|
|
5862
6407
|
if (!r.run_set_id)
|
|
5863
6408
|
throw new UploadError(`POST ${path}: response has no run_set_id`);
|
|
5864
6409
|
return r;
|
|
5865
6410
|
}
|
|
6411
|
+
async function confirmedClose(t, id) {
|
|
6412
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}`;
|
|
6413
|
+
for (let i = 0;i < CONFIRM_READS; i++) {
|
|
6414
|
+
if (i)
|
|
6415
|
+
await pause(CONFIRM_WAIT_MS);
|
|
6416
|
+
const s = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), undefined, "GET").catch(() => null);
|
|
6417
|
+
if (s?.run_set_id && (s.status === "processed" || s.status === "failed"))
|
|
6418
|
+
return s;
|
|
6419
|
+
}
|
|
6420
|
+
return null;
|
|
6421
|
+
}
|
|
5866
6422
|
async function createIngestRun(t, body) {
|
|
5867
6423
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/runs", keyHeader(t), body);
|
|
5868
6424
|
if (!r.run_id || !r.run_token)
|
|
@@ -5887,16 +6443,15 @@ var word = (s, server, blocking) => badgedWord(server || stateWord(s ?? "inconcl
|
|
|
5887
6443
|
var setWord = (v) => word(v?.state ?? null, v?.word, v?.blocking ?? v?.headline?.blocking);
|
|
5888
6444
|
var scenarioWord = (sc) => word(sc.state, sc.word, sc.headline?.blocking);
|
|
5889
6445
|
var notComputed = (s) => !s.verdicts || s.verdicts.state === null && !s.verdicts.blocking;
|
|
5890
|
-
var
|
|
5891
|
-
var
|
|
5892
|
-
var uncheckedGate = (s) => notComputed(s) ? UNCHECKED_GATE : null;
|
|
6446
|
+
var UNCHECKED_GATE = `${NOT_COMPUTED_WORD}: --fail-on-worse checked nothing`;
|
|
6447
|
+
var uncheckedGate = (s) => notComputed(s) ? s.verdicts?.word && s.verdicts.word !== NOT_COMPUTED_WORD ? `${s.verdicts.word}: --fail-on-worse checked nothing` : UNCHECKED_GATE : null;
|
|
5893
6448
|
var countsLine = (c) => countsWords(c);
|
|
5894
6449
|
var noCounts = (c) => !c.worse && !c.warnings && !c.better && !c.noChange && !c.inconclusive;
|
|
5895
|
-
var change = (metric, a, b) => {
|
|
6450
|
+
var change = (metric, a, b, bounds) => {
|
|
5896
6451
|
if (a === null || b === null)
|
|
5897
6452
|
return "";
|
|
5898
|
-
const pct = a === 0 ? "" : ` ${b >= a ? "+" : "-"}${Math.round(Math.abs(b / a - 1) * 100)}
|
|
5899
|
-
return `${pct} (${fieldValueText(metric, a)} -> ${fieldValueText(metric, b)})`;
|
|
6453
|
+
const pct = a === 0 ? "" : ` ${atLeast(`${b >= a ? "+" : "-"}${Math.round(Math.abs(b / a - 1) * 100)}%`, bounds.change_is_bound)}`;
|
|
6454
|
+
return `${pct} (${atMost(fieldValueText(metric, a), bounds.baseline_is_bound)} -> ${fieldValueText(metric, b)})`;
|
|
5900
6455
|
};
|
|
5901
6456
|
function formatRunSet(s, top = 5) {
|
|
5902
6457
|
const lines = [];
|
|
@@ -5926,11 +6481,12 @@ function formatRunSet(s, top = 5) {
|
|
|
5926
6481
|
lines.push(...resultContextLines(sc.context).map((line) => ` ${line}`));
|
|
5927
6482
|
if (heatNote(sc))
|
|
5928
6483
|
lines.push(` ${heatNote(sc)}`);
|
|
6484
|
+
lines.push(...(sc.headline?.session_speed ?? []).map((line) => ` ${line}`));
|
|
5929
6485
|
const allMoved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
5930
6486
|
const moved = allMoved.slice(0, top);
|
|
5931
6487
|
for (const i of moved) {
|
|
5932
6488
|
const what = i.label_text ? `${i.step && i.step !== "*" ? `${placeLabel(i.step)}: ` : ""}${i.label_text}` : i.step ? `${placeLabel(i.step)}: ${fieldWord(i.metric)}` : plainMetric(i.metric);
|
|
5933
|
-
lines.push(` ${stateWord(i.state === "worse" && i.grade === "warn" ? "warn" : i.state).padEnd(WORD_WIDTH)} ${what}${i.change_text ? `: ${i.change_text}` : change(i.metric, i.baseline, i.candidate)}`);
|
|
6489
|
+
lines.push(` ${stateWord(i.state === "worse" && i.grade === "warn" ? "warn" : i.state).padEnd(WORD_WIDTH)} ${what}${i.change_text ? `: ${i.change_text}` : change(i.metric, i.baseline, i.candidate, i)}`);
|
|
5934
6490
|
}
|
|
5935
6491
|
if (allMoved.length > moved.length)
|
|
5936
6492
|
lines.push(` ${allMoved.length - moved.length} more`);
|
|
@@ -5947,35 +6503,12 @@ function formatRunSet(s, top = 5) {
|
|
|
5947
6503
|
`);
|
|
5948
6504
|
}
|
|
5949
6505
|
function markdownRunSet(s, fallback) {
|
|
5950
|
-
if (!s || !s.verdicts)
|
|
6506
|
+
if (!s || !s.verdicts)
|
|
5951
6507
|
return `### KeepSnappy
|
|
5952
6508
|
|
|
5953
6509
|
Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [open KeepSnappy](${fallback.dashboardUrl})
|
|
5954
6510
|
`;
|
|
5955
|
-
|
|
5956
|
-
const v = s.verdicts;
|
|
5957
|
-
const heated = v.scenarios.flatMap((sc) => heatNote(sc) ?? []);
|
|
5958
|
-
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", sc.device?.model ?? "-"));
|
|
5959
|
-
const rows = v.scenarios.map((sc) => {
|
|
5960
|
-
const c = movedCounts(sc);
|
|
5961
|
-
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${scenarioWord(sc)}${sc.low_data ? ` (${FEW_RUNS})` : ""} | ${noCounts(c) ? "–" : `${c.worse} / ${c.warnings} / ${c.better} / ${c.noChange} / ${c.inconclusive}`} | ${sc.dashboard_url ? `[open](${sc.dashboard_url})` : "-"} |`;
|
|
5962
|
-
});
|
|
5963
|
-
return [
|
|
5964
|
-
`### KeepSnappy: ${setWord(v)}`,
|
|
5965
|
-
"",
|
|
5966
|
-
...setupLine(v.headline) ? [`${setupLine(v.headline)}.`, ""] : [],
|
|
5967
|
-
`\`${s.branch}\`${s.commit_sha ? ` @ \`${s.commit_sha.slice(0, 7)}\`` : ""}, ${s.runs.valid}/${s.runs.total} runs used.${v.low_data ? ` ${FEW_RUNS}.` : ""}`,
|
|
5968
|
-
"",
|
|
5969
|
-
`| Journey | Device | Result | ${STATE_WORDS.worse} / ${STATE_WORDS.warning} / ${STATE_WORDS.better} / ${STATE_WORDS["no-change"]} / ${STATE_WORDS["cant-tell"]} | Details |`,
|
|
5970
|
-
"|---|---|---|---|---|",
|
|
5971
|
-
...rows,
|
|
5972
|
-
...contextLines.length ? ["", ...contextLines] : [],
|
|
5973
|
-
...heated.length ? ["", ...heated.map((note) => `${note}.`)] : [],
|
|
5974
|
-
"",
|
|
5975
|
-
`[Open the dashboard](${s.dashboard_url})`,
|
|
5976
|
-
""
|
|
5977
|
-
].join(`
|
|
5978
|
-
`);
|
|
6511
|
+
return isPending(s) ? pendingMarkdown(s.branch, linkOf(s) ?? s.dashboard_url) : resultMarkdown(s);
|
|
5979
6512
|
}
|
|
5980
6513
|
|
|
5981
6514
|
// apps/agent/src/run.ts
|
|
@@ -9351,7 +9884,7 @@ function parseArms(spec) {
|
|
|
9351
9884
|
return spec.split(",").map((part) => {
|
|
9352
9885
|
const eq = part.indexOf("=");
|
|
9353
9886
|
if (eq <= 0)
|
|
9354
|
-
throw new
|
|
9887
|
+
throw new ConfigError([`bad --arms entry "${part}", expected NAME=<app-path-or-bundle-id>`]);
|
|
9355
9888
|
return { name: part.slice(0, eq).trim(), target: part.slice(eq + 1).trim() };
|
|
9356
9889
|
});
|
|
9357
9890
|
}
|
|
@@ -9361,10 +9894,10 @@ function parseArmEnv(specs, arms) {
|
|
|
9361
9894
|
for (const spec of specs ?? []) {
|
|
9362
9895
|
const m = /^([^:=]+):([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(spec);
|
|
9363
9896
|
if (!m)
|
|
9364
|
-
throw new
|
|
9897
|
+
throw new ConfigError([`bad --arm-env "${spec}", expected ARM:KEY=VALUE`]);
|
|
9365
9898
|
const [, arm = "", key = "", value = ""] = m;
|
|
9366
9899
|
if (!names.has(arm))
|
|
9367
|
-
throw new
|
|
9900
|
+
throw new ConfigError([`--arm-env "${spec}": no arm named ${arm} (arms: ${[...names].join(", ")})`]);
|
|
9368
9901
|
const e = env.get(arm) ?? {};
|
|
9369
9902
|
e[key] = value;
|
|
9370
9903
|
if (!key.startsWith("MAESTRO_"))
|
|
@@ -9375,12 +9908,15 @@ function parseArmEnv(specs, arms) {
|
|
|
9375
9908
|
}
|
|
9376
9909
|
|
|
9377
9910
|
// apps/agent/src/runner.ts
|
|
9911
|
+
var TEST_ERROR = "test-error";
|
|
9912
|
+
var TEST_FAILED = "test-failed";
|
|
9913
|
+
var failedTest = (reason) => reason.startsWith(`${TEST_FAILED}:`) || reason.startsWith(`${TEST_ERROR}:`);
|
|
9378
9914
|
var isBuildPath = (target) => !!target && /\.(app|ipa)\/?$/.test(target);
|
|
9379
9915
|
async function measure(deps, source, installer, opts) {
|
|
9380
9916
|
const { fs, clock, spawner, log } = deps;
|
|
9381
9917
|
const flows = opts.flows?.length ? opts.flows : [{ name: undefined, command: opts.testCommand }];
|
|
9382
9918
|
if (flows.some((f) => !f.command[0]))
|
|
9383
|
-
throw new
|
|
9919
|
+
throw new ConfigError(["no test command given (put it after `--`)"]);
|
|
9384
9920
|
await fs.mkdir(opts.outDir);
|
|
9385
9921
|
const runsPath = join7(opts.outDir, "runs.ndjson");
|
|
9386
9922
|
await fs.writeFile(runsPath, "");
|
|
@@ -9421,9 +9957,9 @@ async function measure(deps, source, installer, opts) {
|
|
|
9421
9957
|
--- stderr ---
|
|
9422
9958
|
${r.stderr}` : ""}`);
|
|
9423
9959
|
if (r.error)
|
|
9424
|
-
reason =
|
|
9960
|
+
reason = `${TEST_ERROR}: ${r.error}`;
|
|
9425
9961
|
else if (r.code !== 0)
|
|
9426
|
-
reason =
|
|
9962
|
+
reason = `${TEST_FAILED}: exit ${r.code}`;
|
|
9427
9963
|
}
|
|
9428
9964
|
const t1 = clock.now();
|
|
9429
9965
|
let windows = [];
|
|
@@ -9521,7 +10057,7 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
9521
10057
|
const runs = [];
|
|
9522
10058
|
for (const src of sources) {
|
|
9523
10059
|
const text = await deps.fs.readFile(join8(src.dir, "runs.ndjson")).catch(() => {
|
|
9524
|
-
throw new
|
|
10060
|
+
throw new ConfigError([`--replay ${src.dir}: no runs.ndjson (not a keepsnappy out dir)`]);
|
|
9525
10061
|
});
|
|
9526
10062
|
for (const line of text.split(`
|
|
9527
10063
|
`)) {
|
|
@@ -9541,7 +10077,7 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
9541
10077
|
}
|
|
9542
10078
|
}
|
|
9543
10079
|
if (runs.length === 0)
|
|
9544
|
-
throw new
|
|
10080
|
+
throw new ConfigError([`--replay: no valid runs in ${sources.map((x) => x.dir + (x.arm ? `:${x.arm}` : "")).join(", ")}`]);
|
|
9545
10081
|
await deps.fs.writeFile(join8(outDir, "runs.ndjson"), runs.map((r) => JSON.stringify(r)).join(`
|
|
9546
10082
|
`) + `
|
|
9547
10083
|
`);
|
|
@@ -9555,7 +10091,7 @@ var DEFAULT_API_URL = "https://keepsnappy-api-922918702810.europe-west1.run.app"
|
|
|
9555
10091
|
// apps/agent/src/journeys.ts
|
|
9556
10092
|
import { isAbsolute, relative, resolve as resolve2, sep } from "node:path";
|
|
9557
10093
|
var charCount = (s) => [...s].length;
|
|
9558
|
-
function
|
|
10094
|
+
function clip2(s, max) {
|
|
9559
10095
|
if (charCount(s) <= max)
|
|
9560
10096
|
return s;
|
|
9561
10097
|
const parts = typeof Intl.Segmenter === "function" ? Array.from(new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(s), (x) => x.segment) : [...s];
|
|
@@ -9589,7 +10125,7 @@ function flowHeader(text) {
|
|
|
9589
10125
|
para.push(t);
|
|
9590
10126
|
}
|
|
9591
10127
|
const description = para.join(" ").replace(/\s+/g, " ").trim();
|
|
9592
|
-
return { ...title ? { title:
|
|
10128
|
+
return { ...title ? { title: clip2(title, TITLE_MAX) } : {}, ...description ? { description: clip2(description, DESCRIPTION_MAX) } : {} };
|
|
9593
10129
|
}
|
|
9594
10130
|
async function repoRoot(deps) {
|
|
9595
10131
|
const r = await deps.spawner.run("git", ["rev-parse", "--show-toplevel"]).catch(() => null);
|
|
@@ -9643,10 +10179,11 @@ function baseRefs(env) {
|
|
|
9643
10179
|
return target ? [`origin/${target}`] : ["origin/main", "main"];
|
|
9644
10180
|
}
|
|
9645
10181
|
var SHA = /^[0-9a-f]{40,64}$/;
|
|
10182
|
+
var onMain = (env, branch) => !!branch && baseRefs(env).some((r) => !r.startsWith("-") && r.replace(REMOTE, "") === branch);
|
|
9646
10183
|
var FIX = "fetch main and the commit, in CI with fetch-depth: 0, or pass --base-commit SHA";
|
|
9647
10184
|
async function defaultBaseCommit(deps, env, o) {
|
|
9648
10185
|
const refs = baseRefs(env).filter((r) => !r.startsWith("-"));
|
|
9649
|
-
if (
|
|
10186
|
+
if (onMain(env, o.branch))
|
|
9650
10187
|
return {};
|
|
9651
10188
|
const tip = o.commit?.trim();
|
|
9652
10189
|
if (!tip)
|
|
@@ -9682,6 +10219,34 @@ async function commitFields(deps, env, shas) {
|
|
|
9682
10219
|
const pr = await pullRequestInfo(deps, env);
|
|
9683
10220
|
return { ...commit ? { commit } : {}, ...base ? { base_commit: base } : {}, ...pr ? { pull_request: { ...pr } } : {} };
|
|
9684
10221
|
}
|
|
10222
|
+
var MAIN_MERGES_LIMIT = 200;
|
|
10223
|
+
var SQUASH = /^(.*?)\s*\(#(\d+)\)$/;
|
|
10224
|
+
var MERGE = /^Merge pull request #(\d+) from \S+$/;
|
|
10225
|
+
function mergeOf(sha, subject, body) {
|
|
10226
|
+
const merge = MERGE.exec(subject);
|
|
10227
|
+
const squash = merge ? null : SQUASH.exec(subject);
|
|
10228
|
+
const number = Number(merge?.[1] ?? squash?.[2] ?? 0);
|
|
10229
|
+
const title = (merge ? body.split(`
|
|
10230
|
+
`).find((l) => l.trim())?.trim() : squash?.[1]) || subject.trim() || sha.slice(0, 7);
|
|
10231
|
+
return { sha, ...number > 0 ? { number } : {}, title: Array.from(title).slice(0, 300).join("") };
|
|
10232
|
+
}
|
|
10233
|
+
async function mainMerges(deps, env, o) {
|
|
10234
|
+
if (!onMain(env, o.branch))
|
|
10235
|
+
return {};
|
|
10236
|
+
const tip = o.commit?.trim();
|
|
10237
|
+
if (!tip || tip.startsWith("-"))
|
|
10238
|
+
return { warning: "main merges not recorded: no commit is recorded for the run set (pass --commit, or run in GitHub Actions)" };
|
|
10239
|
+
const r = await deps.spawner.run("git", ["log", "--first-parent", `--max-count=${MAIN_MERGES_LIMIT}`, "--format=%H%x1f%s%x1f%b%x1e", tip, "--"], { timeoutMs: 1e4 }).catch(() => null);
|
|
10240
|
+
const merges = r && r.code === 0 ? r.stdout.split("\x1E").map((x) => x.replace(/^\n/, "").split("\x1F")).filter(([sha]) => sha && SHA.test(sha)).map(([sha, s, b]) => mergeOf(sha, s ?? "", b ?? "")) : [];
|
|
10241
|
+
if (!merges.length)
|
|
10242
|
+
return { warning: `main merges not recorded: git log ${tip.slice(0, 12)} failed (fetch main and the commit, in CI with fetch-depth: 0)` };
|
|
10243
|
+
if (merges.length < MAIN_MERGES_LIMIT) {
|
|
10244
|
+
const shallow = await deps.spawner.run("git", ["rev-parse", "--is-shallow-repository"], { timeoutMs: 5000 }).catch(() => null);
|
|
10245
|
+
if (shallow?.code === 0 && shallow.stdout.trim() === "true")
|
|
10246
|
+
return { merges, warning: `main merges stop at the clone's depth (${merges.length}): fetch main with fetch-depth: 0 for the PRs of main changes` };
|
|
10247
|
+
}
|
|
10248
|
+
return { merges };
|
|
10249
|
+
}
|
|
9685
10250
|
|
|
9686
10251
|
// apps/agent/src/run-command.ts
|
|
9687
10252
|
var EXIT = { ok: 0, infra: 1, config: 2, worse: 3 };
|
|
@@ -9727,7 +10292,7 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
9727
10292
|
api = { url, key, ...rt.fetch ? { fetch: rt.fetch } : {} };
|
|
9728
10293
|
}
|
|
9729
10294
|
if (o.wait && !o.upload)
|
|
9730
|
-
throw new ConfigError(["--wait needs --upload (the
|
|
10295
|
+
throw new ConfigError(["--wait needs --upload (it waits until the server has processed the uploaded runs)"]);
|
|
9731
10296
|
if (o.wait && !git.branch)
|
|
9732
10297
|
throw new ConfigError(["--wait needs the branch: pass --branch, or run in GitHub Actions (GITHUB_HEAD_REF / GITHUB_REF_NAME)"]);
|
|
9733
10298
|
if (o["fail-on-worse"] && !o.wait)
|
|
@@ -9746,11 +10311,15 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
9746
10311
|
let runSet;
|
|
9747
10312
|
if (api && git.branch) {
|
|
9748
10313
|
const base = await baseCommitFor(deps, rt.env, { explicit: o["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
|
|
10314
|
+
const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
|
|
10315
|
+
if (main.warning)
|
|
10316
|
+
rt.print(main.warning);
|
|
9749
10317
|
runSet = await createRunSet(api, {
|
|
9750
10318
|
branch: git.branch,
|
|
9751
10319
|
...git.commit ? { commit_sha: git.commit } : {},
|
|
9752
10320
|
...base ? { base_commit_sha: base } : {},
|
|
9753
10321
|
...await commitFields(deps, rt.env, { commit: git.commit, base }),
|
|
10322
|
+
...main.merges ? { merges: main.merges } : {},
|
|
9754
10323
|
device: { ...device, provider },
|
|
9755
10324
|
scenarios: flows.map((f) => f.name),
|
|
9756
10325
|
journeys: await journeysFor(flows, { readFile: (p) => deps.fs.readFile(p), root: await repoRoot(deps) }),
|
|
@@ -9792,8 +10361,14 @@ ${outcome.summary}`);
|
|
|
9792
10361
|
const info = { device, ...git.branch ? { branch: git.branch } : {}, ...git.commit ? { commit: git.commit } : {} };
|
|
9793
10362
|
await deps.fs.writeFile(join9(out, RUN_INFO), `${JSON.stringify({ ...info, ...runSet ? { run_set_id: runSet.run_set_id } : {} }, null, 2)}
|
|
9794
10363
|
`);
|
|
9795
|
-
if (!api)
|
|
10364
|
+
if (!api) {
|
|
10365
|
+
if (!runs.some((run) => run.valid)) {
|
|
10366
|
+
const reasons = [...new Set(runs.map((run) => run.invalid_reason).filter(Boolean))];
|
|
10367
|
+
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(", ")})` : ""}`}.`);
|
|
10368
|
+
return EXIT.infra;
|
|
10369
|
+
}
|
|
9796
10370
|
return EXIT.ok;
|
|
10371
|
+
}
|
|
9797
10372
|
const up = await uploadRuns(deps, out, runs, {
|
|
9798
10373
|
ingestUrl: api.url,
|
|
9799
10374
|
key: api.key,
|
|
@@ -9808,7 +10383,9 @@ ${outcome.summary}`);
|
|
|
9808
10383
|
if (up.uploaded === 0) {
|
|
9809
10384
|
if (runSet)
|
|
9810
10385
|
await completeRunSet(api, runSet.run_set_id, "on-read").catch(infra("no run was uploaded, and the run set could not be closed"));
|
|
9811
|
-
|
|
10386
|
+
const failed = up.skipped.length > 0 && up.skipped.every((x) => failedTest(x.why));
|
|
10387
|
+
const why = [...new Set(up.skipped.map((x) => x.why))].join(", ");
|
|
10388
|
+
rt.print(`${BLOCKED_WORD}: no run sent any data. ${failed ? `Every run failed its test command (${why}); see test.log in each run folder of ${out}.` : "Check that the build carries the SDK and that the app reaches the API."}`);
|
|
9812
10389
|
if (runSet)
|
|
9813
10390
|
await writeResult(deps, out, rt, runSet, null, git.branch, true);
|
|
9814
10391
|
return EXIT.infra;
|
|
@@ -9817,7 +10394,7 @@ ${outcome.summary}`);
|
|
|
9817
10394
|
return EXIT.ok;
|
|
9818
10395
|
let summary = null;
|
|
9819
10396
|
if (o.wait) {
|
|
9820
|
-
summary = await completeRunSet(api, runSet.run_set_id, "inline").catch(infra("the runs are uploaded, but the
|
|
10397
|
+
summary = await completeRunSet(api, runSet.run_set_id, "inline").catch(infra("the runs are uploaded, but the run set could not be closed"));
|
|
9821
10398
|
rt.print(`
|
|
9822
10399
|
${formatRunSet(summary)}`);
|
|
9823
10400
|
} else {
|
|
@@ -9854,10 +10431,7 @@ async function writeResult(deps, out, rt, runSet, summary, branch, blocked = fal
|
|
|
9854
10431
|
`);
|
|
9855
10432
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
9856
10433
|
if (step)
|
|
9857
|
-
await deps.fs.appendFile(step, blocked ?
|
|
9858
|
-
|
|
9859
|
-
No run sent any data. [Open the dashboard](${url})
|
|
9860
|
-
` : markdownRunSet(summary, { dashboardUrl: url, branch }));
|
|
10434
|
+
await deps.fs.appendFile(step, blocked ? noResultMarkdown(branch, "No run sent any data.", url) : markdownRunSet(summary, { dashboardUrl: url, branch }));
|
|
9861
10435
|
}
|
|
9862
10436
|
|
|
9863
10437
|
// apps/agent/src/doctor.ts
|
|
@@ -10319,7 +10893,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
10319
10893
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
10320
10894
|
import { fileURLToPath } from "node:url";
|
|
10321
10895
|
function driverVersion() {
|
|
10322
|
-
const built = "0.1.0-alpha.
|
|
10896
|
+
const built = "0.1.0-alpha.19";
|
|
10323
10897
|
if (built)
|
|
10324
10898
|
return built;
|
|
10325
10899
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -10374,6 +10948,600 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
|
10374
10948
|
var FLUSH_MARGIN_MS = 30000;
|
|
10375
10949
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10376
10950
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
10951
|
+
// packages/verdict-core/dist/src/window-count.js
|
|
10952
|
+
var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
|
|
10953
|
+
var OFF = new WeakSet;
|
|
10954
|
+
// packages/verdict-core/dist/src/clauses.js
|
|
10955
|
+
var row = (id, what, unit, cmp, ruling, uncalibrated, value) => ({ id, what, unit, cmp, ruling, uncalibrated, value });
|
|
10956
|
+
var NO_RULING = "no ruling: a starting value of the deciding config (L2 audit F-13)";
|
|
10957
|
+
var FLOOR = { rel: 0.3, abs: 5 };
|
|
10958
|
+
var COMPONENT_WARN_MS = { update: 5, work: 10 };
|
|
10959
|
+
var CLAUSES = {
|
|
10960
|
+
"G-MINRUNS": row("G-MINRUNS", `Valid runs a side needs to be compared at all; fewer is "Can't tell: too few runs".`, "runs", "≥", "no ruling for the deciding config (L2 audit G-MINRUNS)", true, { runs: 2 }),
|
|
10961
|
+
"B-BASELINE": row("B-BASELINE", "The baseline is main's newest sessions on the same phone, journey and renderer.", "sessions", "≥", "REQ-VER-33 (Q4); the number is a starting value", true, { sessions: 3 }),
|
|
10962
|
+
"M-LEVELSHIFT": row("M-LEVELSHIFT", "A change on main is confirmed when this many following main sessions agree.", "sessions", "≥", "REQ-VER-33, plan 63 §3 (controller 2026-10-17)", false, { confirmSessions: 2 }),
|
|
10963
|
+
"S-UNCERTAIN": row("S-UNCERTAIN", "A worse result is uncertain and does not block while main has fewer sessions than this.", "sessions", "≥", 'REQ-VER-38, Arek 2026-10-16 "Nie blokuj przy małej bazie"', false, { belowMainSessions: 3 }),
|
|
10964
|
+
"G-JUDGE-FROM": row("G-JUDGE-FROM", "A phone or simulator is judged only with at least this many main sessions.", "sessions", "≥", "REQ-VER-41, Arek 2026-10-18", false, { mainSessions: 3 }),
|
|
10965
|
+
"W-WORKKIND": row("W-WORKKIND", "The share of render time spent mounting from which a place is an in-screen mount, not an update.", "share", "≥", "REQ-MET-25; TraceSift MOUNT_DOMINANT_SHARE", true, { mountShareAtLeast: 0.8 }),
|
|
10966
|
+
"W-WINDOW-COUNT": row("W-WINDOW-COUNT", "A PR run that opened fewer windows on a screen than this share of main's median there leaves that screen (its taps, scrolls and steps) and the journey's window-dependent totals out of its comparisons.", "share", "≥", "plan 51 D2 floor (90 % of the usual window count), Arek 2026-10-22 ~07:20, per screen (plan 79 S8)", false, { below: 0.9 }),
|
|
10967
|
+
"T-FRAME": row("T-FRAME", "One frame, the unit of every frame limit, the same on every screen (a 120 Hz phone is not held to 8.3 ms).", "ms", ">", "REQ-MET-23, Arek 2026-10-16", false, { ms: LIMIT_FRAME_MS }),
|
|
10968
|
+
"C-DETAIL-MS": row("C-DETAIL-MS", "A component below this keeps key and ms only and is not listed as New or Removed.", "ms", "≥", "plan 79 S9 r2, Arek 2026-10-23 13:20; half the lowest component rule floor (judged and evidence)", false, { ms: Math.min(FLOOR.abs, COMPONENT_WARN_MS.update, COMPONENT_WARN_MS.work) / 2 }),
|
|
10969
|
+
"T-WARN-MS": row("T-WARN-MS", "A judged change counts as Slightly worse past at least this many ms (an update, or an entry / in-screen mount).", "ms", ">", "REQ-MET-26 (Arek 2026-10-16 ~14:30, ~17:30); followups [RULED 2026-10-18 ~09:40]", false, COMPONENT_WARN_MS),
|
|
10970
|
+
"TIER-ERROR": row("TIER-ERROR", "A judged change is Worse past at least this many ms (an update, or an entry / in-screen mount).", "ms", ">", "REQ-MET-26 (Arek 2026-10-16 ~17:30); over the 100 ms budget alone is only a warning (Arek 2026-10-17)", false, { update: 15, work: 30 }),
|
|
10971
|
+
"S-BETTER": row("S-BETTER", "Nothing worse and a judged improvement past the Worse line measured from main reads Better; the same magnitude must make a regression Worse. Applies to timing noise curves and memory thresholds.", "", null, "REQ-VER-47; Arek 2026-10-22 ~15:35, option 1 (standing-rulings.md): Better only past the Worse line; controller review ruling 2026-10-22: measure from main using the same timing noise and memory thresholds as Worse; no new number", false, { from: "error-tier" }),
|
|
10972
|
+
"T-WARN-NOISE": row("T-WARN-NOISE", "No-change noise curve per judged measure: Slightly worse past c x k95 x sqrt(main), Worse past c x k99 x sqrt(main).", "ms", ">", 'Arek 2026-10-16 "Ok" (the relative part adapts to size), 2026-10-18 ~09:40 (k95 warns, k99 errs); docs/stats/noise/2026-10-16-noise-fit.md "Result to encode"', false, {
|
|
10973
|
+
render: { c: 0.9244, k95: 1.5678, k99: 2.2255 },
|
|
10974
|
+
commit: { c: 0.8326, k95: 1.7675, k99: 2.8108 },
|
|
10975
|
+
component: { c: 0.6481, k95: 1.8075, k99: 2.6648 }
|
|
10976
|
+
}),
|
|
10977
|
+
"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, {
|
|
10978
|
+
jank: { c: 0.9159, k95: 2.5757, k99: 3.2072, zeroWarn: 10.38, zeroError: 12.92 }
|
|
10979
|
+
}),
|
|
10980
|
+
"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 }),
|
|
10981
|
+
"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 } }),
|
|
10982
|
+
"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 } }),
|
|
10983
|
+
"E-RENDER-UPPER": row("E-RENDER-UPPER", "Timer-carried render time is evidence past max(floor, % of main).", "ms", ">", "REQ-MEAS-32 option A (Arek 2026-10-17); the number has no ruling", true, { render_upper: FLOOR }),
|
|
10984
|
+
"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 }),
|
|
10985
|
+
"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 }),
|
|
10986
|
+
"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 }),
|
|
10987
|
+
"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 }),
|
|
10988
|
+
"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 } }),
|
|
10989
|
+
"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 } }),
|
|
10990
|
+
"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 } }),
|
|
10991
|
+
"E-MEMORY-JOURNEY": row("E-MEMORY-JOURNEY", "Memory at launch, end growth and peak growth are evidence past max(2 MB, +8 % of main).", "MB", ">", NO_RULING, true, { memory_launch: { rel: 0.08, abs: 2 }, memory_end_growth: { rel: 0.08, abs: 2 }, memory_peak_growth: { rel: 0.08, abs: 2 } }),
|
|
10992
|
+
"E-CPU-JOURNEY": row("E-CPU-JOURNEY", "Whole-journey CPU is evidence past +5 % of main.", "ms (relative part only)", ">", 'no ruling: "a +10 % whole-app CPU change must show as evidence"', true, { cpu: { rel: 0.05 } }),
|
|
10993
|
+
"E-WINDOW-LOAD": row("E-WINDOW-LOAD", "Window CPU, long tasks, GC time, requests and JS CPU are evidence past max(floor, % of main).", "ms (requests: count)", ">", NO_RULING, true, {
|
|
10994
|
+
window_cpu: { rel: 0.5, abs: 20 },
|
|
10995
|
+
long_tasks: { rel: 0.3, abs: 50 },
|
|
10996
|
+
gc: { rel: 0.5, abs: 2 },
|
|
10997
|
+
requests: { rel: 0.5, abs: 1 },
|
|
10998
|
+
js_cpu: { rel: 0.5, abs: 5 }
|
|
10999
|
+
}),
|
|
11000
|
+
"P-SLOW-COMMIT": row("P-SLOW-COMMIT", "Single problem: the slowest commit over this many frames in every PR run, on phones.", "frames", ">", "REQ-MET-27, Arek 2026-10-16 (a commit over one 60 Hz frame drops below 60 fps)", true, { frames: 1 }),
|
|
11001
|
+
"P-HEAVY-COMPONENT": row("P-HEAVY-COMPONENT", "Single problem: a component's self time over this many frames in every PR run, on phones.", "frames", ">", "REQ-MET-27, Arek 2026-10-16", true, { frames: 1 }),
|
|
11002
|
+
"P-HEAVY-ENTRY": row("P-HEAVY-ENTRY", "Single problem: React render time over this budget in every PR run at an entry or in-screen mount; also the budget of both kinds of work.", "ms", ">", "REQ-MET-25 ruling d (about 100 ms)", true, { ms: 100 }),
|
|
11003
|
+
"P-BUDGETS": row("P-BUDGETS", "The budget of an update, shown with a single problem, in frames.", "frames", ">", "REQ-MET-25 ruling d", true, { updateFrames: 1 }),
|
|
11004
|
+
"U-NOISY": row("U-NOISY", "A place is unstable on main above this ratio of spread to allowed, and stays so down to `below`; from this many main runs in this many sessions.", "ratio", ">", "REQ-MET-29, Arek 2026-10-18 ~12:00-13:20; thresholds provisional", false, { above: 1, below: 0.75, minMainRuns: 6, minSessions: 3 })
|
|
11005
|
+
};
|
|
11006
|
+
var CLAUSE_LIST = Object.values(CLAUSES);
|
|
11007
|
+
// packages/verdict-core/dist/src/config.js
|
|
11008
|
+
var WINDOWS = ["screen", "press", "drag", "step", "other"];
|
|
11009
|
+
var STEP = ["step"];
|
|
11010
|
+
var JUDGED_WINDOWS = WINDOWS.filter((k) => !STEP.includes(k));
|
|
11011
|
+
var BACKGROUND = ["background"];
|
|
11012
|
+
var LAUNCH = ["launch"];
|
|
11013
|
+
var SHOWN2 = [...WINDOWS, ...LAUNCH];
|
|
11014
|
+
var REACT_PLACES = [...SHOWN2, ...BACKGROUND];
|
|
11015
|
+
var JOURNEY = ["journey"];
|
|
11016
|
+
var DEVICE = ["device"];
|
|
11017
|
+
var ANY_DEVICE = ["device", "sim"];
|
|
11018
|
+
var ENTRY_BUDGET_MS = 100;
|
|
11019
|
+
var UNCERTAIN_BELOW_MAIN_SESSIONS = 3;
|
|
11020
|
+
var GUESS = (from) => ({ uncalibrated: true, source: `${from}; starting value carried from the plan 58 table (today's warn value), plan 57 calibrates it` });
|
|
11021
|
+
var evidenceRule = (id, on, places, threshold, from, more = {}) => ({
|
|
11022
|
+
id,
|
|
11023
|
+
role: "evidence",
|
|
11024
|
+
on,
|
|
11025
|
+
places,
|
|
11026
|
+
devices: DEVICE,
|
|
11027
|
+
threshold,
|
|
11028
|
+
...GUESS(from),
|
|
11029
|
+
...more
|
|
11030
|
+
});
|
|
11031
|
+
var MEMORY_MEASURES = Object.keys(MEASURES).filter((id) => MEASURES[id].unit.startsWith("MB"));
|
|
11032
|
+
var REACT_MEASURES = [
|
|
11033
|
+
"commit_max_ms",
|
|
11034
|
+
"render_ms",
|
|
11035
|
+
"commits",
|
|
11036
|
+
"renders",
|
|
11037
|
+
"mounts",
|
|
11038
|
+
"commits_over_budget",
|
|
11039
|
+
"mount_share",
|
|
11040
|
+
"self_ms_top",
|
|
11041
|
+
"component_ms",
|
|
11042
|
+
"dimension:component",
|
|
11043
|
+
"dimension:updater",
|
|
11044
|
+
"jank_ms"
|
|
11045
|
+
];
|
|
11046
|
+
var WINDOW_MODEL_AFFECTS = [
|
|
11047
|
+
"commits",
|
|
11048
|
+
"renders",
|
|
11049
|
+
"mounts",
|
|
11050
|
+
"alloc_mb",
|
|
11051
|
+
"gc_count",
|
|
11052
|
+
"gc_ms",
|
|
11053
|
+
"render_ms",
|
|
11054
|
+
"js_cpu_ms",
|
|
11055
|
+
"cpu_ms",
|
|
11056
|
+
"commit_max_ms",
|
|
11057
|
+
"commits_over_budget",
|
|
11058
|
+
"longtask_ms",
|
|
11059
|
+
"longtask_max_ms",
|
|
11060
|
+
"touch_max_ms",
|
|
11061
|
+
"net_requests",
|
|
11062
|
+
"net_duplicates",
|
|
11063
|
+
"dur_ms",
|
|
11064
|
+
"sampler_frames_missed",
|
|
11065
|
+
"sampler_footprint_delta_mb",
|
|
11066
|
+
"memory_left_mb",
|
|
11067
|
+
"retained_mb_per_cycle",
|
|
11068
|
+
"dimension:component",
|
|
11069
|
+
"dimension:updater",
|
|
11070
|
+
"jank_ms"
|
|
11071
|
+
];
|
|
11072
|
+
var MEASURES_AT = (kind) => [...Object.keys(MEASURES).filter((id) => MEASURES[id].at.includes(kind)), "dimension:component", "dimension:updater"];
|
|
11073
|
+
var SPLIT_AFFECTS = [
|
|
11074
|
+
...REACT_MEASURES,
|
|
11075
|
+
...Object.keys(MEASURES).filter((id) => /^(commits_over_|slow_commit_|mount_share_|unsettled_commit_)/.test(id))
|
|
11076
|
+
];
|
|
11077
|
+
var GC_STEP_AFFECTS = [
|
|
11078
|
+
"gc_heap_before_mb",
|
|
11079
|
+
"gc_heap_kept_mb",
|
|
11080
|
+
"gc_heap_garbage_mb",
|
|
11081
|
+
"gc_memory_before_mb",
|
|
11082
|
+
"gc_memory_after_mb",
|
|
11083
|
+
"gc_memory_step_added_mb",
|
|
11084
|
+
"gc_memory_kept_mb",
|
|
11085
|
+
"gc_command_ms",
|
|
11086
|
+
"gc_queue_ms"
|
|
11087
|
+
];
|
|
11088
|
+
var SAMPLER_EDGE_AFFECTS = [
|
|
11089
|
+
"cpu_ms",
|
|
11090
|
+
"sampler.cpu_ms",
|
|
11091
|
+
"sampler_frames_missed",
|
|
11092
|
+
"sampler_longest_gap_ms",
|
|
11093
|
+
"sampler.frames.longest_gap_ms",
|
|
11094
|
+
"sampler.frames.missed",
|
|
11095
|
+
"sampler.frames.presented",
|
|
11096
|
+
...MEMORY_MEASURES.filter((m) => !GC_STEP_AFFECTS.includes(m))
|
|
11097
|
+
];
|
|
11098
|
+
var DRAG_TIME_AND_CPU = ["dur_ms", "action_span_ms", "next_input_gap_ms", "action_uncovered_ms", "cpu_ms", "main_thread_ms", "sampler_cpu_cores", "js_cpu_ms", "js_thread_ms", "js_thread_app_ms"];
|
|
11099
|
+
var LOADED_MEMORY = Object.keys(MEASURES).filter((id) => /^(mem|sampler\.memory|ri\.memory)\./.test(id) || id === "memory_left_mb");
|
|
11100
|
+
var END_READ_DECLARES = [{ affects: ["mem.retained_growth_mb", "mem.end_after_gc_mb"], places: ["journey"] }];
|
|
11101
|
+
var END_READ_SOURCE = `plan 78 G4/G5, Arek 2026-10-21 (D5, "po GC powinniśmy robić manualny odczyt pamięci"): right after the end GC the driver asks the SDK for one memory reading (derivation 23, docs/design/endgc-manual-memory-read.md); memory kept is that reading minus launch, not the GC's own post record. REQ-FARM-35`;
|
|
11102
|
+
var PLACE_KEY_KINDS = ["screen", "launch", "press", "drag"];
|
|
11103
|
+
var SDK_A17_PLACES = ["screen", "press", "drag", "step"];
|
|
11104
|
+
var SDK_RELEASES = [
|
|
11105
|
+
{
|
|
11106
|
+
version: "0.1.0-alpha.9",
|
|
11107
|
+
previous: "0.1.0-alpha.8",
|
|
11108
|
+
declares: [{ affects: WINDOW_MODEL_AFFECTS, places: ["press", "drag"] }, { affects: ["mount_share", "mount_share_min", "mount_share_max"] }],
|
|
11109
|
+
uncalibrated: false,
|
|
11110
|
+
source: 'packages/react-native/CHANGELOG.md "window model 2" and "mountSelfMs"; git merge-base --is-ancestor 988945d / 5995729 v0.1.0-alpha.9 (yes), v0.1.0-alpha.8 (no)'
|
|
11111
|
+
},
|
|
11112
|
+
{
|
|
11113
|
+
version: "0.1.0-alpha.10",
|
|
11114
|
+
previous: "0.1.0-alpha.9",
|
|
11115
|
+
declares: [{ affects: SAMPLER_EDGE_AFFECTS }],
|
|
11116
|
+
uncalibrated: false,
|
|
11117
|
+
source: 'packages/react-native/CHANGELOG.md 0.1.0-alpha.10 "a sample at every window edge"; git merge-base --is-ancestor 7331c8f v0.1.0-alpha.10 (yes), v0.1.0-alpha.9 (no)'
|
|
11118
|
+
},
|
|
11119
|
+
{
|
|
11120
|
+
version: "0.1.0-alpha.11",
|
|
11121
|
+
previous: "0.1.0-alpha.10",
|
|
11122
|
+
declares: [
|
|
11123
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
11124
|
+
{ affects: SPLIT_AFFECTS, places: WINDOWS },
|
|
11125
|
+
{ affects: MEASURES_AT("launch"), places: LAUNCH },
|
|
11126
|
+
{ affects: GC_STEP_AFFECTS, places: ["step"] }
|
|
11127
|
+
],
|
|
11128
|
+
uncalibrated: false,
|
|
11129
|
+
source: `pending release: window model 3, screen windows end at the next action with no 3 s cap (REQ-MEAS-12, packages/react-native/CHANGELOG.md; commits 741c36a and 62c0107 on main, not in v0.1.0-alpha.10); work split by its cause, every window's React measures are its judged part (REQ-MEAS-32, CHANGELOG "work is split by its cause", branch feat/action-causes-d16); the launch screen starts when the app starts a surface (REQ-MEAS-22, same branch); screen windows start at the action that caused them, with no time limits (REQ-MEAS-28, same branch); the GC command before a declared step adds the gc_* step rows (docs/plans/gc-before-action.md, branch feat/gc-before-action)`
|
|
11130
|
+
},
|
|
11131
|
+
{
|
|
11132
|
+
version: "0.1.0-alpha.12",
|
|
11133
|
+
previous: "0.1.0-alpha.11",
|
|
11134
|
+
declares: [{ affects: [...new Set([...MEASURES_AT("press"), ...MEASURES_AT("drag")])], places: ["press", "drag"] }],
|
|
11135
|
+
uncalibrated: false,
|
|
11136
|
+
source: 'packages/react-native/CHANGELOG.md 0.1.0-alpha.12 "Drag names that hold across runs, and flings on a moving list": back-to-back flings on a moving list are drags, not presses (`press:<row>` becomes `drag:<list>`), and generated list ids are no longer names; those press and drag places are not comparable across the change. Arek 2026-10-21 (A/A "Slightly worse" decision, Q-10): alpha.12 declares this rename; nothing else in it changes a measure (REQ-FARM-35).',
|
|
11137
|
+
renamed: { places: ["press", "drag"] }
|
|
11138
|
+
},
|
|
11139
|
+
{
|
|
11140
|
+
version: "0.1.0-alpha.13",
|
|
11141
|
+
previous: "0.1.0-alpha.12",
|
|
11142
|
+
declares: "nothing",
|
|
11143
|
+
uncalibrated: false,
|
|
11144
|
+
source: 'KeepSnappy native module, capture protocol and artifact naming; measurement calculations unchanged from alpha.12 (REQ-FARM-35, Arek 2026-10-19: declared "changes nothing"). Requires rebuilt native clients and coordinated server migration (docs/keepsnappy-migration.md).'
|
|
11145
|
+
},
|
|
11146
|
+
{
|
|
11147
|
+
version: "0.1.0-alpha.14",
|
|
11148
|
+
previous: "0.1.0-alpha.13",
|
|
11149
|
+
declares: END_READ_DECLARES,
|
|
11150
|
+
uncalibrated: false,
|
|
11151
|
+
source: `SDK answers the driver's memread command with one sample (reason endgc-read:<n>); ${END_READ_SOURCE}. Nothing else since alpha.13 changes a measure: the flush always ends and is acknowledged (#140), local builds carry +local (#147), each "Not an instrument change" in packages/react-native/CHANGELOG.md`
|
|
11152
|
+
},
|
|
11153
|
+
{
|
|
11154
|
+
version: "0.1.0-alpha.15",
|
|
11155
|
+
previous: "0.1.0-alpha.14",
|
|
11156
|
+
declares: "nothing",
|
|
11157
|
+
uncalibrated: false,
|
|
11158
|
+
source: "Shared release version only: git log v0.1.0-alpha.14..main -- packages/react-native is empty; SDK capture and measurement calculations are unchanged from alpha.14 (REQ-FARM-35)."
|
|
11159
|
+
},
|
|
11160
|
+
{
|
|
11161
|
+
version: "0.1.0-alpha.16",
|
|
11162
|
+
previous: "0.1.0-alpha.15",
|
|
11163
|
+
declares: [
|
|
11164
|
+
{ affects: ["dimension:component", "dimension:updater", "self_ms_top", "component_ms"] },
|
|
11165
|
+
{ affects: [...new Set(PLACE_KEY_KINDS.flatMap((k) => MEASURES_AT(k)))], places: PLACE_KEY_KINDS }
|
|
11166
|
+
],
|
|
11167
|
+
renamed: { places: PLACE_KEY_KINDS, dimensions: ["component", "updater"] },
|
|
11168
|
+
uncalibrated: false,
|
|
11169
|
+
source: "plan 79 (docs/plans/79-ui-change-flexibility.md): client component keys with library time rolled up; places keyed by client components (D4, Arek 2026-10-21 ~19:15 and 2026-10-22 ~19:20); S9: every client component retained by value, compact detail below C-DETAIL-MS, SDK omission and derivation-26 completeness"
|
|
11170
|
+
},
|
|
11171
|
+
{
|
|
11172
|
+
version: "0.1.0-alpha.17",
|
|
11173
|
+
previous: "0.1.0-alpha.16",
|
|
11174
|
+
declares: [
|
|
11175
|
+
{ affects: [...new Set(SDK_A17_PLACES.flatMap((k) => MEASURES_AT(k)))], places: SDK_A17_PLACES },
|
|
11176
|
+
{ affects: [...WINDOW_DEPENDENT_JOURNEY_MEASURES], places: ["journey"] }
|
|
11177
|
+
],
|
|
11178
|
+
renamed: { places: ["screen", "press", "drag"] },
|
|
11179
|
+
uncalibrated: false,
|
|
11180
|
+
source: "#277 (plan 74 step 4 SDK review): screen windows end at every touch delivered while open; late-mounting screens keep their component key (3-commit cap gone), so screen keys and the press/drag keys on them change; the run commit stream is cut at the flush, not inside a window (window times of long journeys and journey CPU change); a gesture whose touchEnd never reached JS moves press → drag; a late press takes its own open sample (press sampler measures). Flush answers lost data as error (validity only, no measure). Launch and journey memory stay compared."
|
|
11181
|
+
}
|
|
11182
|
+
];
|
|
11183
|
+
var DRIVER_RELEASES = [
|
|
11184
|
+
{
|
|
11185
|
+
version: "0.1.0-alpha.12",
|
|
11186
|
+
previous: "0.1.0-alpha.11",
|
|
11187
|
+
declares: "nothing",
|
|
11188
|
+
uncalibrated: false,
|
|
11189
|
+
source: 'Host clock round trips each run (c1a430ed): the clock error falls from 126-197 ms on the S23, so step edges line up better; a stale element is looked up again; flingTimes, flingUntil, stableCentres and tapAt added for journeys. REQ-FARM-35, Arek 2026-10-19 ~21:40 (standing-rulings): declared "changes nothing" although it holds c1a430ed.'
|
|
11190
|
+
},
|
|
11191
|
+
{
|
|
11192
|
+
version: "0.1.0-alpha.13",
|
|
11193
|
+
previous: "0.1.0-alpha.12",
|
|
11194
|
+
declares: "nothing",
|
|
11195
|
+
uncalibrated: false,
|
|
11196
|
+
source: 'KeepSnappy link route, flush and GC answer element names and Android command name (the protocol cutover, docs/keepsnappy-migration.md); timing and touches unchanged. REQ-FARM-35, Arek 2026-10-19: declared "changes nothing".'
|
|
11197
|
+
},
|
|
11198
|
+
{
|
|
11199
|
+
version: "0.1.0-alpha.14",
|
|
11200
|
+
previous: "0.1.0-alpha.13",
|
|
11201
|
+
declares: END_READ_DECLARES,
|
|
11202
|
+
uncalibrated: false,
|
|
11203
|
+
source: `driver sends memread after the end GC's answer and waits for its answer before the flush; ${END_READ_SOURCE}. Nothing else since alpha.13 changes a measure: the flush is re-sent once and an unacknowledged flush fails the run (#140), driver-log.json is written as the run goes (#157), each "Not an instrument change" in packages/appium-driver/CHANGELOG.md`
|
|
11204
|
+
},
|
|
11205
|
+
{
|
|
11206
|
+
version: "0.1.0-alpha.15",
|
|
11207
|
+
previous: "0.1.0-alpha.14",
|
|
11208
|
+
declares: "nothing",
|
|
11209
|
+
uncalibrated: false,
|
|
11210
|
+
source: "Activation settles for 2 s before the journey relaunch (be050585, #167, ACTIVATION_SETTLE_MS); absent Android attributes read as null (3fed6c8c, #159). The delay is before the measured journey; attribute normalization is element lookup and post-window acknowledgement handling. No window edge, touch, GC, memory read or flush timing changes (REQ-FARM-35)."
|
|
11211
|
+
},
|
|
11212
|
+
{
|
|
11213
|
+
version: "0.1.0-alpha.16",
|
|
11214
|
+
previous: "0.1.0-alpha.15",
|
|
11215
|
+
declares: "nothing",
|
|
11216
|
+
uncalibrated: false,
|
|
11217
|
+
source: '#260 list and gesture helpers in appium-driver (not in the run path; adopting modules send identical gestures; CHANGELOG "Not an instrument change"); #211 driver.unit is evidence only (meta.device_unit). No window edge, touch, GC, memory read or flush timing changes (REQ-FARM-35).'
|
|
11218
|
+
},
|
|
11219
|
+
{
|
|
11220
|
+
version: "0.1.0-alpha.17",
|
|
11221
|
+
previous: "0.1.0-alpha.16",
|
|
11222
|
+
declares: [
|
|
11223
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
11224
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
11225
|
+
{ affects: LOADED_MEMORY }
|
|
11226
|
+
],
|
|
11227
|
+
uncalibrated: false,
|
|
11228
|
+
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 still compare unflagged until the run's setup.capture (#344) is judged (follow-up). Screens and taps stay compared (REQ-FARM-35)."
|
|
11229
|
+
},
|
|
11230
|
+
{
|
|
11231
|
+
version: "0.1.0-alpha.18",
|
|
11232
|
+
previous: "0.1.0-alpha.17",
|
|
11233
|
+
declares: [{ affects: DRAG_TIME_AND_CPU, places: ["drag"], platforms: ["ios"] }],
|
|
11234
|
+
uncalibrated: false,
|
|
11235
|
+
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)."
|
|
11236
|
+
},
|
|
11237
|
+
{
|
|
11238
|
+
version: "0.1.0-alpha.19",
|
|
11239
|
+
previous: "0.1.0-alpha.18",
|
|
11240
|
+
declares: [
|
|
11241
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"], platforms: ["ios"] },
|
|
11242
|
+
{ affects: MEASURES_AT("step"), places: ["step"], platforms: ["ios"] }
|
|
11243
|
+
],
|
|
11244
|
+
uncalibrated: false,
|
|
11245
|
+
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)."
|
|
11246
|
+
}
|
|
11247
|
+
];
|
|
11248
|
+
var UNSTABLE_NOISY = {
|
|
11249
|
+
reason: "noisy",
|
|
11250
|
+
spread: "sessions",
|
|
11251
|
+
robust: true,
|
|
11252
|
+
between: false,
|
|
11253
|
+
above: 1,
|
|
11254
|
+
below: 0.75,
|
|
11255
|
+
minMainRuns: 6,
|
|
11256
|
+
minSessions: 3,
|
|
11257
|
+
curve: { render: 0.9244, commit: 0.8326, component: 0.6481 },
|
|
11258
|
+
uncalibrated: false,
|
|
11259
|
+
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"
|
|
11260
|
+
};
|
|
11261
|
+
|
|
11262
|
+
class ConfigError2 extends Error {
|
|
11263
|
+
problems;
|
|
11264
|
+
constructor(problems) {
|
|
11265
|
+
super(`invalid verdict config: ${problems.join("; ")}`);
|
|
11266
|
+
this.problems = problems;
|
|
11267
|
+
this.name = "ConfigError";
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
var noRow = (id) => {
|
|
11271
|
+
throw new ConfigError2([`${id}: no row in the clause table`]);
|
|
11272
|
+
};
|
|
11273
|
+
var NOISE_FITS = { ...CLAUSES["T-WARN-NOISE"].value, ...CLAUSES["T-NOISE-JANK"].value };
|
|
11274
|
+
var TIER_REL = 0.3;
|
|
11275
|
+
var TIER_UPDATE_MS = { warnMs: 5, errorMs: 15 };
|
|
11276
|
+
var TIER_WORK_MS = { warnMs: 10, errorMs: 30 };
|
|
11277
|
+
var NOISE_SOURCE = 'Arek 2026-10-16 "Ok" (the relative part adapts to size, "przy małych pomiarach 50 %, przy coraz większych stopniowo coraz mniejszy"; sqrt form, one curve per measure and the two lifts chosen by the controller): ' + 'keepsnappy-measure scratchpad/measure/noise/fit.md "Result to encode" (scripts fit.py MODE=dedup-nomi, lifts.py). Data: S23 TapIn live-ui-appium 11 sessions x 3 (A/A 6073ac76, c86689e2, 4a784c05, f8d36522, 6814be6b; #1 b6d080ec, 52f47229, df55466a; #2 2a55a595, ab33b26e, 3042c714; changed places removed) and iPhone 15 Pro Appium A/A 6 x 3 (a18884dd, 9e364887, c775eab0, d45eb2c1, d3637624, b745fa1c), derivation 14; one place per window (+3s twins and one-window steps dropped), press:new-messages-indicator and its step left out (pushed chat, external work); 41 S23 and 40 iPhone places. ' + "R² of c x sqrt(m) before the dedup (fit.md §2): render 0.66, commit 0.44, component 0.42 (fitted on self_ms_top, the largest component); the dedup moves c by at most 0.03. Places above the lifted curve, S23 / iPhone: k95 render 0 z 41 / 4 z 40, commit 1 / 3, component 2 / 2; k99 render 0 / 1, commit 0 / 1, component 0 / 1. " + "Limits: the iPhone is not fully inside at k95; the iPhone set has 6 sessions (20 splits per place), SDK alpha.7 and window model 1; no split by kind of work";
|
|
11278
|
+
var CAPPED_SOURCE = 'Arek 2026-10-16 ~23:00 "Ok" (followups "[RULED 2026-10-16 ~23:00]"): REQ-MET-26 tiers, the relative part min(30 % of base, error tier ms) of the ~19:00 session-bar ruling, on all three judged measures (~17:50); the 30 % and the ms are not calibrated';
|
|
11279
|
+
var TIERS_SOURCE = 'REQ-MET-26: update error past 15 ms, entry and mount past 30 ms; going over the 100 ms limit is a warning, not an error (Arek 2026-10-17, followups 530fb5a: "Sam w sobie nie, to raczej ostrzeżenie"); same numbers as the pairs candidate (tiers2 BASE_LEVELS)';
|
|
11280
|
+
var MEMORY_PAIR_SOURCE = "REQ-MET-4, Arek 2026-10-20 ~08:00, standing-rulings.md pair B: Slightly worse above max(20 MB, 10 % of main), Worse above max(150 MB, 30 %); provisional, one rule for every device; docs/stats/memory/2026-10-20-memory-kept-two-thresholds.md";
|
|
11281
|
+
var COUNT_SOURCE = "REQ-MET-13, Arek 2026-10-20 ruling (docs/handoffs/2026-10-20-morning-rulings.md, Layer 2): counts are evidence only; commits above max(50 %, +2), renders above max(50 %, +10); provisional, not calibrated";
|
|
11282
|
+
var noiseJudged = (id, on) => {
|
|
11283
|
+
const fit = NOISE_FITS[id] ?? noRow(id);
|
|
11284
|
+
const noise = { a: 0, b: fit.c, power: 0.5, lift: fit.k95 };
|
|
11285
|
+
return {
|
|
11286
|
+
id,
|
|
11287
|
+
role: "judged",
|
|
11288
|
+
on,
|
|
11289
|
+
places: JUDGED_WINDOWS,
|
|
11290
|
+
devices: ANY_DEVICE,
|
|
11291
|
+
threshold: [{ abs: TIER_UPDATE_MS.warnMs, noise }],
|
|
11292
|
+
uncalibrated: false,
|
|
11293
|
+
source: `k95 lift ${fit.k95}, error tier k99 lift ${fit.k99}, c ${fit.c}; ${NOISE_SOURCE}`,
|
|
11294
|
+
thresholdByWork: { entry: [{ abs: TIER_WORK_MS.warnMs, noise }], mount: [{ abs: TIER_WORK_MS.warnMs, noise }] },
|
|
11295
|
+
errorNoise: { ...noise, lift: fit.k99 }
|
|
11296
|
+
};
|
|
11297
|
+
};
|
|
11298
|
+
var part = (b) => ({ ...b.rel !== undefined ? { rel: b.rel } : {}, ...b.abs !== undefined ? { abs: b.abs } : {} });
|
|
11299
|
+
var EVIDENCE_BARS = new Map(Object.values(CLAUSES).filter((c) => c.id.startsWith("E-")).flatMap((c) => Object.entries(c.value).map(([rule, bar]) => [rule, { bar, uncalibrated: c.uncalibrated }])));
|
|
11300
|
+
function withClauseValues(base) {
|
|
11301
|
+
if (!base.better)
|
|
11302
|
+
throw new ConfigError2(["better: missing from base config"]);
|
|
11303
|
+
const warnMs = CLAUSES["T-WARN-MS"].value;
|
|
11304
|
+
const errorMs = CLAUSES["TIER-ERROR"].value;
|
|
11305
|
+
const memory = CLAUSES["J-MEMORY-RETAINED"];
|
|
11306
|
+
const judgedByNoise = (r) => {
|
|
11307
|
+
const fit = NOISE_FITS[r.id] ?? noRow(r.id);
|
|
11308
|
+
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95, ...fit.zeroWarn !== undefined ? { atZero: fit.zeroWarn } : {} };
|
|
11309
|
+
return {
|
|
11310
|
+
...r,
|
|
11311
|
+
threshold: [{ abs: warnMs.update, noise: n }],
|
|
11312
|
+
thresholdByWork: { entry: [{ abs: warnMs.work, noise: n }], mount: [{ abs: warnMs.work, noise: n }] },
|
|
11313
|
+
errorNoise: { a: 0, b: fit.c, power: 0.5, lift: fit.k99, ...fit.zeroError !== undefined ? { atZero: fit.zeroError } : {} },
|
|
11314
|
+
uncalibrated: (r.id in CLAUSES["T-NOISE-JANK"].value ? CLAUSES["T-NOISE-JANK"] : CLAUSES["T-WARN-NOISE"]).uncalibrated
|
|
11315
|
+
};
|
|
11316
|
+
};
|
|
11317
|
+
const evidence = (r) => {
|
|
11318
|
+
const e = EVIDENCE_BARS.get(r.id) ?? noRow(r.id);
|
|
11319
|
+
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)]]));
|
|
11320
|
+
return { ...r, threshold: [part(e.bar)], ...at ? { thresholdAt: at } : {}, uncalibrated: e.uncalibrated };
|
|
11321
|
+
};
|
|
11322
|
+
const valued = (r) => {
|
|
11323
|
+
if (r.id === "memory_retained")
|
|
11324
|
+
return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
|
|
11325
|
+
return r.role === "judged" ? judgedByNoise(r) : evidence(r);
|
|
11326
|
+
};
|
|
11327
|
+
const rules = base.rules.map(valued);
|
|
11328
|
+
const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
|
|
11329
|
+
const entry = CLAUSES["P-HEAVY-ENTRY"];
|
|
11330
|
+
const unstable = CLAUSES["U-NOISY"];
|
|
11331
|
+
const c = (id) => CLAUSES[id];
|
|
11332
|
+
return {
|
|
11333
|
+
...base,
|
|
11334
|
+
better: { ...base.better, from: c("S-BETTER").value.from, uncalibrated: c("S-BETTER").uncalibrated },
|
|
11335
|
+
minRuns: c("G-MINRUNS").value.runs,
|
|
11336
|
+
baseline: { ...base.baseline, sessions: c("B-BASELINE").value.sessions, uncalibrated: c("B-BASELINE").uncalibrated },
|
|
11337
|
+
levelShift: { ...base.levelShift, confirmSessions: c("M-LEVELSHIFT").value.confirmSessions, uncalibrated: c("M-LEVELSHIFT").uncalibrated },
|
|
11338
|
+
uncertain: { ...base.uncertain, belowMainSessions: c("S-UNCERTAIN").value.belowMainSessions, uncalibrated: c("S-UNCERTAIN").uncalibrated },
|
|
11339
|
+
judgeFrom: { ...base.judgeFrom, mainSessions: c("G-JUDGE-FROM").value.mainSessions, uncalibrated: c("G-JUDGE-FROM").uncalibrated },
|
|
11340
|
+
work: { ...base.work, mountShareAtLeast: c("W-WORKKIND").value.mountShareAtLeast, uncalibrated: c("W-WORKKIND").uncalibrated },
|
|
11341
|
+
...base.windowCount ? { windowCount: { ...base.windowCount, below: c("W-WINDOW-COUNT").value.below, uncalibrated: c("W-WINDOW-COUNT").uncalibrated } } : {},
|
|
11342
|
+
frame: { ...base.frame, ms: c("T-FRAME").value.ms, uncalibrated: c("T-FRAME").uncalibrated },
|
|
11343
|
+
tiers: {
|
|
11344
|
+
...base.tiers,
|
|
11345
|
+
rel: c("T-REL").value.rel,
|
|
11346
|
+
update: { warnMs: warnMs.update, errorMs: errorMs.update },
|
|
11347
|
+
work: { warnMs: warnMs.work, errorMs: errorMs.work },
|
|
11348
|
+
uncalibrated: c("T-WARN-MS").uncalibrated || c("TIER-ERROR").uncalibrated || c("T-REL").uncalibrated,
|
|
11349
|
+
memory: { error: part(memory.value.error), uncalibrated: memory.uncalibrated }
|
|
11350
|
+
},
|
|
11351
|
+
rules,
|
|
11352
|
+
problems: base.problems.map((p) => {
|
|
11353
|
+
if (p.id === "slow-commit")
|
|
11354
|
+
return { ...p, ...frames("P-SLOW-COMMIT") };
|
|
11355
|
+
if (p.id === "heavy-component")
|
|
11356
|
+
return { ...p, ...frames("P-HEAVY-COMPONENT") };
|
|
11357
|
+
if (p.id === "heavy-entry")
|
|
11358
|
+
return { ...p, above: { value: entry.value.ms }, uncalibrated: entry.uncalibrated };
|
|
11359
|
+
return noRow(p.id);
|
|
11360
|
+
}),
|
|
11361
|
+
budgets: { ...base.budgets, entry: { value: entry.value.ms }, mount: { value: entry.value.ms }, update: { frames: c("P-BUDGETS").value.updateFrames }, uncalibrated: c("P-BUDGETS").uncalibrated },
|
|
11362
|
+
unstable: base.unstable.map((u) => ({
|
|
11363
|
+
...u,
|
|
11364
|
+
above: unstable.value.above,
|
|
11365
|
+
below: unstable.value.below,
|
|
11366
|
+
minMainRuns: unstable.value.minMainRuns,
|
|
11367
|
+
minSessions: unstable.value.minSessions,
|
|
11368
|
+
uncalibrated: unstable.uncalibrated,
|
|
11369
|
+
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c])),
|
|
11370
|
+
...u.reads ? { reads: u.reads.map(valued) } : {}
|
|
11371
|
+
}))
|
|
11372
|
+
};
|
|
11373
|
+
}
|
|
11374
|
+
var COMMIT_MAX_JUDGED = noiseJudged("commit", { measure: "commit_max_ms" });
|
|
11375
|
+
var RENDER_JUDGED = noiseJudged("render", { measure: "render_ms" });
|
|
11376
|
+
var COMPONENT_JUDGED = noiseJudged("component", { dimension: "component", measure: "self_ms" });
|
|
11377
|
+
var MEMORY_RETAINED = {
|
|
11378
|
+
id: "memory_retained",
|
|
11379
|
+
role: "judged",
|
|
11380
|
+
on: { measure: "mem.retained_growth_mb" },
|
|
11381
|
+
places: JOURNEY,
|
|
11382
|
+
devices: ANY_DEVICE,
|
|
11383
|
+
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11384
|
+
uncalibrated: true,
|
|
11385
|
+
source: MEMORY_PAIR_SOURCE
|
|
11386
|
+
};
|
|
11387
|
+
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)`;
|
|
11388
|
+
var SHAPE = {
|
|
11389
|
+
id: "simple-4.26+noise95-99",
|
|
11390
|
+
minRuns: 2,
|
|
11391
|
+
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)") },
|
|
11392
|
+
repeats: { kind: "every-pr-beyond-every-main" },
|
|
11393
|
+
frame: { ms: LIMIT_FRAME_MS, uncalibrated: false, source: "Arek 2026-10-16: one frame is 16.7 ms on every screen; lag starts below 60 fps, so a 120 Hz phone is not held to 8.3 ms" },
|
|
11394
|
+
levelShift: {
|
|
11395
|
+
confirmSessions: 2,
|
|
11396
|
+
uncalibrated: false,
|
|
11397
|
+
source: "the smallest number of agreeing main sessions with zero noise confirmations on the data (plan 63 §3; controller 2026-10-17), reviewer scripts scratchpad/rev-p6-scripts on the S23 sessions with candidates only after 3 sessions in their bounded baseline: aa-shifts (no change): A/A all 120 orders 0 shifts at m = 1 and m = 2, 11 sessions x 60 random orders 1 of 60 at both; aa-inject (false later shifts after an injected real step): m = 1 2 of 40 sequences, m = 2 0 of 40. Limit: a component with two typical levels (AnimatedView at step:gia.history-first gave the 1 of 60) can fool any m"
|
|
11398
|
+
},
|
|
11399
|
+
uncertain: { belowMainSessions: UNCERTAIN_BELOW_MAIN_SESSIONS, uncalibrated: false, source: 'Arek 2026-10-16: "Nie blokuj przy małej bazie". With fewer than 3 main sessions a worse result is reported as uncertain and does not fail the check' },
|
|
11400
|
+
work: {
|
|
11401
|
+
mountShareAtLeast: 0.8,
|
|
11402
|
+
uncalibrated: true,
|
|
11403
|
+
source: "TraceSift MOUNT_DOMINANT_SHARE (0.8); the share is a lower-biased estimate (PR C review), so it only separates an in-screen mount from an update; an entry is decided by the route-change label alone"
|
|
11404
|
+
},
|
|
11405
|
+
instrument: {
|
|
11406
|
+
aspects: ["sdk", "driver", "driverVersion", "journeyVersion", "actionGc"],
|
|
11407
|
+
sdkReleases: SDK_RELEASES,
|
|
11408
|
+
uncalibrated: false,
|
|
11409
|
+
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)',
|
|
11410
|
+
driverReleases: DRIVER_RELEASES,
|
|
11411
|
+
unknownDiffers: ["journeyVersion"]
|
|
11412
|
+
},
|
|
11413
|
+
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)' },
|
|
11414
|
+
rules: [
|
|
11415
|
+
{ ...COMMIT_MAX_JUDGED, id: "jank", on: { measure: "jank_ms" }, source: JANK_SOURCE },
|
|
11416
|
+
evidenceRule("commit_max", { measure: "commit_max_ms" }, JUDGED_WINDOWS, [], "W8 option A: the slowest commit is shown, jank time is judged", { devices: ANY_DEVICE }),
|
|
11417
|
+
RENDER_JUDGED,
|
|
11418
|
+
COMPONENT_JUDGED,
|
|
11419
|
+
{ ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11420
|
+
{ ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11421
|
+
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"),
|
|
11422
|
+
evidenceRule("background_commit", { measure: "commit_max_ms" }, BACKGROUND, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "background places are evidence only (REQ-MEAS-32)"),
|
|
11423
|
+
evidenceRule("background_render", { measure: "render_ms" }, BACKGROUND, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "background places are evidence only (REQ-MEAS-32)"),
|
|
11424
|
+
evidenceRule("background_component", { dimension: "component", measure: "self_ms" }, BACKGROUND, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "background places are evidence only (REQ-MEAS-32)"),
|
|
11425
|
+
evidenceRule("launch_commit", { measure: "commit_max_ms" }, LAUNCH, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "the launch screen is evidence only (REQ-VER-15)"),
|
|
11426
|
+
evidenceRule("launch_render", { measure: "render_ms" }, LAUNCH, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "the launch screen is evidence only (REQ-VER-15)"),
|
|
11427
|
+
evidenceRule("launch_component", { dimension: "component", measure: "self_ms" }, LAUNCH, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "the launch screen is evidence only (REQ-VER-15)"),
|
|
11428
|
+
evidenceRule("step_commit", { measure: "commit_max_ms" }, STEP, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "a declared step is evidence only (REQ-MET-14)"),
|
|
11429
|
+
evidenceRule("step_render", { measure: "render_ms" }, STEP, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "a declared step is evidence only (REQ-MET-14)"),
|
|
11430
|
+
evidenceRule("step_component", { dimension: "component", measure: "self_ms" }, STEP, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "a declared step is evidence only (REQ-MET-14)"),
|
|
11431
|
+
evidenceRule("touch", { measure: "touch_max_ms" }, SHOWN2, [{ rel: 0.2, abs: 10 }], "touch response is not judged (ruling)"),
|
|
11432
|
+
evidenceRule("freeze", { measure: "sampler_longest_gap_ms" }, SHOWN2, [{ rel: 0.5, abs: 17 }], "v5.3 warn level"),
|
|
11433
|
+
evidenceRule("freeze_journey", { measure: "sampler.frames.longest_gap_ms" }, JOURNEY, [{ rel: 0.5, abs: 17 }], "v5.3 warn level"),
|
|
11434
|
+
evidenceRule("memory_added", { measure: "memory_left_mb" }, SHOWN2, [{ abs: 2 }], "v5.3 warn level", { thresholdAt: { screen: [{ abs: 10 }] }, devices: ANY_DEVICE }),
|
|
11435
|
+
evidenceRule("memory_launch", { measure: "mem.launch_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11436
|
+
evidenceRule("memory_end_growth", { measure: "mem.end_growth_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11437
|
+
evidenceRule("memory_peak_growth", { measure: "mem.peak_growth_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11438
|
+
evidenceRule("cpu", { measure: "sampler.cpu_ms" }, JOURNEY, [{ rel: 0.05 }], "a +10 % whole-app CPU change must show as evidence (hard case, with room for run noise on both sides); the prototype used 20 %"),
|
|
11439
|
+
evidenceRule("window_cpu", { measure: "cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 20 }], "v5.3 warn level"),
|
|
11440
|
+
evidenceRule("long_tasks", { measure: "longtask_ms" }, SHOWN2, [{ rel: 0.3, abs: 50 }], "v5.3 warn level"),
|
|
11441
|
+
evidenceRule("gc", { measure: "gc_ms" }, SHOWN2, [{ rel: 0.5, abs: 2 }], "v5.3 warn level"),
|
|
11442
|
+
evidenceRule("requests", { measure: "net_requests" }, SHOWN2, [{ rel: 0.5, abs: 1 }], "v5.3 warn level"),
|
|
11443
|
+
evidenceRule("js_cpu", { measure: "js_cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 5 }], "v5.3 warn level"),
|
|
11444
|
+
MEMORY_RETAINED
|
|
11445
|
+
],
|
|
11446
|
+
problems: [
|
|
11447
|
+
{
|
|
11448
|
+
id: "slow-commit",
|
|
11449
|
+
on: { measure: "commit_max_ms" },
|
|
11450
|
+
places: JUDGED_WINDOWS,
|
|
11451
|
+
devices: DEVICE,
|
|
11452
|
+
above: { frames: 1 },
|
|
11453
|
+
inRuns: "all",
|
|
11454
|
+
uncalibrated: true,
|
|
11455
|
+
source: "Arek 2026-10-16: a commit over one 60 Hz frame (16.7 ms) drops below 60 fps, where lag starts; present in every PR run (TapIn Appium: 37 places, 9 of them at 17-22 ms in only some sessions)"
|
|
11456
|
+
},
|
|
11457
|
+
{
|
|
11458
|
+
id: "heavy-component",
|
|
11459
|
+
on: { dimension: "component", measure: "self_ms" },
|
|
11460
|
+
places: JUDGED_WINDOWS,
|
|
11461
|
+
devices: DEVICE,
|
|
11462
|
+
above: { frames: 1 },
|
|
11463
|
+
inRuns: "all",
|
|
11464
|
+
uncalibrated: true,
|
|
11465
|
+
source: "Arek 2026-10-16: one 60 Hz frame, as the slowest commit; a component with a long render in the PR's own runs; a component that exists only on the PR side lands here, never as a regression row (Q3)"
|
|
11466
|
+
},
|
|
11467
|
+
{
|
|
11468
|
+
id: "heavy-entry",
|
|
11469
|
+
on: { measure: "render_ms" },
|
|
11470
|
+
places: JUDGED_WINDOWS,
|
|
11471
|
+
devices: DEVICE,
|
|
11472
|
+
above: { value: ENTRY_BUDGET_MS },
|
|
11473
|
+
inRuns: "all",
|
|
11474
|
+
work: ["entry", "mount"],
|
|
11475
|
+
uncalibrated: true,
|
|
11476
|
+
source: 'ruling d: an entry or in-screen mount has a budget of about 100 ms of React work; its second half, "no dropped frames during the transition", is covered by `slow-commit` at one frame (the earlier `blocking-commit` at 2 frames only repeated it)'
|
|
11477
|
+
}
|
|
11478
|
+
],
|
|
11479
|
+
budgets: {
|
|
11480
|
+
entry: { value: ENTRY_BUDGET_MS },
|
|
11481
|
+
mount: { value: ENTRY_BUDGET_MS },
|
|
11482
|
+
update: { frames: 1 },
|
|
11483
|
+
uncalibrated: true,
|
|
11484
|
+
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)"
|
|
11485
|
+
},
|
|
11486
|
+
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' },
|
|
11487
|
+
notJudgedLabels: ["launch"],
|
|
11488
|
+
unstable: [{
|
|
11489
|
+
...UNSTABLE_NOISY,
|
|
11490
|
+
step: "own",
|
|
11491
|
+
reads: [COMMIT_MAX_JUDGED, RENDER_JUDGED, COMPONENT_JUDGED, MEMORY_RETAINED],
|
|
11492
|
+
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`
|
|
11493
|
+
}],
|
|
11494
|
+
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' },
|
|
11495
|
+
measuredWith: [
|
|
11496
|
+
{ aspect: "renderer", affects: REACT_MEASURES },
|
|
11497
|
+
{ aspect: "reactExport", affects: MEMORY_MEASURES },
|
|
11498
|
+
{ aspect: "windowModel", affects: WINDOW_MODEL_AFFECTS, places: ["press", "drag"], marks: true },
|
|
11499
|
+
{ aspect: "screenWindowModel", affects: WINDOW_MODEL_AFFECTS, places: ["screen"], marks: true },
|
|
11500
|
+
{ aspect: "cpuSource", affects: ["sampler.cpu_ms"] },
|
|
11501
|
+
{ aspect: "causeSplit", affects: REACT_MEASURES, places: REACT_PLACES, marks: true },
|
|
11502
|
+
{ aspect: "endGc", affects: MEMORY_MEASURES }
|
|
11503
|
+
],
|
|
11504
|
+
tiers: {
|
|
11505
|
+
rel: TIER_REL,
|
|
11506
|
+
update: TIER_UPDATE_MS,
|
|
11507
|
+
work: TIER_WORK_MS,
|
|
11508
|
+
uncalibrated: false,
|
|
11509
|
+
source: `${NOISE_SOURCE}; ${CAPPED_SOURCE}; ${TIERS_SOURCE}; ${MEMORY_PAIR_SOURCE}`,
|
|
11510
|
+
relAtMostErrorMs: true,
|
|
11511
|
+
memory: { error: { rel: 0.3, abs: 150 }, uncalibrated: true }
|
|
11512
|
+
},
|
|
11513
|
+
better: { from: CLAUSES["S-BETTER"].value.from, uncalibrated: false, source: CLAUSES["S-BETTER"].ruling },
|
|
11514
|
+
placeCoverage: {
|
|
11515
|
+
uncalibrated: false,
|
|
11516
|
+
source: "plan 79 S1/S2, Arek 2026-10-21 ~18:55 and 2026-10-22 ~16:45: removed never scores; list main-only places; count main sessions that have each place and value"
|
|
11517
|
+
},
|
|
11518
|
+
windowCount: { below: CLAUSES["W-WINDOW-COUNT"].value.below, uncalibrated: CLAUSES["W-WINDOW-COUNT"].uncalibrated, source: 'plan 51 D2 floor (90 % of the usual window count), Arek 2026-10-22 ~07:20 "a changed number of swipes/windows on one screen must affect only that screen"; per PR run, a decrease only (an increase is not marked)' },
|
|
11519
|
+
detail: { componentMs: CLAUSES["C-DETAIL-MS"].value.ms, censored: "certify" },
|
|
11520
|
+
sdkExclusionByPlace: {
|
|
11521
|
+
uncalibrated: false,
|
|
11522
|
+
source: `REQ-VER-35 amendment 2026-10-21 (Arek, "porównywać to co się da"), option (a): only the places an SDK step moved are measured differently; comparable places decide, Can't tell only when no comparable place is left (one test for instrument marks, window counts and SDK exclusions); an unstable place is never judged, so its left-out values are not counted (REQ-MET-29)`
|
|
11523
|
+
}
|
|
11524
|
+
};
|
|
11525
|
+
var CONFIG = withClauseValues(SHAPE);
|
|
11526
|
+
var P2A_SLIGHT = withClauseValues({
|
|
11527
|
+
...CONFIG,
|
|
11528
|
+
id: `${CONFIG.id}+p2a-sw`,
|
|
11529
|
+
unstable: CONFIG.unstable.map((u) => ({
|
|
11530
|
+
...u,
|
|
11531
|
+
judgeAt: { line: "error", tier: "warning" },
|
|
11532
|
+
source: `${u.source}; P2a candidate (plan 81, Arek 2026-10-23 ~15:45): judged only past the error line, as Slightly worse, never blocking`
|
|
11533
|
+
}))
|
|
11534
|
+
});
|
|
11535
|
+
var P4_EVEN = {
|
|
11536
|
+
...CONFIG,
|
|
11537
|
+
id: `${CONFIG.id}+p4-even`,
|
|
11538
|
+
evenSlowdown: { maxExcess: 0.3, renders: 0.1, commits: 0.25, uncalibrated: true, source: "plan 81 P4 (Opus O2 guesses); Arek 2026-10-23 ~12:45 direction (4); Arek 2026-10-26 ctl-arek-p81-p3p4: render and commit counts may block a downgrade as a guard, never colour a verdict alone" }
|
|
11539
|
+
};
|
|
11540
|
+
var P3_COMMIT = withClauseValues({
|
|
11541
|
+
...CONFIG,
|
|
11542
|
+
id: `${CONFIG.id}+p3-commit`,
|
|
11543
|
+
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" }
|
|
11544
|
+
});
|
|
10377
11545
|
// apps/agent/src/device-model.ts
|
|
10378
11546
|
import { tmpdir } from "node:os";
|
|
10379
11547
|
import { join as join12 } from "node:path";
|
|
@@ -10452,173 +11620,10 @@ async function detectSimulator(deps, udid) {
|
|
|
10452
11620
|
}
|
|
10453
11621
|
|
|
10454
11622
|
// apps/agent/src/slack.ts
|
|
10455
|
-
var COLOR = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
10456
|
-
var MAX_ROWS = 3;
|
|
10457
|
-
var NAME_W = 30;
|
|
10458
|
-
var COLUMN_GAP = 1;
|
|
10459
|
-
function metricWord(item) {
|
|
10460
|
-
if (item.label_text)
|
|
10461
|
-
return item.label_text;
|
|
10462
|
-
if (item.metric === "dur_ms" && placeKindWord(item.step ?? "") !== "Tap")
|
|
10463
|
-
return "length";
|
|
10464
|
-
return measureDef(item.metric)?.short ?? item.metric.replace(/_/g, " ");
|
|
10465
|
-
}
|
|
10466
|
-
function change2(item) {
|
|
10467
|
-
if (item.change_text)
|
|
10468
|
-
return item.change_text.replace(/ \(.*\)$/, "");
|
|
10469
|
-
const { baseline: b, candidate: c } = item;
|
|
10470
|
-
if (b === null || c === null)
|
|
10471
|
-
return "";
|
|
10472
|
-
if (b === 0)
|
|
10473
|
-
return `${c - b >= 0 ? "+" : ""}${fieldValueText(item.metric, c - b)}`;
|
|
10474
|
-
const p = Math.round((c / b - 1) * 100);
|
|
10475
|
-
return `${p >= 0 ? "+" : ""}${p} %`;
|
|
10476
|
-
}
|
|
10477
|
-
var size = (i) => i.baseline === null || i.candidate === null ? 0 : i.baseline === 0 ? i.candidate === 0 ? 0 : Number.POSITIVE_INFINITY : Math.abs(Math.log(Math.max(i.candidate, 0.000000001) / i.baseline));
|
|
10478
|
-
var bySize = (a, b) => size(a) === size(b) ? 0 : size(a) > size(b) ? -1 : 1;
|
|
10479
|
-
var worseOrder = (a, b) => Number(a.grade === "warn") - Number(b.grade === "warn") || bySize(a, b);
|
|
10480
|
-
var isWorse = (i) => i.state === "worse";
|
|
10481
|
-
var isBetter = (i) => i.state === "better";
|
|
10482
|
-
var differs = (sc) => sc?.headline?.measured_differently ?? [];
|
|
10483
|
-
var scenarioBlocks = (sc) => sc?.headline?.blocking !== false;
|
|
10484
|
-
var isRed = (i, sc) => i.grade !== "warn" && scenarioBlocks(sc);
|
|
10485
|
-
var mark = (i, sc) => isBetter(i) ? "\uD83D\uDFE2" : isRed(i, sc) ? "\uD83D\uDD34" : i.grade !== "warn" ? "⚪" : "\uD83D\uDFE1";
|
|
10486
|
-
var fit = (s, w) => s.length > w ? `${s.slice(0, w - 1)}…` : s.padEnd(w);
|
|
10487
|
-
var right = (s, w) => s.padStart(w);
|
|
10488
|
-
var phone = (sc) => sc?.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
10489
|
-
function commitLine(ctx) {
|
|
10490
|
-
const c = ctx.commit;
|
|
10491
|
-
if (!c)
|
|
10492
|
-
return null;
|
|
10493
|
-
const short = c.sha.slice(0, 7);
|
|
10494
|
-
return c.title ? `Commit: *${c.title}*${c.author ? ` · ${c.author}` : ""} (${short})` : `Commit ${short}`;
|
|
10495
|
-
}
|
|
10496
|
-
var section = (text) => ({ type: "section", text: { type: "mrkdwn", text } });
|
|
10497
|
-
var context = (text) => ({ type: "context", elements: [{ type: "mrkdwn", text }] });
|
|
10498
|
-
function prTable(items, sc) {
|
|
10499
|
-
const cells = items.map((i) => ({ i, main: fieldValueText(i.metric, i.baseline), pr: fieldValueText(i.metric, i.candidate), change: change2(i) }));
|
|
10500
|
-
const widest = (head, pick) => Math.max(head.length, ...cells.map((c) => pick(c).length)) + COLUMN_GAP;
|
|
10501
|
-
const w = [NAME_W, widest("main", (c) => c.main), widest("PR", (c) => c.pr), widest("change", (c) => c.change)];
|
|
10502
|
-
const head = `${" ".repeat(3)}${fit("", w[0])}${right("main", w[1])}${right("PR", w[2])}${right("change", w[3])}`;
|
|
10503
|
-
const rows = cells.flatMap((c) => [
|
|
10504
|
-
`${mark(c.i, sc)} ${fit(placeLabel(c.i.step ?? "*"), w[0])}${right(c.main, w[1])}${right(c.pr, w[2])}${right(c.change, w[3])}`,
|
|
10505
|
-
` ${metricWord(c.i)}`
|
|
10506
|
-
]);
|
|
10507
|
-
return ["```", head, ...rows, "```"].join(`
|
|
10508
|
-
`);
|
|
10509
|
-
}
|
|
10510
|
-
function mainCard(i) {
|
|
10511
|
-
const label = (s) => ` ${s.padEnd(16)}`;
|
|
10512
|
-
return [
|
|
10513
|
-
"```",
|
|
10514
|
-
placeLabel(i.step ?? "*"),
|
|
10515
|
-
`${label(metricWord(i))}${right(fieldValueText(i.metric, i.candidate), 9)}`,
|
|
10516
|
-
`${label("usually")}${right(fieldValueText(i.metric, i.baseline), 9)}`,
|
|
10517
|
-
`${label("change")}${right(change2(i), 9)}`,
|
|
10518
|
-
"```"
|
|
10519
|
-
].join(`
|
|
10520
|
-
`);
|
|
10521
|
-
}
|
|
10522
|
-
function blockedMessage(s, ctx) {
|
|
10523
|
-
const text = `⚪ No result for ${ctx.branch}: the measurement did not finish`;
|
|
10524
|
-
const blocks = [
|
|
10525
|
-
section(`*${text}*
|
|
10526
|
-
${ctx.blocked?.reason ?? "No iteration sent any data."}
|
|
10527
|
-
Nothing was judged, so this is not a "${STATE_WORDS["no-change"].toLowerCase()}".`),
|
|
10528
|
-
...commitLine(ctx) ? [context(commitLine(ctx))] : [],
|
|
10529
|
-
...s ? [section(`\uD83D\uDC49 <${s.dashboard_url}|See what happened>`)] : []
|
|
10530
|
-
];
|
|
10531
|
-
return { text, attachments: [{ color: COLOR.none, blocks }] };
|
|
10532
|
-
}
|
|
10533
11623
|
function slackMessage(s, ctx) {
|
|
10534
11624
|
if (ctx.blocked)
|
|
10535
|
-
return
|
|
10536
|
-
|
|
10537
|
-
if (!s || !v)
|
|
10538
|
-
return null;
|
|
10539
|
-
const items = v.scenarios.flatMap((sc) => sc.state === "inconclusive" ? [] : movedItems(v, sc));
|
|
10540
|
-
const scenarioOf = (i) => v.scenarios.find((sc) => sc.scenario === i.scenario && (!i.device_id || sc.device?.id === i.device_id)) ?? v.scenarios.find((sc) => sc.scenario === i.scenario);
|
|
10541
|
-
const isPr = v.scenarios.some((sc) => sc.role === "comparison");
|
|
10542
|
-
const worse = items.filter(isWorse).sort(worseOrder);
|
|
10543
|
-
const better = items.filter(isBetter).sort(bySize);
|
|
10544
|
-
const commit = commitLine(ctx);
|
|
10545
|
-
const heated = v.scenarios.filter((sc) => heatNote(sc));
|
|
10546
|
-
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", phone(sc))).map(context);
|
|
10547
|
-
const heat = heated.map((sc) => context(heatNote(sc)));
|
|
10548
|
-
const errorRows = worse.filter((i) => i.grade !== "warn");
|
|
10549
|
-
const warningRows = worse.filter((i) => i.grade === "warn");
|
|
10550
|
-
const errors = placeCount(errorRows);
|
|
10551
|
-
const warns = placeCount(worse) - errors;
|
|
10552
|
-
const red = errorRows.some((i) => isRed(i, scenarioOf(i)));
|
|
10553
|
-
const notBlocking = errors > 0 && !red;
|
|
10554
|
-
const worseMark = red ? "\uD83D\uDD34" : warns > 0 ? "\uD83D\uDFE1" : "⚪";
|
|
10555
|
-
const worseColor = red ? COLOR.error : warns > 0 ? COLOR.warn : COLOR.none;
|
|
10556
|
-
if (isPr) {
|
|
10557
|
-
if (!worse.length) {
|
|
10558
|
-
if (!heat.length)
|
|
10559
|
-
return null;
|
|
10560
|
-
const head = `⚪ ${ctx.branch}: ${heat.length ? INFO_WORDS.heat : v.word ?? STATE_WORDS["no-change"]}`;
|
|
10561
|
-
const blocks = [section(`*${head}*`), ...heat, ...contextLines, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0]?.dashboard_url ?? s.dashboard_url}|View the comparison>`)];
|
|
10562
|
-
return { text: head, attachments: [{ color: COLOR.none, blocks }] };
|
|
10563
|
-
}
|
|
10564
|
-
const graded = worse.some((i) => i.grade);
|
|
10565
|
-
const places = (word, n) => `${word.toLowerCase()} in ${n} place${n > 1 ? "s" : ""}`;
|
|
10566
|
-
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : `${places(STATE_WORDS.worse, placeCount(worse))}${notBlocking ? ` (${NOT_BLOCKING})` : ""}`;
|
|
10567
|
-
const head = `${worseMark} ${ctx.branch}: ${counts}`;
|
|
10568
|
-
const blocks = [section(`*${head}*`), ...contextLines];
|
|
10569
|
-
const groups = new Map;
|
|
10570
|
-
for (const i of worse)
|
|
10571
|
-
groups.set(scenarioOf(i), [...groups.get(scenarioOf(i)) ?? [], i]);
|
|
10572
|
-
let shown = 0;
|
|
10573
|
-
for (const [sc, list] of groups) {
|
|
10574
|
-
if (shown >= MAX_ROWS)
|
|
10575
|
-
break;
|
|
10576
|
-
const rows = list.slice(0, MAX_ROWS - shown);
|
|
10577
|
-
shown += rows.length;
|
|
10578
|
-
const nB = Math.max(...rows.map((r) => r.n_baseline ?? 0));
|
|
10579
|
-
const nC = Math.max(...rows.map((r) => r.n_candidate ?? 0)) || (sc?.runs?.valid ?? 0);
|
|
10580
|
-
const runs = nB && nC ? ` · ${nC} PR run${nC === 1 ? "" : "s"} vs ${nB} main run${nB === 1 ? "" : "s"}` : "";
|
|
10581
|
-
const note = differs(sc).length ? `
|
|
10582
|
-
_Measured differently (${differs(sc).join(", ")})${scenarioBlocks(sc) ? "" : ` — ${NOT_BLOCKING}`}_` : "";
|
|
10583
|
-
const nonblocking = !scenarioBlocks(sc) && rows.some((i) => i.grade !== "warn") && !differs(sc).length ? ` · ${NOT_BLOCKING}` : "";
|
|
10584
|
-
blocks.push(section(`*${sc?.scenario ?? "journey"}*${nonblocking} · ${phone(sc)}${runs}${note}
|
|
10585
|
-
${prTable(rows, sc)}`));
|
|
10586
|
-
}
|
|
10587
|
-
const more = worse.length - shown;
|
|
10588
|
-
blocks.push(...heat);
|
|
10589
|
-
if (commit)
|
|
10590
|
-
blocks.push(context(commit));
|
|
10591
|
-
blocks.push(section(`\uD83D\uDC49 <${groups.keys().next().value?.dashboard_url ?? s.dashboard_url}|View the comparison>${more > 0 ? ` · ${more} more` : ""}`));
|
|
10592
|
-
return { text: head, attachments: [{ color: worseColor, blocks }] };
|
|
10593
|
-
}
|
|
10594
|
-
const improved = better.length > 0 || v.state === "better";
|
|
10595
|
-
if (!worse.length && !improved && !ctx.always && !heat.length)
|
|
10596
|
-
return null;
|
|
10597
|
-
const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
|
|
10598
|
-
const quiet = improved ? STATE_WORDS.better.toLowerCase() : (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
|
|
10599
|
-
const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : !improved && heat.length && !ctx.always ? `${ctx.branch}: ${INFO_WORDS.heat}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
|
|
10600
|
-
const head = `${worse.length ? worseMark : improved ? "\uD83D\uDFE2" : v.state === "inconclusive" && (ctx.always || !heat.length) ? "❔" : "⚪"} ${status}`;
|
|
10601
|
-
const journeys = new Set(v.scenarios.map((sc) => `${sc.scenario}|${sc.device?.id}`)).size;
|
|
10602
|
-
const blocks = [section(`*${head}*
|
|
10603
|
-
${journeys} journey${journeys === 1 ? "" : "s"} checked`), ...contextLines];
|
|
10604
|
-
let shown = 0;
|
|
10605
|
-
for (const [title, list] of [[STATE_WORDS.worse, errorRows], [STATE_WORDS.warning, warningRows], [STATE_WORDS.better, better]]) {
|
|
10606
|
-
for (const i of list) {
|
|
10607
|
-
if (shown >= MAX_ROWS)
|
|
10608
|
-
break;
|
|
10609
|
-
shown++;
|
|
10610
|
-
const sc = scenarioOf(i);
|
|
10611
|
-
blocks.push(section(`*${title}*${i.grade !== "warn" && isWorse(i) && !scenarioBlocks(sc) ? ` (${NOT_BLOCKING})` : ""} · *${sc?.scenario ?? "journey"}* · ${phone(sc)}
|
|
10612
|
-
${mainCard(i)}`));
|
|
10613
|
-
}
|
|
10614
|
-
}
|
|
10615
|
-
const more = worse.length + better.length - shown;
|
|
10616
|
-
blocks.push(...heat);
|
|
10617
|
-
if (commit)
|
|
10618
|
-
blocks.push(context(commit));
|
|
10619
|
-
blocks.push(section(`\uD83D\uDC49 <${s.dashboard_url}|Open KeepSnappy>${more > 0 ? ` · ${more} more changes` : ""}`));
|
|
10620
|
-
const color = worse.length ? worseColor : improved ? COLOR.better : COLOR.none;
|
|
10621
|
-
return { text: head, attachments: [{ color, blocks }] };
|
|
11625
|
+
return noResultSlackPayload(ctx.branch, ctx.blocked.reason, s ? linkOf(s) : null);
|
|
11626
|
+
return s ? resultSlackPayload(s, { always: ctx.always }) : null;
|
|
10622
11627
|
}
|
|
10623
11628
|
async function postToSlack(url, payload, fetchImpl = fetch) {
|
|
10624
11629
|
try {
|
|
@@ -10703,7 +11708,7 @@ function instrumentFlags(f) {
|
|
|
10703
11708
|
return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
|
|
10704
11709
|
}
|
|
10705
11710
|
var TOKENS_STUB = "// Replaced by `keepsnappy farm open` for one upload. Never commit real tokens.\noutput.keepsnappyTokens = {};\n";
|
|
10706
|
-
var
|
|
11711
|
+
var JOURNEY2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
10707
11712
|
var DEFAULT_TTL = 21600;
|
|
10708
11713
|
var POLL_MS = 5000;
|
|
10709
11714
|
function tokensJs(api, runSetId, tokens, links = maestroLinkFields()) {
|
|
@@ -10790,7 +11795,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
10790
11795
|
if (journeys.length === 0)
|
|
10791
11796
|
problems.push("--journeys a,b is required (the flow names the device runs, one history per journey)");
|
|
10792
11797
|
for (const j of journeys)
|
|
10793
|
-
if (!
|
|
11798
|
+
if (!JOURNEY2.test(j))
|
|
10794
11799
|
problems.push(`--journeys: "${j}" is not a journey name (letters, digits, . _ -)`);
|
|
10795
11800
|
if (new Set(journeys).size !== journeys.length)
|
|
10796
11801
|
problems.push("--journeys lists a journey twice");
|
|
@@ -10818,11 +11823,15 @@ async function farmOpen(deps, f, rt) {
|
|
|
10818
11823
|
const withInstrument = { ...f, ...instrumentFlags(f) };
|
|
10819
11824
|
const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
|
|
10820
11825
|
const base = await baseCommitFor(deps, rt.env, { explicit: f["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
|
|
11826
|
+
const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
|
|
11827
|
+
if (main.warning)
|
|
11828
|
+
rt.print(main.warning);
|
|
10821
11829
|
const rs = await createRunSet(api, {
|
|
10822
11830
|
branch: git.branch,
|
|
10823
11831
|
...git.commit ? { commit_sha: git.commit } : {},
|
|
10824
11832
|
...base ? { base_commit_sha: base } : {},
|
|
10825
11833
|
...await commitFields(deps, rt.env, { commit: git.commit, base }),
|
|
11834
|
+
...main.merges ? { merges: main.merges } : {},
|
|
10826
11835
|
device: { model: device.model, ...device.os ? { os: device.os } : {}, provider: device.provider, ...f["device-id"]?.trim() ? { id: f["device-id"].trim() } : {} },
|
|
10827
11836
|
scenarios: journeys,
|
|
10828
11837
|
journeys: journeyInfo,
|
|
@@ -10939,7 +11948,7 @@ async function settle(api, runSetId, o) {
|
|
|
10939
11948
|
}
|
|
10940
11949
|
}
|
|
10941
11950
|
var FAILED_SESSION = new Set(["failed", "error", "timedout", "stopped", "skipped"]);
|
|
10942
|
-
var
|
|
11951
|
+
var median6 = (xs) => {
|
|
10943
11952
|
const v = [...xs].sort((a, b) => a - b);
|
|
10944
11953
|
return v.length % 2 ? v[(v.length - 1) / 2] : (v[v.length / 2 - 1] + v[v.length / 2]) / 2;
|
|
10945
11954
|
};
|
|
@@ -10961,10 +11970,11 @@ function brokenRun(state, runs, session) {
|
|
|
10961
11970
|
const others = ordered.filter((r) => r !== last && r.journey === last.journey && has(r)).map((r) => byId.get(r.run_id).windows ?? 0);
|
|
10962
11971
|
const journey = typeof s.usual_windows === "number" && s.usual_windows > 0 ? s.usual_windows : null;
|
|
10963
11972
|
const mine = ordered.filter((r) => r.journey === last.journey && has(r) && byId.get(r.run_id).complete !== false).map((r) => byId.get(r.run_id).windows ?? 0).filter((n) => n > 0);
|
|
10964
|
-
const m = mine.length >= 2 ?
|
|
10965
|
-
const
|
|
10966
|
-
const
|
|
10967
|
-
|
|
11973
|
+
const m = mine.length >= 2 ? median6(mine) : null;
|
|
11974
|
+
const floor = CLAUSES["W-WINDOW-COUNT"].value.below;
|
|
11975
|
+
const changed = m !== null && mine.every((n) => n >= floor * m && floor * n <= m) && (journey === null || m < floor * journey);
|
|
11976
|
+
const usual = changed ? m : journey ?? (others.length ? median6(others) : null);
|
|
11977
|
+
if (usual !== null && (s.windows ?? 0) < floor * usual)
|
|
10968
11978
|
return last;
|
|
10969
11979
|
return next;
|
|
10970
11980
|
}
|
|
@@ -11000,6 +12010,7 @@ async function farmClose(deps, f, rt) {
|
|
|
11000
12010
|
await scrubTokens(deps, state.tokens_js, rt);
|
|
11001
12011
|
}
|
|
11002
12012
|
}
|
|
12013
|
+
var blockedReason = (session) => `No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}.`;
|
|
11003
12014
|
async function closeRunSet(deps, f, rt, state, statePath) {
|
|
11004
12015
|
const settleSeconds = Number(f["settle-seconds"] ?? 60);
|
|
11005
12016
|
const maxWaitSeconds = Number(f["max-wait-seconds"] ?? Math.max(300, settleSeconds * 3));
|
|
@@ -11054,7 +12065,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11054
12065
|
const blocked = summary.runs.valid === 0 && withData === 0;
|
|
11055
12066
|
if (blocked) {
|
|
11056
12067
|
rt.print("");
|
|
11057
|
-
rt.print(`KeepSnappy result for ${where}:
|
|
12068
|
+
rt.print(`KeepSnappy result for ${where}: ${BLOCKED_WORD} — no iteration sent any data${session.known && !session.passed ? ` (device session ${session.status})` : ""}.`);
|
|
11058
12069
|
rt.print(" Check that the build carries the SDK, the activation link ran, and the device reached the API.");
|
|
11059
12070
|
rt.print(`Dashboard: ${summary.dashboard_url}`);
|
|
11060
12071
|
} else if (f.wait) {
|
|
@@ -11075,36 +12086,14 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11075
12086
|
`);
|
|
11076
12087
|
await githubOutputs(deps, rt, { state: state2.result.state ?? "none", dashboard_url: summary.dashboard_url, run_set_id: state.run_set_id });
|
|
11077
12088
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
11078
|
-
if (step)
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}. [Open the dashboard](${summary.dashboard_url})
|
|
11082
|
-
` : markdownRunSet(f.wait ? summary : null, { dashboardUrl: summary.dashboard_url, branch: state.branch });
|
|
11083
|
-
const noData = summary.runs.no_data?.length ?? 0;
|
|
11084
|
-
const tail = [
|
|
11085
|
-
session.known && !session.passed ? `Device session ${session.status}${session.message ? `: ${session.message}` : ""}.` : "",
|
|
11086
|
-
noData ? `${noData} of ${state.runs.length} iterations sent no data.` : ""
|
|
11087
|
-
].filter(Boolean);
|
|
11088
|
-
await deps.fs.appendFile(step, `${head}${tail.length ? `
|
|
11089
|
-
${tail.join(" ")}
|
|
11090
|
-
` : ""}`);
|
|
11091
|
-
}
|
|
12089
|
+
if (step)
|
|
12090
|
+
await deps.fs.appendFile(step, blocked ? noResultMarkdown(state.branch, blockedReason(session), summary.dashboard_url) : markdownRunSet(f.wait ? summary : null, { dashboardUrl: summary.dashboard_url, branch: state.branch }));
|
|
11092
12091
|
const hook = envValue(rt.env, "SLACK_WEBHOOK");
|
|
11093
|
-
if (hook) {
|
|
11094
|
-
const
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
...blocked ? { blocked: { reason: `No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}.` } } : {},
|
|
11099
|
-
always: envValue(rt.env, "SLACK_ALWAYS") === "1"
|
|
11100
|
-
});
|
|
11101
|
-
if (!msg) {
|
|
11102
|
-
rt.print(!f.wait ? "Slack: skipped, the message needs --wait" : notComputed(summary) ? "Slack: nothing to report, the verdict is not computed yet" : `Slack: nothing to report (${resultWord(summary).toLowerCase()})`);
|
|
11103
|
-
} else {
|
|
11104
|
-
const r = await postToSlack(hook, msg, rt.fetch ?? fetch);
|
|
11105
|
-
rt.print(r.ok ? "Slack: posted" : `Slack: not posted (${r.error}); the job is not affected`);
|
|
11106
|
-
}
|
|
11107
|
-
}
|
|
12092
|
+
if (hook && blocked) {
|
|
12093
|
+
const r = await postToSlack(hook, slackMessage(summary, { branch: state.branch, blocked: { reason: blockedReason(session) } }), rt.fetch ?? fetch);
|
|
12094
|
+
rt.print(r.ok ? "Slack: posted" : `Slack: not posted (${r.error}); the job is not affected`);
|
|
12095
|
+
} else if (hook)
|
|
12096
|
+
rt.print("Slack: results are posted after the verdict is computed");
|
|
11108
12097
|
if (blocked)
|
|
11109
12098
|
return EXIT.infra;
|
|
11110
12099
|
if (f["fail-on-worse"] && isBlocking(summary)) {
|
|
@@ -11417,7 +12406,7 @@ function nodeFarmRunPorts(deps, rt) {
|
|
|
11417
12406
|
}
|
|
11418
12407
|
var TUNNEL_TARGET = /^(?:localhost|127\.0\.0\.1):(\d{1,5})$/;
|
|
11419
12408
|
var ENV_PAIR = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
11420
|
-
var
|
|
12409
|
+
var DEVICE2 = /^(.+)-([^-]+)$/;
|
|
11421
12410
|
var SECRETISH = /KEY|TOKEN|SECRET|PASSWORD/i;
|
|
11422
12411
|
var RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
11423
12412
|
function planFarmRun(f) {
|
|
@@ -11427,7 +12416,7 @@ function planFarmRun(f) {
|
|
|
11427
12416
|
const platform = f.platform ?? "ios";
|
|
11428
12417
|
if (platform !== "ios" && platform !== "android")
|
|
11429
12418
|
problems.push("--platform must be ios or android");
|
|
11430
|
-
if (!f.device || !
|
|
12419
|
+
if (!f.device || !DEVICE2.test(f.device))
|
|
11431
12420
|
problems.push('--device "<model>-<os>" is required, e.g. "iPhone 15 Pro-17"');
|
|
11432
12421
|
const driver = f.driver ?? "maestro";
|
|
11433
12422
|
if (driver !== "maestro" && driver !== "appium")
|
|
@@ -11436,7 +12425,7 @@ function planFarmRun(f) {
|
|
|
11436
12425
|
if (journeys.length === 0)
|
|
11437
12426
|
problems.push("--journeys a,b is required");
|
|
11438
12427
|
for (const j of journeys)
|
|
11439
|
-
if (!
|
|
12428
|
+
if (!JOURNEY2.test(j))
|
|
11440
12429
|
problems.push(`--journeys: "${j}" is not a journey name (letters, digits, . _ -)`);
|
|
11441
12430
|
const repeat = Number(f.repeat ?? 5);
|
|
11442
12431
|
if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100)
|
|
@@ -11533,7 +12522,7 @@ function printPlan(p, f, rt) {
|
|
|
11533
12522
|
line(` env ${k}=${substitute(v, {})}`);
|
|
11534
12523
|
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${join16(p.out, `browserstack.${p.platform}.json`)}`);
|
|
11535
12524
|
line(" 7. always: stop the tunnel, put the empty stub back over the tokens file");
|
|
11536
|
-
line(
|
|
12525
|
+
line(` exit: 0 any verdict, 1 infrastructure / driver failed / ${BLOCKED_WORD}, 2 configuration, 3 --fail-on-worse (a computed blocking Worse only; no computed verdict: nothing checked)`);
|
|
11537
12526
|
}
|
|
11538
12527
|
var ANNOTATION = /^::(error|warning|notice)( [^:]*)?::/;
|
|
11539
12528
|
async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
@@ -11636,7 +12625,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
11636
12625
|
let driver = { ok: true, message: null };
|
|
11637
12626
|
let closeCode = EXIT.ok;
|
|
11638
12627
|
try {
|
|
11639
|
-
const [model, os] = [
|
|
12628
|
+
const [model, os] = [DEVICE2.exec(f.device)[1], DEVICE2.exec(f.device)[2]];
|
|
11640
12629
|
const openCode = await farmOpen(deps, {
|
|
11641
12630
|
...f,
|
|
11642
12631
|
journeys: p.journeys.join(","),
|
|
@@ -11957,6 +12946,7 @@ import { parseArgs as parseArgs3 } from "node:util";
|
|
|
11957
12946
|
var SHORT_SESSIONS = 3;
|
|
11958
12947
|
var SHORT_POINTS = 5;
|
|
11959
12948
|
var SHORT_PLACES = 5;
|
|
12949
|
+
var SHORT_COMPARE_MEASURES = 3;
|
|
11960
12950
|
var SHORT_FORM_NOTES = {
|
|
11961
12951
|
list_findings: "the how-sure lines, the place lines of findings that are not Worse and the lanes that were judged",
|
|
11962
12952
|
get_finding: "the why, seen and layer lines, the component lines, the memory evidence lines other than the judged measure and the prompt",
|
|
@@ -11966,7 +12956,8 @@ var SHORT_FORM_NOTES = {
|
|
|
11966
12956
|
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
11967
12957
|
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
11968
12958
|
pr_status: "the per-journey dashboard links and the heat note",
|
|
11969
|
-
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}
|
|
12959
|
+
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}`,
|
|
12960
|
+
compare_runs: `the component lines and the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`
|
|
11970
12961
|
};
|
|
11971
12962
|
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
11972
12963
|
|
|
@@ -11992,9 +12983,13 @@ class Rows {
|
|
|
11992
12983
|
this.rows.push({ text: line, short: false, notice: false });
|
|
11993
12984
|
return this;
|
|
11994
12985
|
}
|
|
12986
|
+
twin(full, short) {
|
|
12987
|
+
this.rows.push({ text: full, short: false, notice: false }, { text: short, short: true, shortOnly: true });
|
|
12988
|
+
return this;
|
|
12989
|
+
}
|
|
11995
12990
|
render(form, left = []) {
|
|
11996
12991
|
if (form !== "short")
|
|
11997
|
-
return this.rows.map((r) => r.text).join(`
|
|
12992
|
+
return this.rows.filter((r) => !r.shortOnly).map((r) => r.text).join(`
|
|
11998
12993
|
`);
|
|
11999
12994
|
const kept = [];
|
|
12000
12995
|
for (const r of this.rows) {
|
|
@@ -12023,7 +13018,7 @@ function formatFinding(f, opts = {}) {
|
|
|
12023
13018
|
r.both(...laneContextLines(f.lane.context, f.lane.journey, f.lane.phone_name, opts.verbose, opts.form === "short"));
|
|
12024
13019
|
r.full(line("Why", f.answer.why));
|
|
12025
13020
|
r.both(line("What to do", f.answer.what_to_do));
|
|
12026
|
-
r.full(line("Seen", f.answer.lifecycle), line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
13021
|
+
r.full(line("Seen", f.answer.lifecycle), ...f.lifecycle.unstable_note ? [line("", f.lifecycle.unstable_note)] : [], line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
12027
13022
|
r.both(line("Id", f.id));
|
|
12028
13023
|
if (f.why.components.length) {
|
|
12029
13024
|
r.full(line("Components", ""));
|
|
@@ -12051,7 +13046,7 @@ function formatFinding(f, opts = {}) {
|
|
|
12051
13046
|
}
|
|
12052
13047
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
12053
13048
|
var LANES_SHOWN = 8;
|
|
12054
|
-
var
|
|
13049
|
+
var STATE_WORDS3 = { present: "still present", resolved: "resolved", "not-judged": "not judged in the newest run set" };
|
|
12055
13050
|
function measuredLines(l, r, opts) {
|
|
12056
13051
|
const f = l.filters;
|
|
12057
13052
|
const scope = [f.branch ?? "all branches", f.journey, f.phone].filter(Boolean).join(" · ");
|
|
@@ -12069,15 +13064,19 @@ function measuredLines(l, r, opts) {
|
|
|
12069
13064
|
};
|
|
12070
13065
|
if (m.lanes.length === 1) {
|
|
12071
13066
|
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
13067
|
+
r.both(...(n.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
13068
|
+
r.full(...(n.session_speed ?? []).map((line) => ` ${line}`));
|
|
12072
13069
|
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
12073
13070
|
return [];
|
|
12074
13071
|
}
|
|
12075
13072
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
12076
13073
|
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
12077
13074
|
for (const x of shown) {
|
|
12078
|
-
const keep = !x.judged || !!x.context;
|
|
13075
|
+
const keep = !x.judged || !!x.context || !!x.comparison_notes;
|
|
12079
13076
|
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12080
13077
|
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
13078
|
+
r.add(keep, ...(x.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
13079
|
+
r.full(...(x.session_speed ?? []).map((line) => ` ${line}`));
|
|
12081
13080
|
}
|
|
12082
13081
|
if (m.lanes.length > shown.length)
|
|
12083
13082
|
r.full(` … ${m.lanes.length - shown.length} more (--journey / --phone narrow it)`);
|
|
@@ -12096,7 +13095,7 @@ function countsLine2(l) {
|
|
|
12096
13095
|
].filter(Boolean);
|
|
12097
13096
|
const state = l.filters.state;
|
|
12098
13097
|
const unjudged = l.measured.lanes.filter((x) => !x.judged);
|
|
12099
|
-
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${
|
|
13098
|
+
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS3[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS3[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : `Nothing ${STATE_WORDS.worse.toLowerCase()}, nothing ${STATE_WORDS.warning.toLowerCase()}, nothing ${STATE_WORDS.better.toLowerCase()}`;
|
|
12100
13099
|
const heated = c.heat_runs ?? 0;
|
|
12101
13100
|
const more = [
|
|
12102
13101
|
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (run health, layer 3)` : "",
|
|
@@ -12140,10 +13139,12 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12140
13139
|
if (s.lanes.length)
|
|
12141
13140
|
r.both("");
|
|
12142
13141
|
for (const l of s.lanes) {
|
|
12143
|
-
const shown = l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)
|
|
13142
|
+
const shown = `${l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)}${l.stale ? ` (${STALE_FLAG})` : ""}`;
|
|
12144
13143
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
12145
13144
|
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)}` : ""}`);
|
|
12146
13145
|
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
13146
|
+
r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
13147
|
+
r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
|
|
12147
13148
|
for (const f of l.worst)
|
|
12148
13149
|
r.both(` ${f.id} ${f.headline}`);
|
|
12149
13150
|
if (l.present_findings > l.worst.length)
|
|
@@ -12152,6 +13153,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12152
13153
|
r.full(` ${l.link}`);
|
|
12153
13154
|
}
|
|
12154
13155
|
const links = s.lanes.filter((l) => l.link).length;
|
|
13156
|
+
const staleLanes = s.problems?.stale_lanes ?? 0;
|
|
12155
13157
|
const heated = s.counts.heat_runs ?? 0;
|
|
12156
13158
|
const more = [
|
|
12157
13159
|
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (information, never blocks)` : ""],
|
|
@@ -12160,6 +13162,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12160
13162
|
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` : ""
|
|
12161
13163
|
],
|
|
12162
13164
|
[true, statusProblemsLine(s)],
|
|
13165
|
+
[true, staleLanes ? `${staleWords(staleLanes)}: ${staleLanes === 1 ? "its word and problems are" : "their words and problems are"} of older rules` : ""],
|
|
12163
13166
|
[true, s.problems?.unread_lanes ? `${unreadWords(s.problems.unread_lanes)} for problems: ${staleResult(s.problems.unread_lanes)}` : ""],
|
|
12164
13167
|
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
12165
13168
|
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
@@ -12175,9 +13178,11 @@ function statusProblemsLine(s) {
|
|
|
12175
13178
|
const p = s.problems;
|
|
12176
13179
|
if (!p?.count)
|
|
12177
13180
|
return "";
|
|
12178
|
-
return `${
|
|
13181
|
+
return `${problemCountsText(p, "this branch’s")}. Not a regression, never blocks: list_problems, or keepsnappy problems --branch ${s.branch}`;
|
|
12179
13182
|
}
|
|
12180
13183
|
var unreadWords = (n) => `${n} journey × phone not read`;
|
|
13184
|
+
var STALE_FLAG = staleFlagText(["config"]);
|
|
13185
|
+
var staleWords = (n) => `${n} journey × phone from a stale result (${STALE_FLAG})`;
|
|
12181
13186
|
var staleResult = (n) => `${n === 1 ? "its stored result is" : "their stored results are"} not current`;
|
|
12182
13187
|
function formatProblemList(l, opts = {}) {
|
|
12183
13188
|
const r = new Rows;
|
|
@@ -12194,7 +13199,7 @@ function formatProblemList(l, opts = {}) {
|
|
|
12194
13199
|
return r.render(opts.form);
|
|
12195
13200
|
}
|
|
12196
13201
|
if (c.count)
|
|
12197
|
-
r.both(`${b}: ${
|
|
13202
|
+
r.both(`${b}: ${problemCountsText(c, b === l.baseline_branch ? "main’s" : "this branch’s")}`);
|
|
12198
13203
|
else if (read)
|
|
12199
13204
|
r.both(`${b}: no problem over budget in the newest run set (${read} journey × phone read)`);
|
|
12200
13205
|
else
|
|
@@ -12202,10 +13207,12 @@ function formatProblemList(l, opts = {}) {
|
|
|
12202
13207
|
r.both(l.note);
|
|
12203
13208
|
if (read && unread)
|
|
12204
13209
|
r.both(`${unreadWords(unread)}: ${staleResult(unread)}, so nothing is known about ${unread === 1 ? "its" : "their"} problems.`);
|
|
13210
|
+
if (c.stale_lanes)
|
|
13211
|
+
r.both(`${staleWords(c.stale_lanes)}: ${c.stale_lanes === 1 ? "its" : "their"} problems are listed, each marked “${STALE_MARK}”.`);
|
|
12205
13212
|
l.problems.forEach((p, i) => {
|
|
12206
13213
|
const keep = i < SHORT_PLACES;
|
|
12207
13214
|
r.add(keep, "", `${l.offset + i + 1}. ${p.text}: ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
|
|
12208
|
-
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}`);
|
|
13215
|
+
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}` : ""}`);
|
|
12209
13216
|
const top = p.top_components[0];
|
|
12210
13217
|
const topName = componentReadNames(p.top_components)[0];
|
|
12211
13218
|
const detail = [p.work, top ? `heaviest ${topName}${opts.verbose && top.key ? ` (${top.key})` : ""} ${top.self_text}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
|
|
@@ -12228,76 +13235,92 @@ function formatProblemList(l, opts = {}) {
|
|
|
12228
13235
|
}
|
|
12229
13236
|
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";
|
|
12230
13237
|
function formatCompare(r, opts = {}) {
|
|
12231
|
-
const
|
|
12232
|
-
|
|
12233
|
-
`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 })}`,
|
|
12234
|
-
`${r.same_journey ? "Same journey" : "Different journeys"}, ${r.same_phone ? "same phone" : "different phones"}. ${r.note}`
|
|
12235
|
-
];
|
|
13238
|
+
const rows = new Rows;
|
|
13239
|
+
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}`);
|
|
12236
13240
|
if (r.setup_differs.length)
|
|
12237
|
-
|
|
13241
|
+
rows.both(`Measured differently: ${r.setup_differs.map((d) => `${d.field} ${JSON.stringify(d.a)} → ${JSON.stringify(d.b)}`).join(", ")}`);
|
|
12238
13242
|
for (const w of r.windows) {
|
|
12239
13243
|
const ms = w.metrics.filter((m) => m.a !== null || m.b !== null);
|
|
12240
13244
|
const c = w.component_page ?? w.components;
|
|
12241
13245
|
if (!ms.length && !c)
|
|
12242
13246
|
continue;
|
|
12243
|
-
|
|
13247
|
+
rows.both("", `${w.window_name}${opts.verbose ? ` (${w.window})` : ""}`);
|
|
12244
13248
|
const singles = [...c?.added ?? [], ...c?.removed ?? [], ...w.component_page?.uncompared?.a ?? [], ...w.component_page?.uncompared?.b ?? []];
|
|
12245
13249
|
const components = [...c?.rows ?? [], ...singles];
|
|
12246
13250
|
const names = componentReadNames(components.map((row) => ({ key: row.key, name: row.component })));
|
|
12247
13251
|
const shown = new Map(components.map((row, i) => [row.key, names[i]]));
|
|
12248
13252
|
for (const row of c?.rows ?? [])
|
|
12249
|
-
|
|
12250
|
-
for (const [label,
|
|
13253
|
+
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})` : ""}`);
|
|
13254
|
+
for (const [label, list] of [
|
|
12251
13255
|
[READ_WORDS.onlyB, c?.added ?? []],
|
|
12252
13256
|
[READ_WORDS.onlyA, c?.removed ?? []]
|
|
12253
13257
|
]) {
|
|
12254
|
-
for (const row of
|
|
12255
|
-
|
|
13258
|
+
for (const row of list)
|
|
13259
|
+
rows.full(` ${shown.get(row.key)}: ${label}; ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12256
13260
|
}
|
|
12257
13261
|
const page = w.component_page;
|
|
12258
13262
|
if (page) {
|
|
12259
13263
|
for (const side of ["a", "b"]) {
|
|
12260
13264
|
if (!page.measured[side])
|
|
12261
|
-
|
|
13265
|
+
rows.full(` ${notMeasuredText(side)}`);
|
|
12262
13266
|
for (const row of page.uncompared?.[side] ?? [])
|
|
12263
|
-
|
|
13267
|
+
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})` : ""}`);
|
|
12264
13268
|
}
|
|
12265
|
-
|
|
13269
|
+
rows.full(` ${componentPageText(page.page, components.length)}`);
|
|
12266
13270
|
} else if (c?.totals)
|
|
12267
|
-
|
|
13271
|
+
rows.full(` ${componentListsText(c.totals, { rows: c.rows.length, added: c.added?.length ?? 0, removed: c.removed?.length ?? 0 })}`);
|
|
12268
13272
|
if (c?.omissions)
|
|
12269
13273
|
for (const side of ["a", "b"]) {
|
|
12270
13274
|
if (page && !page.measured[side])
|
|
12271
13275
|
continue;
|
|
12272
13276
|
const o = c.omissions[side];
|
|
12273
13277
|
if (o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0)
|
|
12274
|
-
|
|
13278
|
+
rows.full(` ${omissionText({ run_id: side === "a" ? READ_WORDS.runA : READ_WORDS.runB, ...o })}`);
|
|
12275
13279
|
}
|
|
13280
|
+
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));
|
|
12276
13281
|
for (const m of ms)
|
|
12277
|
-
|
|
13282
|
+
rows.add(top.has(m), ` ${m.label.padEnd(28)}${m.a_text.padStart(10)} → ${m.b_text}`);
|
|
12278
13283
|
}
|
|
12279
|
-
|
|
12280
|
-
|
|
13284
|
+
const p = r.windows_page;
|
|
13285
|
+
if (p) {
|
|
13286
|
+
const order = p.order === "moved" ? ", the ones whose time moved most first" : "";
|
|
13287
|
+
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`;
|
|
13288
|
+
rows.both("", p.next_offset !== null ? `${shownText}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).` : `${shownText}.`);
|
|
13289
|
+
}
|
|
13290
|
+
return rows.render(opts.form, ["the component lines", `the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`]);
|
|
12281
13291
|
}
|
|
12282
13292
|
function formatHistory(h, opts = {}) {
|
|
12283
13293
|
const r = new Rows;
|
|
12284
13294
|
r.both(`${h.window_name}${opts.verbose ? ` (${h.window})` : ""} in ${h.journey_title ?? h.journey} on ${h.branch}: ${h.points.length ? `${plural2(h.points.length, "run")}, oldest first` : h.note ?? "no runs"}`);
|
|
12285
13295
|
const ms = h.measures.slice(0, 4);
|
|
13296
|
+
const renamed = h.renamed;
|
|
13297
|
+
const renamedAt = renamed ? h.points.findIndex((p) => p.run_id === renamed.from_run_id) : -1;
|
|
13298
|
+
const renamedShown = renamedAt >= h.points.length - SHORT_POINTS;
|
|
12286
13299
|
if (h.points.length) {
|
|
12287
13300
|
r.both(` ${"when".padEnd(17)}${ms.map((m) => (m.label.length > 22 ? `${m.label.slice(0, 21)}…` : m.label).padStart(24)).join("")} commit`);
|
|
12288
13301
|
h.points.forEach((p, i) => {
|
|
12289
|
-
|
|
13302
|
+
if (renamed && i === renamedAt) {
|
|
13303
|
+
const full = `${placeNamesChangedTitle(renamed.sdk)}: ${placeWasText(renamed.legacy_window)}`;
|
|
13304
|
+
if (renamedShown)
|
|
13305
|
+
r.twin(full, `${placeChartMarker(sdkTag(renamed.sdk))}: ${placeWasText(renamed.legacy_window)}`);
|
|
13306
|
+
else
|
|
13307
|
+
r.full(full);
|
|
13308
|
+
}
|
|
13309
|
+
r.add(i >= h.points.length - SHORT_POINTS, ` ${(p.at ?? "").slice(0, 16).replace("T", " ").padEnd(17)}${ms.map((m) => fieldValueText(m.metric, p.values[m.metric]).padStart(24)).join("")} ${commitText(p.commit)}${opts.verbose && p.window ? ` (${p.window})` : ""}`);
|
|
12290
13310
|
});
|
|
12291
13311
|
}
|
|
12292
13312
|
if (h.findings.length)
|
|
12293
13313
|
r.both("", "Findings on this window:", ...h.findings.map((f) => ` ${f.answer.headline} — ${f.where.branch} (${f.id})`));
|
|
12294
13314
|
const older = Math.max(0, h.points.length - SHORT_POINTS);
|
|
12295
|
-
return r.render(opts.form,
|
|
13315
|
+
return r.render(opts.form, [
|
|
13316
|
+
...older ? [plural2(older, "older run")] : [],
|
|
13317
|
+
...renamed && renamedAt >= 0 && !renamedShown ? [placeNamesChangedOmitted(sdkTag(renamed.sdk))] : []
|
|
13318
|
+
]);
|
|
12296
13319
|
}
|
|
12297
13320
|
function formatCommits(c) {
|
|
12298
13321
|
return [
|
|
12299
|
-
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) on ${c.branch}, oldest first:`,
|
|
12300
|
-
...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)}` : ""}`),
|
|
13322
|
+
`${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:`,
|
|
13323
|
+
...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)}` : ""}`),
|
|
12301
13324
|
c.note
|
|
12302
13325
|
].join(`
|
|
12303
13326
|
`);
|
|
@@ -12333,7 +13356,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
12333
13356
|
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);
|
|
12334
13357
|
const by = p.filters.measure;
|
|
12335
13358
|
const byLabel = p.places.flatMap((x) => x.measures).find((m) => m.measure === by)?.label ?? by;
|
|
12336
|
-
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` : ""}.`, "");
|
|
13359
|
+
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)}` : ""}.`, "");
|
|
12337
13360
|
let componentLines = 0;
|
|
12338
13361
|
p.places.forEach((place, i) => {
|
|
12339
13362
|
const m = place.measures.find((x) => x.measure === by);
|
|
@@ -12406,13 +13429,13 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12406
13429
|
if (!s.places)
|
|
12407
13430
|
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
12408
13431
|
else
|
|
12409
|
-
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)}` : ""}`);
|
|
13432
|
+
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)}` : ""}`);
|
|
12410
13433
|
if (s.waiting_sessions)
|
|
12411
13434
|
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
12412
13435
|
if (s.memory)
|
|
12413
13436
|
r.add(k, ` Memory: ${MAIN_TIER_WORDS[s.memory.tier]}, ${s.memory.median} MB (n ${s.memory.n})${s.memory.cant_reason ? `; ${mainCantWords(s.memory.cant_reason, baseline)}` : ""}`);
|
|
12414
13437
|
for (const v of h.versions.filter((v) => v.from_run_set_id === s.run_set_id))
|
|
12415
|
-
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}
|
|
13438
|
+
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}`, ...renamedReleaseLines(v.renamed));
|
|
12416
13439
|
for (const c of h.changes.filter((c) => c.from_run_set_id === s.run_set_id)) {
|
|
12417
13440
|
r.add(s === latest, ` Main change: ${windowQualified(c.place)}${opts.verbose ? ` (${c.place}; ${c.ref})` : ""} ${storedRefWords(c.ref, h.changes.filter((x) => x.place === c.place && x.from_run_set_id === c.from_run_set_id).map((x) => x.ref))}, ${c.before_median ?? "–"} → ${c.after_median ?? "–"}`);
|
|
12418
13441
|
if (k && s !== latest)
|
|
@@ -12425,15 +13448,36 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12425
13448
|
...otherChanges ? [plural2(otherChanges, "main change") + " of the other recent sessions"] : []
|
|
12426
13449
|
]);
|
|
12427
13450
|
}
|
|
13451
|
+
function formatMainChanges(m) {
|
|
13452
|
+
const r = new Rows().both("Main changes: confirmed level shifts of main, newest first, with the commits from the session before to the first session at the new level.");
|
|
13453
|
+
if (!m.changes.length)
|
|
13454
|
+
r.both("No main changes recorded for this journey and phone.");
|
|
13455
|
+
const short = (sha) => sha?.slice(0, 7) ?? "–";
|
|
13456
|
+
for (const c of m.changes) {
|
|
13457
|
+
r.both(`${when(c.at)} ${windowQualified(c.place)} ${storedRefWords(c.measure, m.changes.filter((x) => x.place === c.place && x.at === c.at).map((x) => x.measure))}, ${c.from ?? "–"} → ${c.to ?? "–"}, ${MAIN_TIER_WORDS[c.direction]}`);
|
|
13458
|
+
r.both(` Commits ${short(c.commits[0])} → ${short(c.commits[1])}`);
|
|
13459
|
+
if (c.prs === null)
|
|
13460
|
+
r.both(" PRs not known: no stored merges reach the session before (an older agent, a shallow clone, or no session before)");
|
|
13461
|
+
else if (!c.prs.length)
|
|
13462
|
+
r.both(" No PRs in this range: both sessions ran on the same commit");
|
|
13463
|
+
for (const p of c.prs ?? [])
|
|
13464
|
+
r.both(` ${p.number === null ? `${short(p.sha)} ${p.title} (no PR)` : `#${p.number} ${p.title}`}`);
|
|
13465
|
+
}
|
|
13466
|
+
return r.render("full");
|
|
13467
|
+
}
|
|
12428
13468
|
function formatMainSessionPlaces(p, opts = {}) {
|
|
12429
13469
|
const r = new Rows;
|
|
12430
13470
|
if (p.status !== "ok")
|
|
12431
13471
|
return r.both(`${MAIN_SUMMARY_PENDING}${p.summary_reason ? ` (${p.summary_reason})` : ""}`).render(opts.form);
|
|
12432
13472
|
const baselineSessions = p.baseline_sessions;
|
|
12433
13473
|
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
13474
|
+
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);
|
|
13475
|
+
const places = p.places.filter((x) => kept.has(x));
|
|
13476
|
+
for (const x of places)
|
|
13477
|
+
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)}` : ""}`);
|
|
13478
|
+
if (places.length < p.places.length)
|
|
13479
|
+
r.both(` ${foldedTwinsText(p.places.length - places.length)}`);
|
|
13480
|
+
const inNorm = places.filter((x) => x.tier === "same").length;
|
|
12437
13481
|
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
12438
13482
|
}
|
|
12439
13483
|
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
@@ -12443,7 +13487,7 @@ function formatMainVsPrevious(m, opts = {}) {
|
|
|
12443
13487
|
if (m.cut === "unknown")
|
|
12444
13488
|
r.both("History restart: unknown; current summaries unavailable.");
|
|
12445
13489
|
else if (m.cut)
|
|
12446
|
-
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart
|
|
13490
|
+
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart.`, ...renamedReleaseLines(m.cut.renamed));
|
|
12447
13491
|
if (m.not_computed.length)
|
|
12448
13492
|
r.both(`not_computed: ${m.not_computed.join(", ")}`);
|
|
12449
13493
|
if (m.stale.length)
|
|
@@ -12456,6 +13500,28 @@ function formatMainVsPrevious(m, opts = {}) {
|
|
|
12456
13500
|
const smaller = m.places.filter((p, i) => i >= SHORT_PLACES && p.status === "compared").length;
|
|
12457
13501
|
return r.render(opts.form, smaller ? [plural2(smaller, "smaller mover")] : []);
|
|
12458
13502
|
}
|
|
13503
|
+
function renamedReleaseLines(renamed) {
|
|
13504
|
+
if (!renamed)
|
|
13505
|
+
return [];
|
|
13506
|
+
return [...renamed.places.length ? [` ${placeNamesChangedTitle(renamed.sdk)}`] : [], ...renamed.dimensions.includes("component") ? [` ${componentNotJoinedText(renamed.sdk)}`] : []];
|
|
13507
|
+
}
|
|
13508
|
+
function renamedLines(renamed, places) {
|
|
13509
|
+
if (!renamed)
|
|
13510
|
+
return [];
|
|
13511
|
+
const asked = places.filter((p) => p.legacy_window !== undefined);
|
|
13512
|
+
const windows = [...new Map(asked.map((p) => [p.window, p.legacy_window ?? null])).entries()];
|
|
13513
|
+
const component = renamed.dimensions.includes("component") && places.some((p) => p.ref.startsWith("b:component:"));
|
|
13514
|
+
return [
|
|
13515
|
+
...windows.length ? [` ${placeNamesChangedTitle(renamed.sdk)}`, ...windows.map(([w, old]) => ` ${windowQualified(w)}: ${old ? placeWasText(old) : PLACE_NOT_JOINED_TEXT}`)] : [],
|
|
13516
|
+
...component ? [` ${componentNotJoinedText(renamed.sdk)}`] : []
|
|
13517
|
+
];
|
|
13518
|
+
}
|
|
13519
|
+
function storedValueText(ref, v, unit) {
|
|
13520
|
+
const rest = ref.slice(2);
|
|
13521
|
+
const field = ref.startsWith("b:") ? `${rest.slice(0, rest.lastIndexOf(":"))}.${rest.slice(rest.lastIndexOf(":") + 1)}` : rest;
|
|
13522
|
+
const text = fieldValueText(field, v);
|
|
13523
|
+
return unit && !/[^\d.,\s-]/.test(text) ? `${text} ${unit}` : text;
|
|
13524
|
+
}
|
|
12459
13525
|
function formatPlaceTrend(t, opts = {}) {
|
|
12460
13526
|
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
12461
13527
|
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
@@ -12467,6 +13533,8 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12467
13533
|
r.both(`History restarts unknown: ${session.run_set_id} (no current main summary in the history window)`);
|
|
12468
13534
|
for (const restart of t.restarts.filter((x) => x.from_run_set_id === session.run_set_id)) {
|
|
12469
13535
|
r.add(k, `History restarts: ${restart.label} (${restart.cuts.join(", ")})`);
|
|
13536
|
+
for (const line of renamedLines(restart.renamed, t.places))
|
|
13537
|
+
r.add(k, line);
|
|
12470
13538
|
if (!k)
|
|
12471
13539
|
olderRestarts++;
|
|
12472
13540
|
}
|
|
@@ -12474,10 +13542,10 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12474
13542
|
const value = place.sessions.find((s) => s.run_set_id === session.run_set_id);
|
|
12475
13543
|
if (!value)
|
|
12476
13544
|
continue;
|
|
12477
|
-
const
|
|
13545
|
+
const value_ = (v) => storedValueText(place.ref, v, place.unit);
|
|
12478
13546
|
if (!k)
|
|
12479
13547
|
olderSessions.add(session.run_set_id);
|
|
12480
|
-
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}
|
|
13548
|
+
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}`);
|
|
12481
13549
|
}
|
|
12482
13550
|
}
|
|
12483
13551
|
if (!t.sessions.length)
|
|
@@ -12534,6 +13602,8 @@ var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
|
12534
13602
|
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 });
|
|
12535
13603
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
12536
13604
|
var compareRuns = (t, a, b, page = {}) => get(t, "v1/compare", { a, b, ...page });
|
|
13605
|
+
var SHORT_COMPARE_PLACES = 10;
|
|
13606
|
+
var compareQuery = (short, page) => short ? { order: "moved", limit: SHORT_COMPARE_PLACES, ...page } : page;
|
|
12537
13607
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
12538
13608
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
12539
13609
|
var layers = (t) => get(t, "v1/layers");
|
|
@@ -12554,6 +13624,7 @@ var sessionPlaces = (t, f) => get(t, "v1/sessions/places", {
|
|
|
12554
13624
|
});
|
|
12555
13625
|
var SESSION_SORTS = ["median", "p90", "mad"];
|
|
12556
13626
|
var mainSessions = (t, q) => get(t, "v1/main/sessions", { ...q });
|
|
13627
|
+
var mainChanges = (t, q) => get(t, "v1/main/changes", { ...q });
|
|
12557
13628
|
var mainSessionPlaces = (t, runSetId, q) => get(t, `v1/main/sessions/${encodeURIComponent(runSetId)}/places`, { ...q });
|
|
12558
13629
|
var mainVsPrevious = (t, q) => {
|
|
12559
13630
|
const { run_set, ...query } = q;
|
|
@@ -12567,12 +13638,13 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
12567
13638
|
problems --branch NAME | --main [--journey KEY] [--phone MODEL] [--limit N] [--offset N] [--short] [--json]
|
|
12568
13639
|
(the single problems "Over budget" / "Far over budget" in the branch's own runs, whatever main does: not a regression, never blocking)
|
|
12569
13640
|
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
12570
|
-
compare <runA> <runB> [--json]
|
|
13641
|
+
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)
|
|
12571
13642
|
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
12572
13643
|
commits <from> [<to>] [--branch NAME] [--json] (measured commits between two run sets or commit hashes)
|
|
12573
13644
|
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]
|
|
12574
13645
|
(every place of one session from its stored statistics: no verdict; the newest closed run set of the journey when --run-set is not given)
|
|
12575
13646
|
main --journey KEY --phone MODEL [--sessions N | --session ID] [--vs-previous] [--measure REF] [--limit N] [--offset N] [--short] [--json]
|
|
13647
|
+
main --journey KEY --phone MODEL --changes [--limit N] [--json] (main's confirmed changes, newest first: commit range from the session before to the first session at the new level, and the PRs merged in it)
|
|
12576
13648
|
layers [--json]
|
|
12577
13649
|
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
12578
13650
|
(the words after the journey are one window name; more windows with repeated --window)
|
|
@@ -12582,7 +13654,7 @@ ${READ_USAGE.detailFlag}
|
|
|
12582
13654
|
--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)
|
|
12583
13655
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
12584
13656
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
12585
|
-
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "history", "places", "main", "trend"]);
|
|
13657
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "places", "main", "trend"]);
|
|
12586
13658
|
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
12587
13659
|
async function findingsCommand(cmd, argv, rt) {
|
|
12588
13660
|
const args = (() => {
|
|
@@ -12602,6 +13674,7 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12602
13674
|
state: { type: "string" },
|
|
12603
13675
|
session: { type: "string" },
|
|
12604
13676
|
"vs-previous": { type: "boolean" },
|
|
13677
|
+
changes: { type: "boolean" },
|
|
12605
13678
|
limit: { type: "string" },
|
|
12606
13679
|
offset: { type: "string" },
|
|
12607
13680
|
"run-set": { type: "string" },
|
|
@@ -12662,6 +13735,11 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12662
13735
|
if (sessions !== undefined && (o.session || o["vs-previous"]))
|
|
12663
13736
|
throw new UsageError(["--sessions is for the session list"]);
|
|
12664
13737
|
const lane = { journey: o.journey, phone: o.phone };
|
|
13738
|
+
if (o.changes) {
|
|
13739
|
+
if (sessions !== undefined || o.session || o["vs-previous"] || o.measure || o.short || offset !== undefined)
|
|
13740
|
+
throw new UsageError(["--changes takes only --journey, --phone, --limit and --json"]);
|
|
13741
|
+
return out(await mainChanges(t, { ...lane, ...limit ? { limit } : {} }), formatMainChanges);
|
|
13742
|
+
}
|
|
12665
13743
|
if (o["vs-previous"])
|
|
12666
13744
|
return out(await mainVsPrevious(t, { ...lane, run_set: o.session, measure: o.measure, limit, offset }), formatMainVsPrevious);
|
|
12667
13745
|
if (o.session)
|
|
@@ -12715,7 +13793,8 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12715
13793
|
const size = o["top-components"] === undefined ? 12 : Number(o["top-components"]);
|
|
12716
13794
|
if (!Number.isInteger(size) || size < 1 || size > 200)
|
|
12717
13795
|
throw new UsageError([READ_USAGE.compareTop]);
|
|
12718
|
-
const
|
|
13796
|
+
const components = page || o.all || o["top-components"] !== undefined ? { component_offset: ((page ?? 1) - 1) * size, component_limit: size } : {};
|
|
13797
|
+
const query = compareQuery(o.short === true, { ...components, ...limit ? { limit } : {}, ...offset !== undefined ? { offset } : {} });
|
|
12719
13798
|
const compared = await compareRuns(t, p[0], p[1], query);
|
|
12720
13799
|
return out(o.all ? await allCompare(t, compared, size) : compared, formatCompare);
|
|
12721
13800
|
}
|
|
@@ -12868,7 +13947,7 @@ var USAGE = `keepsnappy <command>
|
|
|
12868
13947
|
[--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
12869
13948
|
[--no-markers] [-- <e2e command...>]
|
|
12870
13949
|
(--fail-on-worse: exit 3 only when the verdict computed on that run set is Worse and blocking; closing never computes one,
|
|
12871
|
-
so with no computed verdict it checks nothing: "
|
|
13950
|
+
so with no computed verdict it checks nothing: "${UNCHECKED_GATE}", exit 0)
|
|
12872
13951
|
measure --repeat N [--arms A=<app|bundle>,B=<app|bundle>] [--arm-env 'A:KEY=VAL' ...] [--bundle-id ID] [--platform ios-sim|ios-device]
|
|
12873
13952
|
[--device UDID] [--out DIR] [--timeout SEC] [--ri-capture observations.ndjson] [--mem-window NAME]
|
|
12874
13953
|
[--clock-offset-ms N] [--no-markers]
|
|
@@ -12898,7 +13977,7 @@ async function bundleIdOf(deps, target, explicit) {
|
|
|
12898
13977
|
if (explicit)
|
|
12899
13978
|
return explicit;
|
|
12900
13979
|
if (!target)
|
|
12901
|
-
throw new
|
|
13980
|
+
throw new ConfigError(["give --bundle-id (or an arm whose target is a bundle id or .app)"]);
|
|
12902
13981
|
if (!isBuildPath(target))
|
|
12903
13982
|
return target;
|
|
12904
13983
|
const r = await deps.spawner.run("plutil", ["-extract", "CFBundleIdentifier", "raw", `${target.replace(/\/$/, "")}/Info.plist`]);
|