stim 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
- package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
- package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
- package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
- package/dist/android-cas-PnKfAdB2.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
- package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
- package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
- package/dist/build-progress-C2Jv1AtA.mjs +872 -0
- package/dist/build-slots-Df1ApSuH.mjs +127 -0
- package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
- package/dist/cli-0cX5s4NM.mjs +45 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-C2IS36Qf.mjs +1333 -0
- package/dist/collector-run.mjs +8 -7
- package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
- package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
- package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
- package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
- package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CjzLoNs0.mjs +80 -0
- package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
- package/dist/device-capacity-DMuKC7nx.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +769 -0
- package/dist/device-ios-DMQBUPus.mjs +381 -0
- package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
- package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
- package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
- package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
- package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
- package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
- package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
- package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
- package/dist/hosted-macos-DwowNU7W.mjs +447 -0
- package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
- package/dist/in-use-CndPMilN.mjs +110 -0
- package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
- package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
- package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
- package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
- package/dist/machine-usage-D_YYMzfR.mjs +283 -0
- package/dist/machines-CYtOCqcj.mjs +259 -0
- package/dist/macos-BwtbUtTN.mjs +468 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
- package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
- package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
- package/dist/native-run-DFDoIRbY.mjs +147 -0
- package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
- package/dist/offload-worker.d.mts +121 -0
- package/dist/offload-worker.mjs +601 -0
- package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
- package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
- package/dist/prebuild-BvxDlD6C.mjs +213 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-GEXp4iBT.mjs +37 -0
- package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
- package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
- package/dist/run-B-xqRrsg.mjs +119 -0
- package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
- package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
- package/dist/server-expo-KJeD109I.mjs +2 -0
- package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
- package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
- package/dist/settings.schema.json +357 -2
- package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
- package/dist/slot-launch-BY0BrOLh.mjs +65 -0
- package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-DncpJvWL.mjs +16 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CPO4wRg7.mjs +2 -0
- package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/state-BxCgeTQu.mjs +41 -0
- package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
- package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
- package/dist/stats-DBLLFmKd.mjs +66 -0
- package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
- package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
- package/dist/stop-Bvf1MflS.mjs +48 -0
- package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
- package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
- package/dist/supervisor-run.d.mts +44 -12
- package/dist/supervisor-run.mjs +276 -33
- package/dist/support-CDQbDkL5.mjs +315 -0
- package/dist/support-WLOzVKUv.mjs +176 -0
- package/dist/teardown-BYuQafiA.mjs +718 -0
- package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
- package/dist/warm-progress-DVHLN4uC.mjs +463 -0
- package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
- package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
- package/dist/web-run.mjs +326 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-CL2V2Ons.mjs +371 -0
- package/dist/worktree-BT_uULFx.mjs +2 -0
- package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
- package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
- package/package.json +10 -5
- package/shim/bundle-response.cjs +55 -10
- package/shim/bundle-response.d.cts +1 -0
- package/shim/expo-metro-config.cjs +10 -2
- package/dist/build-progress-l7d-X-ZP.mjs +0 -247
- package/dist/in-use-wYEaKQM9.mjs +0 -249
- package/dist/ownership-PUP4Dwai.mjs +0 -1138
- package/dist/server-expo-BeYwP_Xy.mjs +0 -2
- package/dist/slot-launch-BIWEW-wI.mjs +0 -19
- package/dist/start-Rw2GD3nV.mjs +0 -2
- package/dist/stats-CaSwmsSZ.mjs +0 -62
- package/dist/stats-D8rUTPHd.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/worktree-3cBa-bQt.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { D as withConfigLock$1 } from "./config-D0DbpZtM.mjs";
|
|
2
|
+
import { n as ownership_claim_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
3
|
+
import { a as updateWorkspaceState, t as clearWorkspaceStateKey } from "./workspace-state-CL2V2Ons.mjs";
|
|
4
|
+
import { a as formatElapsed, u as plural } from "./command-output-CliHHn7a.mjs";
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, LAST_BUILD_KEYS, STATS_VERSION, decodeStats, parseMissReason, readRunEstimates, readStats, statsFile, statsProjectKey as statsProjectKey$1, trimSamples, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
|
|
8
|
+
//#region src/engine/stats.ts
|
|
9
|
+
const PLACEMENT_MAX_AGE_MS = 6048e5;
|
|
10
|
+
function emptyStats() {
|
|
11
|
+
return {
|
|
12
|
+
version: STATS_VERSION,
|
|
13
|
+
machine: {},
|
|
14
|
+
projects: {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function updateStats(record, run, now) {
|
|
18
|
+
const at = new Date(now).toISOString();
|
|
19
|
+
const durationMs = wholeMs(run.durationMs);
|
|
20
|
+
const phases = {
|
|
21
|
+
coldBuildMs: wholeMs(run.coldBuildMs),
|
|
22
|
+
podsMs: wholeMs(run.podsMs)
|
|
23
|
+
};
|
|
24
|
+
const projects = { ...record.projects };
|
|
25
|
+
const scope = { ...projects[run.projectKey] };
|
|
26
|
+
const machine = { ...record.machine };
|
|
27
|
+
const before = scope[run.platform] ?? null;
|
|
28
|
+
const credit = creditMs(before, run, durationMs);
|
|
29
|
+
scope[run.platform] = applyRun(before, run, {
|
|
30
|
+
at,
|
|
31
|
+
durationMs,
|
|
32
|
+
credit,
|
|
33
|
+
phases
|
|
34
|
+
});
|
|
35
|
+
projects[run.projectKey] = scope;
|
|
36
|
+
machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
|
|
37
|
+
at,
|
|
38
|
+
durationMs,
|
|
39
|
+
credit,
|
|
40
|
+
phases
|
|
41
|
+
});
|
|
42
|
+
const history = { ...record.history };
|
|
43
|
+
if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
|
|
44
|
+
const outcome = isHit(run.cacheHit) ? "hit" : "cold";
|
|
45
|
+
const project = { ...history[run.projectKey] };
|
|
46
|
+
const lists = { ...project[run.platform] };
|
|
47
|
+
const sample = {
|
|
48
|
+
at,
|
|
49
|
+
durationMs,
|
|
50
|
+
phases: wholePhases(run.phases),
|
|
51
|
+
...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
|
|
52
|
+
};
|
|
53
|
+
lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
|
|
54
|
+
project[run.platform] = lists;
|
|
55
|
+
history[run.projectKey] = project;
|
|
56
|
+
}
|
|
57
|
+
let placements = record.placements;
|
|
58
|
+
let buildMachines = record.buildMachines;
|
|
59
|
+
if (run.placement) {
|
|
60
|
+
const placement = buildPlacement(run, before, at);
|
|
61
|
+
placements = trimPlacements([...placements ?? [], placement], now);
|
|
62
|
+
if (placement.machine && placement.decision !== "here") buildMachines = {
|
|
63
|
+
...buildMachines,
|
|
64
|
+
[placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: STATS_VERSION,
|
|
69
|
+
machine,
|
|
70
|
+
projects,
|
|
71
|
+
...Object.keys(history).length ? { history } : {},
|
|
72
|
+
...placements?.length ? { placements } : {},
|
|
73
|
+
...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buildPlacement(run, before, at) {
|
|
77
|
+
const placement = run.placement;
|
|
78
|
+
const buildMs = wholeMs(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
|
|
79
|
+
const localEstimateMs = wholeMs(before?.lastColdBuildMs);
|
|
80
|
+
return {
|
|
81
|
+
at,
|
|
82
|
+
project: run.projectKey,
|
|
83
|
+
platform: run.platform,
|
|
84
|
+
decision: placement.decision,
|
|
85
|
+
reason: placement.reason,
|
|
86
|
+
...placement.machine ? { machine: placement.machine } : {},
|
|
87
|
+
...buildMs > 0 ? { buildMs } : {},
|
|
88
|
+
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
89
|
+
...run.failed ? { failed: true } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function applyPlacement(totals, placement) {
|
|
93
|
+
const next = totals ? { ...totals } : {
|
|
94
|
+
offloaded: 0,
|
|
95
|
+
offloadedMs: 0,
|
|
96
|
+
savedMs: 0,
|
|
97
|
+
fallbacks: 0
|
|
98
|
+
};
|
|
99
|
+
if (placement.decision === "fell-back") {
|
|
100
|
+
next.fallbacks += 1;
|
|
101
|
+
next.lastFallbackAt = placement.at;
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
next.offloaded += 1;
|
|
105
|
+
next.offloadedMs += placement.buildMs ?? 0;
|
|
106
|
+
if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
|
|
107
|
+
next.lastOffloadAt = placement.at;
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
function trimPlacements(placements, now) {
|
|
111
|
+
return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
|
|
112
|
+
}
|
|
113
|
+
function recordRunStats(run, now) {
|
|
114
|
+
return withConfigLock$1(() => {
|
|
115
|
+
const path = statsFile();
|
|
116
|
+
const loaded = loadForUpdate(path, now);
|
|
117
|
+
if (loaded.newerVersion !== null) return {
|
|
118
|
+
recorded: false,
|
|
119
|
+
note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
|
|
120
|
+
};
|
|
121
|
+
writeStats(path, updateStats(loaded.record, run, now));
|
|
122
|
+
return {
|
|
123
|
+
recorded: true,
|
|
124
|
+
note: loaded.note
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
|
|
129
|
+
let projectKey = null;
|
|
130
|
+
let cacheKey = null;
|
|
131
|
+
let coldBuildMs = 0;
|
|
132
|
+
let podsMs = 0;
|
|
133
|
+
let placement = null;
|
|
134
|
+
let recorded = false;
|
|
135
|
+
return {
|
|
136
|
+
setProject(key) {
|
|
137
|
+
projectKey = key;
|
|
138
|
+
},
|
|
139
|
+
setCacheKey(key) {
|
|
140
|
+
cacheKey = key;
|
|
141
|
+
},
|
|
142
|
+
setBuildMs(ms) {
|
|
143
|
+
coldBuildMs = wholeMs(ms);
|
|
144
|
+
},
|
|
145
|
+
setPodsMs(ms) {
|
|
146
|
+
podsMs = wholeMs(ms);
|
|
147
|
+
},
|
|
148
|
+
setPlacement(next) {
|
|
149
|
+
placement = next;
|
|
150
|
+
},
|
|
151
|
+
record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
|
|
152
|
+
if (!projectKey || !cacheKey || recorded) return;
|
|
153
|
+
recorded = true;
|
|
154
|
+
const ran = failed ? {} : phases?.() ?? {};
|
|
155
|
+
try {
|
|
156
|
+
const outcome = write({
|
|
157
|
+
platform,
|
|
158
|
+
projectKey,
|
|
159
|
+
failed,
|
|
160
|
+
cacheHit,
|
|
161
|
+
waitedForBuild: Boolean(waited),
|
|
162
|
+
durationMs,
|
|
163
|
+
...offloadedTo ? { offloadedTo } : {},
|
|
164
|
+
...coldBuildMs > 0 ? { coldBuildMs } : {},
|
|
165
|
+
...podsMs > 0 ? { podsMs } : {},
|
|
166
|
+
...Object.keys(ran).length ? { phases: ran } : {},
|
|
167
|
+
...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
|
|
168
|
+
...placement ? { placement } : {}
|
|
169
|
+
}, now());
|
|
170
|
+
if (outcome?.note) note(outcome.note);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
note(`Run statistics could not be recorded: ${error?.message || error}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function loadForUpdate(path, now) {
|
|
178
|
+
let text;
|
|
179
|
+
try {
|
|
180
|
+
text = readFileSync(path, "utf-8");
|
|
181
|
+
} catch {
|
|
182
|
+
return {
|
|
183
|
+
record: emptyStats(),
|
|
184
|
+
note: null,
|
|
185
|
+
newerVersion: null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const parsed = decodeStats(text);
|
|
189
|
+
if (!parsed.record && parsed.newerVersion === null) {
|
|
190
|
+
const aside = `${path}.corrupt-${now}`;
|
|
191
|
+
renameSync(path, aside);
|
|
192
|
+
return {
|
|
193
|
+
record: emptyStats(),
|
|
194
|
+
note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
|
|
195
|
+
newerVersion: null
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (parsed.newerVersion !== null) return {
|
|
199
|
+
record: emptyStats(),
|
|
200
|
+
note: null,
|
|
201
|
+
newerVersion: parsed.newerVersion
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
record: parsed.record,
|
|
205
|
+
note: null,
|
|
206
|
+
newerVersion: null
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function writeStats(path, record) {
|
|
210
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
211
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`);
|
|
212
|
+
try {
|
|
213
|
+
renameSync(tmp, path);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
rmSync(tmp, { force: true });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function applyRun(bucket, run, { at, durationMs, credit, phases }) {
|
|
220
|
+
const next = bucket ? { ...bucket } : {
|
|
221
|
+
runs: 0,
|
|
222
|
+
failed: 0,
|
|
223
|
+
hits: 0,
|
|
224
|
+
misses: 0,
|
|
225
|
+
coldRuns: 0,
|
|
226
|
+
coldRunMs: 0,
|
|
227
|
+
hitRuns: 0,
|
|
228
|
+
hitRunMs: 0,
|
|
229
|
+
timeSavedMs: 0,
|
|
230
|
+
firstRunAt: at,
|
|
231
|
+
lastRunAt: at
|
|
232
|
+
};
|
|
233
|
+
next.runs += 1;
|
|
234
|
+
next.lastRunAt = at;
|
|
235
|
+
if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
|
|
236
|
+
if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
|
|
237
|
+
if (run.failed) {
|
|
238
|
+
next.failed += 1;
|
|
239
|
+
return next;
|
|
240
|
+
}
|
|
241
|
+
if (isHit(run.cacheHit)) {
|
|
242
|
+
next.hits += 1;
|
|
243
|
+
if (!run.waitedForBuild) {
|
|
244
|
+
next.hitRuns += 1;
|
|
245
|
+
next.hitRunMs += durationMs;
|
|
246
|
+
next.timeSavedMs += credit;
|
|
247
|
+
}
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
next.misses += 1;
|
|
251
|
+
if (run.offloadedTo) {
|
|
252
|
+
next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
|
|
253
|
+
next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
|
|
254
|
+
next.lastOffloadHost = run.offloadedTo;
|
|
255
|
+
return next;
|
|
256
|
+
}
|
|
257
|
+
next.coldRuns += 1;
|
|
258
|
+
next.coldRunMs += durationMs;
|
|
259
|
+
return next;
|
|
260
|
+
}
|
|
261
|
+
function creditMs(bucket, run, durationMs) {
|
|
262
|
+
if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
|
|
263
|
+
if (!bucket || bucket.coldRuns <= 0) return 0;
|
|
264
|
+
return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
|
|
265
|
+
}
|
|
266
|
+
function isHit(cacheHit) {
|
|
267
|
+
return cacheHit === "local" || cacheHit === "remote";
|
|
268
|
+
}
|
|
269
|
+
function wholeMs(value) {
|
|
270
|
+
const ms = Number(value);
|
|
271
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/engine/build-detail.ts
|
|
275
|
+
const LINE_MAX = 160;
|
|
276
|
+
const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
|
|
277
|
+
const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
|
|
278
|
+
const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
|
|
279
|
+
const GRADLE_TASK = /^> Task (:\S+)/;
|
|
280
|
+
/**
|
|
281
|
+
* xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
|
|
282
|
+
* that touches nothing still signs framework products last. Targets that end any other way are not counted.
|
|
283
|
+
*/
|
|
284
|
+
const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
|
|
285
|
+
const GRADLE_CONFIGURE = /^> Configure project /;
|
|
286
|
+
const XCODE_STEPS = [
|
|
287
|
+
[/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
|
|
288
|
+
[/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
|
|
289
|
+
[/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
|
|
290
|
+
[/^PhaseScriptExecution$/, "script"],
|
|
291
|
+
[/^CodeSign$/, "sign"]
|
|
292
|
+
];
|
|
293
|
+
const GRADLE_STEPS = [
|
|
294
|
+
[/[Dd]ex/, "dex"],
|
|
295
|
+
[/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
|
|
296
|
+
[/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
|
|
297
|
+
[/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
|
|
298
|
+
[/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
|
|
299
|
+
];
|
|
300
|
+
function stepOf(name, table) {
|
|
301
|
+
return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
302
|
+
}
|
|
303
|
+
function clip(text) {
|
|
304
|
+
return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
|
|
305
|
+
}
|
|
306
|
+
function unescape(token) {
|
|
307
|
+
return token.replace(/\\(.)/g, "$1");
|
|
308
|
+
}
|
|
309
|
+
function baseName(path) {
|
|
310
|
+
return path.split("/").findLast(Boolean) ?? path;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* An xcodebuild action line in short form: the action, the file it works on and its target, as in
|
|
314
|
+
* `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
|
|
315
|
+
* as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
|
|
316
|
+
* shows its name instead.
|
|
317
|
+
*/
|
|
318
|
+
function shortXcodeAction(action, args, target) {
|
|
319
|
+
const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
|
|
320
|
+
const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
|
|
321
|
+
return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
|
|
322
|
+
}
|
|
323
|
+
/** What one line of xcodebuild or Gradle output says about the build's progress, or null for any other line. */
|
|
324
|
+
function parseBuildToolLine(msg) {
|
|
325
|
+
const graph = TARGET_GRAPH.exec(msg);
|
|
326
|
+
if (graph) return {
|
|
327
|
+
kind: "total",
|
|
328
|
+
unit: "targets",
|
|
329
|
+
total: Number(graph[1])
|
|
330
|
+
};
|
|
331
|
+
if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
332
|
+
const action = XCODE_ACTION.exec(msg);
|
|
333
|
+
if (action) {
|
|
334
|
+
const [, name, args, target, project] = action;
|
|
335
|
+
const step = stepOf(name, XCODE_STEPS);
|
|
336
|
+
return {
|
|
337
|
+
kind: "unit",
|
|
338
|
+
unit: "targets",
|
|
339
|
+
id: `${project}/${target}`,
|
|
340
|
+
step,
|
|
341
|
+
line: step ? shortXcodeAction(name, args, target) : null,
|
|
342
|
+
finished: XCODE_TARGET_DONE.test(name)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const task = GRADLE_TASK.exec(msg);
|
|
346
|
+
if (task) {
|
|
347
|
+
const path = task[1];
|
|
348
|
+
return {
|
|
349
|
+
kind: "unit",
|
|
350
|
+
unit: "tasks",
|
|
351
|
+
id: path,
|
|
352
|
+
step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
|
|
353
|
+
line: clip(`> Task ${path}`),
|
|
354
|
+
finished: false
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
|
|
361
|
+
* total its dependency graph note gives; Gradle counts the task lines it printed and has no total.
|
|
362
|
+
*/
|
|
363
|
+
function createBuildDetailParser() {
|
|
364
|
+
let seen = false;
|
|
365
|
+
let step = null;
|
|
366
|
+
let unit = null;
|
|
367
|
+
let total = null;
|
|
368
|
+
let line = null;
|
|
369
|
+
let tasks = 0;
|
|
370
|
+
const targets = /* @__PURE__ */ new Set();
|
|
371
|
+
return {
|
|
372
|
+
push(msg) {
|
|
373
|
+
const parsed = parseBuildToolLine(msg);
|
|
374
|
+
if (!parsed) return false;
|
|
375
|
+
seen = true;
|
|
376
|
+
if (parsed.kind === "total") {
|
|
377
|
+
unit = "targets";
|
|
378
|
+
total = parsed.total;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
if (parsed.kind === "configure") {
|
|
382
|
+
step = "configure";
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
unit = parsed.unit;
|
|
386
|
+
if (parsed.unit === "tasks") tasks += 1;
|
|
387
|
+
else if (parsed.finished) targets.add(parsed.id);
|
|
388
|
+
if (parsed.step) step = parsed.step;
|
|
389
|
+
if (parsed.line) line = parsed.line;
|
|
390
|
+
return true;
|
|
391
|
+
},
|
|
392
|
+
detail(updatedAt) {
|
|
393
|
+
if (!seen) return null;
|
|
394
|
+
const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
|
|
395
|
+
return {
|
|
396
|
+
step,
|
|
397
|
+
unit,
|
|
398
|
+
done: done !== null && total !== null ? Math.min(done, total) : done,
|
|
399
|
+
total: unit === "tasks" ? null : total,
|
|
400
|
+
line,
|
|
401
|
+
updatedAt
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/engine/build-progress.ts
|
|
408
|
+
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
409
|
+
const NO_BUILD_PROGRESS = {
|
|
410
|
+
step: () => {},
|
|
411
|
+
estimate: () => {},
|
|
412
|
+
miss: () => {},
|
|
413
|
+
hit: () => {},
|
|
414
|
+
deviceSetup: () => {},
|
|
415
|
+
deviceSetupKnown: () => void 0,
|
|
416
|
+
place: () => {},
|
|
417
|
+
waitingOn: () => {},
|
|
418
|
+
output: () => {},
|
|
419
|
+
durations: () => ({}),
|
|
420
|
+
clear: () => {}
|
|
421
|
+
};
|
|
422
|
+
/** `writer`, with each record it writes also passed to `progress.output`. */
|
|
423
|
+
function tapBuildLog(writer, progress) {
|
|
424
|
+
if (progress === NO_BUILD_PROGRESS) return writer;
|
|
425
|
+
return {
|
|
426
|
+
get file() {
|
|
427
|
+
return writer.file;
|
|
428
|
+
},
|
|
429
|
+
write(record) {
|
|
430
|
+
progress.output(record);
|
|
431
|
+
return writer.write(record);
|
|
432
|
+
},
|
|
433
|
+
close: () => writer.close(),
|
|
434
|
+
get written() {
|
|
435
|
+
return writer.written;
|
|
436
|
+
},
|
|
437
|
+
get dropped() {
|
|
438
|
+
return writer.dropped;
|
|
439
|
+
},
|
|
440
|
+
get lastError() {
|
|
441
|
+
return writer.lastError;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const DETAIL_WRITE_MS = 2e3;
|
|
446
|
+
function toolLine(record) {
|
|
447
|
+
if (!record || typeof record !== "object") return null;
|
|
448
|
+
const { src, level, msg } = record;
|
|
449
|
+
return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
|
|
450
|
+
}
|
|
451
|
+
function writeBuildDetail(root, claimId, detail) {
|
|
452
|
+
const file = workspaceBuildDetailFile(root);
|
|
453
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
454
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
455
|
+
writeFileSync(tmp, JSON.stringify({
|
|
456
|
+
claimId,
|
|
457
|
+
detail
|
|
458
|
+
}));
|
|
459
|
+
renameSync(tmp, file);
|
|
460
|
+
}
|
|
461
|
+
const COLD_PHASES = [
|
|
462
|
+
"prebuild",
|
|
463
|
+
"pods",
|
|
464
|
+
"compile"
|
|
465
|
+
];
|
|
466
|
+
const HIT_PHASES = ["install", "launch"];
|
|
467
|
+
function settledOutcome(phase, entered) {
|
|
468
|
+
if (COLD_PHASES.includes(phase)) return "cold";
|
|
469
|
+
if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
|
|
470
|
+
return HIT_PHASES.includes(phase) ? "hit" : null;
|
|
471
|
+
}
|
|
472
|
+
function projectHistory(projectKey) {
|
|
473
|
+
try {
|
|
474
|
+
return readStats().record?.history?.[projectKey];
|
|
475
|
+
} catch {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
|
|
480
|
+
const startedAt = new Date(now()).toISOString();
|
|
481
|
+
const record = {
|
|
482
|
+
platform,
|
|
483
|
+
slot,
|
|
484
|
+
startedAt,
|
|
485
|
+
phase: "prepare",
|
|
486
|
+
phaseStartedAt: startedAt,
|
|
487
|
+
phases: [{
|
|
488
|
+
phase: "prepare",
|
|
489
|
+
startedAt
|
|
490
|
+
}],
|
|
491
|
+
claim: {
|
|
492
|
+
root: claim.root,
|
|
493
|
+
path: claim.path,
|
|
494
|
+
claimId: claim.claimId,
|
|
495
|
+
pid: claim.owner.pid
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
let warned = false;
|
|
499
|
+
const guard = (fn) => {
|
|
500
|
+
try {
|
|
501
|
+
fn();
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (warned) return;
|
|
504
|
+
warned = true;
|
|
505
|
+
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
|
|
509
|
+
...before(state),
|
|
510
|
+
[ACTIVE_BUILD_KEY]: record
|
|
511
|
+
})));
|
|
512
|
+
write(withInterruptedBuild);
|
|
513
|
+
let projectKey = null;
|
|
514
|
+
const parser = createBuildDetailParser();
|
|
515
|
+
let detailWrittenAt = -Infinity;
|
|
516
|
+
let detailTimer = null;
|
|
517
|
+
const flushDetail = () => {
|
|
518
|
+
detailTimer = null;
|
|
519
|
+
detailWrittenAt = now();
|
|
520
|
+
const detail = parser.detail(new Date(detailWrittenAt).toISOString());
|
|
521
|
+
if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
|
|
522
|
+
};
|
|
523
|
+
const settle = (outcome) => {
|
|
524
|
+
record.outcome = outcome;
|
|
525
|
+
if (projectKey) record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
step(phase) {
|
|
529
|
+
if (phase === record.phase) return;
|
|
530
|
+
const at = new Date(now()).toISOString();
|
|
531
|
+
record.phase = phase;
|
|
532
|
+
record.phaseStartedAt = at;
|
|
533
|
+
record.phases.push({
|
|
534
|
+
phase,
|
|
535
|
+
startedAt: at
|
|
536
|
+
});
|
|
537
|
+
if (phase !== "wait") delete record.waitingOn;
|
|
538
|
+
const outcome = record.outcome ? null : settledOutcome(phase, record.phases);
|
|
539
|
+
if (outcome) settle(outcome);
|
|
540
|
+
write();
|
|
541
|
+
},
|
|
542
|
+
estimate(key) {
|
|
543
|
+
projectKey = key;
|
|
544
|
+
record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
545
|
+
write();
|
|
546
|
+
},
|
|
547
|
+
miss(reason, provisional = false) {
|
|
548
|
+
record.missReason = reason;
|
|
549
|
+
settle("cold");
|
|
550
|
+
if (provisional) record.missProvisional = true;
|
|
551
|
+
else delete record.missProvisional;
|
|
552
|
+
write();
|
|
553
|
+
},
|
|
554
|
+
hit() {
|
|
555
|
+
delete record.missReason;
|
|
556
|
+
delete record.missProvisional;
|
|
557
|
+
settle("hit");
|
|
558
|
+
write();
|
|
559
|
+
},
|
|
560
|
+
deviceSetup(setup) {
|
|
561
|
+
if (setup === void 0) return;
|
|
562
|
+
record.deviceSetup = setup;
|
|
563
|
+
write();
|
|
564
|
+
},
|
|
565
|
+
deviceSetupKnown() {
|
|
566
|
+
return record.deviceSetup;
|
|
567
|
+
},
|
|
568
|
+
place(remote) {
|
|
569
|
+
const current = record.placement;
|
|
570
|
+
if (!remote) {
|
|
571
|
+
if (!current) return;
|
|
572
|
+
delete record.placement;
|
|
573
|
+
return write();
|
|
574
|
+
}
|
|
575
|
+
if (current?.host === remote.host && current.phase === remote.phase) return;
|
|
576
|
+
const at = new Date(now()).toISOString();
|
|
577
|
+
record.placement = {
|
|
578
|
+
host: remote.host,
|
|
579
|
+
phase: remote.phase,
|
|
580
|
+
startedAt: current?.host === remote.host ? current.startedAt : at,
|
|
581
|
+
phaseStartedAt: at
|
|
582
|
+
};
|
|
583
|
+
write();
|
|
584
|
+
},
|
|
585
|
+
waitingOn(holder) {
|
|
586
|
+
if (holder === null) {
|
|
587
|
+
if (!record.waitingOn) return;
|
|
588
|
+
delete record.waitingOn;
|
|
589
|
+
} else {
|
|
590
|
+
if (record.waitingOn?.path === holder) return;
|
|
591
|
+
record.waitingOn = { path: holder };
|
|
592
|
+
}
|
|
593
|
+
write();
|
|
594
|
+
},
|
|
595
|
+
output(line) {
|
|
596
|
+
const msg = toolLine(line);
|
|
597
|
+
if (msg === null || !parser.push(msg) || detailTimer) return;
|
|
598
|
+
const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
|
|
599
|
+
if (wait <= 0) return flushDetail();
|
|
600
|
+
detailTimer = setTimeout(flushDetail, wait);
|
|
601
|
+
detailTimer.unref?.();
|
|
602
|
+
},
|
|
603
|
+
durations() {
|
|
604
|
+
return phaseDurations(record.phases, now());
|
|
605
|
+
},
|
|
606
|
+
clear() {
|
|
607
|
+
if (detailTimer) clearTimeout(detailTimer);
|
|
608
|
+
detailTimer = null;
|
|
609
|
+
guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
|
|
610
|
+
guard(() => {
|
|
611
|
+
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function phaseDurations(phases, now) {
|
|
617
|
+
const out = {};
|
|
618
|
+
phases.forEach(({ phase, startedAt }, index) => {
|
|
619
|
+
const start = Date.parse(startedAt);
|
|
620
|
+
const next = phases[index + 1];
|
|
621
|
+
const end = next ? Date.parse(next.startedAt) : now;
|
|
622
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
623
|
+
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
624
|
+
});
|
|
625
|
+
return out;
|
|
626
|
+
}
|
|
627
|
+
const HISTORY_FIELDS = [
|
|
628
|
+
"platform",
|
|
629
|
+
"status",
|
|
630
|
+
"configuration",
|
|
631
|
+
"fingerprint",
|
|
632
|
+
"cacheKey",
|
|
633
|
+
"cacheHit",
|
|
634
|
+
"cacheSkipped",
|
|
635
|
+
"durationMs",
|
|
636
|
+
"startedAt",
|
|
637
|
+
"errorCode",
|
|
638
|
+
"missReason",
|
|
639
|
+
"buildMachine",
|
|
640
|
+
"builtOn",
|
|
641
|
+
"offloadedTo",
|
|
642
|
+
"offloadFallback",
|
|
643
|
+
"diagnostics"
|
|
644
|
+
];
|
|
645
|
+
function withHistoryEntry(state, platform, entry) {
|
|
646
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
647
|
+
const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
|
|
648
|
+
const list = Array.isArray(history[platform]) ? history[platform] : [];
|
|
649
|
+
return {
|
|
650
|
+
...state,
|
|
651
|
+
[BUILD_HISTORY_KEY]: {
|
|
652
|
+
...history,
|
|
653
|
+
[platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function finishedResult(record) {
|
|
658
|
+
if (record.status === "ok") return "succeeded";
|
|
659
|
+
return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
|
|
660
|
+
}
|
|
661
|
+
function recordFinishedBuild(root, record, { update = updateWorkspaceState, now = Date.now } = {}) {
|
|
662
|
+
const platform = record.platform;
|
|
663
|
+
update(root, (state) => {
|
|
664
|
+
const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
665
|
+
const own = active?.platform === platform ? active : null;
|
|
666
|
+
const entry = {
|
|
667
|
+
...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
|
|
668
|
+
result: finishedResult(record),
|
|
669
|
+
slot: own?.slot ?? "default",
|
|
670
|
+
phases: own ? phaseDurations(own.phases, now()) : {}
|
|
671
|
+
};
|
|
672
|
+
return withHistoryEntry({
|
|
673
|
+
...state,
|
|
674
|
+
lastBuild: record,
|
|
675
|
+
[LAST_BUILD_KEYS[platform]]: record
|
|
676
|
+
}, platform, entry);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function withInterruptedBuild(state) {
|
|
680
|
+
const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
681
|
+
if (!left) return state;
|
|
682
|
+
const last = state[LAST_BUILD_KEYS[left.platform]];
|
|
683
|
+
if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
|
|
684
|
+
return withHistoryEntry(state, left.platform, {
|
|
685
|
+
platform: left.platform,
|
|
686
|
+
status: "failed",
|
|
687
|
+
result: "interrupted",
|
|
688
|
+
slot: left.slot,
|
|
689
|
+
configuration: null,
|
|
690
|
+
fingerprint: null,
|
|
691
|
+
cacheKey: null,
|
|
692
|
+
cacheHit: false,
|
|
693
|
+
cacheSkipped: false,
|
|
694
|
+
durationMs: null,
|
|
695
|
+
startedAt: left.startedAt,
|
|
696
|
+
phases: {
|
|
697
|
+
...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
|
|
698
|
+
[left.phase]: 0
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function parseActiveBuild(value) {
|
|
703
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
704
|
+
const record = value;
|
|
705
|
+
const claim = record.claim;
|
|
706
|
+
if (record.platform !== "ios" && record.platform !== "android") return null;
|
|
707
|
+
if (!isPhase(record.phase)) return null;
|
|
708
|
+
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
709
|
+
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
710
|
+
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
711
|
+
const missReason = parseMissReason(record.missReason);
|
|
712
|
+
const placement = parsePlacement(record.placement);
|
|
713
|
+
const waitingOn = parseWaitingOn(record.waitingOn);
|
|
714
|
+
const estimate = parseEstimate(record.estimate);
|
|
715
|
+
return {
|
|
716
|
+
platform: record.platform,
|
|
717
|
+
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
718
|
+
startedAt: record.startedAt,
|
|
719
|
+
phase: record.phase,
|
|
720
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
721
|
+
phases,
|
|
722
|
+
...missReason ? { missReason } : {},
|
|
723
|
+
...missReason && record.missProvisional === true ? { missProvisional: true } : {},
|
|
724
|
+
...placement ? { placement } : {},
|
|
725
|
+
...waitingOn ? { waitingOn } : {},
|
|
726
|
+
...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
|
|
727
|
+
...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
|
|
728
|
+
...estimate ? { estimate } : {},
|
|
729
|
+
claim: {
|
|
730
|
+
root: claim.root,
|
|
731
|
+
path: typeof claim.path === "string" ? claim.path : "",
|
|
732
|
+
claimId: claim.claimId,
|
|
733
|
+
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function parsePlacement(value) {
|
|
738
|
+
if (!value || typeof value !== "object") return null;
|
|
739
|
+
const { host, phase, startedAt, phaseStartedAt } = value;
|
|
740
|
+
if ([
|
|
741
|
+
host,
|
|
742
|
+
phase,
|
|
743
|
+
startedAt,
|
|
744
|
+
phaseStartedAt
|
|
745
|
+
].some((field) => typeof field !== "string")) return null;
|
|
746
|
+
return {
|
|
747
|
+
host,
|
|
748
|
+
phase,
|
|
749
|
+
startedAt,
|
|
750
|
+
phaseStartedAt
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function parseWaitingOn(value) {
|
|
754
|
+
const path = value?.path;
|
|
755
|
+
return typeof path === "string" && path ? { path } : null;
|
|
756
|
+
}
|
|
757
|
+
function parseEstimate(value) {
|
|
758
|
+
if (!value || typeof value !== "object") return null;
|
|
759
|
+
const { outcome, expectedMs, basis, phaseMs, planned } = value;
|
|
760
|
+
if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
|
|
761
|
+
if (expectedMs !== null && typeof expectedMs !== "number") return null;
|
|
762
|
+
if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
|
|
763
|
+
const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
|
|
764
|
+
return {
|
|
765
|
+
outcome,
|
|
766
|
+
expectedMs,
|
|
767
|
+
basis,
|
|
768
|
+
phaseMs: Object.fromEntries(durations),
|
|
769
|
+
planned: planned.filter(isPhase)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function isPhase(value) {
|
|
773
|
+
return BUILD_PHASES.includes(value);
|
|
774
|
+
}
|
|
775
|
+
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
776
|
+
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
777
|
+
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
778
|
+
return survey.unresolved.length ? "unknown" : "stale";
|
|
779
|
+
}
|
|
780
|
+
function median(values) {
|
|
781
|
+
if (!values.length) return null;
|
|
782
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
783
|
+
const mid = Math.floor(sorted.length / 2);
|
|
784
|
+
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
785
|
+
}
|
|
786
|
+
function estimateBuild(history, platform, known, deviceSetup) {
|
|
787
|
+
const lists = history?.[platform];
|
|
788
|
+
const outcome = known ?? latestOutcome(lists);
|
|
789
|
+
const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
|
|
790
|
+
const phaseMs = {};
|
|
791
|
+
const planned = [];
|
|
792
|
+
for (const phase of BUILD_PHASES) {
|
|
793
|
+
const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
794
|
+
const ms = median(durations);
|
|
795
|
+
if (ms === null) continue;
|
|
796
|
+
phaseMs[phase] = ms;
|
|
797
|
+
if (durations.length * 2 >= samples.length) planned.push(phase);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
outcome,
|
|
801
|
+
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
802
|
+
basis: samples.length,
|
|
803
|
+
phaseMs,
|
|
804
|
+
planned
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const MIN_TAGGED_SAMPLES = 3;
|
|
808
|
+
function matchDeviceSetup(samples, deviceSetup) {
|
|
809
|
+
if (deviceSetup === void 0) return samples;
|
|
810
|
+
const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
|
|
811
|
+
if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
|
|
812
|
+
return samples.filter((sample) => sample.deviceSetup !== true);
|
|
813
|
+
}
|
|
814
|
+
function latestOutcome(lists) {
|
|
815
|
+
let latest = null;
|
|
816
|
+
for (const outcome of ["hit", "cold"]) {
|
|
817
|
+
const last = lists?.[outcome]?.at(-1);
|
|
818
|
+
const at = last ? Date.parse(last.at) : NaN;
|
|
819
|
+
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
820
|
+
outcome,
|
|
821
|
+
at
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return latest?.outcome ?? null;
|
|
825
|
+
}
|
|
826
|
+
function buildReport(record, { state, history }) {
|
|
827
|
+
const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
828
|
+
const plannedPhases = estimate.planned.flatMap((phase) => {
|
|
829
|
+
const expectedMs = estimate.phaseMs[phase];
|
|
830
|
+
return expectedMs === void 0 ? [] : [{
|
|
831
|
+
phase,
|
|
832
|
+
expectedMs
|
|
833
|
+
}];
|
|
834
|
+
});
|
|
835
|
+
return {
|
|
836
|
+
platform: record.platform,
|
|
837
|
+
slot: record.slot,
|
|
838
|
+
state,
|
|
839
|
+
phase: record.phase,
|
|
840
|
+
startedAt: record.startedAt,
|
|
841
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
842
|
+
outcome: estimate.outcome,
|
|
843
|
+
outcomeKnown: record.outcome !== void 0,
|
|
844
|
+
...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
|
|
845
|
+
expectedMs: estimate.expectedMs,
|
|
846
|
+
expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
|
|
847
|
+
basis: estimate.basis,
|
|
848
|
+
plannedPhases: plannedPhases.length ? plannedPhases : null,
|
|
849
|
+
...record.missReason ? { missReason: record.missReason } : {},
|
|
850
|
+
...record.missReason && record.missProvisional ? { missProvisional: true } : {},
|
|
851
|
+
placement: record.placement ?? "local",
|
|
852
|
+
...record.waitingOn ? { waitingOn: record.waitingOn } : {}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function remainingText(remainingMs) {
|
|
856
|
+
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
857
|
+
}
|
|
858
|
+
function buildStatusLine(report, now) {
|
|
859
|
+
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
860
|
+
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
861
|
+
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
862
|
+
const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
|
|
863
|
+
const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
|
|
864
|
+
const head = `build: ${report.platform}${slot} ${report.phase}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
865
|
+
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
866
|
+
if (report.expectedMs === null) return head;
|
|
867
|
+
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
868
|
+
const remainingMs = report.expectedMs - elapsedMs;
|
|
869
|
+
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
export { buildStatusLine as a, recordFinishedBuild as c, createRunRecorder as d, readRunEstimates as f, statsProjectKey$1 as h, buildReport as i, startBuildProgress as l, recordRunStats as m, NO_BUILD_PROGRESS as n, estimateBuild as o, readStats as p, activeBuildState as r, parseActiveBuild as s, ACTIVE_BUILD_KEY as t, tapBuildLog as u };
|