@keepsnappy/agent 0.1.0-alpha.17 → 0.1.0-alpha.18
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 +5 -5
- package/dist/appium-driver.js +8 -4
- package/dist/keepsnappy.js +1262 -413
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -2198,6 +2198,7 @@ function isComponentConfiguration(value) {
|
|
|
2198
2198
|
}
|
|
2199
2199
|
|
|
2200
2200
|
// packages/facts/dist/src/places.js
|
|
2201
|
+
var COMMIT_IDENTITY_KINDS = ["screen", "press", "drag", "other"];
|
|
2201
2202
|
var placeWithoutVisit = (id) => placeRef(id).stem.replace(/#(\d+|repeat)$/, "");
|
|
2202
2203
|
var AFTER_3S = "+3s";
|
|
2203
2204
|
var kindOf = (stem) => {
|
|
@@ -2323,7 +2324,7 @@ function withLaunchNames(windows) {
|
|
|
2323
2324
|
}
|
|
2324
2325
|
|
|
2325
2326
|
// packages/derive/dist/src/model.js
|
|
2326
|
-
var DERIVATION_VERSION =
|
|
2327
|
+
var DERIVATION_VERSION = 28;
|
|
2327
2328
|
// packages/stats/dist/src/core/rng.js
|
|
2328
2329
|
function splitmix32(seed) {
|
|
2329
2330
|
let s = seed >>> 0;
|
|
@@ -3180,6 +3181,44 @@ function estimateClockOffset(markers, windows, byOrder = false) {
|
|
|
3180
3181
|
const negatives = gaps.filter((g) => g < 0);
|
|
3181
3182
|
return negatives.length * 2 > starts.length ? Math.min(...negatives) : 0;
|
|
3182
3183
|
}
|
|
3184
|
+
var ulp = (x) => x === 0 ? Number.MIN_VALUE : 2 ** (Math.floor(Math.log2(Math.abs(x))) - 52);
|
|
3185
|
+
function fitClockOffset(markers, windows, offset, errorMs) {
|
|
3186
|
+
if (!(errorMs > 0))
|
|
3187
|
+
return offset;
|
|
3188
|
+
const inputs = windows.map(inputWall).filter((x) => x !== undefined).sort((a, b) => a - b);
|
|
3189
|
+
const ss = spans(markers, 0).sort((a, b) => a.markerStart - b.markerStart);
|
|
3190
|
+
const score = (o) => {
|
|
3191
|
+
let started = 0;
|
|
3192
|
+
let inside = 0;
|
|
3193
|
+
for (const [i, s] of ss.entries()) {
|
|
3194
|
+
const next = ss[i + 1]?.markerStart;
|
|
3195
|
+
const t = inputs.find((x) => x >= s.markerStart + o);
|
|
3196
|
+
if (t === undefined || next !== undefined && t >= next + o)
|
|
3197
|
+
continue;
|
|
3198
|
+
started += 1;
|
|
3199
|
+
const end = Math.min(s.markerEnd ?? Infinity, next ?? Infinity) + o;
|
|
3200
|
+
inside += inputs.filter((x) => x >= s.markerStart + o && x < end).length;
|
|
3201
|
+
}
|
|
3202
|
+
return [started, inside];
|
|
3203
|
+
};
|
|
3204
|
+
let best = offset;
|
|
3205
|
+
let top = score(offset);
|
|
3206
|
+
for (const [i, s] of ss.entries()) {
|
|
3207
|
+
const prevEnd = ss[i - 1]?.markerEnd;
|
|
3208
|
+
for (const t of inputs) {
|
|
3209
|
+
if (prevEnd !== undefined && t >= prevEnd + offset && t - (prevEnd + offset) <= s.markerStart + offset - t)
|
|
3210
|
+
continue;
|
|
3211
|
+
const o = t - s.markerStart - ulp(t);
|
|
3212
|
+
if (o >= offset || o < offset - errorMs)
|
|
3213
|
+
continue;
|
|
3214
|
+
const sc = score(o);
|
|
3215
|
+
const better = sc[0] > top[0] || sc[0] === top[0] && (sc[1] > top[1] || sc[1] === top[1] && o > best);
|
|
3216
|
+
if (better)
|
|
3217
|
+
[best, top] = [o, sc];
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
return best;
|
|
3221
|
+
}
|
|
3183
3222
|
function spans(markers, offset) {
|
|
3184
3223
|
const sorted = [...markers].sort((a, b) => a.t_host - b.t_host);
|
|
3185
3224
|
const out = [];
|
|
@@ -3206,7 +3245,8 @@ function ownWindowEnd(windows, t) {
|
|
|
3206
3245
|
}
|
|
3207
3246
|
function alignSteps(markers, windows, opts = {}) {
|
|
3208
3247
|
const byOrder = opts.byOrder === true;
|
|
3209
|
-
const
|
|
3248
|
+
const given = opts.clockOffsetMs;
|
|
3249
|
+
const offset = given === undefined ? estimateClockOffset(markers, windows, byOrder) : byOrder && opts.clockErrorMs ? fitClockOffset(markers, windows, given, opts.clockErrorMs) : given;
|
|
3210
3250
|
const ss = spans(markers, offset).sort((a, b) => a.markerStart - b.markerStart);
|
|
3211
3251
|
const inputs = windows.map(inputWall).filter((x) => x !== undefined).sort((a, b) => a - b);
|
|
3212
3252
|
const starts = ss.map((s, i) => {
|
|
@@ -3256,7 +3296,7 @@ function clusters(windows) {
|
|
|
3256
3296
|
}
|
|
3257
3297
|
return out;
|
|
3258
3298
|
}
|
|
3259
|
-
function
|
|
3299
|
+
function processSegments(windows, version) {
|
|
3260
3300
|
const order = windows.map((w, i) => ({ w, i })).sort((a, b) => a.w.endWall - b.w.endWall || a.i - b.i);
|
|
3261
3301
|
const segment = new Map;
|
|
3262
3302
|
let seg = 0;
|
|
@@ -3274,24 +3314,35 @@ function onAppClock(windows, version) {
|
|
|
3274
3314
|
prevId = id;
|
|
3275
3315
|
segment.set(w, seg);
|
|
3276
3316
|
}
|
|
3317
|
+
return segment;
|
|
3318
|
+
}
|
|
3319
|
+
function onAppClock(windows, version) {
|
|
3320
|
+
const process2 = processSegments(windows, version);
|
|
3321
|
+
const step = (w) => version >= 28 ? w.clockStep : undefined;
|
|
3277
3322
|
const offset = new Map;
|
|
3278
|
-
for (const [w,
|
|
3323
|
+
for (const [w, seg] of process2) {
|
|
3279
3324
|
const t1 = num(w.raw.t1);
|
|
3280
3325
|
if (t1 === undefined)
|
|
3281
3326
|
continue;
|
|
3282
|
-
const
|
|
3283
|
-
if (
|
|
3284
|
-
|
|
3327
|
+
const st = step(w);
|
|
3328
|
+
if (st && w.endWall - t1 < st.floor)
|
|
3329
|
+
continue;
|
|
3330
|
+
const o = w.endWall - t1 - (st?.end ?? 0);
|
|
3331
|
+
if (!offset.has(seg) || o < offset.get(seg))
|
|
3332
|
+
offset.set(seg, o);
|
|
3285
3333
|
}
|
|
3286
3334
|
return windows.map((w) => {
|
|
3287
|
-
const
|
|
3288
|
-
const o = s === undefined ? undefined : offset.get(s);
|
|
3289
|
-
const t0 = num(w.raw.t0);
|
|
3335
|
+
const seg = process2.get(w);
|
|
3290
3336
|
const t1 = num(w.raw.t1);
|
|
3337
|
+
const own = seg === undefined || t1 === undefined ? undefined : w.endWall - t1 - (step(w)?.end ?? 0);
|
|
3338
|
+
const o = seg === undefined ? undefined : offset.get(seg) ?? own;
|
|
3339
|
+
const t0 = num(w.raw.t0);
|
|
3291
3340
|
if (o === undefined || t0 === undefined || t1 === undefined)
|
|
3292
3341
|
return w;
|
|
3342
|
+
const st = step(w);
|
|
3343
|
+
const [atStart, atEnd] = [st?.start ?? 0, st?.end ?? 0];
|
|
3293
3344
|
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 } };
|
|
3345
|
+
return { ...w, startWall: t0 + o + atStart, endWall: t1 + o + atEnd, ...touch === undefined ? {} : { touchWall: touch + o + atStart } };
|
|
3295
3346
|
});
|
|
3296
3347
|
}
|
|
3297
3348
|
|
|
@@ -3683,7 +3734,7 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3683
3734
|
const kind = placeRef(key).kind;
|
|
3684
3735
|
const active = occ.filter((o) => !o.idle);
|
|
3685
3736
|
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`) &&
|
|
3737
|
+
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && COMMIT_IDENTITY_KINDS.includes(kind) && selected?.slowestCommit;
|
|
3687
3738
|
const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
|
|
3688
3739
|
profiles.push({ ...profile, ...identity ? { slowestCommit: identity } : {}, ...openedBy ? { openedBy } : {} });
|
|
3689
3740
|
}
|
|
@@ -3829,7 +3880,7 @@ function withActionWindows(windows, steps = [], version) {
|
|
|
3829
3880
|
}
|
|
3830
3881
|
const durMs = w.raw.endReason === "timeout" ? undefined : num3(w.raw.durMs);
|
|
3831
3882
|
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 };
|
|
3883
|
+
return { name: w.name, startWall: w.startWall, endWall: w.endWall, raw, ...w.clockStep === undefined ? {} : { clockStep: w.clockStep } };
|
|
3833
3884
|
});
|
|
3834
3885
|
}
|
|
3835
3886
|
function actionMetrics(windows, steps = [], version) {
|
|
@@ -4942,7 +4993,7 @@ function keyFirstOpens(windows) {
|
|
|
4942
4993
|
}
|
|
4943
4994
|
|
|
4944
4995
|
// 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];
|
|
4996
|
+
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
4997
|
var windowsForVersion = (windows, version = DERIVATION_VERSION) => version >= 22 ? keyFirstOpens(windows) : windows;
|
|
4947
4998
|
function stepWindowsFor(windows, version = DERIVATION_VERSION) {
|
|
4948
4999
|
return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version);
|
|
@@ -4978,7 +5029,7 @@ function profileOptions(windows, version, processWindows = windows) {
|
|
|
4978
5029
|
return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, openedBy };
|
|
4979
5030
|
}
|
|
4980
5031
|
function deriveRun(input) {
|
|
4981
|
-
const { run, markers = [], ri, clockOffsetMs, samples = [], memWindow } = input;
|
|
5032
|
+
const { run, markers = [], ri, clockOffsetMs, clockErrorMs, samples = [], memWindow } = input;
|
|
4982
5033
|
const version = input.derivationVersion ?? DERIVATION_VERSION;
|
|
4983
5034
|
if (!DERIVATION_VERSIONS.includes(version))
|
|
4984
5035
|
throw new Error(`unknown derivation version ${String(version)}`);
|
|
@@ -4987,7 +5038,7 @@ function deriveRun(input) {
|
|
|
4987
5038
|
const cpu = input.cpu && renamed.size ? new Map([...input.cpu].map(([k, v]) => [renamed.get(k) ?? k, v])) : input.cpu;
|
|
4988
5039
|
const background = version >= 16 ? named.filter(isBackgroundWindow) : [];
|
|
4989
5040
|
const sdkWindows = stepWindowsFor(input.windows, version);
|
|
4990
|
-
const off = clockOffsetMs === undefined ? {} : { clockOffsetMs };
|
|
5041
|
+
const off = { ...clockOffsetMs === undefined ? {} : { clockOffsetMs }, ...version >= 28 && clockErrorMs !== undefined ? { clockErrorMs } : {} };
|
|
4991
5042
|
const preSteps = version >= 11 && markers.length ? alignRunSteps(markers, sdkWindows, off, version) : [];
|
|
4992
5043
|
const windows = version >= 11 ? withActionWindows(sdkWindows, preSteps, version) : sdkWindows;
|
|
4993
5044
|
const steps = version >= 11 ? preSteps : alignRunSteps(markers, windows, clockOffsetMs === undefined ? {} : { clockOffsetMs }, version);
|
|
@@ -5150,100 +5201,6 @@ var markerEnv = (url, runId) => ({
|
|
|
5150
5201
|
MAESTRO_KEEPSNAPPY_RUN_ID: runId
|
|
5151
5202
|
});
|
|
5152
5203
|
|
|
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
5204
|
// packages/present/dist/src/capital.js
|
|
5248
5205
|
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5249
5206
|
|
|
@@ -5285,7 +5242,12 @@ var STATE_WORDS = {
|
|
|
5285
5242
|
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5286
5243
|
var isWorseState = (state) => state === "worse";
|
|
5287
5244
|
var NOT_BLOCKING = "not blocking";
|
|
5288
|
-
var badgedWord = (word, blocking) =>
|
|
5245
|
+
var badgedWord = (word, blocking) => `${word}${notBlockingBadge(word, blocking)}`;
|
|
5246
|
+
var notBlockingBadge = (word, blocking) => word === STATE_WORDS.worse && blocking === false ? ` (${NOT_BLOCKING})` : "";
|
|
5247
|
+
var BLOCKED_WORD = "Blocked";
|
|
5248
|
+
var NOT_COMPUTED_WORD = "Verdict not computed yet (computed on demand)";
|
|
5249
|
+
var NO_BASELINE_WORD = "Waiting for main";
|
|
5250
|
+
var NOT_JUDGED_WORD = "Not judged";
|
|
5289
5251
|
|
|
5290
5252
|
// packages/present/dist/src/values.js
|
|
5291
5253
|
var NO_VALUE = "–";
|
|
@@ -5314,17 +5276,23 @@ function roundTo(v, decimals) {
|
|
|
5314
5276
|
return Number(magnitude.toFixed(decimals)) * Math.sign(v);
|
|
5315
5277
|
return Number(`${Math.round(scaled)}e-${decimals}`) * Math.sign(v);
|
|
5316
5278
|
}
|
|
5279
|
+
function written(unit, decimals, v) {
|
|
5280
|
+
if (unit === "ms" && Math.abs(v) >= LONG_MS && Math.abs(v) < 1e9)
|
|
5281
|
+
return { n: roundTo(v / 1000, 1) + 0, seconds: true };
|
|
5282
|
+
return { n: roundTo(v * shownUnit(unit).scale, decimals) + 0, seconds: false };
|
|
5283
|
+
}
|
|
5317
5284
|
function format(unit, decimals, v) {
|
|
5318
5285
|
if (v === null || v === undefined || !Number.isFinite(v))
|
|
5319
5286
|
return NO_VALUE;
|
|
5287
|
+
const { n, seconds } = written(unit, decimals, v);
|
|
5288
|
+
if (seconds)
|
|
5289
|
+
return `${String(n)} s`;
|
|
5320
5290
|
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;
|
|
5291
|
+
return shown.unit ? `${String(n)} ${shown.unit}` : String(n);
|
|
5325
5292
|
}
|
|
5293
|
+
var defOf = (measure) => isMeasureId(measure) ? MEASURES[measure] : BREAKDOWN_MEASURES[measure];
|
|
5326
5294
|
function valueText(measure, v) {
|
|
5327
|
-
const def =
|
|
5295
|
+
const def = defOf(measure);
|
|
5328
5296
|
return format(def.unit, def.decimals, v);
|
|
5329
5297
|
}
|
|
5330
5298
|
var KEY_SEPARATOR = ":";
|
|
@@ -5370,6 +5338,14 @@ function notInRunRowsText(c, page) {
|
|
|
5370
5338
|
const them = c.screens + c.places === 1 ? "it" : "them";
|
|
5371
5339
|
return { lead: `${what} ${GONE_TAILS[page].notInRun}:`, rest: ` the ${page === "journey" ? "newest session" : "journey"} did not reach ${them}; not counted.` };
|
|
5372
5340
|
}
|
|
5341
|
+
var sdkTag = (version) => version.replace(/^\d+\.\d+\.\d+-/, "");
|
|
5342
|
+
var placeChartMarker = (tag) => `Place names changed with SDK ${tag}`;
|
|
5343
|
+
var placeNamesChangedOmitted = (tag) => `the line where place names changed (SDK ${tag})`;
|
|
5344
|
+
var componentChartMarker = (tag) => `SDK ${tag}: names changed, library time now counted`;
|
|
5345
|
+
var ownTimeOnlyBefore = (version) => `own time only, before SDK ${version}`;
|
|
5346
|
+
var placeWasText = (oldKey) => `before it this place was ${oldKey}`;
|
|
5347
|
+
var PLACE_NOT_JOINED_TEXT = "no recorded old name, its history starts here";
|
|
5348
|
+
var componentNotJoinedText = (sdk) => `${componentChartMarker(sdk)}; values before it were ${ownTimeOnlyBefore(sdk)}, not joined`;
|
|
5373
5349
|
var OUTSIDE_APP_NAME = "Outside app components";
|
|
5374
5350
|
var OTHER_RENDER_WORK_NAME = "Other render work";
|
|
5375
5351
|
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
@@ -5544,8 +5520,34 @@ var UNSTABLE_WORDS = {
|
|
|
5544
5520
|
line: "Main varies too much here to tell",
|
|
5545
5521
|
ratioTip: "1.0 = main’s run-to-run noise alone would pass the warning step about once in 100 comparisons.",
|
|
5546
5522
|
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."
|
|
5523
|
+
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.",
|
|
5524
|
+
hint: {
|
|
5525
|
+
work: "The work differs between runs or sessions ({count} {min}–{max}): likely the code or the test steps.",
|
|
5526
|
+
speed: "The same work ran at different speeds ({count} {n} in every run): likely the phone or the session.",
|
|
5527
|
+
unclear: "The stored facts don’t show whether the work or the speed varies."
|
|
5528
|
+
}
|
|
5548
5529
|
};
|
|
5530
|
+
function breakdownParts(ref) {
|
|
5531
|
+
const [kind, ...rest] = ref.split(":");
|
|
5532
|
+
return kind === "b" && rest.length >= 3 ? { key: rest.slice(1, -1).join(":"), measure: rest[rest.length - 1] } : null;
|
|
5533
|
+
}
|
|
5534
|
+
var countText = (n) => String(Math.round(n * 10) / 10);
|
|
5535
|
+
function countName(count, ref) {
|
|
5536
|
+
if (count !== "component.renders" && count !== "component.mounts")
|
|
5537
|
+
return MEASURES[count].short;
|
|
5538
|
+
const key = (ref ? breakdownParts(ref)?.key : undefined) ?? "component";
|
|
5539
|
+
return `${key} ${BREAKDOWN_MEASURES[count === "component.renders" ? "renders" : "mounts"].label}`;
|
|
5540
|
+
}
|
|
5541
|
+
function unstableHintLine(hint, ref) {
|
|
5542
|
+
if (hint.kind === "unclear" || hint.count === null || hint.range === null)
|
|
5543
|
+
return UNSTABLE_WORDS.hint.unclear;
|
|
5544
|
+
const count = countName(hint.count, ref);
|
|
5545
|
+
if (hint.kind === "work")
|
|
5546
|
+
return UNSTABLE_WORDS.hint.work.replace("{count}", count).replace("{min}", countText(hint.range[0])).replace("{max}", countText(hint.range[1]));
|
|
5547
|
+
return hint.median === null ? UNSTABLE_WORDS.hint.unclear : UNSTABLE_WORDS.hint.speed.replace("{count}", count).replace("{n}", countText(hint.median));
|
|
5548
|
+
}
|
|
5549
|
+
var unstableNoteWords = (u) => u.hint ? `${UNSTABLE_WORDS.note} ${unstableHintLine(u.hint, u.stabilityRef)}` : UNSTABLE_WORDS.note;
|
|
5550
|
+
var mainUnstableWords = (reason) => reason.kind === "noisy_main" ? unstableNoteWords(reason) : "";
|
|
5549
5551
|
|
|
5550
5552
|
// packages/present/dist/src/present.js
|
|
5551
5553
|
var ASPECT_WORDS = {
|
|
@@ -5684,6 +5686,10 @@ var REASON_WORDS = {
|
|
|
5684
5686
|
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5685
5687
|
};
|
|
5686
5688
|
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5689
|
+
var AT_MOST = "≤ ";
|
|
5690
|
+
var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
|
|
5691
|
+
var AT_LEAST = "≥ ";
|
|
5692
|
+
var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
|
|
5687
5693
|
// packages/present/dist/src/main-history.js
|
|
5688
5694
|
var MAIN_TIER_WORDS = {
|
|
5689
5695
|
same: "In norm",
|
|
@@ -5761,6 +5767,409 @@ function mainCutWords(v) {
|
|
|
5761
5767
|
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5762
5768
|
}
|
|
5763
5769
|
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5770
|
+
// packages/present/dist/src/verdict-section.js
|
|
5771
|
+
var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5772
|
+
var of = (n, total) => `${n.toLocaleString("en-US")} of ${places(total)}`;
|
|
5773
|
+
var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
|
|
5774
|
+
var VERDICT_SECTION_WORDS = {
|
|
5775
|
+
slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
|
|
5776
|
+
blocksMerge: "blocks the merge",
|
|
5777
|
+
faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
|
|
5778
|
+
mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
|
|
5779
|
+
unpublishedSdk: "ran an unpublished SDK build",
|
|
5780
|
+
noneMoved: (total, judged = total) => judged < total ? `No place moved out of its normal range (${judged.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} judged)` : `No place moved out of its normal range (${places(total)})`,
|
|
5781
|
+
noneMovedMeasured: (measured, same) => same ? `Nothing moved at ${of(same, same + measured)}; ${measured} measured differently` : `Nothing moved; ${measured} ${measured === 1 ? "place was" : "places were"} measured differently`,
|
|
5782
|
+
tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
|
|
5783
|
+
tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
|
|
5784
|
+
tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
|
|
5785
|
+
noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
|
|
5786
|
+
noisy: "Not judged: main’s own runs vary too much at every place here",
|
|
5787
|
+
versionMismatch: "Not compared: the sides were computed with different analysis versions",
|
|
5788
|
+
unconfirmed: (n) => `Can’t confirm ${places(n)}: a run has no value there`,
|
|
5789
|
+
notCompared: (reason) => `Not compared: ${reason}`,
|
|
5790
|
+
notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
|
|
5791
|
+
collecting: (collected, baseline, after) => after ? `Main is collecting a new baseline after ${after}: ${collected} of ${baseline} sessions` : `Main is collecting its baseline: ${collected} of ${baseline} sessions`,
|
|
5792
|
+
doesNotBlock: "doesn’t block",
|
|
5793
|
+
sides: {
|
|
5794
|
+
branch: { left: "Main", right: "This branch", rightShort: "Branch" },
|
|
5795
|
+
main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
|
|
5796
|
+
runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
|
|
5797
|
+
},
|
|
5798
|
+
branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
|
|
5799
|
+
inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
|
|
5800
|
+
whatChangedLabel: "What changed",
|
|
5801
|
+
forInformation: "for information",
|
|
5802
|
+
allFilter: "All",
|
|
5803
|
+
filter: {
|
|
5804
|
+
label: "Show places",
|
|
5805
|
+
choice: (tierWord) => upperFirst(tierWord),
|
|
5806
|
+
showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
|
|
5807
|
+
},
|
|
5808
|
+
countsLabel: "Places by result",
|
|
5809
|
+
links: {
|
|
5810
|
+
openChart: "Open chart",
|
|
5811
|
+
seeAll: (n) => `See all ${places(n)}`,
|
|
5812
|
+
whichPlaces: "Which places",
|
|
5813
|
+
whichChanges: "Which changes",
|
|
5814
|
+
howToFix: "How to fix",
|
|
5815
|
+
whatDiffers: "What differs",
|
|
5816
|
+
whatChanged: "What changed",
|
|
5817
|
+
openSetup: "Open setup",
|
|
5818
|
+
openScreen: "Open screen"
|
|
5819
|
+
}
|
|
5820
|
+
};
|
|
5821
|
+
// packages/present/dist/src/summary-message.js
|
|
5822
|
+
var SUMMARY_WORDS = {
|
|
5823
|
+
notComputed: "Not judged yet",
|
|
5824
|
+
notComputedWhy: "waiting for the judgement",
|
|
5825
|
+
placesOnPage: "the places are on the page",
|
|
5826
|
+
noResult: "No result",
|
|
5827
|
+
main: "Main",
|
|
5828
|
+
newestSlower: "newest session slower",
|
|
5829
|
+
journeys: (n) => `${n} journeys`,
|
|
5830
|
+
phones: (n) => `${n} phones`,
|
|
5831
|
+
heated: (n, of) => `${INFO_WORDS.heat.toLowerCase()} in ${n} of ${of} runs`,
|
|
5832
|
+
fewRuns: "few runs",
|
|
5833
|
+
measuredDifferently: "measured differently",
|
|
5834
|
+
namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
|
|
5835
|
+
link: {
|
|
5836
|
+
lane: (journey, phone) => `Open ${journey} on ${phone}`,
|
|
5837
|
+
phone: (phone) => `Open on ${phone}`,
|
|
5838
|
+
branch: (branch) => `Open ${branch}`,
|
|
5839
|
+
runSet: "Open the run set"
|
|
5840
|
+
}
|
|
5841
|
+
};
|
|
5842
|
+
var ORDER = ["worse", "warning", "cant-tell", "not-computed", "better", "no-change"];
|
|
5843
|
+
var rank = (l) => ORDER.indexOf(l.state);
|
|
5844
|
+
var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
|
|
5845
|
+
var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
|
|
5846
|
+
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
5847
|
+
var NAME = { name: 32, beside: 18, place: 16, link: 60, button: 30 };
|
|
5848
|
+
var DETAIL = 64;
|
|
5849
|
+
var upper = (s) => s ? `${s[0].toUpperCase()}${s.slice(1)}` : s;
|
|
5850
|
+
var fromTo = (a, b) => {
|
|
5851
|
+
const unit = (v) => / [^\d\s]+$/.exec(v)?.[0];
|
|
5852
|
+
return unit(a) && unit(a) === unit(b) ? `${a.slice(0, -unit(a).length)} → ${b}` : `${a} → ${b}`;
|
|
5853
|
+
};
|
|
5854
|
+
function clipPlace(place, n) {
|
|
5855
|
+
if (place.length <= n)
|
|
5856
|
+
return place;
|
|
5857
|
+
let cut = place.slice(0, n);
|
|
5858
|
+
if (place[n] !== " " && cut.includes(" "))
|
|
5859
|
+
cut = cut.slice(0, cut.lastIndexOf(" "));
|
|
5860
|
+
cut = cut.trimEnd();
|
|
5861
|
+
const open = (cut.match(/“/g) ?? []).length > (cut.match(/”/g) ?? []).length;
|
|
5862
|
+
return `${cut}…${open ? "”" : ""}`;
|
|
5863
|
+
}
|
|
5864
|
+
var changeText2 = (c, room) => {
|
|
5865
|
+
const what = `${c.measure} ${c.main !== null && c.now !== null ? fromTo(c.main, c.now) : c.change}`;
|
|
5866
|
+
return c.place ? `${clipPlace(c.place, Math.max(NAME.place, room - what.length - 3))} · ${what}` : upper(what);
|
|
5867
|
+
};
|
|
5868
|
+
var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
|
|
5869
|
+
var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
|
|
5870
|
+
function why(l) {
|
|
5871
|
+
if (l.state === "not-computed")
|
|
5872
|
+
return notJudgedWhy(l) ?? upper(SUMMARY_WORDS.notComputedWhy);
|
|
5873
|
+
if (l.state === "cant-tell" && l.namesChanged)
|
|
5874
|
+
return SUMMARY_WORDS.namesChanged(l.namesChanged.sdk, l.namesChanged.mainSessions, l.namesChanged.needed);
|
|
5875
|
+
if (l.answer)
|
|
5876
|
+
return l.answer;
|
|
5877
|
+
if (l.state === "no-change")
|
|
5878
|
+
return l.places ? VERDICT_SECTION_WORDS.noneMoved(l.places) : noneMoved;
|
|
5879
|
+
if (l.state !== "cant-tell")
|
|
5880
|
+
return upper(SUMMARY_WORDS.placesOnPage);
|
|
5881
|
+
return upper(wordReason(l.word) ?? (l.word === NO_BASELINE_WORD ? l.word : REASON_WORDS["judged-measures-missing"]));
|
|
5882
|
+
}
|
|
5883
|
+
var noneMoved = VERDICT_SECTION_WORDS.noneMoved(1).replace(/ \(.*\)$/, "");
|
|
5884
|
+
function conditions(l, pr) {
|
|
5885
|
+
const moved = l.state === "worse" || l.state === "warning";
|
|
5886
|
+
const heat = l.heatedRuns ? SUMMARY_WORDS.heated(l.heatedRuns, l.runs) : "";
|
|
5887
|
+
const heatIsNews = l.state === "no-change" && !!heat;
|
|
5888
|
+
return {
|
|
5889
|
+
always: [pr && l.state === "worse" ? l.blocking ? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "", heatIsNews ? heat : ""].filter(Boolean),
|
|
5890
|
+
optional: [
|
|
5891
|
+
moved && l.measuredDifferently ? SUMMARY_WORDS.measuredDifferently : "",
|
|
5892
|
+
(moved || l.state === "better") && l.fewRuns ? SUMMARY_WORDS.fewRuns : "",
|
|
5893
|
+
heatIsNews ? "" : heat
|
|
5894
|
+
].filter(Boolean)
|
|
5895
|
+
};
|
|
5896
|
+
}
|
|
5897
|
+
function summaryMessage(input) {
|
|
5898
|
+
const l = shownLane(input.lanes);
|
|
5899
|
+
if (!l) {
|
|
5900
|
+
return {
|
|
5901
|
+
state: "not-computed",
|
|
5902
|
+
newestSlower: false,
|
|
5903
|
+
lead: `${SUMMARY_WORDS.notComputed} · ${clip(input.branch, NAME.name)}`,
|
|
5904
|
+
detail: upper(SUMMARY_WORDS.notComputedWhy),
|
|
5905
|
+
link: { label: SUMMARY_WORDS.link.branch(clip(input.branch, NAME.button - 5)), url: input.url }
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
const journeys = new Set(input.lanes.map((x) => x.journey ?? "-")).size;
|
|
5909
|
+
const single = input.lanes.length === 1;
|
|
5910
|
+
const phone = l.phone ?? "unknown phone";
|
|
5911
|
+
const several = journeys === 1 ? SUMMARY_WORDS.phones(new Set(input.lanes.map((x) => x.phone ?? "-")).size) : SUMMARY_WORDS.journeys(journeys);
|
|
5912
|
+
const newestSlower = !input.pr && (l.state === "worse" || l.state === "warning");
|
|
5913
|
+
const named = (subject, rest) => [clip(subject, rest.length ? NAME.beside : NAME.name), ...rest];
|
|
5914
|
+
const what = input.pr ? named(input.branch, single ? [] : [several]) : single || journeys === 1 ? named(l.journey ?? "journey", single ? [] : [several]) : [several];
|
|
5915
|
+
const lead = newestSlower ? [SUMMARY_WORDS.main, ...what, SUMMARY_WORDS.newestSlower].join(" · ") : [notJudgedWhy(l) ? NOT_JUDGED_WORD : WORD[l.state], ...what].join(" · ");
|
|
5916
|
+
const top = l.state === "worse" || l.state === "warning" || l.state === "better" ? l.changes[0] : undefined;
|
|
5917
|
+
const { always, optional } = conditions(l, input.pr);
|
|
5918
|
+
const answer = top ? null : always.includes(SUMMARY_WORDS.heated(l.heatedRuns, l.runs)) ? noneMoved : why(l);
|
|
5919
|
+
let extra = [...always];
|
|
5920
|
+
for (const c of optional)
|
|
5921
|
+
if ((answer ?? changeText2(top, 0)).length + `; ${[...extra, c].join(", ")}`.length <= DETAIL)
|
|
5922
|
+
extra = [...extra, c];
|
|
5923
|
+
const tail = extra.length ? `; ${extra.join(", ")}` : "";
|
|
5924
|
+
const detail = `${top ? changeText2(top, DETAIL - tail.length) : answer}${tail}`;
|
|
5925
|
+
const journey = clip(l.journey ?? "journey", NAME.link);
|
|
5926
|
+
const short = (full, fallback) => full.length <= NAME.button ? full : fallback;
|
|
5927
|
+
const label = single ? short(SUMMARY_WORDS.link.lane(journey, phone), SUMMARY_WORDS.link.phone(phone)) : input.pr ? SUMMARY_WORDS.link.branch(clip(input.branch, NAME.button - 5)) : journeys === 1 ? short(SUMMARY_WORDS.link.lane(journey, several), SUMMARY_WORDS.link.phone(several)) : SUMMARY_WORDS.link.runSet;
|
|
5928
|
+
return { state: l.state, newestSlower, lead, detail, link: { label, url: (single ? l.url : null) ?? input.url } };
|
|
5929
|
+
}
|
|
5930
|
+
function pendingMessage(branch, url) {
|
|
5931
|
+
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.notComputed} · ${clip(branch, NAME.name)}`, detail: upper(SUMMARY_WORDS.notComputedWhy), link: { label: SUMMARY_WORDS.link.runSet, url } };
|
|
5932
|
+
}
|
|
5933
|
+
function noResultMessage(branch, reason, url) {
|
|
5934
|
+
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`, detail: reason.replace(/\.$/, ""), link: url ? { label: SUMMARY_WORDS.link.runSet, url } : null };
|
|
5935
|
+
}
|
|
5936
|
+
// packages/present/dist/src/run-set-message.js
|
|
5937
|
+
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
5938
|
+
function movedItems(v, sc) {
|
|
5939
|
+
const flat = v.items.filter((i) => i.scenario === sc.scenario && (!sc.device || !i.device_id || i.device_id === sc.device.id));
|
|
5940
|
+
const h = sc.headline;
|
|
5941
|
+
if (h?.errors === undefined)
|
|
5942
|
+
return flat.filter((i) => i.state === "worse" || i.state === "better");
|
|
5943
|
+
const graded = [...h.errors, ...h.warnings ?? []].map((g) => {
|
|
5944
|
+
const same = flat.find((i) => i.step === g.window && i.metric === g.metric);
|
|
5945
|
+
return {
|
|
5946
|
+
...same ?? { kind: "change", low_data: false, low_data_reason: null },
|
|
5947
|
+
scenario: sc.scenario,
|
|
5948
|
+
step: g.window,
|
|
5949
|
+
metric: g.metric,
|
|
5950
|
+
state: "worse",
|
|
5951
|
+
grade: g.grade,
|
|
5952
|
+
baseline: g.baseline ?? same?.baseline ?? null,
|
|
5953
|
+
...(g.baseline != null ? g.baseline_is_bound : same?.baseline_is_bound) ? { baseline_is_bound: true } : {},
|
|
5954
|
+
...(g.baseline != null ? g.change_is_bound : same?.change_is_bound) ? { change_is_bound: true } : {},
|
|
5955
|
+
candidate: g.candidate ?? same?.candidate ?? null,
|
|
5956
|
+
change_text: g.change,
|
|
5957
|
+
...g.finding ? { finding: g.finding } : {},
|
|
5958
|
+
...g.label ? { label_text: midLine(g.label, g.metric) } : {},
|
|
5959
|
+
...sc.device ? { device_id: sc.device.id } : {}
|
|
5960
|
+
};
|
|
5961
|
+
});
|
|
5962
|
+
return [...graded, ...flat.filter((i) => i.state === "better" && (i.judged ?? "outcome") === "outcome")];
|
|
5963
|
+
}
|
|
5964
|
+
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
5965
|
+
var linkOf = (x, set) => x?.share_url || set?.share_url || x?.dashboard_url || set?.dashboard_url || null;
|
|
5966
|
+
var measureWord = (i) => {
|
|
5967
|
+
if (i.label_text)
|
|
5968
|
+
return i.label_text;
|
|
5969
|
+
if (i.metric === "dur_ms" && placeKindWord(i.step ?? "") !== "Tap")
|
|
5970
|
+
return "length";
|
|
5971
|
+
return measureDef(i.metric)?.short ?? i.metric.replace(/_/g, " ");
|
|
5972
|
+
};
|
|
5973
|
+
var changeWords = (i) => {
|
|
5974
|
+
if (i.change_text)
|
|
5975
|
+
return i.change_text.replace(/ \(.*\)$/, "");
|
|
5976
|
+
const { baseline: b, candidate: c } = i;
|
|
5977
|
+
if (b === null || c === null)
|
|
5978
|
+
return "";
|
|
5979
|
+
if (b === 0)
|
|
5980
|
+
return atLeast(`${c - b >= 0 ? "+" : ""}${fieldValueText(i.metric, c - b)}`, i.change_is_bound);
|
|
5981
|
+
const p = Math.round((c / b - 1) * 100);
|
|
5982
|
+
return atLeast(`${p >= 0 ? "+" : ""}${p} %`, i.change_is_bound);
|
|
5983
|
+
};
|
|
5984
|
+
var placeOf = (step) => !step || step === "flow" || step === "*" ? null : placeLabel(step);
|
|
5985
|
+
function stateOf(sc, errors) {
|
|
5986
|
+
if (sc.state === null)
|
|
5987
|
+
return sc.word === NO_BASELINE_WORD ? "cant-tell" : "not-computed";
|
|
5988
|
+
if (sc.state === "inconclusive")
|
|
5989
|
+
return "cant-tell";
|
|
5990
|
+
if (sc.state === "warn")
|
|
5991
|
+
return errors || sc.word?.startsWith(STATE_WORDS.worse) ? "worse" : "warning";
|
|
5992
|
+
return sc.state;
|
|
5993
|
+
}
|
|
5994
|
+
function laneOf(v, sc, set) {
|
|
5995
|
+
const moved = sc.state === "inconclusive" ? [] : movedItems(v, sc);
|
|
5996
|
+
const worse = moved.filter((i) => i.state === "worse").sort((a, b) => Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
5997
|
+
const errors = worse.filter((i) => i.grade !== "warn");
|
|
5998
|
+
const state = stateOf(sc, errors.length);
|
|
5999
|
+
const shown = state === "better" ? moved.filter((i) => i.state === "better") : worse;
|
|
6000
|
+
return {
|
|
6001
|
+
journey: sc.scenario,
|
|
6002
|
+
phone: sc.device?.model ?? null,
|
|
6003
|
+
state,
|
|
6004
|
+
answer: sc.headline?.answer ?? null,
|
|
6005
|
+
word: sc.word ?? null,
|
|
6006
|
+
changes: shown.map((i) => ({
|
|
6007
|
+
place: placeOf(i.step),
|
|
6008
|
+
measure: measureWord(i),
|
|
6009
|
+
main: i.baseline === null ? null : atMost(fieldValueText(i.metric, i.baseline), i.baseline_is_bound),
|
|
6010
|
+
now: i.candidate === null ? null : fieldValueText(i.metric, i.candidate),
|
|
6011
|
+
change: changeWords(i)
|
|
6012
|
+
})),
|
|
6013
|
+
places: placeCount(shown),
|
|
6014
|
+
blocking: sc.headline?.blocking ?? v.blocking ?? v.headline?.blocking ?? true,
|
|
6015
|
+
measuredDifferently: !!sc.headline?.measured_differently?.length,
|
|
6016
|
+
fewRuns: sc.low_data,
|
|
6017
|
+
heatedRuns: sc.heated_runs ?? 0,
|
|
6018
|
+
runs: sc.runs.valid + sc.runs.invalid,
|
|
6019
|
+
namesChanged: sc.context?.namesChanged ?? null,
|
|
6020
|
+
url: linkOf(sc, set)
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
|
|
6024
|
+
var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
|
|
6025
|
+
var isPr = (s) => !!s.verdicts?.scenarios.some((sc) => sc.role === "comparison");
|
|
6026
|
+
var runSetMessage = (s) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url });
|
|
6027
|
+
var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
6028
|
+
function messageTone(s) {
|
|
6029
|
+
const lanes = s ? lanesOf(s) : [];
|
|
6030
|
+
if (lanes.some((l) => l.state === "worse" && l.blocking))
|
|
6031
|
+
return { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error };
|
|
6032
|
+
if (lanes.some((l) => l.state === "warning"))
|
|
6033
|
+
return { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn };
|
|
6034
|
+
switch (shownLane(lanes)?.state) {
|
|
6035
|
+
case "better":
|
|
6036
|
+
return { mark: "\uD83D\uDFE2", color: MESSAGE_COLORS.better };
|
|
6037
|
+
case "cant-tell":
|
|
6038
|
+
return { mark: "❔", color: MESSAGE_COLORS.none };
|
|
6039
|
+
default:
|
|
6040
|
+
return { mark: "⚪", color: MESSAGE_COLORS.none };
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6044
|
+
function slackPayload(m, tone) {
|
|
6045
|
+
const lead = `${tone.mark} ${esc(m.lead)}`;
|
|
6046
|
+
return {
|
|
6047
|
+
attachments: [{
|
|
6048
|
+
color: tone.color,
|
|
6049
|
+
fallback: lead,
|
|
6050
|
+
blocks: [
|
|
6051
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${lead}*
|
|
6052
|
+
${esc(m.detail)}` } },
|
|
6053
|
+
...m.link ? [{ type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: m.link.label }, url: m.link.url }] }] : []
|
|
6054
|
+
]
|
|
6055
|
+
}]
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
function resultSlackPayload(s, o = {}) {
|
|
6059
|
+
if (!s.verdicts)
|
|
6060
|
+
return null;
|
|
6061
|
+
const lanes = lanesOf(s);
|
|
6062
|
+
const moved = lanes.some((l) => l.state === "worse" || l.state === "warning");
|
|
6063
|
+
const heat = lanes.some((l) => l.heatedRuns > 0);
|
|
6064
|
+
const improved = lanes.some((l) => l.state === "better") || s.verdicts.state === "better";
|
|
6065
|
+
if (isPr(s) ? !moved && !heat : !moved && !improved && !heat && !o.always)
|
|
6066
|
+
return null;
|
|
6067
|
+
return slackPayload(runSetMessage(s), messageTone(s));
|
|
6068
|
+
}
|
|
6069
|
+
var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
|
|
6070
|
+
var markdown = (m, mark) => `### ${mark} ${m.lead}
|
|
6071
|
+
|
|
6072
|
+
${m.detail}
|
|
6073
|
+
${m.link ? `
|
|
6074
|
+
[${m.link.label} ›](${m.link.url})
|
|
6075
|
+
` : ""}`;
|
|
6076
|
+
var resultMarkdown = (s) => markdown(runSetMessage(s), messageTone(s).mark);
|
|
6077
|
+
var noResultMarkdown = (branch, reason, url) => markdown(noResultMessage(branch, reason, url), "⚪");
|
|
6078
|
+
var pendingMarkdown = (branch, url) => markdown(pendingMessage(branch, url), "⚪");
|
|
6079
|
+
// apps/agent/src/upload.ts
|
|
6080
|
+
import { join as join4 } from "node:path";
|
|
6081
|
+
var MAX_RATE_RETRIES = 5;
|
|
6082
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6083
|
+
class UploadError extends Error {
|
|
6084
|
+
}
|
|
6085
|
+
async function call(f, base, path, headers, body, method = "POST") {
|
|
6086
|
+
let res;
|
|
6087
|
+
for (let attempt = 0;; attempt++) {
|
|
6088
|
+
try {
|
|
6089
|
+
res = await f(`${base.replace(/\/+$/, "")}${path}`, {
|
|
6090
|
+
method,
|
|
6091
|
+
headers: method === "POST" ? { "content-type": "application/json", ...headers } : headers,
|
|
6092
|
+
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
6093
|
+
});
|
|
6094
|
+
} catch (e) {
|
|
6095
|
+
throw new UploadError(`${method} ${path}: ${e.message}`);
|
|
6096
|
+
}
|
|
6097
|
+
if (res.status !== 429 || attempt >= MAX_RATE_RETRIES)
|
|
6098
|
+
break;
|
|
6099
|
+
const wait = Number(res.headers.get("retry-after") ?? "1");
|
|
6100
|
+
await res.text().catch(() => "");
|
|
6101
|
+
await sleep(Math.min(Math.max(Number.isFinite(wait) ? wait : 1, 1), 60) * 1000);
|
|
6102
|
+
}
|
|
6103
|
+
const text = await res.text();
|
|
6104
|
+
if (res.status < 200 || res.status >= 300)
|
|
6105
|
+
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}`);
|
|
6106
|
+
try {
|
|
6107
|
+
return text ? JSON.parse(text) : {};
|
|
6108
|
+
} catch {
|
|
6109
|
+
throw new UploadError(`${method} ${path} -> ${res.status}, the answer is not JSON (is the URL the KeepSnappy API?): ${text.slice(0, 200)}`);
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
async function uploadRuns(deps, outDir, runs, opts) {
|
|
6113
|
+
const f = opts.fetch ?? fetch;
|
|
6114
|
+
const size = opts.batchSize ?? 200;
|
|
6115
|
+
const skipped = [];
|
|
6116
|
+
let uploaded = 0;
|
|
6117
|
+
for (const r of runs) {
|
|
6118
|
+
if (!r.valid) {
|
|
6119
|
+
skipped.push({ run_id: r.run_id, why: r.invalid_reason || "invalid" });
|
|
6120
|
+
continue;
|
|
6121
|
+
}
|
|
6122
|
+
const log = await deps.fs.readFile(join4(outDir, r.run_id, "keepsnappy.log"));
|
|
6123
|
+
const windows = windowsInRun(parseWindows(log), r.t0, r.t1);
|
|
6124
|
+
if (windows.length === 0) {
|
|
6125
|
+
skipped.push({ run_id: r.run_id, why: "no-windows" });
|
|
6126
|
+
continue;
|
|
6127
|
+
}
|
|
6128
|
+
const externalId = `${opts.externalPrefix}-${r.run_id}`;
|
|
6129
|
+
const created = await call(f, opts.ingestUrl, "/v1/runs", { "x-keepsnappy-key": opts.key }, {
|
|
6130
|
+
external_id: externalId,
|
|
6131
|
+
started_at: r.t0,
|
|
6132
|
+
ended_at: r.t1,
|
|
6133
|
+
device: { ...opts.device?.model ? { model: opts.device.model } : {}, ...opts.device?.os ? { os: opts.device.os } : {}, ...opts.device?.id ? { id: opts.device.id } : {}, exclusive: true },
|
|
6134
|
+
meta: {
|
|
6135
|
+
arm: r.arm,
|
|
6136
|
+
driver: opts.driver ?? "keepsnappy-run",
|
|
6137
|
+
...opts.meta,
|
|
6138
|
+
...r.scenario ? { scenario: r.scenario } : {},
|
|
6139
|
+
...opts.runSetId ? { run_set_id: opts.runSetId } : {}
|
|
6140
|
+
}
|
|
6141
|
+
});
|
|
6142
|
+
const runId = String(created.run_id ?? "");
|
|
6143
|
+
const token = String(created.run_token ?? "");
|
|
6144
|
+
if (!runId || !token)
|
|
6145
|
+
throw new UploadError("POST /v1/runs: response has no run_id/run_token");
|
|
6146
|
+
const items = windows.map((w) => `${w.endWall} KEEPSNAPPY window ${JSON.stringify(w.raw)}`);
|
|
6147
|
+
for (let i = 0;i * size < items.length; i++) {
|
|
6148
|
+
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
6149
|
+
batch_id: `${externalId}-${String(i + 1).padStart(4, "0")}`,
|
|
6150
|
+
items: items.slice(i * size, (i + 1) * size)
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
const samples = samplesInRun(parseSamples(log), r.t0, r.t1).map((x) => `${x.wall} KEEPSNAPPY sample ${JSON.stringify(x.raw)}`);
|
|
6154
|
+
for (let i = 0;i * size < samples.length; i++) {
|
|
6155
|
+
await call(f, opts.ingestUrl, "/v1/windows", { "x-keepsnappy-key": opts.key, "x-keepsnappy-run-token": token }, {
|
|
6156
|
+
batch_id: `${externalId}-s${String(i + 1).padStart(4, "0")}`,
|
|
6157
|
+
items: samples.slice(i * size, (i + 1) * size)
|
|
6158
|
+
});
|
|
6159
|
+
}
|
|
6160
|
+
let markers = [];
|
|
6161
|
+
try {
|
|
6162
|
+
markers = parseMarkersNdjson(await deps.fs.readFile(join4(outDir, r.run_id, "markers.ndjson")));
|
|
6163
|
+
} catch {}
|
|
6164
|
+
for (const m of markers) {
|
|
6165
|
+
await call(f, opts.ingestUrl, "/v1/markers", { authorization: `Bearer ${token}` }, { step: m.step, phase: m.phase, t_host: m.t_host });
|
|
6166
|
+
}
|
|
6167
|
+
await call(f, opts.ingestUrl, `/v1/runs/${runId}/finish`, { "x-keepsnappy-key": opts.key }, {});
|
|
6168
|
+
uploaded++;
|
|
6169
|
+
}
|
|
6170
|
+
return { uploaded, skipped };
|
|
6171
|
+
}
|
|
6172
|
+
|
|
5764
6173
|
// apps/agent/src/summary.ts
|
|
5765
6174
|
var fieldWord = (field) => measureDef(field)?.short ?? field;
|
|
5766
6175
|
function plainMetric(metric) {
|
|
@@ -5799,7 +6208,6 @@ function summarize(input) {
|
|
|
5799
6208
|
|
|
5800
6209
|
// apps/agent/src/remote.ts
|
|
5801
6210
|
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
6211
|
function heatNote(sc) {
|
|
5804
6212
|
const n = sc.heated_runs ?? 0;
|
|
5805
6213
|
if (!n)
|
|
@@ -5807,30 +6215,6 @@ function heatNote(sc) {
|
|
|
5807
6215
|
const phone = sc.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
5808
6216
|
return `${INFO_WORDS.heat} in ${n} of ${sc.runs.valid + sc.runs.invalid} runs · ${sc.scenario ?? "journey"} · ${phone}`;
|
|
5809
6217
|
}
|
|
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
6218
|
var placeOfRow = (g) => g.finding ?? placeRef(g.window).stem;
|
|
5835
6219
|
function movedCounts(sc) {
|
|
5836
6220
|
const h = sc.headline;
|
|
@@ -5840,7 +6224,6 @@ function movedCounts(sc) {
|
|
|
5840
6224
|
const slightly = new Set((h.warnings ?? []).map(placeOfRow).filter((p) => !worse.has(p)));
|
|
5841
6225
|
return { worse: worse.size, warnings: slightly.size, better: h.counts.better, noChange: h.counts.no_change, inconclusive: h.counts.inconclusive };
|
|
5842
6226
|
}
|
|
5843
|
-
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
5844
6227
|
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
6228
|
var setupLine = (h) => {
|
|
5846
6229
|
const parts = [
|
|
@@ -5887,16 +6270,15 @@ var word = (s, server, blocking) => badgedWord(server || stateWord(s ?? "inconcl
|
|
|
5887
6270
|
var setWord = (v) => word(v?.state ?? null, v?.word, v?.blocking ?? v?.headline?.blocking);
|
|
5888
6271
|
var scenarioWord = (sc) => word(sc.state, sc.word, sc.headline?.blocking);
|
|
5889
6272
|
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;
|
|
6273
|
+
var UNCHECKED_GATE = `${NOT_COMPUTED_WORD}: --fail-on-worse checked nothing`;
|
|
6274
|
+
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
6275
|
var countsLine = (c) => countsWords(c);
|
|
5894
6276
|
var noCounts = (c) => !c.worse && !c.warnings && !c.better && !c.noChange && !c.inconclusive;
|
|
5895
|
-
var change = (metric, a, b) => {
|
|
6277
|
+
var change = (metric, a, b, bounds) => {
|
|
5896
6278
|
if (a === null || b === null)
|
|
5897
6279
|
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)})`;
|
|
6280
|
+
const pct = a === 0 ? "" : ` ${atLeast(`${b >= a ? "+" : "-"}${Math.round(Math.abs(b / a - 1) * 100)}%`, bounds.change_is_bound)}`;
|
|
6281
|
+
return `${pct} (${atMost(fieldValueText(metric, a), bounds.baseline_is_bound)} -> ${fieldValueText(metric, b)})`;
|
|
5900
6282
|
};
|
|
5901
6283
|
function formatRunSet(s, top = 5) {
|
|
5902
6284
|
const lines = [];
|
|
@@ -5926,11 +6308,12 @@ function formatRunSet(s, top = 5) {
|
|
|
5926
6308
|
lines.push(...resultContextLines(sc.context).map((line) => ` ${line}`));
|
|
5927
6309
|
if (heatNote(sc))
|
|
5928
6310
|
lines.push(` ${heatNote(sc)}`);
|
|
6311
|
+
lines.push(...(sc.headline?.session_speed ?? []).map((line) => ` ${line}`));
|
|
5929
6312
|
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
6313
|
const moved = allMoved.slice(0, top);
|
|
5931
6314
|
for (const i of moved) {
|
|
5932
6315
|
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)}`);
|
|
6316
|
+
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
6317
|
}
|
|
5935
6318
|
if (allMoved.length > moved.length)
|
|
5936
6319
|
lines.push(` ${allMoved.length - moved.length} more`);
|
|
@@ -5947,35 +6330,12 @@ function formatRunSet(s, top = 5) {
|
|
|
5947
6330
|
`);
|
|
5948
6331
|
}
|
|
5949
6332
|
function markdownRunSet(s, fallback) {
|
|
5950
|
-
if (!s || !s.verdicts)
|
|
6333
|
+
if (!s || !s.verdicts)
|
|
5951
6334
|
return `### KeepSnappy
|
|
5952
6335
|
|
|
5953
6336
|
Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [open KeepSnappy](${fallback.dashboardUrl})
|
|
5954
6337
|
`;
|
|
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
|
-
`);
|
|
6338
|
+
return isPending(s) ? pendingMarkdown(s.branch, linkOf(s) ?? s.dashboard_url) : resultMarkdown(s);
|
|
5979
6339
|
}
|
|
5980
6340
|
|
|
5981
6341
|
// apps/agent/src/run.ts
|
|
@@ -9351,7 +9711,7 @@ function parseArms(spec) {
|
|
|
9351
9711
|
return spec.split(",").map((part) => {
|
|
9352
9712
|
const eq = part.indexOf("=");
|
|
9353
9713
|
if (eq <= 0)
|
|
9354
|
-
throw new
|
|
9714
|
+
throw new ConfigError([`bad --arms entry "${part}", expected NAME=<app-path-or-bundle-id>`]);
|
|
9355
9715
|
return { name: part.slice(0, eq).trim(), target: part.slice(eq + 1).trim() };
|
|
9356
9716
|
});
|
|
9357
9717
|
}
|
|
@@ -9361,10 +9721,10 @@ function parseArmEnv(specs, arms) {
|
|
|
9361
9721
|
for (const spec of specs ?? []) {
|
|
9362
9722
|
const m = /^([^:=]+):([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(spec);
|
|
9363
9723
|
if (!m)
|
|
9364
|
-
throw new
|
|
9724
|
+
throw new ConfigError([`bad --arm-env "${spec}", expected ARM:KEY=VALUE`]);
|
|
9365
9725
|
const [, arm = "", key = "", value = ""] = m;
|
|
9366
9726
|
if (!names.has(arm))
|
|
9367
|
-
throw new
|
|
9727
|
+
throw new ConfigError([`--arm-env "${spec}": no arm named ${arm} (arms: ${[...names].join(", ")})`]);
|
|
9368
9728
|
const e = env.get(arm) ?? {};
|
|
9369
9729
|
e[key] = value;
|
|
9370
9730
|
if (!key.startsWith("MAESTRO_"))
|
|
@@ -9375,12 +9735,15 @@ function parseArmEnv(specs, arms) {
|
|
|
9375
9735
|
}
|
|
9376
9736
|
|
|
9377
9737
|
// apps/agent/src/runner.ts
|
|
9738
|
+
var TEST_ERROR = "test-error";
|
|
9739
|
+
var TEST_FAILED = "test-failed";
|
|
9740
|
+
var failedTest = (reason) => reason.startsWith(`${TEST_FAILED}:`) || reason.startsWith(`${TEST_ERROR}:`);
|
|
9378
9741
|
var isBuildPath = (target) => !!target && /\.(app|ipa)\/?$/.test(target);
|
|
9379
9742
|
async function measure(deps, source, installer, opts) {
|
|
9380
9743
|
const { fs, clock, spawner, log } = deps;
|
|
9381
9744
|
const flows = opts.flows?.length ? opts.flows : [{ name: undefined, command: opts.testCommand }];
|
|
9382
9745
|
if (flows.some((f) => !f.command[0]))
|
|
9383
|
-
throw new
|
|
9746
|
+
throw new ConfigError(["no test command given (put it after `--`)"]);
|
|
9384
9747
|
await fs.mkdir(opts.outDir);
|
|
9385
9748
|
const runsPath = join7(opts.outDir, "runs.ndjson");
|
|
9386
9749
|
await fs.writeFile(runsPath, "");
|
|
@@ -9421,9 +9784,9 @@ async function measure(deps, source, installer, opts) {
|
|
|
9421
9784
|
--- stderr ---
|
|
9422
9785
|
${r.stderr}` : ""}`);
|
|
9423
9786
|
if (r.error)
|
|
9424
|
-
reason =
|
|
9787
|
+
reason = `${TEST_ERROR}: ${r.error}`;
|
|
9425
9788
|
else if (r.code !== 0)
|
|
9426
|
-
reason =
|
|
9789
|
+
reason = `${TEST_FAILED}: exit ${r.code}`;
|
|
9427
9790
|
}
|
|
9428
9791
|
const t1 = clock.now();
|
|
9429
9792
|
let windows = [];
|
|
@@ -9521,7 +9884,7 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
9521
9884
|
const runs = [];
|
|
9522
9885
|
for (const src of sources) {
|
|
9523
9886
|
const text = await deps.fs.readFile(join8(src.dir, "runs.ndjson")).catch(() => {
|
|
9524
|
-
throw new
|
|
9887
|
+
throw new ConfigError([`--replay ${src.dir}: no runs.ndjson (not a keepsnappy out dir)`]);
|
|
9525
9888
|
});
|
|
9526
9889
|
for (const line of text.split(`
|
|
9527
9890
|
`)) {
|
|
@@ -9541,7 +9904,7 @@ async function replayRuns(deps, sources, outDir, scenario) {
|
|
|
9541
9904
|
}
|
|
9542
9905
|
}
|
|
9543
9906
|
if (runs.length === 0)
|
|
9544
|
-
throw new
|
|
9907
|
+
throw new ConfigError([`--replay: no valid runs in ${sources.map((x) => x.dir + (x.arm ? `:${x.arm}` : "")).join(", ")}`]);
|
|
9545
9908
|
await deps.fs.writeFile(join8(outDir, "runs.ndjson"), runs.map((r) => JSON.stringify(r)).join(`
|
|
9546
9909
|
`) + `
|
|
9547
9910
|
`);
|
|
@@ -9555,7 +9918,7 @@ var DEFAULT_API_URL = "https://keepsnappy-api-922918702810.europe-west1.run.app"
|
|
|
9555
9918
|
// apps/agent/src/journeys.ts
|
|
9556
9919
|
import { isAbsolute, relative, resolve as resolve2, sep } from "node:path";
|
|
9557
9920
|
var charCount = (s) => [...s].length;
|
|
9558
|
-
function
|
|
9921
|
+
function clip2(s, max) {
|
|
9559
9922
|
if (charCount(s) <= max)
|
|
9560
9923
|
return s;
|
|
9561
9924
|
const parts = typeof Intl.Segmenter === "function" ? Array.from(new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(s), (x) => x.segment) : [...s];
|
|
@@ -9589,7 +9952,7 @@ function flowHeader(text) {
|
|
|
9589
9952
|
para.push(t);
|
|
9590
9953
|
}
|
|
9591
9954
|
const description = para.join(" ").replace(/\s+/g, " ").trim();
|
|
9592
|
-
return { ...title ? { title:
|
|
9955
|
+
return { ...title ? { title: clip2(title, TITLE_MAX) } : {}, ...description ? { description: clip2(description, DESCRIPTION_MAX) } : {} };
|
|
9593
9956
|
}
|
|
9594
9957
|
async function repoRoot(deps) {
|
|
9595
9958
|
const r = await deps.spawner.run("git", ["rev-parse", "--show-toplevel"]).catch(() => null);
|
|
@@ -9643,10 +10006,11 @@ function baseRefs(env) {
|
|
|
9643
10006
|
return target ? [`origin/${target}`] : ["origin/main", "main"];
|
|
9644
10007
|
}
|
|
9645
10008
|
var SHA = /^[0-9a-f]{40,64}$/;
|
|
10009
|
+
var onMain = (env, branch) => !!branch && baseRefs(env).some((r) => !r.startsWith("-") && r.replace(REMOTE, "") === branch);
|
|
9646
10010
|
var FIX = "fetch main and the commit, in CI with fetch-depth: 0, or pass --base-commit SHA";
|
|
9647
10011
|
async function defaultBaseCommit(deps, env, o) {
|
|
9648
10012
|
const refs = baseRefs(env).filter((r) => !r.startsWith("-"));
|
|
9649
|
-
if (
|
|
10013
|
+
if (onMain(env, o.branch))
|
|
9650
10014
|
return {};
|
|
9651
10015
|
const tip = o.commit?.trim();
|
|
9652
10016
|
if (!tip)
|
|
@@ -9682,6 +10046,34 @@ async function commitFields(deps, env, shas) {
|
|
|
9682
10046
|
const pr = await pullRequestInfo(deps, env);
|
|
9683
10047
|
return { ...commit ? { commit } : {}, ...base ? { base_commit: base } : {}, ...pr ? { pull_request: { ...pr } } : {} };
|
|
9684
10048
|
}
|
|
10049
|
+
var MAIN_MERGES_LIMIT = 200;
|
|
10050
|
+
var SQUASH = /^(.*?)\s*\(#(\d+)\)$/;
|
|
10051
|
+
var MERGE = /^Merge pull request #(\d+) from \S+$/;
|
|
10052
|
+
function mergeOf(sha, subject, body) {
|
|
10053
|
+
const merge = MERGE.exec(subject);
|
|
10054
|
+
const squash = merge ? null : SQUASH.exec(subject);
|
|
10055
|
+
const number = Number(merge?.[1] ?? squash?.[2] ?? 0);
|
|
10056
|
+
const title = (merge ? body.split(`
|
|
10057
|
+
`).find((l) => l.trim())?.trim() : squash?.[1]) || subject.trim() || sha.slice(0, 7);
|
|
10058
|
+
return { sha, ...number > 0 ? { number } : {}, title: Array.from(title).slice(0, 300).join("") };
|
|
10059
|
+
}
|
|
10060
|
+
async function mainMerges(deps, env, o) {
|
|
10061
|
+
if (!onMain(env, o.branch))
|
|
10062
|
+
return {};
|
|
10063
|
+
const tip = o.commit?.trim();
|
|
10064
|
+
if (!tip || tip.startsWith("-"))
|
|
10065
|
+
return { warning: "main merges not recorded: no commit is recorded for the run set (pass --commit, or run in GitHub Actions)" };
|
|
10066
|
+
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);
|
|
10067
|
+
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 ?? "")) : [];
|
|
10068
|
+
if (!merges.length)
|
|
10069
|
+
return { warning: `main merges not recorded: git log ${tip.slice(0, 12)} failed (fetch main and the commit, in CI with fetch-depth: 0)` };
|
|
10070
|
+
if (merges.length < MAIN_MERGES_LIMIT) {
|
|
10071
|
+
const shallow = await deps.spawner.run("git", ["rev-parse", "--is-shallow-repository"], { timeoutMs: 5000 }).catch(() => null);
|
|
10072
|
+
if (shallow?.code === 0 && shallow.stdout.trim() === "true")
|
|
10073
|
+
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` };
|
|
10074
|
+
}
|
|
10075
|
+
return { merges };
|
|
10076
|
+
}
|
|
9685
10077
|
|
|
9686
10078
|
// apps/agent/src/run-command.ts
|
|
9687
10079
|
var EXIT = { ok: 0, infra: 1, config: 2, worse: 3 };
|
|
@@ -9727,7 +10119,7 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
9727
10119
|
api = { url, key, ...rt.fetch ? { fetch: rt.fetch } : {} };
|
|
9728
10120
|
}
|
|
9729
10121
|
if (o.wait && !o.upload)
|
|
9730
|
-
throw new ConfigError(["--wait needs --upload (the
|
|
10122
|
+
throw new ConfigError(["--wait needs --upload (it waits until the server has processed the uploaded runs)"]);
|
|
9731
10123
|
if (o.wait && !git.branch)
|
|
9732
10124
|
throw new ConfigError(["--wait needs the branch: pass --branch, or run in GitHub Actions (GITHUB_HEAD_REF / GITHUB_REF_NAME)"]);
|
|
9733
10125
|
if (o["fail-on-worse"] && !o.wait)
|
|
@@ -9746,11 +10138,15 @@ async function runCommand(deps, o, positionals, rt = { env: process.env, print:
|
|
|
9746
10138
|
let runSet;
|
|
9747
10139
|
if (api && git.branch) {
|
|
9748
10140
|
const base = await baseCommitFor(deps, rt.env, { explicit: o["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
|
|
10141
|
+
const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
|
|
10142
|
+
if (main.warning)
|
|
10143
|
+
rt.print(main.warning);
|
|
9749
10144
|
runSet = await createRunSet(api, {
|
|
9750
10145
|
branch: git.branch,
|
|
9751
10146
|
...git.commit ? { commit_sha: git.commit } : {},
|
|
9752
10147
|
...base ? { base_commit_sha: base } : {},
|
|
9753
10148
|
...await commitFields(deps, rt.env, { commit: git.commit, base }),
|
|
10149
|
+
...main.merges ? { merges: main.merges } : {},
|
|
9754
10150
|
device: { ...device, provider },
|
|
9755
10151
|
scenarios: flows.map((f) => f.name),
|
|
9756
10152
|
journeys: await journeysFor(flows, { readFile: (p) => deps.fs.readFile(p), root: await repoRoot(deps) }),
|
|
@@ -9808,7 +10204,9 @@ ${outcome.summary}`);
|
|
|
9808
10204
|
if (up.uploaded === 0) {
|
|
9809
10205
|
if (runSet)
|
|
9810
10206
|
await completeRunSet(api, runSet.run_set_id, "on-read").catch(infra("no run was uploaded, and the run set could not be closed"));
|
|
9811
|
-
|
|
10207
|
+
const failed = up.skipped.length > 0 && up.skipped.every((x) => failedTest(x.why));
|
|
10208
|
+
const why = [...new Set(up.skipped.map((x) => x.why))].join(", ");
|
|
10209
|
+
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
10210
|
if (runSet)
|
|
9813
10211
|
await writeResult(deps, out, rt, runSet, null, git.branch, true);
|
|
9814
10212
|
return EXIT.infra;
|
|
@@ -9817,7 +10215,7 @@ ${outcome.summary}`);
|
|
|
9817
10215
|
return EXIT.ok;
|
|
9818
10216
|
let summary = null;
|
|
9819
10217
|
if (o.wait) {
|
|
9820
|
-
summary = await completeRunSet(api, runSet.run_set_id, "inline").catch(infra("the runs are uploaded, but the
|
|
10218
|
+
summary = await completeRunSet(api, runSet.run_set_id, "inline").catch(infra("the runs are uploaded, but the run set could not be closed"));
|
|
9821
10219
|
rt.print(`
|
|
9822
10220
|
${formatRunSet(summary)}`);
|
|
9823
10221
|
} else {
|
|
@@ -9854,10 +10252,7 @@ async function writeResult(deps, out, rt, runSet, summary, branch, blocked = fal
|
|
|
9854
10252
|
`);
|
|
9855
10253
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
9856
10254
|
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 }));
|
|
10255
|
+
await deps.fs.appendFile(step, blocked ? noResultMarkdown(branch, "No run sent any data.", url) : markdownRunSet(summary, { dashboardUrl: url, branch }));
|
|
9861
10256
|
}
|
|
9862
10257
|
|
|
9863
10258
|
// apps/agent/src/doctor.ts
|
|
@@ -10319,7 +10714,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
10319
10714
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
10320
10715
|
import { fileURLToPath } from "node:url";
|
|
10321
10716
|
function driverVersion() {
|
|
10322
|
-
const built = "0.1.0-alpha.
|
|
10717
|
+
const built = "0.1.0-alpha.18";
|
|
10323
10718
|
if (built)
|
|
10324
10719
|
return built;
|
|
10325
10720
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -10374,6 +10769,576 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
|
10374
10769
|
var FLUSH_MARGIN_MS = 30000;
|
|
10375
10770
|
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10376
10771
|
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
10772
|
+
// packages/verdict-core/dist/src/window-count.js
|
|
10773
|
+
var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
|
|
10774
|
+
var OFF = new WeakSet;
|
|
10775
|
+
// packages/verdict-core/dist/src/clauses.js
|
|
10776
|
+
var row = (id, what, unit, cmp, ruling, uncalibrated, value) => ({ id, what, unit, cmp, ruling, uncalibrated, value });
|
|
10777
|
+
var NO_RULING = "no ruling: a starting value of the deciding config (L2 audit F-13)";
|
|
10778
|
+
var FLOOR = { rel: 0.3, abs: 5 };
|
|
10779
|
+
var COMPONENT_WARN_MS = { update: 5, work: 10 };
|
|
10780
|
+
var CLAUSES = {
|
|
10781
|
+
"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 }),
|
|
10782
|
+
"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 }),
|
|
10783
|
+
"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 }),
|
|
10784
|
+
"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 }),
|
|
10785
|
+
"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 }),
|
|
10786
|
+
"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 }),
|
|
10787
|
+
"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 }),
|
|
10788
|
+
"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 }),
|
|
10789
|
+
"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 }),
|
|
10790
|
+
"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),
|
|
10791
|
+
"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 }),
|
|
10792
|
+
"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" }),
|
|
10793
|
+
"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, {
|
|
10794
|
+
render: { c: 0.9244, k95: 1.5678, k99: 2.2255 },
|
|
10795
|
+
commit: { c: 0.8326, k95: 1.7675, k99: 2.8108 },
|
|
10796
|
+
component: { c: 0.6481, k95: 1.8075, k99: 2.6648 }
|
|
10797
|
+
}),
|
|
10798
|
+
"T-REL": row("T-REL", "The relative part of both tiers when a rule has no noise curve; the deciding config's tiers cap it at the error ms, so no input reaches it.", "share", ">", "REQ-MET-26, controller 2026-10-16", false, { rel: 0.3 }),
|
|
10799
|
+
"J-MEMORY-RETAINED": row("J-MEMORY-RETAINED", "Memory the journey kept after the end GC: Slightly worse past max(warn MB, warn % of main), Worse past max(error MB, error % of main), on every device.", "MB", ">", "REQ-MET-4, Arek 2026-10-20 ~08:00, pair B (standing-rulings.md); provisional", true, { warn: { abs: 20, rel: 0.1 }, error: { abs: 150, rel: 0.3 } }),
|
|
10800
|
+
"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 } }),
|
|
10801
|
+
"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 }),
|
|
10802
|
+
"E-BACKGROUND": row("E-BACKGROUND", "Background places are evidence past max(floor, % of main).", "ms", ">", "REQ-MEAS-32; the number has no ruling", true, { background_commit: FLOOR, background_render: FLOOR, background_component: FLOOR }),
|
|
10803
|
+
"E-LAUNCH": row("E-LAUNCH", "The launch screen is evidence past max(5 ms, +30 % of main).", "ms", ">", "REQ-VER-15; floor adopted 2026-10-21 from config.ts F-13, [ctl-stab-noold-1625]", true, { launch_commit: FLOOR, launch_render: FLOOR, launch_component: FLOOR }),
|
|
10804
|
+
"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 }),
|
|
10805
|
+
"E-TOUCH": row("E-TOUCH", "Touch response is evidence past max(10 ms, +20 % of main).", "ms", ">", 'ruling "touch response is not judged" (REQ-MET-24)', true, { touch: { rel: 0.2, abs: 10 } }),
|
|
10806
|
+
"E-FREEZE": row("E-FREEZE", "The longest freeze (window, journey) is evidence past max(17 ms, +50 % of main).", "ms", ">", NO_RULING, true, { freeze: { rel: 0.5, abs: 17 }, freeze_journey: { rel: 0.5, abs: 17 } }),
|
|
10807
|
+
"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 } }),
|
|
10808
|
+
"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 } }),
|
|
10809
|
+
"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 } }),
|
|
10810
|
+
"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, {
|
|
10811
|
+
window_cpu: { rel: 0.5, abs: 20 },
|
|
10812
|
+
long_tasks: { rel: 0.3, abs: 50 },
|
|
10813
|
+
gc: { rel: 0.5, abs: 2 },
|
|
10814
|
+
requests: { rel: 0.5, abs: 1 },
|
|
10815
|
+
js_cpu: { rel: 0.5, abs: 5 }
|
|
10816
|
+
}),
|
|
10817
|
+
"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 }),
|
|
10818
|
+
"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 }),
|
|
10819
|
+
"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 }),
|
|
10820
|
+
"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 }),
|
|
10821
|
+
"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 })
|
|
10822
|
+
};
|
|
10823
|
+
var CLAUSE_LIST = Object.values(CLAUSES);
|
|
10824
|
+
// packages/verdict-core/dist/src/config.js
|
|
10825
|
+
var WINDOWS = ["screen", "press", "drag", "step", "other"];
|
|
10826
|
+
var STEP = ["step"];
|
|
10827
|
+
var JUDGED_WINDOWS = WINDOWS.filter((k) => !STEP.includes(k));
|
|
10828
|
+
var BACKGROUND = ["background"];
|
|
10829
|
+
var LAUNCH = ["launch"];
|
|
10830
|
+
var SHOWN2 = [...WINDOWS, ...LAUNCH];
|
|
10831
|
+
var REACT_PLACES = [...SHOWN2, ...BACKGROUND];
|
|
10832
|
+
var JOURNEY = ["journey"];
|
|
10833
|
+
var DEVICE = ["device"];
|
|
10834
|
+
var ANY_DEVICE = ["device", "sim"];
|
|
10835
|
+
var ENTRY_BUDGET_MS = 100;
|
|
10836
|
+
var UNCERTAIN_BELOW_MAIN_SESSIONS = 3;
|
|
10837
|
+
var GUESS = (from) => ({ uncalibrated: true, source: `${from}; starting value carried from the plan 58 table (today's warn value), plan 57 calibrates it` });
|
|
10838
|
+
var evidenceRule = (id, on, places, threshold, from, more = {}) => ({
|
|
10839
|
+
id,
|
|
10840
|
+
role: "evidence",
|
|
10841
|
+
on,
|
|
10842
|
+
places,
|
|
10843
|
+
devices: DEVICE,
|
|
10844
|
+
threshold,
|
|
10845
|
+
...GUESS(from),
|
|
10846
|
+
...more
|
|
10847
|
+
});
|
|
10848
|
+
var MEMORY_MEASURES = Object.keys(MEASURES).filter((id) => MEASURES[id].unit.startsWith("MB"));
|
|
10849
|
+
var REACT_MEASURES = [
|
|
10850
|
+
"commit_max_ms",
|
|
10851
|
+
"render_ms",
|
|
10852
|
+
"commits",
|
|
10853
|
+
"renders",
|
|
10854
|
+
"mounts",
|
|
10855
|
+
"commits_over_budget",
|
|
10856
|
+
"mount_share",
|
|
10857
|
+
"self_ms_top",
|
|
10858
|
+
"component_ms",
|
|
10859
|
+
"dimension:component",
|
|
10860
|
+
"dimension:updater"
|
|
10861
|
+
];
|
|
10862
|
+
var WINDOW_MODEL_AFFECTS = [
|
|
10863
|
+
"commits",
|
|
10864
|
+
"renders",
|
|
10865
|
+
"mounts",
|
|
10866
|
+
"alloc_mb",
|
|
10867
|
+
"gc_count",
|
|
10868
|
+
"gc_ms",
|
|
10869
|
+
"render_ms",
|
|
10870
|
+
"js_cpu_ms",
|
|
10871
|
+
"cpu_ms",
|
|
10872
|
+
"commit_max_ms",
|
|
10873
|
+
"commits_over_budget",
|
|
10874
|
+
"longtask_ms",
|
|
10875
|
+
"longtask_max_ms",
|
|
10876
|
+
"touch_max_ms",
|
|
10877
|
+
"net_requests",
|
|
10878
|
+
"net_duplicates",
|
|
10879
|
+
"dur_ms",
|
|
10880
|
+
"sampler_frames_missed",
|
|
10881
|
+
"sampler_footprint_delta_mb",
|
|
10882
|
+
"memory_left_mb",
|
|
10883
|
+
"retained_mb_per_cycle",
|
|
10884
|
+
"dimension:component",
|
|
10885
|
+
"dimension:updater"
|
|
10886
|
+
];
|
|
10887
|
+
var MEASURES_AT = (kind) => [...Object.keys(MEASURES).filter((id) => MEASURES[id].at.includes(kind)), "dimension:component", "dimension:updater"];
|
|
10888
|
+
var SPLIT_AFFECTS = [
|
|
10889
|
+
...REACT_MEASURES,
|
|
10890
|
+
...Object.keys(MEASURES).filter((id) => /^(commits_over_|slow_commit_|mount_share_|unsettled_commit_)/.test(id))
|
|
10891
|
+
];
|
|
10892
|
+
var GC_STEP_AFFECTS = [
|
|
10893
|
+
"gc_heap_before_mb",
|
|
10894
|
+
"gc_heap_kept_mb",
|
|
10895
|
+
"gc_heap_garbage_mb",
|
|
10896
|
+
"gc_memory_before_mb",
|
|
10897
|
+
"gc_memory_after_mb",
|
|
10898
|
+
"gc_memory_step_added_mb",
|
|
10899
|
+
"gc_memory_kept_mb",
|
|
10900
|
+
"gc_command_ms",
|
|
10901
|
+
"gc_queue_ms"
|
|
10902
|
+
];
|
|
10903
|
+
var SAMPLER_EDGE_AFFECTS = [
|
|
10904
|
+
"cpu_ms",
|
|
10905
|
+
"sampler.cpu_ms",
|
|
10906
|
+
"sampler_frames_missed",
|
|
10907
|
+
"sampler_longest_gap_ms",
|
|
10908
|
+
"sampler.frames.longest_gap_ms",
|
|
10909
|
+
"sampler.frames.missed",
|
|
10910
|
+
"sampler.frames.presented",
|
|
10911
|
+
...MEMORY_MEASURES.filter((m) => !GC_STEP_AFFECTS.includes(m))
|
|
10912
|
+
];
|
|
10913
|
+
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"];
|
|
10914
|
+
var LOADED_MEMORY = Object.keys(MEASURES).filter((id) => /^(mem|sampler\.memory|ri\.memory)\./.test(id) || id === "memory_left_mb");
|
|
10915
|
+
var END_READ_DECLARES = [{ affects: ["mem.retained_growth_mb", "mem.end_after_gc_mb"], places: ["journey"] }];
|
|
10916
|
+
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`;
|
|
10917
|
+
var PLACE_KEY_KINDS = ["screen", "launch", "press", "drag"];
|
|
10918
|
+
var SDK_A17_PLACES = ["screen", "press", "drag", "step"];
|
|
10919
|
+
var SDK_RELEASES = [
|
|
10920
|
+
{
|
|
10921
|
+
version: "0.1.0-alpha.9",
|
|
10922
|
+
previous: "0.1.0-alpha.8",
|
|
10923
|
+
declares: [{ affects: WINDOW_MODEL_AFFECTS, places: ["press", "drag"] }, { affects: ["mount_share", "mount_share_min", "mount_share_max"] }],
|
|
10924
|
+
uncalibrated: false,
|
|
10925
|
+
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)'
|
|
10926
|
+
},
|
|
10927
|
+
{
|
|
10928
|
+
version: "0.1.0-alpha.10",
|
|
10929
|
+
previous: "0.1.0-alpha.9",
|
|
10930
|
+
declares: [{ affects: SAMPLER_EDGE_AFFECTS }],
|
|
10931
|
+
uncalibrated: false,
|
|
10932
|
+
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)'
|
|
10933
|
+
},
|
|
10934
|
+
{
|
|
10935
|
+
version: "0.1.0-alpha.11",
|
|
10936
|
+
previous: "0.1.0-alpha.10",
|
|
10937
|
+
declares: [
|
|
10938
|
+
{ affects: MEASURES_AT("screen"), places: ["screen"] },
|
|
10939
|
+
{ affects: SPLIT_AFFECTS, places: WINDOWS },
|
|
10940
|
+
{ affects: MEASURES_AT("launch"), places: LAUNCH },
|
|
10941
|
+
{ affects: GC_STEP_AFFECTS, places: ["step"] }
|
|
10942
|
+
],
|
|
10943
|
+
uncalibrated: false,
|
|
10944
|
+
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)`
|
|
10945
|
+
},
|
|
10946
|
+
{
|
|
10947
|
+
version: "0.1.0-alpha.12",
|
|
10948
|
+
previous: "0.1.0-alpha.11",
|
|
10949
|
+
declares: [{ affects: [...new Set([...MEASURES_AT("press"), ...MEASURES_AT("drag")])], places: ["press", "drag"] }],
|
|
10950
|
+
uncalibrated: false,
|
|
10951
|
+
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).',
|
|
10952
|
+
renamed: { places: ["press", "drag"] }
|
|
10953
|
+
},
|
|
10954
|
+
{
|
|
10955
|
+
version: "0.1.0-alpha.13",
|
|
10956
|
+
previous: "0.1.0-alpha.12",
|
|
10957
|
+
declares: "nothing",
|
|
10958
|
+
uncalibrated: false,
|
|
10959
|
+
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).'
|
|
10960
|
+
},
|
|
10961
|
+
{
|
|
10962
|
+
version: "0.1.0-alpha.14",
|
|
10963
|
+
previous: "0.1.0-alpha.13",
|
|
10964
|
+
declares: END_READ_DECLARES,
|
|
10965
|
+
uncalibrated: false,
|
|
10966
|
+
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`
|
|
10967
|
+
},
|
|
10968
|
+
{
|
|
10969
|
+
version: "0.1.0-alpha.15",
|
|
10970
|
+
previous: "0.1.0-alpha.14",
|
|
10971
|
+
declares: "nothing",
|
|
10972
|
+
uncalibrated: false,
|
|
10973
|
+
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)."
|
|
10974
|
+
},
|
|
10975
|
+
{
|
|
10976
|
+
version: "0.1.0-alpha.16",
|
|
10977
|
+
previous: "0.1.0-alpha.15",
|
|
10978
|
+
declares: [
|
|
10979
|
+
{ affects: ["dimension:component", "dimension:updater", "self_ms_top", "component_ms"] },
|
|
10980
|
+
{ affects: [...new Set(PLACE_KEY_KINDS.flatMap((k) => MEASURES_AT(k)))], places: PLACE_KEY_KINDS }
|
|
10981
|
+
],
|
|
10982
|
+
renamed: { places: PLACE_KEY_KINDS, dimensions: ["component", "updater"] },
|
|
10983
|
+
uncalibrated: false,
|
|
10984
|
+
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"
|
|
10985
|
+
},
|
|
10986
|
+
{
|
|
10987
|
+
version: "0.1.0-alpha.17",
|
|
10988
|
+
previous: "0.1.0-alpha.16",
|
|
10989
|
+
declares: [
|
|
10990
|
+
{ affects: [...new Set(SDK_A17_PLACES.flatMap((k) => MEASURES_AT(k)))], places: SDK_A17_PLACES },
|
|
10991
|
+
{ affects: [...WINDOW_DEPENDENT_JOURNEY_MEASURES], places: ["journey"] }
|
|
10992
|
+
],
|
|
10993
|
+
renamed: { places: ["screen", "press", "drag"] },
|
|
10994
|
+
uncalibrated: false,
|
|
10995
|
+
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."
|
|
10996
|
+
}
|
|
10997
|
+
];
|
|
10998
|
+
var DRIVER_RELEASES = [
|
|
10999
|
+
{
|
|
11000
|
+
version: "0.1.0-alpha.12",
|
|
11001
|
+
previous: "0.1.0-alpha.11",
|
|
11002
|
+
declares: "nothing",
|
|
11003
|
+
uncalibrated: false,
|
|
11004
|
+
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.'
|
|
11005
|
+
},
|
|
11006
|
+
{
|
|
11007
|
+
version: "0.1.0-alpha.13",
|
|
11008
|
+
previous: "0.1.0-alpha.12",
|
|
11009
|
+
declares: "nothing",
|
|
11010
|
+
uncalibrated: false,
|
|
11011
|
+
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".'
|
|
11012
|
+
},
|
|
11013
|
+
{
|
|
11014
|
+
version: "0.1.0-alpha.14",
|
|
11015
|
+
previous: "0.1.0-alpha.13",
|
|
11016
|
+
declares: END_READ_DECLARES,
|
|
11017
|
+
uncalibrated: false,
|
|
11018
|
+
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`
|
|
11019
|
+
},
|
|
11020
|
+
{
|
|
11021
|
+
version: "0.1.0-alpha.15",
|
|
11022
|
+
previous: "0.1.0-alpha.14",
|
|
11023
|
+
declares: "nothing",
|
|
11024
|
+
uncalibrated: false,
|
|
11025
|
+
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)."
|
|
11026
|
+
},
|
|
11027
|
+
{
|
|
11028
|
+
version: "0.1.0-alpha.16",
|
|
11029
|
+
previous: "0.1.0-alpha.15",
|
|
11030
|
+
declares: "nothing",
|
|
11031
|
+
uncalibrated: false,
|
|
11032
|
+
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).'
|
|
11033
|
+
},
|
|
11034
|
+
{
|
|
11035
|
+
version: "0.1.0-alpha.17",
|
|
11036
|
+
previous: "0.1.0-alpha.16",
|
|
11037
|
+
declares: [
|
|
11038
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"] },
|
|
11039
|
+
{ affects: MEASURES_AT("step"), places: ["step"] },
|
|
11040
|
+
{ affects: LOADED_MEMORY }
|
|
11041
|
+
],
|
|
11042
|
+
uncalibrated: false,
|
|
11043
|
+
source: "Drag end points move above the platform bottom system-gesture zone (fix/driver-gesture-zone); start points unchanged; on iOS a shortened drag keeps its duration (slower); on Android it keeps its speed (ends sooner). A drag's end, and the duration of a step whose scroll steps contain such drags, can change. W1 (#306): farm sessions record no video or device logs by default (`--capture off`), so the phone carries less load and every memory measure (mem.*, memory_left_mb, sampler.memory.*, ri.memory.*) is measured differently from capture-on alpha.16 sessions (Elastyczność 2026-10-26 15:45 B1-DIFF; controller Stab 4). The declaration only separates alpha.16 from alpha.17: two alpha.17 sessions with capture on and off still compare unflagged until the run's setup.capture (#344) is judged (follow-up). Screens and taps stay compared (REQ-FARM-35)."
|
|
11044
|
+
},
|
|
11045
|
+
{
|
|
11046
|
+
version: "0.1.0-alpha.18",
|
|
11047
|
+
previous: "0.1.0-alpha.17",
|
|
11048
|
+
declares: [{ affects: DRAG_TIME_AND_CPU, places: ["drag"], platforms: ["ios"] }],
|
|
11049
|
+
uncalibrated: false,
|
|
11050
|
+
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)."
|
|
11051
|
+
}
|
|
11052
|
+
];
|
|
11053
|
+
var UNSTABLE_NOISY = {
|
|
11054
|
+
reason: "noisy",
|
|
11055
|
+
spread: "sessions",
|
|
11056
|
+
robust: true,
|
|
11057
|
+
between: false,
|
|
11058
|
+
above: 1,
|
|
11059
|
+
below: 0.75,
|
|
11060
|
+
minMainRuns: 6,
|
|
11061
|
+
minSessions: 3,
|
|
11062
|
+
curve: { render: 0.9244, commit: 0.8326, component: 0.6481 },
|
|
11063
|
+
uncalibrated: false,
|
|
11064
|
+
source: 'Arek 2026-10-18 ~12:00, ~12:20 (noisy on main = unstable, main only, per comparison, hysteresis), 12:50 (within-session spread only: a regression that lands on main moves whole sessions and must stay judged), 13:20 ("zrób na razie wg rekomendacji"; threshold provisional). ' + "Ratio = z99 x the predicted no-change difference SD from the pooled within-session MAD of main (Var(median) = (pi/2) sigma^2 / n) / the smallest warning step at main's median. " + "Replay (noisy/REPORT.md, derivation 22, 1 PR vs 3 main): S23 night 2 -> 1 false worse z 60 (MAD rule 0), sim 0, NI pool 95 -> 64 warnings z 840 (MAD 92); 3 vs 3 0 everywhere; flagged per split S23 17.8, NI 4.9, sim 0.7; judged per split S23 64.5 -> 46.8; flips S23 8 (MAD 21); planted main steps flagged noisy 0 (MAD 5-29 %, with the between-session part 56-99 %). " + "Curve: kept for the judged-rule lookup; the ratio uses the warning step"
|
|
11065
|
+
};
|
|
11066
|
+
|
|
11067
|
+
class ConfigError2 extends Error {
|
|
11068
|
+
problems;
|
|
11069
|
+
constructor(problems) {
|
|
11070
|
+
super(`invalid verdict config: ${problems.join("; ")}`);
|
|
11071
|
+
this.problems = problems;
|
|
11072
|
+
this.name = "ConfigError";
|
|
11073
|
+
}
|
|
11074
|
+
}
|
|
11075
|
+
var noRow = (id) => {
|
|
11076
|
+
throw new ConfigError2([`${id}: no row in the clause table`]);
|
|
11077
|
+
};
|
|
11078
|
+
var NOISE_FITS = CLAUSES["T-WARN-NOISE"].value;
|
|
11079
|
+
var TIER_REL = 0.3;
|
|
11080
|
+
var TIER_UPDATE_MS = { warnMs: 5, errorMs: 15 };
|
|
11081
|
+
var TIER_WORK_MS = { warnMs: 10, errorMs: 30 };
|
|
11082
|
+
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";
|
|
11083
|
+
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';
|
|
11084
|
+
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)';
|
|
11085
|
+
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";
|
|
11086
|
+
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";
|
|
11087
|
+
var noiseJudged = (id, on) => {
|
|
11088
|
+
const fit = NOISE_FITS[id] ?? noRow(id);
|
|
11089
|
+
const noise = { a: 0, b: fit.c, power: 0.5, lift: fit.k95 };
|
|
11090
|
+
return {
|
|
11091
|
+
id,
|
|
11092
|
+
role: "judged",
|
|
11093
|
+
on,
|
|
11094
|
+
places: JUDGED_WINDOWS,
|
|
11095
|
+
devices: ANY_DEVICE,
|
|
11096
|
+
threshold: [{ abs: TIER_UPDATE_MS.warnMs, noise }],
|
|
11097
|
+
uncalibrated: false,
|
|
11098
|
+
source: `k95 lift ${fit.k95}, error tier k99 lift ${fit.k99}, c ${fit.c}; ${NOISE_SOURCE}`,
|
|
11099
|
+
thresholdByWork: { entry: [{ abs: TIER_WORK_MS.warnMs, noise }], mount: [{ abs: TIER_WORK_MS.warnMs, noise }] },
|
|
11100
|
+
errorNoise: { ...noise, lift: fit.k99 }
|
|
11101
|
+
};
|
|
11102
|
+
};
|
|
11103
|
+
var part = (b) => ({ ...b.rel !== undefined ? { rel: b.rel } : {}, ...b.abs !== undefined ? { abs: b.abs } : {} });
|
|
11104
|
+
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 }])));
|
|
11105
|
+
function withClauseValues(base) {
|
|
11106
|
+
if (!base.better)
|
|
11107
|
+
throw new ConfigError2(["better: missing from base config"]);
|
|
11108
|
+
const warnMs = CLAUSES["T-WARN-MS"].value;
|
|
11109
|
+
const errorMs = CLAUSES["TIER-ERROR"].value;
|
|
11110
|
+
const noise = CLAUSES["T-WARN-NOISE"];
|
|
11111
|
+
const memory = CLAUSES["J-MEMORY-RETAINED"];
|
|
11112
|
+
const judgedByNoise = (r) => {
|
|
11113
|
+
const fit = NOISE_FITS[r.id] ?? noRow(r.id);
|
|
11114
|
+
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95 };
|
|
11115
|
+
return {
|
|
11116
|
+
...r,
|
|
11117
|
+
threshold: [{ abs: warnMs.update, noise: n }],
|
|
11118
|
+
thresholdByWork: { entry: [{ abs: warnMs.work, noise: n }], mount: [{ abs: warnMs.work, noise: n }] },
|
|
11119
|
+
errorNoise: { ...n, lift: fit.k99 },
|
|
11120
|
+
uncalibrated: noise.uncalibrated
|
|
11121
|
+
};
|
|
11122
|
+
};
|
|
11123
|
+
const evidence = (r) => {
|
|
11124
|
+
const e = EVIDENCE_BARS.get(r.id) ?? noRow(r.id);
|
|
11125
|
+
const at = r.thresholdAt && Object.fromEntries(Object.keys(r.thresholdAt).map((kind) => [kind, [part((EVIDENCE_BARS.get(`${r.id}_${kind}`) ?? noRow(`${r.id}_${kind}`)).bar)]]));
|
|
11126
|
+
return { ...r, threshold: [part(e.bar)], ...at ? { thresholdAt: at } : {}, uncalibrated: e.uncalibrated };
|
|
11127
|
+
};
|
|
11128
|
+
const rules = base.rules.map((r) => {
|
|
11129
|
+
if (r.id === "memory_retained")
|
|
11130
|
+
return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
|
|
11131
|
+
return r.role === "judged" ? judgedByNoise(r) : evidence(r);
|
|
11132
|
+
});
|
|
11133
|
+
const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
|
|
11134
|
+
const entry = CLAUSES["P-HEAVY-ENTRY"];
|
|
11135
|
+
const unstable = CLAUSES["U-NOISY"];
|
|
11136
|
+
const c = (id) => CLAUSES[id];
|
|
11137
|
+
return {
|
|
11138
|
+
...base,
|
|
11139
|
+
better: { ...base.better, from: c("S-BETTER").value.from, uncalibrated: c("S-BETTER").uncalibrated },
|
|
11140
|
+
minRuns: c("G-MINRUNS").value.runs,
|
|
11141
|
+
baseline: { ...base.baseline, sessions: c("B-BASELINE").value.sessions, uncalibrated: c("B-BASELINE").uncalibrated },
|
|
11142
|
+
levelShift: { ...base.levelShift, confirmSessions: c("M-LEVELSHIFT").value.confirmSessions, uncalibrated: c("M-LEVELSHIFT").uncalibrated },
|
|
11143
|
+
uncertain: { ...base.uncertain, belowMainSessions: c("S-UNCERTAIN").value.belowMainSessions, uncalibrated: c("S-UNCERTAIN").uncalibrated },
|
|
11144
|
+
judgeFrom: { ...base.judgeFrom, mainSessions: c("G-JUDGE-FROM").value.mainSessions, uncalibrated: c("G-JUDGE-FROM").uncalibrated },
|
|
11145
|
+
work: { ...base.work, mountShareAtLeast: c("W-WORKKIND").value.mountShareAtLeast, uncalibrated: c("W-WORKKIND").uncalibrated },
|
|
11146
|
+
...base.windowCount ? { windowCount: { ...base.windowCount, below: c("W-WINDOW-COUNT").value.below, uncalibrated: c("W-WINDOW-COUNT").uncalibrated } } : {},
|
|
11147
|
+
frame: { ...base.frame, ms: c("T-FRAME").value.ms, uncalibrated: c("T-FRAME").uncalibrated },
|
|
11148
|
+
tiers: {
|
|
11149
|
+
...base.tiers,
|
|
11150
|
+
rel: c("T-REL").value.rel,
|
|
11151
|
+
update: { warnMs: warnMs.update, errorMs: errorMs.update },
|
|
11152
|
+
work: { warnMs: warnMs.work, errorMs: errorMs.work },
|
|
11153
|
+
uncalibrated: c("T-WARN-MS").uncalibrated || c("TIER-ERROR").uncalibrated || c("T-REL").uncalibrated,
|
|
11154
|
+
memory: { error: part(memory.value.error), uncalibrated: memory.uncalibrated }
|
|
11155
|
+
},
|
|
11156
|
+
rules,
|
|
11157
|
+
problems: base.problems.map((p) => {
|
|
11158
|
+
if (p.id === "slow-commit")
|
|
11159
|
+
return { ...p, ...frames("P-SLOW-COMMIT") };
|
|
11160
|
+
if (p.id === "heavy-component")
|
|
11161
|
+
return { ...p, ...frames("P-HEAVY-COMPONENT") };
|
|
11162
|
+
if (p.id === "heavy-entry")
|
|
11163
|
+
return { ...p, above: { value: entry.value.ms }, uncalibrated: entry.uncalibrated };
|
|
11164
|
+
return noRow(p.id);
|
|
11165
|
+
}),
|
|
11166
|
+
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 },
|
|
11167
|
+
unstable: base.unstable.map((u) => ({
|
|
11168
|
+
...u,
|
|
11169
|
+
above: unstable.value.above,
|
|
11170
|
+
below: unstable.value.below,
|
|
11171
|
+
minMainRuns: unstable.value.minMainRuns,
|
|
11172
|
+
minSessions: unstable.value.minSessions,
|
|
11173
|
+
uncalibrated: unstable.uncalibrated,
|
|
11174
|
+
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c]))
|
|
11175
|
+
}))
|
|
11176
|
+
};
|
|
11177
|
+
}
|
|
11178
|
+
var SHAPE = {
|
|
11179
|
+
id: "simple-4.25+noise95-99",
|
|
11180
|
+
minRuns: 2,
|
|
11181
|
+
baseline: { sessions: 3, ...GUESS("Q4 / REQ-VER-33: main's newest 3 sessions against one PR session (the quieter setting of the plan 58 simulation)") },
|
|
11182
|
+
repeats: { kind: "every-pr-beyond-every-main" },
|
|
11183
|
+
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" },
|
|
11184
|
+
levelShift: {
|
|
11185
|
+
confirmSessions: 2,
|
|
11186
|
+
uncalibrated: false,
|
|
11187
|
+
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"
|
|
11188
|
+
},
|
|
11189
|
+
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' },
|
|
11190
|
+
work: {
|
|
11191
|
+
mountShareAtLeast: 0.8,
|
|
11192
|
+
uncalibrated: true,
|
|
11193
|
+
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"
|
|
11194
|
+
},
|
|
11195
|
+
instrument: {
|
|
11196
|
+
aspects: ["sdk", "driver", "driverVersion", "journeyVersion", "actionGc"],
|
|
11197
|
+
sdkReleases: SDK_RELEASES,
|
|
11198
|
+
uncalibrated: false,
|
|
11199
|
+
source: 'Arek 2026-10-16 ("Tak, to też ustalaliśmy"): the SDK, the driver and the journey are the instrument (REQ-VER-35); a release declares the measures it affects (REQ-VER-34); a forced GC before each step is a different instrument (REQ-VER-13, docs/plans/gc-before-action.md)',
|
|
11200
|
+
driverReleases: DRIVER_RELEASES,
|
|
11201
|
+
unknownDiffers: ["journeyVersion"]
|
|
11202
|
+
},
|
|
11203
|
+
calibrated: { platforms: ["ios", "android"], uncalibrated: false, source: 'REQ-FARM-26: Android moved in by Arek ("Ok", 2026-10-17) after the S23 A/A, 5 sessions × 3 runs: 0 z 25 false worse under the deciding rule (stats-android/aa/aa-report.md)' },
|
|
11204
|
+
rules: [
|
|
11205
|
+
noiseJudged("commit", { measure: "commit_max_ms" }),
|
|
11206
|
+
noiseJudged("render", { measure: "render_ms" }),
|
|
11207
|
+
noiseJudged("component", { dimension: "component", measure: "self_ms" }),
|
|
11208
|
+
{ ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11209
|
+
{ ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11210
|
+
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"),
|
|
11211
|
+
evidenceRule("background_commit", { measure: "commit_max_ms" }, BACKGROUND, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "background places are evidence only (REQ-MEAS-32)"),
|
|
11212
|
+
evidenceRule("background_render", { measure: "render_ms" }, BACKGROUND, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "background places are evidence only (REQ-MEAS-32)"),
|
|
11213
|
+
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)"),
|
|
11214
|
+
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)"),
|
|
11215
|
+
evidenceRule("launch_render", { measure: "render_ms" }, LAUNCH, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "the launch screen is evidence only (REQ-VER-15)"),
|
|
11216
|
+
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)"),
|
|
11217
|
+
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)"),
|
|
11218
|
+
evidenceRule("step_render", { measure: "render_ms" }, STEP, [{ rel: TIER_REL, abs: TIER_UPDATE_MS.warnMs }], "a declared step is evidence only (REQ-MET-14)"),
|
|
11219
|
+
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)"),
|
|
11220
|
+
evidenceRule("touch", { measure: "touch_max_ms" }, SHOWN2, [{ rel: 0.2, abs: 10 }], "touch response is not judged (ruling)"),
|
|
11221
|
+
evidenceRule("freeze", { measure: "sampler_longest_gap_ms" }, SHOWN2, [{ rel: 0.5, abs: 17 }], "v5.3 warn level"),
|
|
11222
|
+
evidenceRule("freeze_journey", { measure: "sampler.frames.longest_gap_ms" }, JOURNEY, [{ rel: 0.5, abs: 17 }], "v5.3 warn level"),
|
|
11223
|
+
evidenceRule("memory_added", { measure: "memory_left_mb" }, SHOWN2, [{ abs: 2 }], "v5.3 warn level", { thresholdAt: { screen: [{ abs: 10 }] }, devices: ANY_DEVICE }),
|
|
11224
|
+
evidenceRule("memory_launch", { measure: "mem.launch_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11225
|
+
evidenceRule("memory_end_growth", { measure: "mem.end_growth_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11226
|
+
evidenceRule("memory_peak_growth", { measure: "mem.peak_growth_mb" }, JOURNEY, [{ rel: 0.08, abs: 2 }], "v5.3 warn level"),
|
|
11227
|
+
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 %"),
|
|
11228
|
+
evidenceRule("window_cpu", { measure: "cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 20 }], "v5.3 warn level"),
|
|
11229
|
+
evidenceRule("long_tasks", { measure: "longtask_ms" }, SHOWN2, [{ rel: 0.3, abs: 50 }], "v5.3 warn level"),
|
|
11230
|
+
evidenceRule("gc", { measure: "gc_ms" }, SHOWN2, [{ rel: 0.5, abs: 2 }], "v5.3 warn level"),
|
|
11231
|
+
evidenceRule("requests", { measure: "net_requests" }, SHOWN2, [{ rel: 0.5, abs: 1 }], "v5.3 warn level"),
|
|
11232
|
+
evidenceRule("js_cpu", { measure: "js_cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 5 }], "v5.3 warn level"),
|
|
11233
|
+
{
|
|
11234
|
+
id: "memory_retained",
|
|
11235
|
+
role: "judged",
|
|
11236
|
+
on: { measure: "mem.retained_growth_mb" },
|
|
11237
|
+
places: JOURNEY,
|
|
11238
|
+
devices: ANY_DEVICE,
|
|
11239
|
+
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11240
|
+
uncalibrated: true,
|
|
11241
|
+
source: MEMORY_PAIR_SOURCE
|
|
11242
|
+
}
|
|
11243
|
+
],
|
|
11244
|
+
problems: [
|
|
11245
|
+
{
|
|
11246
|
+
id: "slow-commit",
|
|
11247
|
+
on: { measure: "commit_max_ms" },
|
|
11248
|
+
places: JUDGED_WINDOWS,
|
|
11249
|
+
devices: DEVICE,
|
|
11250
|
+
above: { frames: 1 },
|
|
11251
|
+
inRuns: "all",
|
|
11252
|
+
uncalibrated: true,
|
|
11253
|
+
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)"
|
|
11254
|
+
},
|
|
11255
|
+
{
|
|
11256
|
+
id: "heavy-component",
|
|
11257
|
+
on: { dimension: "component", measure: "self_ms" },
|
|
11258
|
+
places: JUDGED_WINDOWS,
|
|
11259
|
+
devices: DEVICE,
|
|
11260
|
+
above: { frames: 1 },
|
|
11261
|
+
inRuns: "all",
|
|
11262
|
+
uncalibrated: true,
|
|
11263
|
+
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)"
|
|
11264
|
+
},
|
|
11265
|
+
{
|
|
11266
|
+
id: "heavy-entry",
|
|
11267
|
+
on: { measure: "render_ms" },
|
|
11268
|
+
places: JUDGED_WINDOWS,
|
|
11269
|
+
devices: DEVICE,
|
|
11270
|
+
above: { value: ENTRY_BUDGET_MS },
|
|
11271
|
+
inRuns: "all",
|
|
11272
|
+
work: ["entry", "mount"],
|
|
11273
|
+
uncalibrated: true,
|
|
11274
|
+
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)'
|
|
11275
|
+
}
|
|
11276
|
+
],
|
|
11277
|
+
budgets: {
|
|
11278
|
+
entry: { value: ENTRY_BUDGET_MS },
|
|
11279
|
+
mount: { value: ENTRY_BUDGET_MS },
|
|
11280
|
+
update: { frames: 1 },
|
|
11281
|
+
uncalibrated: true,
|
|
11282
|
+
source: "ruling d: entry and in-screen mount about 100 ms; an update one frame (16.7 ms on every screen, Arek 2026-10-16)"
|
|
11283
|
+
},
|
|
11284
|
+
freeze: { on: "sampler_longest_gap_ms", uncalibrated: false, source: 'evidence only: the longest freeze covers the whole window, not the transition; "no dropped frames during the transition" (ruling d) needs per-place frame data that is not available' },
|
|
11285
|
+
notJudgedLabels: ["launch"],
|
|
11286
|
+
unstable: [{
|
|
11287
|
+
...UNSTABLE_NOISY,
|
|
11288
|
+
step: "own",
|
|
11289
|
+
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`
|
|
11290
|
+
}],
|
|
11291
|
+
judgeFrom: { mainSessions: 3, uncalibrated: false, source: 'Arek 2026-10-18 (REQ-VER-41): a phone is judged only when main has at least 3 sessions on that phone and journey; with fewer the result is "not enough main yet", not judged, not blocking, values shown. Replay (agents/release/replay, d22): 1 PR vs 2 main on the S23 night pool gave 13 z 60 false worse+warning, 1 vs 3 gave 0' },
|
|
11292
|
+
measuredWith: [
|
|
11293
|
+
{ aspect: "renderer", affects: REACT_MEASURES },
|
|
11294
|
+
{ aspect: "reactExport", affects: MEMORY_MEASURES },
|
|
11295
|
+
{ aspect: "windowModel", affects: WINDOW_MODEL_AFFECTS, places: ["press", "drag"], marks: true },
|
|
11296
|
+
{ aspect: "screenWindowModel", affects: WINDOW_MODEL_AFFECTS, places: ["screen"], marks: true },
|
|
11297
|
+
{ aspect: "cpuSource", affects: ["sampler.cpu_ms"] },
|
|
11298
|
+
{ aspect: "causeSplit", affects: REACT_MEASURES, places: REACT_PLACES, marks: true },
|
|
11299
|
+
{ aspect: "endGc", affects: MEMORY_MEASURES }
|
|
11300
|
+
],
|
|
11301
|
+
tiers: {
|
|
11302
|
+
rel: TIER_REL,
|
|
11303
|
+
update: TIER_UPDATE_MS,
|
|
11304
|
+
work: TIER_WORK_MS,
|
|
11305
|
+
uncalibrated: false,
|
|
11306
|
+
source: `${NOISE_SOURCE}; ${CAPPED_SOURCE}; ${TIERS_SOURCE}; ${MEMORY_PAIR_SOURCE}`,
|
|
11307
|
+
relAtMostErrorMs: true,
|
|
11308
|
+
memory: { error: { rel: 0.3, abs: 150 }, uncalibrated: true }
|
|
11309
|
+
},
|
|
11310
|
+
better: { from: CLAUSES["S-BETTER"].value.from, uncalibrated: false, source: CLAUSES["S-BETTER"].ruling },
|
|
11311
|
+
placeCoverage: {
|
|
11312
|
+
uncalibrated: false,
|
|
11313
|
+
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"
|
|
11314
|
+
},
|
|
11315
|
+
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)' },
|
|
11316
|
+
detail: { componentMs: CLAUSES["C-DETAIL-MS"].value.ms, censored: "certify" },
|
|
11317
|
+
sdkExclusionByPlace: {
|
|
11318
|
+
uncalibrated: false,
|
|
11319
|
+
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)`
|
|
11320
|
+
}
|
|
11321
|
+
};
|
|
11322
|
+
var CONFIG = withClauseValues(SHAPE);
|
|
11323
|
+
var P2A_SLIGHT = withClauseValues({
|
|
11324
|
+
...CONFIG,
|
|
11325
|
+
id: `${CONFIG.id}+p2a-sw`,
|
|
11326
|
+
unstable: CONFIG.unstable.map((u) => ({
|
|
11327
|
+
...u,
|
|
11328
|
+
judgeAt: { line: "error", tier: "warning" },
|
|
11329
|
+
source: `${u.source}; P2a candidate (plan 81, Arek 2026-10-23 ~15:45): judged only past the error line, as Slightly worse, never blocking`
|
|
11330
|
+
}))
|
|
11331
|
+
});
|
|
11332
|
+
var P4_EVEN = {
|
|
11333
|
+
...CONFIG,
|
|
11334
|
+
id: `${CONFIG.id}+p4-even`,
|
|
11335
|
+
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" }
|
|
11336
|
+
};
|
|
11337
|
+
var P3_COMMIT = withClauseValues({
|
|
11338
|
+
...CONFIG,
|
|
11339
|
+
id: `${CONFIG.id}+p3-commit`,
|
|
11340
|
+
commitGrouping: { uncalibrated: true, source: "plan 81 P3; Arek 2026-10-26 [ctl-arek-p81-p3p4]: two windows that share one proven commit (the same slowest commit in every PR run) may be one finding; never merged without it" }
|
|
11341
|
+
});
|
|
10377
11342
|
// apps/agent/src/device-model.ts
|
|
10378
11343
|
import { tmpdir } from "node:os";
|
|
10379
11344
|
import { join as join12 } from "node:path";
|
|
@@ -10452,173 +11417,10 @@ async function detectSimulator(deps, udid) {
|
|
|
10452
11417
|
}
|
|
10453
11418
|
|
|
10454
11419
|
// 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
11420
|
function slackMessage(s, ctx) {
|
|
10534
11421
|
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 }] };
|
|
11422
|
+
return noResultSlackPayload(ctx.branch, ctx.blocked.reason, s ? linkOf(s) : null);
|
|
11423
|
+
return s ? resultSlackPayload(s, { always: ctx.always }) : null;
|
|
10622
11424
|
}
|
|
10623
11425
|
async function postToSlack(url, payload, fetchImpl = fetch) {
|
|
10624
11426
|
try {
|
|
@@ -10703,7 +11505,7 @@ function instrumentFlags(f) {
|
|
|
10703
11505
|
return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
|
|
10704
11506
|
}
|
|
10705
11507
|
var TOKENS_STUB = "// Replaced by `keepsnappy farm open` for one upload. Never commit real tokens.\noutput.keepsnappyTokens = {};\n";
|
|
10706
|
-
var
|
|
11508
|
+
var JOURNEY2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
10707
11509
|
var DEFAULT_TTL = 21600;
|
|
10708
11510
|
var POLL_MS = 5000;
|
|
10709
11511
|
function tokensJs(api, runSetId, tokens, links = maestroLinkFields()) {
|
|
@@ -10790,7 +11592,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
10790
11592
|
if (journeys.length === 0)
|
|
10791
11593
|
problems.push("--journeys a,b is required (the flow names the device runs, one history per journey)");
|
|
10792
11594
|
for (const j of journeys)
|
|
10793
|
-
if (!
|
|
11595
|
+
if (!JOURNEY2.test(j))
|
|
10794
11596
|
problems.push(`--journeys: "${j}" is not a journey name (letters, digits, . _ -)`);
|
|
10795
11597
|
if (new Set(journeys).size !== journeys.length)
|
|
10796
11598
|
problems.push("--journeys lists a journey twice");
|
|
@@ -10818,11 +11620,15 @@ async function farmOpen(deps, f, rt) {
|
|
|
10818
11620
|
const withInstrument = { ...f, ...instrumentFlags(f) };
|
|
10819
11621
|
const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
|
|
10820
11622
|
const base = await baseCommitFor(deps, rt.env, { explicit: f["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
|
|
11623
|
+
const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
|
|
11624
|
+
if (main.warning)
|
|
11625
|
+
rt.print(main.warning);
|
|
10821
11626
|
const rs = await createRunSet(api, {
|
|
10822
11627
|
branch: git.branch,
|
|
10823
11628
|
...git.commit ? { commit_sha: git.commit } : {},
|
|
10824
11629
|
...base ? { base_commit_sha: base } : {},
|
|
10825
11630
|
...await commitFields(deps, rt.env, { commit: git.commit, base }),
|
|
11631
|
+
...main.merges ? { merges: main.merges } : {},
|
|
10826
11632
|
device: { model: device.model, ...device.os ? { os: device.os } : {}, provider: device.provider, ...f["device-id"]?.trim() ? { id: f["device-id"].trim() } : {} },
|
|
10827
11633
|
scenarios: journeys,
|
|
10828
11634
|
journeys: journeyInfo,
|
|
@@ -10939,7 +11745,7 @@ async function settle(api, runSetId, o) {
|
|
|
10939
11745
|
}
|
|
10940
11746
|
}
|
|
10941
11747
|
var FAILED_SESSION = new Set(["failed", "error", "timedout", "stopped", "skipped"]);
|
|
10942
|
-
var
|
|
11748
|
+
var median6 = (xs) => {
|
|
10943
11749
|
const v = [...xs].sort((a, b) => a - b);
|
|
10944
11750
|
return v.length % 2 ? v[(v.length - 1) / 2] : (v[v.length / 2 - 1] + v[v.length / 2]) / 2;
|
|
10945
11751
|
};
|
|
@@ -10961,10 +11767,11 @@ function brokenRun(state, runs, session) {
|
|
|
10961
11767
|
const others = ordered.filter((r) => r !== last && r.journey === last.journey && has(r)).map((r) => byId.get(r.run_id).windows ?? 0);
|
|
10962
11768
|
const journey = typeof s.usual_windows === "number" && s.usual_windows > 0 ? s.usual_windows : null;
|
|
10963
11769
|
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
|
-
|
|
11770
|
+
const m = mine.length >= 2 ? median6(mine) : null;
|
|
11771
|
+
const floor = CLAUSES["W-WINDOW-COUNT"].value.below;
|
|
11772
|
+
const changed = m !== null && mine.every((n) => n >= floor * m && floor * n <= m) && (journey === null || m < floor * journey);
|
|
11773
|
+
const usual = changed ? m : journey ?? (others.length ? median6(others) : null);
|
|
11774
|
+
if (usual !== null && (s.windows ?? 0) < floor * usual)
|
|
10968
11775
|
return last;
|
|
10969
11776
|
return next;
|
|
10970
11777
|
}
|
|
@@ -11000,6 +11807,7 @@ async function farmClose(deps, f, rt) {
|
|
|
11000
11807
|
await scrubTokens(deps, state.tokens_js, rt);
|
|
11001
11808
|
}
|
|
11002
11809
|
}
|
|
11810
|
+
var blockedReason = (session) => `No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}.`;
|
|
11003
11811
|
async function closeRunSet(deps, f, rt, state, statePath) {
|
|
11004
11812
|
const settleSeconds = Number(f["settle-seconds"] ?? 60);
|
|
11005
11813
|
const maxWaitSeconds = Number(f["max-wait-seconds"] ?? Math.max(300, settleSeconds * 3));
|
|
@@ -11054,7 +11862,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11054
11862
|
const blocked = summary.runs.valid === 0 && withData === 0;
|
|
11055
11863
|
if (blocked) {
|
|
11056
11864
|
rt.print("");
|
|
11057
|
-
rt.print(`KeepSnappy result for ${where}:
|
|
11865
|
+
rt.print(`KeepSnappy result for ${where}: ${BLOCKED_WORD} — no iteration sent any data${session.known && !session.passed ? ` (device session ${session.status})` : ""}.`);
|
|
11058
11866
|
rt.print(" Check that the build carries the SDK, the activation link ran, and the device reached the API.");
|
|
11059
11867
|
rt.print(`Dashboard: ${summary.dashboard_url}`);
|
|
11060
11868
|
} else if (f.wait) {
|
|
@@ -11075,36 +11883,14 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
11075
11883
|
`);
|
|
11076
11884
|
await githubOutputs(deps, rt, { state: state2.result.state ?? "none", dashboard_url: summary.dashboard_url, run_set_id: state.run_set_id });
|
|
11077
11885
|
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
|
-
}
|
|
11886
|
+
if (step)
|
|
11887
|
+
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
11888
|
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
|
-
}
|
|
11889
|
+
if (hook && blocked) {
|
|
11890
|
+
const r = await postToSlack(hook, slackMessage(summary, { branch: state.branch, blocked: { reason: blockedReason(session) } }), rt.fetch ?? fetch);
|
|
11891
|
+
rt.print(r.ok ? "Slack: posted" : `Slack: not posted (${r.error}); the job is not affected`);
|
|
11892
|
+
} else if (hook)
|
|
11893
|
+
rt.print("Slack: results are posted after the verdict is computed");
|
|
11108
11894
|
if (blocked)
|
|
11109
11895
|
return EXIT.infra;
|
|
11110
11896
|
if (f["fail-on-worse"] && isBlocking(summary)) {
|
|
@@ -11417,7 +12203,7 @@ function nodeFarmRunPorts(deps, rt) {
|
|
|
11417
12203
|
}
|
|
11418
12204
|
var TUNNEL_TARGET = /^(?:localhost|127\.0\.0\.1):(\d{1,5})$/;
|
|
11419
12205
|
var ENV_PAIR = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
11420
|
-
var
|
|
12206
|
+
var DEVICE2 = /^(.+)-([^-]+)$/;
|
|
11421
12207
|
var SECRETISH = /KEY|TOKEN|SECRET|PASSWORD/i;
|
|
11422
12208
|
var RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
11423
12209
|
function planFarmRun(f) {
|
|
@@ -11427,7 +12213,7 @@ function planFarmRun(f) {
|
|
|
11427
12213
|
const platform = f.platform ?? "ios";
|
|
11428
12214
|
if (platform !== "ios" && platform !== "android")
|
|
11429
12215
|
problems.push("--platform must be ios or android");
|
|
11430
|
-
if (!f.device || !
|
|
12216
|
+
if (!f.device || !DEVICE2.test(f.device))
|
|
11431
12217
|
problems.push('--device "<model>-<os>" is required, e.g. "iPhone 15 Pro-17"');
|
|
11432
12218
|
const driver = f.driver ?? "maestro";
|
|
11433
12219
|
if (driver !== "maestro" && driver !== "appium")
|
|
@@ -11436,7 +12222,7 @@ function planFarmRun(f) {
|
|
|
11436
12222
|
if (journeys.length === 0)
|
|
11437
12223
|
problems.push("--journeys a,b is required");
|
|
11438
12224
|
for (const j of journeys)
|
|
11439
|
-
if (!
|
|
12225
|
+
if (!JOURNEY2.test(j))
|
|
11440
12226
|
problems.push(`--journeys: "${j}" is not a journey name (letters, digits, . _ -)`);
|
|
11441
12227
|
const repeat = Number(f.repeat ?? 5);
|
|
11442
12228
|
if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100)
|
|
@@ -11533,7 +12319,7 @@ function printPlan(p, f, rt) {
|
|
|
11533
12319
|
line(` env ${k}=${substitute(v, {})}`);
|
|
11534
12320
|
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${join16(p.out, `browserstack.${p.platform}.json`)}`);
|
|
11535
12321
|
line(" 7. always: stop the tunnel, put the empty stub back over the tokens file");
|
|
11536
|
-
line(
|
|
12322
|
+
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
12323
|
}
|
|
11538
12324
|
var ANNOTATION = /^::(error|warning|notice)( [^:]*)?::/;
|
|
11539
12325
|
async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
@@ -11636,7 +12422,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
11636
12422
|
let driver = { ok: true, message: null };
|
|
11637
12423
|
let closeCode = EXIT.ok;
|
|
11638
12424
|
try {
|
|
11639
|
-
const [model, os] = [
|
|
12425
|
+
const [model, os] = [DEVICE2.exec(f.device)[1], DEVICE2.exec(f.device)[2]];
|
|
11640
12426
|
const openCode = await farmOpen(deps, {
|
|
11641
12427
|
...f,
|
|
11642
12428
|
journeys: p.journeys.join(","),
|
|
@@ -11992,9 +12778,13 @@ class Rows {
|
|
|
11992
12778
|
this.rows.push({ text: line, short: false, notice: false });
|
|
11993
12779
|
return this;
|
|
11994
12780
|
}
|
|
12781
|
+
twin(full, short) {
|
|
12782
|
+
this.rows.push({ text: full, short: false, notice: false }, { text: short, short: true, shortOnly: true });
|
|
12783
|
+
return this;
|
|
12784
|
+
}
|
|
11995
12785
|
render(form, left = []) {
|
|
11996
12786
|
if (form !== "short")
|
|
11997
|
-
return this.rows.map((r) => r.text).join(`
|
|
12787
|
+
return this.rows.filter((r) => !r.shortOnly).map((r) => r.text).join(`
|
|
11998
12788
|
`);
|
|
11999
12789
|
const kept = [];
|
|
12000
12790
|
for (const r of this.rows) {
|
|
@@ -12023,7 +12813,7 @@ function formatFinding(f, opts = {}) {
|
|
|
12023
12813
|
r.both(...laneContextLines(f.lane.context, f.lane.journey, f.lane.phone_name, opts.verbose, opts.form === "short"));
|
|
12024
12814
|
r.full(line("Why", f.answer.why));
|
|
12025
12815
|
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("; ")}` : ""}`));
|
|
12816
|
+
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
12817
|
r.both(line("Id", f.id));
|
|
12028
12818
|
if (f.why.components.length) {
|
|
12029
12819
|
r.full(line("Components", ""));
|
|
@@ -12051,7 +12841,7 @@ function formatFinding(f, opts = {}) {
|
|
|
12051
12841
|
}
|
|
12052
12842
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
12053
12843
|
var LANES_SHOWN = 8;
|
|
12054
|
-
var
|
|
12844
|
+
var STATE_WORDS3 = { present: "still present", resolved: "resolved", "not-judged": "not judged in the newest run set" };
|
|
12055
12845
|
function measuredLines(l, r, opts) {
|
|
12056
12846
|
const f = l.filters;
|
|
12057
12847
|
const scope = [f.branch ?? "all branches", f.journey, f.phone].filter(Boolean).join(" · ");
|
|
@@ -12069,6 +12859,7 @@ function measuredLines(l, r, opts) {
|
|
|
12069
12859
|
};
|
|
12070
12860
|
if (m.lanes.length === 1) {
|
|
12071
12861
|
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
12862
|
+
r.full(...(n.session_speed ?? []).map((line) => ` ${line}`));
|
|
12072
12863
|
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
12073
12864
|
return [];
|
|
12074
12865
|
}
|
|
@@ -12078,6 +12869,7 @@ function measuredLines(l, r, opts) {
|
|
|
12078
12869
|
const keep = !x.judged || !!x.context;
|
|
12079
12870
|
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12080
12871
|
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
12872
|
+
r.full(...(x.session_speed ?? []).map((line) => ` ${line}`));
|
|
12081
12873
|
}
|
|
12082
12874
|
if (m.lanes.length > shown.length)
|
|
12083
12875
|
r.full(` … ${m.lanes.length - shown.length} more (--journey / --phone narrow it)`);
|
|
@@ -12096,7 +12888,7 @@ function countsLine2(l) {
|
|
|
12096
12888
|
].filter(Boolean);
|
|
12097
12889
|
const state = l.filters.state;
|
|
12098
12890
|
const unjudged = l.measured.lanes.filter((x) => !x.judged);
|
|
12099
|
-
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${
|
|
12891
|
+
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
12892
|
const heated = c.heat_runs ?? 0;
|
|
12101
12893
|
const more = [
|
|
12102
12894
|
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (run health, layer 3)` : "",
|
|
@@ -12144,6 +12936,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12144
12936
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
12145
12937
|
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
|
|
12146
12938
|
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
12939
|
+
r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
|
|
12147
12940
|
for (const f of l.worst)
|
|
12148
12941
|
r.both(` ${f.id} ${f.headline}`);
|
|
12149
12942
|
if (l.present_findings > l.worst.length)
|
|
@@ -12283,16 +13076,29 @@ function formatHistory(h, opts = {}) {
|
|
|
12283
13076
|
const r = new Rows;
|
|
12284
13077
|
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
13078
|
const ms = h.measures.slice(0, 4);
|
|
13079
|
+
const renamed = h.renamed;
|
|
13080
|
+
const renamedAt = renamed ? h.points.findIndex((p) => p.run_id === renamed.from_run_id) : -1;
|
|
13081
|
+
const renamedShown = renamedAt >= h.points.length - SHORT_POINTS;
|
|
12286
13082
|
if (h.points.length) {
|
|
12287
13083
|
r.both(` ${"when".padEnd(17)}${ms.map((m) => (m.label.length > 22 ? `${m.label.slice(0, 21)}…` : m.label).padStart(24)).join("")} commit`);
|
|
12288
13084
|
h.points.forEach((p, i) => {
|
|
12289
|
-
|
|
13085
|
+
if (renamed && i === renamedAt) {
|
|
13086
|
+
const full = `${placeNamesChangedTitle(renamed.sdk)}: ${placeWasText(renamed.legacy_window)}`;
|
|
13087
|
+
if (renamedShown)
|
|
13088
|
+
r.twin(full, `${placeChartMarker(sdkTag(renamed.sdk))}: ${placeWasText(renamed.legacy_window)}`);
|
|
13089
|
+
else
|
|
13090
|
+
r.full(full);
|
|
13091
|
+
}
|
|
13092
|
+
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
13093
|
});
|
|
12291
13094
|
}
|
|
12292
13095
|
if (h.findings.length)
|
|
12293
13096
|
r.both("", "Findings on this window:", ...h.findings.map((f) => ` ${f.answer.headline} — ${f.where.branch} (${f.id})`));
|
|
12294
13097
|
const older = Math.max(0, h.points.length - SHORT_POINTS);
|
|
12295
|
-
return r.render(opts.form,
|
|
13098
|
+
return r.render(opts.form, [
|
|
13099
|
+
...older ? [plural2(older, "older run")] : [],
|
|
13100
|
+
...renamed && renamedAt >= 0 && !renamedShown ? [placeNamesChangedOmitted(sdkTag(renamed.sdk))] : []
|
|
13101
|
+
]);
|
|
12296
13102
|
}
|
|
12297
13103
|
function formatCommits(c) {
|
|
12298
13104
|
return [
|
|
@@ -12412,7 +13218,7 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12412
13218
|
if (s.memory)
|
|
12413
13219
|
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
13220
|
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)}
|
|
13221
|
+
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}`, ...renamedReleaseLines(v.renamed));
|
|
12416
13222
|
for (const c of h.changes.filter((c) => c.from_run_set_id === s.run_set_id)) {
|
|
12417
13223
|
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
13224
|
if (k && s !== latest)
|
|
@@ -12425,6 +13231,23 @@ function formatMainSessions(h, opts = {}) {
|
|
|
12425
13231
|
...otherChanges ? [plural2(otherChanges, "main change") + " of the other recent sessions"] : []
|
|
12426
13232
|
]);
|
|
12427
13233
|
}
|
|
13234
|
+
function formatMainChanges(m) {
|
|
13235
|
+
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.");
|
|
13236
|
+
if (!m.changes.length)
|
|
13237
|
+
r.both("No main changes recorded for this journey and phone.");
|
|
13238
|
+
const short = (sha) => sha?.slice(0, 7) ?? "–";
|
|
13239
|
+
for (const c of m.changes) {
|
|
13240
|
+
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]}`);
|
|
13241
|
+
r.both(` Commits ${short(c.commits[0])} → ${short(c.commits[1])}`);
|
|
13242
|
+
if (c.prs === null)
|
|
13243
|
+
r.both(" PRs not known: no stored merges reach the session before (an older agent, a shallow clone, or no session before)");
|
|
13244
|
+
else if (!c.prs.length)
|
|
13245
|
+
r.both(" No PRs in this range: both sessions ran on the same commit");
|
|
13246
|
+
for (const p of c.prs ?? [])
|
|
13247
|
+
r.both(` ${p.number === null ? `${short(p.sha)} ${p.title} (no PR)` : `#${p.number} ${p.title}`}`);
|
|
13248
|
+
}
|
|
13249
|
+
return r.render("full");
|
|
13250
|
+
}
|
|
12428
13251
|
function formatMainSessionPlaces(p, opts = {}) {
|
|
12429
13252
|
const r = new Rows;
|
|
12430
13253
|
if (p.status !== "ok")
|
|
@@ -12432,7 +13255,7 @@ function formatMainSessionPlaces(p, opts = {}) {
|
|
|
12432
13255
|
const baselineSessions = p.baseline_sessions;
|
|
12433
13256
|
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
12434
13257
|
for (const x of p.places)
|
|
12435
|
-
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, p.places.filter((row) => row.place === x.place).map((row) => row.ref))}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}`);
|
|
13258
|
+
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, p.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)}` : ""}`);
|
|
12436
13259
|
const inNorm = p.places.filter((x) => x.tier === "same").length;
|
|
12437
13260
|
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
12438
13261
|
}
|
|
@@ -12443,7 +13266,7 @@ function formatMainVsPrevious(m, opts = {}) {
|
|
|
12443
13266
|
if (m.cut === "unknown")
|
|
12444
13267
|
r.both("History restart: unknown; current summaries unavailable.");
|
|
12445
13268
|
else if (m.cut)
|
|
12446
|
-
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart
|
|
13269
|
+
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart.`, ...renamedReleaseLines(m.cut.renamed));
|
|
12447
13270
|
if (m.not_computed.length)
|
|
12448
13271
|
r.both(`not_computed: ${m.not_computed.join(", ")}`);
|
|
12449
13272
|
if (m.stale.length)
|
|
@@ -12456,6 +13279,22 @@ function formatMainVsPrevious(m, opts = {}) {
|
|
|
12456
13279
|
const smaller = m.places.filter((p, i) => i >= SHORT_PLACES && p.status === "compared").length;
|
|
12457
13280
|
return r.render(opts.form, smaller ? [plural2(smaller, "smaller mover")] : []);
|
|
12458
13281
|
}
|
|
13282
|
+
function renamedReleaseLines(renamed) {
|
|
13283
|
+
if (!renamed)
|
|
13284
|
+
return [];
|
|
13285
|
+
return [...renamed.places.length ? [` ${placeNamesChangedTitle(renamed.sdk)}`] : [], ...renamed.dimensions.includes("component") ? [` ${componentNotJoinedText(renamed.sdk)}`] : []];
|
|
13286
|
+
}
|
|
13287
|
+
function renamedLines(renamed, places) {
|
|
13288
|
+
if (!renamed)
|
|
13289
|
+
return [];
|
|
13290
|
+
const asked = places.filter((p) => p.legacy_window !== undefined);
|
|
13291
|
+
const windows = [...new Map(asked.map((p) => [p.window, p.legacy_window ?? null])).entries()];
|
|
13292
|
+
const component = renamed.dimensions.includes("component") && places.some((p) => p.ref.startsWith("b:component:"));
|
|
13293
|
+
return [
|
|
13294
|
+
...windows.length ? [` ${placeNamesChangedTitle(renamed.sdk)}`, ...windows.map(([w, old]) => ` ${windowQualified(w)}: ${old ? placeWasText(old) : PLACE_NOT_JOINED_TEXT}`)] : [],
|
|
13295
|
+
...component ? [` ${componentNotJoinedText(renamed.sdk)}`] : []
|
|
13296
|
+
];
|
|
13297
|
+
}
|
|
12459
13298
|
function formatPlaceTrend(t, opts = {}) {
|
|
12460
13299
|
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
12461
13300
|
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
@@ -12467,6 +13306,8 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12467
13306
|
r.both(`History restarts unknown: ${session.run_set_id} (no current main summary in the history window)`);
|
|
12468
13307
|
for (const restart of t.restarts.filter((x) => x.from_run_set_id === session.run_set_id)) {
|
|
12469
13308
|
r.add(k, `History restarts: ${restart.label} (${restart.cuts.join(", ")})`);
|
|
13309
|
+
for (const line of renamedLines(restart.renamed, t.places))
|
|
13310
|
+
r.add(k, line);
|
|
12470
13311
|
if (!k)
|
|
12471
13312
|
olderRestarts++;
|
|
12472
13313
|
}
|
|
@@ -12477,7 +13318,7 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
12477
13318
|
const unit = place.unit ? ` ${place.unit}` : "";
|
|
12478
13319
|
if (!k)
|
|
12479
13320
|
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}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
13321
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${value.window ?? place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
12481
13322
|
}
|
|
12482
13323
|
}
|
|
12483
13324
|
if (!t.sessions.length)
|
|
@@ -12554,6 +13395,7 @@ var sessionPlaces = (t, f) => get(t, "v1/sessions/places", {
|
|
|
12554
13395
|
});
|
|
12555
13396
|
var SESSION_SORTS = ["median", "p90", "mad"];
|
|
12556
13397
|
var mainSessions = (t, q) => get(t, "v1/main/sessions", { ...q });
|
|
13398
|
+
var mainChanges = (t, q) => get(t, "v1/main/changes", { ...q });
|
|
12557
13399
|
var mainSessionPlaces = (t, runSetId, q) => get(t, `v1/main/sessions/${encodeURIComponent(runSetId)}/places`, { ...q });
|
|
12558
13400
|
var mainVsPrevious = (t, q) => {
|
|
12559
13401
|
const { run_set, ...query } = q;
|
|
@@ -12573,6 +13415,7 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
12573
13415
|
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
13416
|
(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
13417
|
main --journey KEY --phone MODEL [--sessions N | --session ID] [--vs-previous] [--measure REF] [--limit N] [--offset N] [--short] [--json]
|
|
13418
|
+
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
13419
|
layers [--json]
|
|
12577
13420
|
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
12578
13421
|
(the words after the journey are one window name; more windows with repeated --window)
|
|
@@ -12602,6 +13445,7 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12602
13445
|
state: { type: "string" },
|
|
12603
13446
|
session: { type: "string" },
|
|
12604
13447
|
"vs-previous": { type: "boolean" },
|
|
13448
|
+
changes: { type: "boolean" },
|
|
12605
13449
|
limit: { type: "string" },
|
|
12606
13450
|
offset: { type: "string" },
|
|
12607
13451
|
"run-set": { type: "string" },
|
|
@@ -12662,6 +13506,11 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
12662
13506
|
if (sessions !== undefined && (o.session || o["vs-previous"]))
|
|
12663
13507
|
throw new UsageError(["--sessions is for the session list"]);
|
|
12664
13508
|
const lane = { journey: o.journey, phone: o.phone };
|
|
13509
|
+
if (o.changes) {
|
|
13510
|
+
if (sessions !== undefined || o.session || o["vs-previous"] || o.measure || o.short || offset !== undefined)
|
|
13511
|
+
throw new UsageError(["--changes takes only --journey, --phone, --limit and --json"]);
|
|
13512
|
+
return out(await mainChanges(t, { ...lane, ...limit ? { limit } : {} }), formatMainChanges);
|
|
13513
|
+
}
|
|
12665
13514
|
if (o["vs-previous"])
|
|
12666
13515
|
return out(await mainVsPrevious(t, { ...lane, run_set: o.session, measure: o.measure, limit, offset }), formatMainVsPrevious);
|
|
12667
13516
|
if (o.session)
|
|
@@ -12868,7 +13717,7 @@ var USAGE = `keepsnappy <command>
|
|
|
12868
13717
|
[--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
12869
13718
|
[--no-markers] [-- <e2e command...>]
|
|
12870
13719
|
(--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: "
|
|
13720
|
+
so with no computed verdict it checks nothing: "${UNCHECKED_GATE}", exit 0)
|
|
12872
13721
|
measure --repeat N [--arms A=<app|bundle>,B=<app|bundle>] [--arm-env 'A:KEY=VAL' ...] [--bundle-id ID] [--platform ios-sim|ios-device]
|
|
12873
13722
|
[--device UDID] [--out DIR] [--timeout SEC] [--ri-capture observations.ndjson] [--mem-window NAME]
|
|
12874
13723
|
[--clock-offset-ms N] [--no-markers]
|
|
@@ -12898,7 +13747,7 @@ async function bundleIdOf(deps, target, explicit) {
|
|
|
12898
13747
|
if (explicit)
|
|
12899
13748
|
return explicit;
|
|
12900
13749
|
if (!target)
|
|
12901
|
-
throw new
|
|
13750
|
+
throw new ConfigError(["give --bundle-id (or an arm whose target is a bundle id or .app)"]);
|
|
12902
13751
|
if (!isBuildPath(target))
|
|
12903
13752
|
return target;
|
|
12904
13753
|
const r = await deps.spawner.run("plutil", ["-extract", "CFBundleIdentifier", "raw", `${target.replace(/\/$/, "")}/Info.plist`]);
|