stim 1.17.2 → 1.19.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 +50 -1
- package/dist/{activity-IRvJY_ru.mjs → activity-BSw0x84R.mjs} +4 -4
- package/dist/{agent-device-usage-output-BK41vnzN.mjs → agent-device-usage-output-Bjxu_VmX.mjs} +8 -8
- package/dist/{android-B9yaNKwa.mjs → android-BXc6BmEi.mjs} +658 -404
- package/dist/{android-B_YNZy61.mjs → android-BdQC6zs1.mjs} +9 -9
- package/dist/android-BvV1gGdN.mjs +4 -0
- package/dist/{android-DE4v59Mb.mjs → android-DE5g_5gv.mjs} +2 -2
- package/dist/{android-cas-ZFpjJBL_.mjs → android-cas-C87geAg-.mjs} +5 -5
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/api-DIf_s86s.mjs +150 -0
- package/dist/api-run.d.mts +1 -0
- package/dist/api-run.mjs +145 -0
- package/dist/api.d.mts +218 -0
- package/dist/api.mjs +2 -0
- package/dist/{app-install-VPkI5WEg.mjs → app-install-CFI4ptUL.mjs} +33 -13
- package/dist/{attempt-Bk2ouQVL.mjs → attempt-DW-mQqM7.mjs} +14 -4
- package/dist/{web-Dl0KSRHf.mjs → browser-web-CDhqQxhv.mjs} +138 -176
- package/dist/{budget-DfcgFdQB.mjs → budget-Dz-kyXLv.mjs} +1391 -58
- package/dist/build-facts-B87vo-Im.d.mts +142 -0
- package/dist/{cache-manifest-DnUHBJaU.mjs → cache-manifest-DiwKSyv9.mjs} +3 -3
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BrcVWQyD.mjs → chrome-BTUOo8p2.mjs} +1 -1
- package/dist/cli-C9Zvgbdc.mjs +68 -0
- package/dist/cli.mjs +1 -1
- package/dist/{client-SvBdgObD.mjs → client-CuUo8Nvg.mjs} +381 -49
- package/dist/collector-run.d.mts +6 -0
- package/dist/collector-run.mjs +9 -9
- package/dist/{command-output-CDAIFD-8.mjs → command-output-Bt8ygHE7.mjs} +2 -2
- package/dist/{config-D0DbpZtM.mjs → config-DC5jEtd1.mjs} +9 -5
- package/dist/{created-devices-CpvzBL25.mjs → created-devices-D0s26Hx5.mjs} +2 -2
- package/dist/{dependency-state-CLcBHH2M.mjs → dependency-state-DKaS1OM9.mjs} +2 -2
- package/dist/{detached-entry-CiTEgMR1.mjs → detached-entry-pB_IPfV4.mjs} +1 -1
- package/dist/{dev-client-B-XC4uRH.mjs → dev-client-CDY_zAmD.mjs} +2 -2
- package/dist/{device-C8IOoJPv.mjs → device-BHw0WlHl.mjs} +8 -8
- package/dist/{device-capacity-ZC4XSZ4R.mjs → device-capacity-BoN1jQkO.mjs} +25 -14
- package/dist/device-host-worker.mjs +19 -19
- package/dist/{gradle-dJpre3Yn.mjs → device-ios-B2ruohE-.mjs} +661 -107
- package/dist/{device-lease-DpoS36_u.mjs → device-lease-Cu7PtQpZ.mjs} +9 -9
- package/dist/{device-lease-run-B8YltIH_.mjs → device-lease-run-D38SnZtt.mjs} +4 -4
- package/dist/{device-pool-3o7s5lo4.mjs → device-pool-CP4rAsHx.mjs} +3 -3
- package/dist/{device-remote-hFSkpfQu.mjs → device-remote-B5GiUifk.mjs} +15 -16
- package/dist/{doctor-CFv1fCrF.mjs → doctor-PRJH94v6.mjs} +61 -34
- package/dist/doctor-watchman-wZhyTdp4.mjs +259 -0
- package/dist/{error-diagnostics-C-NMnhrk.mjs → error-diagnostics--v5HGOU0.mjs} +11 -11
- package/dist/{exec-CaErVv7t.mjs → exec-i8GDPwq9.mjs} +93 -6
- package/dist/{gc-DEmkg3Xh.mjs → gc-CyHMcKcn.mjs} +25 -471
- package/dist/{guide-CSmhMTf8.mjs → guide-dRaQJJk9.mjs} +681 -507
- package/dist/{hosted-android-E1Heoqnv.mjs → hosted-android-Bs7fytj7.mjs} +3 -3
- package/dist/{hosted-client-Bp4knSwu.mjs → hosted-client-C5G4h_OJ.mjs} +2 -2
- package/dist/{hosted-ios-BnbeYOUD.mjs → hosted-ios-Bik035eL.mjs} +1 -1
- package/dist/{hosted-logs-OcgIBvCT.mjs → hosted-logs-CI9Az9a4.mjs} +9 -9
- package/dist/{hosted-macos-DcxKjtfH.mjs → hosted-macos-DABm3ScO.mjs} +5 -5
- package/dist/{hosted-native-1Ebbig4w.mjs → hosted-native-DbV2aUVw.mjs} +13 -9
- package/dist/{idle-shutdown-IGyNV8Cr.mjs → idle-shutdown-DYiMBn34.mjs} +7 -8
- package/dist/{ios-DSfnPfse.mjs → ios-BWbHFZVz.mjs} +19 -7
- package/dist/{ios-Cr1h5gnO.mjs → ios-DCrkgXEM.mjs} +1 -1
- package/dist/ios-DYUr9LGc.mjs +8 -0
- package/dist/ios-ITYno9rO.mjs +2241 -0
- package/dist/{ios-device-pAN4t54-.mjs → ios-device-CJ_ZOGky.mjs} +3 -3
- package/dist/{ios-device-Ee7sDXfp.mjs → ios-device-CrW8aMb5.mjs} +1 -1
- package/dist/{ios-state-8L3lgum6.mjs → ios-state-By0XuLTQ.mjs} +1 -1
- package/dist/{launch-verify-lTnWZikR.mjs → launch-verify-DCveSE6d.mjs} +5 -5
- package/dist/{logs-BAaS7gS2.mjs → logs-Bao1HOMy.mjs} +13 -13
- package/dist/{macos-BIRQS90f.mjs → macos-ciyBdTY5.mjs} +194 -132
- package/dist/macos-nIOATJmR.mjs +2 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +349 -47
- package/dist/manifest-CX0d1IkZ.mjs +148 -0
- package/dist/{metro-g8NqiJp-.mjs → metro-DI2AjPF_.mjs} +196 -43
- package/dist/{metro-gateway-C-ijGI2F.mjs → metro-gateway-BxpJLIwh.mjs} +4 -4
- package/dist/{metro-store-CDDZHtPl.mjs → metro-store-BAHFcJm6.mjs} +16 -2
- package/dist/{build-plan-BDsSi09V.mjs → miss-reason-CjQCOKqq.mjs} +207 -249
- package/dist/{named-ports-C8m8EQs2.mjs → named-ports-B4iWH4E-.mjs} +70 -8
- package/dist/{native-runtime-BEK171xQ.mjs → native-runtime-BGxsbfny.mjs} +6 -6
- package/dist/{ndjson-PclOIwVY.mjs → ndjson-LGZAMsqa.mjs} +8 -4
- package/dist/offload-worker.d.mts +3 -45
- package/dist/offload-worker.mjs +11 -11
- package/dist/{ownership-DyRQru0q.mjs → ownership-EB0ySdGK.mjs} +3 -3
- package/dist/{page-QF_Ke10s.mjs → page-Bv9s2KwY.mjs} +1 -1
- package/dist/placement-log-Dxe34rJm.mjs +88 -0
- package/dist/plan-placement-BVY59qa8.mjs +290 -0
- package/dist/{ports-4nYqxPWC.mjs → ports-ByPc0_97.mjs} +3 -3
- package/dist/{prebuild-D7RUMkwN.mjs → prebuild-BKJ3wJI1.mjs} +5 -5
- package/dist/preview-BbSottmO.mjs +986 -0
- package/dist/{rolldown-runtime-BsOwBuO_.mjs → process-identity-Biv3arCO.mjs} +11 -1
- package/dist/project-B5eQ1USs.mjs +612 -0
- package/dist/{pull-request-pxnDCQzn.mjs → pull-request-IHNdMnG8.mjs} +1 -1
- package/dist/pull-requests.mjs +1 -1
- package/dist/react-native-ios-BMh2i25I.mjs +2 -0
- package/dist/react-native-ios-CSpIxexZ.mjs +2083 -0
- package/dist/{recordings-BNgsD1Tr.mjs → recordings-Bv8McZN4.mjs} +1 -1
- package/dist/{reload-DcRxj3qG.mjs → reload-rAPdqeRR.mjs} +14 -15
- package/dist/{remote-cache-Ci4DQYYj.mjs → remote-cache-N3hAEaAL.mjs} +5 -5
- package/dist/{run-CkE9y0nc.mjs → run-lAkle1-c.mjs} +5 -5
- package/dist/{server-bare-BkrSxSsZ.mjs → server-bare-CkIjnegq.mjs} +5 -6
- package/dist/server-expo-DGsdUGX2.mjs +2 -0
- package/dist/{server-expo-CHTyXD0G.mjs → server-expo-n3tUi3Em.mjs} +5 -6
- package/dist/{settings-SR9OmW9h.mjs → settings-2am7TSxp.mjs} +5 -5
- package/dist/{settings-Cfd-0r2d.mjs → settings-CxgngUZk.mjs} +116 -103
- package/dist/{settings-BBpMcDbD.mjs → settings-Djl-mpjX.mjs} +1 -1
- package/dist/settings.schema.json +198 -9
- package/dist/{simslim-BWNrc3M3.mjs → simslim-D9LkJxIO.mjs} +7 -7
- package/dist/{slot-launch-DmjBmpQV.mjs → slot-launch-HV_KtPIw.mjs} +6 -6
- package/dist/{spawn-claims-D0XfSe8u.mjs → spawn-claims-Kqiifsk2.mjs} +1 -1
- package/dist/{spawn-entry-jw19m8Nl.mjs → spawn-entry-Dcb7sDUz.mjs} +1 -0
- package/dist/{stage-CzaiwFsI.mjs → stage-BgaZJVZY.mjs} +14 -3
- package/dist/{start-AlfCPRVU.mjs → start-D8NbxNBb.mjs} +1 -1
- package/dist/{start-DUz3mYdb.mjs → start-DtxthMfI.mjs} +34 -27
- package/dist/{state-PeQiJuIu.mjs → state-D2KtnALG.mjs} +4 -4
- package/dist/{state-DyaNTgfR.mjs → state-Tp4uImDS.mjs} +5 -3
- package/dist/{state-B4PQFXMW.d.mts → state-lKh8shBp.d.mts} +2 -3
- package/dist/{stats-Bq4fJOdB.mjs → stats-8EJ5ivFT.mjs} +6 -6
- package/dist/{status-DlKvVJBE.mjs → status-BEeL6IKz.mjs} +6 -4
- package/dist/{status-BA2pehwf.mjs → status-BOyp0CUl.mjs} +31 -34
- package/dist/{stim-desktop-Cnz8CPnl.mjs → stim-desktop-C65jk_q2.mjs} +1 -1
- package/dist/{stim-installations-QMEI32v1.mjs → stim-installations-DbmUjsLv.mjs} +1 -1
- package/dist/stop-D0UIpZuh.mjs +72 -0
- package/dist/{stop-zF1y04f2.mjs → stop-DRipAg-e.mjs} +2 -2
- package/dist/{stop-DQIasgKp.mjs → stop-zDlknFoz.mjs} +79 -33
- package/dist/supervisor-run.d.mts +24 -22
- package/dist/supervisor-run.mjs +21 -22
- package/dist/{support-CsJNVfOc.mjs → support-CNqYkV0y.mjs} +7 -7
- package/dist/{support-WvD__tSV.mjs → support-DsNaFg6D.mjs} +5 -5
- package/dist/swiftpm-macos-aJzPAj1t.mjs +119 -0
- package/dist/{toolchain-DNyR3NPq.mjs → toolchain-C3xSoyUl.mjs} +475 -269
- package/dist/{trigger-Bemu8ytw.mjs → trigger-CAhmG-4s.mjs} +6 -6
- package/dist/trigger-CoalsMTV.mjs +2 -0
- package/dist/{warm-progress-DF4JRvYI.mjs → warm-progress-pFLB2e6P.mjs} +135 -38
- package/dist/{status-CzS4bPO9.mjs → watchman-BL1IdOtq.mjs} +75 -7
- package/dist/web-DFpFeZu5.mjs +2 -0
- package/dist/web-DbbrPUn0.mjs +127 -0
- package/dist/web-run.mjs +10 -10
- package/dist/{workspace-state-DqcCbpE7.mjs → workspace-state-CR9X86xJ.mjs} +3 -3
- package/dist/{ownership-BMLqyBgJ.mjs → workspaces-Djb2gfi9.mjs} +661 -23
- package/dist/{worktree-Be_7NLKy.mjs → worktree-BL4M1rhB.mjs} +252 -50
- package/dist/{worktree-j3IcFAI5.mjs → worktree-DV-A0LNL.mjs} +1 -1
- package/package.json +10 -5
- package/shim/bundle-response.cjs +2 -1
- package/shim/bundle-response.d.cts +1 -1
- package/shim/expo-metro-config.cjs +1 -1
- package/dist/build-progress-D0WkjCPd.mjs +0 -1036
- package/dist/build-slots-41v_d2x9.mjs +0 -142
- package/dist/cli-CYXb29oE.mjs +0 -51
- package/dist/deps-iKw0eD4P.mjs +0 -438
- package/dist/device-ios-C2Um-XUR.mjs +0 -412
- package/dist/errors-B6W7FkxQ.mjs +0 -15
- package/dist/guide-status-Du-ZQ3FD.mjs +0 -125
- package/dist/idle-BXG__1BA.mjs +0 -176
- package/dist/in-use-CvGOUn-x.mjs +0 -130
- package/dist/ios-BJ4XUSe1.mjs +0 -3867
- package/dist/native-run-D1Q56rAD.mjs +0 -158
- package/dist/preview-B-g9d7kV.mjs +0 -318
- package/dist/process-identity-BmRm1qV0.mjs +0 -12
- package/dist/project-D47Rh0ly.mjs +0 -276
- package/dist/server-expo-CeakhI8P.mjs +0 -2
- package/dist/state-BKa8wNN5.mjs +0 -148
- package/dist/stop-BIiM49EY.mjs +0 -48
- package/dist/stop-cause-rhmGfwvt.mjs +0 -97
- package/dist/trigger-CXts6qt1.mjs +0 -2
- package/dist/watchman-APYjXSQa.mjs +0 -48
- package/dist/workspace-process-lock-2mNzOlaB.mjs +0 -58
- package/dist/workspaces-Bcib97-6.mjs +0 -306
|
@@ -1,38 +1,33 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { a as capacity,
|
|
9
|
-
import { i as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { i as idleShutdownCandidates, n as collectOwnedDeviceActivity, o as shutDownIdleDevices, s as workspaceBuildInProgress, t as collectIdleDevices } from "./idle-BXG__1BA.mjs";
|
|
20
|
-
import { a as collectWorkspaceOutputs, r as clearWorkspaceOutputs } from "./workspaces-Bcib97-6.mjs";
|
|
21
|
-
import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
|
|
22
|
-
import { l as withdrawDevServerStopRequest, s as requestDevServerStop } from "./stop-cause-rhmGfwvt.mjs";
|
|
23
|
-
import { existsSync, readFileSync, statSync, statfsSync } from "node:fs";
|
|
1
|
+
import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-Biv3arCO.mjs";
|
|
2
|
+
import { t as getExecutor } from "./exec-i8GDPwq9.mjs";
|
|
3
|
+
import { a as formatElapsed, l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-Bt8ygHE7.mjs";
|
|
4
|
+
import { A as directorySize, D as withConfigLock$1, F as volumeRootFor, H as projectDeviceSlots, M as isOnMountedVolume, P as measuredDirectorySize, _ as loadConfig$1, a as clearDevice, d as getConfigDir, j as formatBytes, o as clearSupervisor, p as getProject, z as deviceSlotPlatforms } from "./config-DC5jEtd1.mjs";
|
|
5
|
+
import { a as updateWorkspaceState, c as writeWorkspaceState, d as sharedBuildCache, r as readWorkspaceState$1, t as clearWorkspaceStateKey, v as workspaceDir, x as workspaceLogsDir$1 } from "./workspace-state-CR9X86xJ.mjs";
|
|
6
|
+
import { D as readPidFile, T as clearWorkspaceSupervisor } from "./project-B5eQ1USs.mjs";
|
|
7
|
+
import { r as killMetroTree, u as resolveProjectMetro } from "./metro-DI2AjPF_.mjs";
|
|
8
|
+
import { a as parseWatchmanPid, d as capacity, g as formatSpace, h as environmentState, i as parseWatchmanClients, n as parseTriggerCount, r as parseWatchRoots, t as parseSubscriberNames } from "./watchman-BL1IdOtq.mjs";
|
|
9
|
+
import { i as registeredCaches } from "./cache-manifest-DiwKSyv9.mjs";
|
|
10
|
+
import { A as teardownParkedAvd, D as teardownOwnedAvd, F as recordGcResult, L as canonicalPath, N as listBuildSlots, T as eraseParkedIosSim, a as collectWorkspaceOutputs, f as withIdleWorkspace, j as teardownParkedIosSim, k as teardownOwnedIosSim, m as resolveSupervisorTarget, r as clearWorkspaceOutputs, w as eraseParkedAvd } from "./workspaces-Djb2gfi9.mjs";
|
|
11
|
+
import { i as createDeviceProcessTables, o as idleForMs, p as workspaceActivity, r as createActivityReader } from "./activity-BSw0x84R.mjs";
|
|
12
|
+
import { h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-Cu7PtQpZ.mjs";
|
|
13
|
+
import { A as listAvds, I as ownedAvdDirectory, at as isStimOwnedSim, ct as dropParked, it as isStimOwnedAvd, pt as readParked, z as ownedAvdSerialResolver } from "./android-BdQC6zs1.mjs";
|
|
14
|
+
import { d as listAllIosSims, p as listIosDeviceTypes, v as parseRuntimeVersion } from "./ios-BWbHFZVz.mjs";
|
|
15
|
+
import { a as withWorkspaceProcessLock, o as workspaceProcessLockError } from "./named-ports-B4iWH4E-.mjs";
|
|
16
|
+
import { c as supervisorError } from "./metro-store-BAHFcJm6.mjs";
|
|
17
|
+
import { r as createNdjsonWriter } from "./ndjson-LGZAMsqa.mjs";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
|
|
24
19
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
25
|
-
import { existsSync as existsSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
26
|
-
import { homedir, tmpdir } from "os";
|
|
27
20
|
import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
|
|
28
|
-
import {
|
|
29
|
-
import { MACHINE_OWNER_KINDS, buildWorkerRoot, loadConfig, settingDefinition, settingValueError } from "@stim-cli/core/state";
|
|
21
|
+
import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, DEV_SERVER_STOP_REQUEST_KEY, LAST_BUILD_KEYS, MACHINE_OWNER_KINDS, STATS_VERSION, buildWorkerRoot, decodeStats, isJsonObject, loadConfig, parseMissReason, readDevServerStop, readDevServerStopRequest, readRunEstimates, readStats as readStats$1, settingDefinition, settingValueError, statsFile, statsProjectKey as statsProjectKey$1, trimCapacityRefusals, trimCapacityWaits, trimSamples, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
|
|
30
22
|
import { fileURLToPath } from "node:url";
|
|
31
|
-
import { homedir
|
|
23
|
+
import { homedir, platform, totalmem } from "node:os";
|
|
32
24
|
import chalk from "chalk";
|
|
25
|
+
import { existsSync as existsSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
26
|
+
import { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, compiledHelper, configDir } from "@stim-cli/core";
|
|
27
|
+
import { homedir as homedir$1, tmpdir as tmpdir$1 } from "os";
|
|
33
28
|
//#region src/cache/caches.ts
|
|
34
29
|
function xcodeDerivedDataRoot() {
|
|
35
|
-
return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
30
|
+
return join$1(homedir$1(), "Library", "Developer", "Xcode", "DerivedData");
|
|
36
31
|
}
|
|
37
32
|
function compilationCache() {
|
|
38
33
|
const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
@@ -45,7 +40,7 @@ function compilationCache() {
|
|
|
45
40
|
};
|
|
46
41
|
}
|
|
47
42
|
function gradleBuildCache() {
|
|
48
|
-
const gradleHome = process.env.GRADLE_USER_HOME || join$1(homedir(), ".gradle");
|
|
43
|
+
const gradleHome = process.env.GRADLE_USER_HOME || join$1(homedir$1(), ".gradle");
|
|
49
44
|
const dir = resolve$1(gradleHome, "caches", "build-cache-1");
|
|
50
45
|
if (!existsSync$1(dir)) return null;
|
|
51
46
|
return {
|
|
@@ -56,7 +51,7 @@ function gradleBuildCache() {
|
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
function metroFileMaps() {
|
|
59
|
-
const root = tmpdir();
|
|
54
|
+
const root = tmpdir$1();
|
|
60
55
|
if (!existsSync$1(root)) return null;
|
|
61
56
|
let names;
|
|
62
57
|
try {
|
|
@@ -201,7 +196,7 @@ function canonicalCacheDir(dir) {
|
|
|
201
196
|
function sizeCaches(caches) {
|
|
202
197
|
return caches.map((c) => Object.assign({}, c, { bytes: c.bytes ?? directorySize(c.dir) }));
|
|
203
198
|
}
|
|
204
|
-
function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
199
|
+
function pruneCache(cache, { olderThanDays, now = Date.now(), protect, evictBytes, byMtime = false } = {}) {
|
|
205
200
|
const cutoff = now - olderThanDays * 24 * 60 * 60 * 1e3;
|
|
206
201
|
if (cache.prune === "report-only") return {
|
|
207
202
|
removed: 0,
|
|
@@ -214,27 +209,38 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
214
209
|
skipped: "index-backed; empty it whole or not at all"
|
|
215
210
|
};
|
|
216
211
|
const entries = cache.files ?? entriesAtDepth(cache.dir, cache.entriesDepth ?? 1);
|
|
212
|
+
const candidates = [];
|
|
213
|
+
let protectedEntries = 0;
|
|
214
|
+
for (const entry of entries) try {
|
|
215
|
+
const st = statSync$1(entry);
|
|
216
|
+
const used = byMtime ? st.mtimeMs : Math.max(st.atimeMs, st.mtimeMs);
|
|
217
|
+
if (used >= cutoff) continue;
|
|
218
|
+
if (protect?.(entry)) {
|
|
219
|
+
protectedEntries++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
candidates.push({
|
|
223
|
+
entry,
|
|
224
|
+
used,
|
|
225
|
+
isDirectory: st.isDirectory(),
|
|
226
|
+
size: st.size
|
|
227
|
+
});
|
|
228
|
+
} catch {}
|
|
229
|
+
if (evictBytes !== void 0) candidates.sort((a, b) => a.used - b.used);
|
|
217
230
|
let removed = 0;
|
|
218
231
|
let bytes = 0;
|
|
219
232
|
let failed = 0;
|
|
220
|
-
for (const entry of
|
|
221
|
-
|
|
222
|
-
let size;
|
|
223
|
-
try {
|
|
224
|
-
const st = statSync$1(entry);
|
|
225
|
-
used = Math.max(st.atimeMs, st.mtimeMs);
|
|
226
|
-
size = st.isDirectory() ? directorySize(entry) : st.size;
|
|
227
|
-
} catch {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (used >= cutoff) continue;
|
|
233
|
+
for (const { entry, isDirectory, size } of candidates) {
|
|
234
|
+
if (evictBytes !== void 0 && bytes >= evictBytes) break;
|
|
231
235
|
try {
|
|
236
|
+
if (!existsSync$1(entry) || statSync$1(entry).mtimeMs >= cutoff) continue;
|
|
237
|
+
const entryBytes = isDirectory ? directorySize(entry) : size;
|
|
232
238
|
rmSync$1(entry, {
|
|
233
239
|
recursive: true,
|
|
234
240
|
force: true
|
|
235
241
|
});
|
|
236
242
|
removed++;
|
|
237
|
-
bytes +=
|
|
243
|
+
bytes += entryBytes;
|
|
238
244
|
} catch {
|
|
239
245
|
failed++;
|
|
240
246
|
}
|
|
@@ -243,7 +249,8 @@ function pruneCache(cache, { olderThanDays, now = Date.now() } = {}) {
|
|
|
243
249
|
removed,
|
|
244
250
|
bytes,
|
|
245
251
|
failed,
|
|
246
|
-
skipped: null
|
|
252
|
+
skipped: null,
|
|
253
|
+
protectedEntries
|
|
247
254
|
};
|
|
248
255
|
}
|
|
249
256
|
function entriesAtDepth(dir, depth) {
|
|
@@ -1524,8 +1531,8 @@ function ownsItsDirectory(cache) {
|
|
|
1524
1531
|
const dir = canonicalPath(cache.dir);
|
|
1525
1532
|
if (dirname$1(dir) === dir) return false;
|
|
1526
1533
|
return ![
|
|
1527
|
-
homedir(),
|
|
1528
|
-
tmpdir(),
|
|
1534
|
+
homedir$1(),
|
|
1535
|
+
tmpdir$1(),
|
|
1529
1536
|
getConfigDir()
|
|
1530
1537
|
].map(canonicalPath).includes(dir);
|
|
1531
1538
|
}
|
|
@@ -2112,6 +2119,1322 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
|
|
|
2112
2119
|
return deleteFailures;
|
|
2113
2120
|
}
|
|
2114
2121
|
//#endregion
|
|
2122
|
+
//#region src/engine/stats.ts
|
|
2123
|
+
const PLACEMENT_MAX_AGE_MS = 6048e5;
|
|
2124
|
+
function emptyStats() {
|
|
2125
|
+
return {
|
|
2126
|
+
version: STATS_VERSION,
|
|
2127
|
+
machine: {},
|
|
2128
|
+
projects: {}
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function updateStats(record, run, now) {
|
|
2132
|
+
const at = new Date(now).toISOString();
|
|
2133
|
+
const durationMs = wholeMs(run.durationMs);
|
|
2134
|
+
const phases = {
|
|
2135
|
+
coldBuildMs: wholeMs(run.coldBuildMs),
|
|
2136
|
+
podsMs: wholeMs(run.podsMs)
|
|
2137
|
+
};
|
|
2138
|
+
const projects = { ...record.projects };
|
|
2139
|
+
const scope = { ...projects[run.projectKey] };
|
|
2140
|
+
const machine = { ...record.machine };
|
|
2141
|
+
const before = scope[run.platform] ?? null;
|
|
2142
|
+
const credit = creditMs(before, run, durationMs);
|
|
2143
|
+
scope[run.platform] = applyRun(before, run, {
|
|
2144
|
+
at,
|
|
2145
|
+
durationMs,
|
|
2146
|
+
credit,
|
|
2147
|
+
phases
|
|
2148
|
+
});
|
|
2149
|
+
projects[run.projectKey] = scope;
|
|
2150
|
+
machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
|
|
2151
|
+
at,
|
|
2152
|
+
durationMs,
|
|
2153
|
+
credit,
|
|
2154
|
+
phases
|
|
2155
|
+
});
|
|
2156
|
+
const history = { ...record.history };
|
|
2157
|
+
if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
|
|
2158
|
+
const outcome = isHit(run.cacheHit) ? "hit" : "cold";
|
|
2159
|
+
const project = { ...history[run.projectKey] };
|
|
2160
|
+
const lists = { ...project[run.platform] };
|
|
2161
|
+
const sample = {
|
|
2162
|
+
at,
|
|
2163
|
+
durationMs,
|
|
2164
|
+
phases: wholePhases(run.phases),
|
|
2165
|
+
...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
|
|
2166
|
+
};
|
|
2167
|
+
lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
|
|
2168
|
+
project[run.platform] = lists;
|
|
2169
|
+
history[run.projectKey] = project;
|
|
2170
|
+
}
|
|
2171
|
+
let placements = record.placements;
|
|
2172
|
+
let buildMachines = record.buildMachines;
|
|
2173
|
+
if (run.placement) {
|
|
2174
|
+
const placement = buildPlacement(run, before, at);
|
|
2175
|
+
placements = trimPlacements([...placements ?? [], placement], now);
|
|
2176
|
+
if (placement.machine && placement.decision !== "here") buildMachines = {
|
|
2177
|
+
...buildMachines,
|
|
2178
|
+
[placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
|
|
2182
|
+
const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
|
|
2183
|
+
return {
|
|
2184
|
+
version: STATS_VERSION,
|
|
2185
|
+
machine,
|
|
2186
|
+
projects,
|
|
2187
|
+
...Object.keys(history).length ? { history } : {},
|
|
2188
|
+
...placements?.length ? { placements } : {},
|
|
2189
|
+
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
2190
|
+
...capacityWaits.length ? { capacityWaits } : {},
|
|
2191
|
+
...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
function buildPlacement(run, before, at) {
|
|
2195
|
+
const placement = run.placement;
|
|
2196
|
+
const buildMs = wholeMs(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
|
|
2197
|
+
const localEstimateMs = wholeMs(before?.lastColdBuildMs);
|
|
2198
|
+
const slotWaitMs = wholeMs(placement.slotWaitMs);
|
|
2199
|
+
const deviceSlotWaitMs = wholeMs(placement.deviceSlotWaitMs);
|
|
2200
|
+
return {
|
|
2201
|
+
at,
|
|
2202
|
+
project: run.projectKey,
|
|
2203
|
+
platform: run.platform,
|
|
2204
|
+
decision: placement.decision,
|
|
2205
|
+
reason: placement.reason,
|
|
2206
|
+
...placement.machine ? { machine: placement.machine } : {},
|
|
2207
|
+
...buildMs > 0 ? { buildMs } : {},
|
|
2208
|
+
...slotWaitMs > 0 ? { slotWaitMs } : {},
|
|
2209
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
|
|
2210
|
+
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
2211
|
+
...run.failed ? { failed: true } : {}
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
function applyPlacement(totals, placement) {
|
|
2215
|
+
const next = totals ? { ...totals } : {
|
|
2216
|
+
offloaded: 0,
|
|
2217
|
+
offloadedMs: 0,
|
|
2218
|
+
savedMs: 0,
|
|
2219
|
+
fallbacks: 0
|
|
2220
|
+
};
|
|
2221
|
+
if (placement.decision === "fell-back") {
|
|
2222
|
+
next.fallbacks += 1;
|
|
2223
|
+
next.lastFallbackAt = placement.at;
|
|
2224
|
+
return next;
|
|
2225
|
+
}
|
|
2226
|
+
next.offloaded += 1;
|
|
2227
|
+
next.offloadedMs += placement.buildMs ?? 0;
|
|
2228
|
+
if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
|
|
2229
|
+
next.lastOffloadAt = placement.at;
|
|
2230
|
+
return next;
|
|
2231
|
+
}
|
|
2232
|
+
function trimPlacements(placements, now) {
|
|
2233
|
+
return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
|
|
2234
|
+
}
|
|
2235
|
+
function recordRunStats(run, now) {
|
|
2236
|
+
return withConfigLock$1(() => {
|
|
2237
|
+
const path = statsFile();
|
|
2238
|
+
const loaded = loadForUpdate(path, now);
|
|
2239
|
+
if (loaded.newerVersion !== null) return {
|
|
2240
|
+
recorded: false,
|
|
2241
|
+
note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
|
|
2242
|
+
};
|
|
2243
|
+
writeStats(path, updateStats(loaded.record, run, now));
|
|
2244
|
+
return {
|
|
2245
|
+
recorded: true,
|
|
2246
|
+
note: loaded.note
|
|
2247
|
+
};
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
function recordCapacityRefusal(event, now) {
|
|
2251
|
+
try {
|
|
2252
|
+
withConfigLock$1(() => {
|
|
2253
|
+
const path = statsFile();
|
|
2254
|
+
const loaded = loadForUpdate(path, now, false);
|
|
2255
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
2256
|
+
const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
|
|
2257
|
+
...event,
|
|
2258
|
+
at: new Date(now).toISOString(),
|
|
2259
|
+
kind: "device"
|
|
2260
|
+
}], now);
|
|
2261
|
+
writeStats(path, {
|
|
2262
|
+
...loaded.record,
|
|
2263
|
+
capacityRefusals
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
} catch {}
|
|
2267
|
+
}
|
|
2268
|
+
function recordCapacityWait(event, now) {
|
|
2269
|
+
try {
|
|
2270
|
+
withConfigLock$1(() => {
|
|
2271
|
+
const path = statsFile();
|
|
2272
|
+
const loaded = loadForUpdate(path, now, false);
|
|
2273
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
2274
|
+
const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
|
|
2275
|
+
platform: event.platform,
|
|
2276
|
+
max: event.max,
|
|
2277
|
+
workspace: event.workspace,
|
|
2278
|
+
ms: wholeMs(event.ms),
|
|
2279
|
+
...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
|
|
2280
|
+
at: new Date(now).toISOString(),
|
|
2281
|
+
kind: "device-wait"
|
|
2282
|
+
}], now);
|
|
2283
|
+
writeStats(path, {
|
|
2284
|
+
...loaded.record,
|
|
2285
|
+
capacityWaits
|
|
2286
|
+
});
|
|
2287
|
+
});
|
|
2288
|
+
} catch {}
|
|
2289
|
+
}
|
|
2290
|
+
function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
|
|
2291
|
+
let projectKey = null;
|
|
2292
|
+
let cacheKey = null;
|
|
2293
|
+
let coldBuildMs = 0;
|
|
2294
|
+
let podsMs = 0;
|
|
2295
|
+
let placement = null;
|
|
2296
|
+
let deviceSlotWaitMs = 0;
|
|
2297
|
+
let recorded = false;
|
|
2298
|
+
return {
|
|
2299
|
+
setProject(key) {
|
|
2300
|
+
projectKey = key;
|
|
2301
|
+
},
|
|
2302
|
+
setCacheKey(key) {
|
|
2303
|
+
cacheKey = key;
|
|
2304
|
+
},
|
|
2305
|
+
setBuildMs(ms) {
|
|
2306
|
+
coldBuildMs = wholeMs(ms);
|
|
2307
|
+
},
|
|
2308
|
+
setPodsMs(ms) {
|
|
2309
|
+
podsMs = wholeMs(ms);
|
|
2310
|
+
},
|
|
2311
|
+
addDeviceSlotWaitMs(ms) {
|
|
2312
|
+
deviceSlotWaitMs += wholeMs(ms);
|
|
2313
|
+
},
|
|
2314
|
+
deviceSlotWaitMs() {
|
|
2315
|
+
return deviceSlotWaitMs || void 0;
|
|
2316
|
+
},
|
|
2317
|
+
setPlacement(next) {
|
|
2318
|
+
placement = next;
|
|
2319
|
+
},
|
|
2320
|
+
record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
|
|
2321
|
+
if (!projectKey || !cacheKey || recorded) return;
|
|
2322
|
+
recorded = true;
|
|
2323
|
+
const ran = failed ? {} : phases?.() ?? {};
|
|
2324
|
+
try {
|
|
2325
|
+
const outcome = write({
|
|
2326
|
+
platform,
|
|
2327
|
+
projectKey,
|
|
2328
|
+
failed,
|
|
2329
|
+
cacheHit,
|
|
2330
|
+
waitedForBuild: Boolean(waited),
|
|
2331
|
+
durationMs,
|
|
2332
|
+
...offloadedTo ? { offloadedTo } : {},
|
|
2333
|
+
...coldBuildMs > 0 ? { coldBuildMs } : {},
|
|
2334
|
+
...podsMs > 0 ? { podsMs } : {},
|
|
2335
|
+
...Object.keys(ran).length ? { phases: ran } : {},
|
|
2336
|
+
...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
|
|
2337
|
+
...placement ? { placement: {
|
|
2338
|
+
...placement,
|
|
2339
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
|
|
2340
|
+
} } : {}
|
|
2341
|
+
}, now());
|
|
2342
|
+
if (outcome?.note) note(outcome.note);
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
note(`Run statistics could not be recorded: ${error?.message || error}`);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function loadForUpdate(path, now, recoverCorrupt = true) {
|
|
2350
|
+
let text;
|
|
2351
|
+
try {
|
|
2352
|
+
text = readFileSync(path, "utf-8");
|
|
2353
|
+
} catch {
|
|
2354
|
+
return {
|
|
2355
|
+
record: emptyStats(),
|
|
2356
|
+
note: null,
|
|
2357
|
+
newerVersion: null
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
const parsed = decodeStats(text);
|
|
2361
|
+
if (!parsed.record && parsed.newerVersion === null) {
|
|
2362
|
+
if (!recoverCorrupt) return {
|
|
2363
|
+
record: null,
|
|
2364
|
+
note: null,
|
|
2365
|
+
newerVersion: null
|
|
2366
|
+
};
|
|
2367
|
+
const aside = `${path}.corrupt-${now}`;
|
|
2368
|
+
renameSync(path, aside);
|
|
2369
|
+
return {
|
|
2370
|
+
record: emptyStats(),
|
|
2371
|
+
note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
|
|
2372
|
+
newerVersion: null
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
if (parsed.newerVersion !== null) return {
|
|
2376
|
+
record: emptyStats(),
|
|
2377
|
+
note: null,
|
|
2378
|
+
newerVersion: parsed.newerVersion
|
|
2379
|
+
};
|
|
2380
|
+
return {
|
|
2381
|
+
record: parsed.record,
|
|
2382
|
+
note: null,
|
|
2383
|
+
newerVersion: null
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
function writeStats(path, record) {
|
|
2387
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2388
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`);
|
|
2389
|
+
try {
|
|
2390
|
+
renameSync(tmp, path);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
rmSync(tmp, { force: true });
|
|
2393
|
+
throw error;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function applyRun(bucket, run, { at, durationMs, credit, phases }) {
|
|
2397
|
+
const next = bucket ? { ...bucket } : {
|
|
2398
|
+
runs: 0,
|
|
2399
|
+
failed: 0,
|
|
2400
|
+
hits: 0,
|
|
2401
|
+
misses: 0,
|
|
2402
|
+
coldRuns: 0,
|
|
2403
|
+
coldRunMs: 0,
|
|
2404
|
+
hitRuns: 0,
|
|
2405
|
+
hitRunMs: 0,
|
|
2406
|
+
timeSavedMs: 0,
|
|
2407
|
+
firstRunAt: at,
|
|
2408
|
+
lastRunAt: at
|
|
2409
|
+
};
|
|
2410
|
+
next.runs += 1;
|
|
2411
|
+
next.lastRunAt = at;
|
|
2412
|
+
if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
|
|
2413
|
+
if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
|
|
2414
|
+
if (run.failed) {
|
|
2415
|
+
next.failed += 1;
|
|
2416
|
+
return next;
|
|
2417
|
+
}
|
|
2418
|
+
if (isHit(run.cacheHit)) {
|
|
2419
|
+
next.hits += 1;
|
|
2420
|
+
if (!run.waitedForBuild) {
|
|
2421
|
+
next.hitRuns += 1;
|
|
2422
|
+
next.hitRunMs += durationMs;
|
|
2423
|
+
next.timeSavedMs += credit;
|
|
2424
|
+
}
|
|
2425
|
+
return next;
|
|
2426
|
+
}
|
|
2427
|
+
next.misses += 1;
|
|
2428
|
+
if (run.offloadedTo) {
|
|
2429
|
+
next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
|
|
2430
|
+
next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
|
|
2431
|
+
next.lastOffloadHost = run.offloadedTo;
|
|
2432
|
+
return next;
|
|
2433
|
+
}
|
|
2434
|
+
next.coldRuns += 1;
|
|
2435
|
+
next.coldRunMs += durationMs;
|
|
2436
|
+
return next;
|
|
2437
|
+
}
|
|
2438
|
+
function creditMs(bucket, run, durationMs) {
|
|
2439
|
+
if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
|
|
2440
|
+
if (!bucket || bucket.coldRuns <= 0) return 0;
|
|
2441
|
+
return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
|
|
2442
|
+
}
|
|
2443
|
+
function isHit(cacheHit) {
|
|
2444
|
+
return cacheHit === "local" || cacheHit === "remote";
|
|
2445
|
+
}
|
|
2446
|
+
function wholeMs(value) {
|
|
2447
|
+
const ms = Number(value);
|
|
2448
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
2449
|
+
}
|
|
2450
|
+
//#endregion
|
|
2451
|
+
//#region src/engine/build-detail.ts
|
|
2452
|
+
const LINE_MAX = 160;
|
|
2453
|
+
const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
|
|
2454
|
+
const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
|
|
2455
|
+
const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
|
|
2456
|
+
const GRADLE_TASK = /^> Task (:\S+)/;
|
|
2457
|
+
/**
|
|
2458
|
+
* xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
|
|
2459
|
+
* that touches nothing still signs framework products last. Targets that end any other way are not counted.
|
|
2460
|
+
*/
|
|
2461
|
+
const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
|
|
2462
|
+
const GRADLE_CONFIGURE = /^> Configure project /;
|
|
2463
|
+
/**
|
|
2464
|
+
* SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
|
|
2465
|
+
* output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
|
|
2466
|
+
* `<product>-product` is the link of a product.
|
|
2467
|
+
*/
|
|
2468
|
+
const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
|
|
2469
|
+
const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
|
|
2470
|
+
const XCODE_STEPS = [
|
|
2471
|
+
[/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
|
|
2472
|
+
[/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
|
|
2473
|
+
[/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
|
|
2474
|
+
[/^PhaseScriptExecution$/, "script"],
|
|
2475
|
+
[/^CodeSign$/, "sign"]
|
|
2476
|
+
];
|
|
2477
|
+
const GRADLE_STEPS = [
|
|
2478
|
+
[/[Dd]ex/, "dex"],
|
|
2479
|
+
[/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
|
|
2480
|
+
[/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
|
|
2481
|
+
[/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
|
|
2482
|
+
[/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
|
|
2483
|
+
];
|
|
2484
|
+
function stepOf(name, table) {
|
|
2485
|
+
return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
2486
|
+
}
|
|
2487
|
+
function clip(text) {
|
|
2488
|
+
return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
|
|
2489
|
+
}
|
|
2490
|
+
function unescape(token) {
|
|
2491
|
+
return token.replace(/\\(.)/g, "$1");
|
|
2492
|
+
}
|
|
2493
|
+
function baseName(path) {
|
|
2494
|
+
return path.split("/").findLast(Boolean) ?? path;
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* An xcodebuild action line in short form: the action, the file it works on and its target, as in
|
|
2498
|
+
* `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
|
|
2499
|
+
* as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
|
|
2500
|
+
* shows its name instead.
|
|
2501
|
+
*/
|
|
2502
|
+
function shortXcodeAction(action, args, target) {
|
|
2503
|
+
const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
|
|
2504
|
+
const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
|
|
2505
|
+
return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
|
|
2506
|
+
}
|
|
2507
|
+
function swiftpmStep(text) {
|
|
2508
|
+
return /^Linking\b|-product$/.test(text) ? "link" : "compile";
|
|
2509
|
+
}
|
|
2510
|
+
/** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
|
|
2511
|
+
function parseBuildToolLine(msg) {
|
|
2512
|
+
const counted = SWIFTPM_PROGRESS.exec(msg);
|
|
2513
|
+
if (counted) {
|
|
2514
|
+
const text = counted[3]?.trim() ?? "";
|
|
2515
|
+
return {
|
|
2516
|
+
kind: "count",
|
|
2517
|
+
done: Number(counted[1]),
|
|
2518
|
+
total: Number(counted[2]),
|
|
2519
|
+
step: swiftpmStep(text),
|
|
2520
|
+
line: text ? clip(text) : null
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
2524
|
+
const graph = TARGET_GRAPH.exec(msg);
|
|
2525
|
+
if (graph) return {
|
|
2526
|
+
kind: "total",
|
|
2527
|
+
unit: "targets",
|
|
2528
|
+
total: Number(graph[1])
|
|
2529
|
+
};
|
|
2530
|
+
if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
2531
|
+
const action = XCODE_ACTION.exec(msg);
|
|
2532
|
+
if (action) {
|
|
2533
|
+
const [, name, args, target, project] = action;
|
|
2534
|
+
const step = stepOf(name, XCODE_STEPS);
|
|
2535
|
+
return {
|
|
2536
|
+
kind: "unit",
|
|
2537
|
+
unit: "targets",
|
|
2538
|
+
id: `${project}/${target}`,
|
|
2539
|
+
step,
|
|
2540
|
+
line: step ? shortXcodeAction(name, args, target) : null,
|
|
2541
|
+
finished: XCODE_TARGET_DONE.test(name)
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
const task = GRADLE_TASK.exec(msg);
|
|
2545
|
+
if (task) {
|
|
2546
|
+
const path = task[1];
|
|
2547
|
+
return {
|
|
2548
|
+
kind: "unit",
|
|
2549
|
+
unit: "tasks",
|
|
2550
|
+
id: path,
|
|
2551
|
+
step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
|
|
2552
|
+
line: clip(`> Task ${path}`),
|
|
2553
|
+
finished: false
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
|
|
2560
|
+
* total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
|
|
2561
|
+
* its own `[done / total]` counts, which the parser passes through.
|
|
2562
|
+
*/
|
|
2563
|
+
function createBuildDetailParser() {
|
|
2564
|
+
let seen = false;
|
|
2565
|
+
let step = null;
|
|
2566
|
+
let unit = null;
|
|
2567
|
+
let total = null;
|
|
2568
|
+
let line = null;
|
|
2569
|
+
let tasks = 0;
|
|
2570
|
+
let counted = null;
|
|
2571
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2572
|
+
return {
|
|
2573
|
+
push(msg) {
|
|
2574
|
+
const parsed = parseBuildToolLine(msg);
|
|
2575
|
+
if (!parsed) return false;
|
|
2576
|
+
seen = true;
|
|
2577
|
+
if (parsed.kind === "total") {
|
|
2578
|
+
unit = "targets";
|
|
2579
|
+
total = parsed.total;
|
|
2580
|
+
return true;
|
|
2581
|
+
}
|
|
2582
|
+
if (parsed.kind === "configure") {
|
|
2583
|
+
step = "configure";
|
|
2584
|
+
return true;
|
|
2585
|
+
}
|
|
2586
|
+
if (parsed.kind === "count") {
|
|
2587
|
+
unit = "steps";
|
|
2588
|
+
counted = {
|
|
2589
|
+
done: parsed.done,
|
|
2590
|
+
total: parsed.total
|
|
2591
|
+
};
|
|
2592
|
+
step = parsed.step;
|
|
2593
|
+
if (parsed.line) line = parsed.line;
|
|
2594
|
+
return true;
|
|
2595
|
+
}
|
|
2596
|
+
unit = parsed.unit;
|
|
2597
|
+
if (parsed.unit === "tasks") tasks += 1;
|
|
2598
|
+
else if (parsed.finished) targets.add(parsed.id);
|
|
2599
|
+
if (parsed.step) step = parsed.step;
|
|
2600
|
+
if (parsed.line) line = parsed.line;
|
|
2601
|
+
return true;
|
|
2602
|
+
},
|
|
2603
|
+
detail(updatedAt) {
|
|
2604
|
+
if (!seen) return null;
|
|
2605
|
+
if (unit === "steps" && counted) return {
|
|
2606
|
+
step,
|
|
2607
|
+
unit,
|
|
2608
|
+
done: Math.min(counted.done, counted.total),
|
|
2609
|
+
total: counted.total,
|
|
2610
|
+
line,
|
|
2611
|
+
updatedAt
|
|
2612
|
+
};
|
|
2613
|
+
const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
|
|
2614
|
+
return {
|
|
2615
|
+
step,
|
|
2616
|
+
unit,
|
|
2617
|
+
done: done !== null && total !== null ? Math.min(done, total) : done,
|
|
2618
|
+
total: unit === "tasks" ? null : total,
|
|
2619
|
+
line,
|
|
2620
|
+
updatedAt
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
//#endregion
|
|
2626
|
+
//#region src/engine/build-progress.ts
|
|
2627
|
+
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
2628
|
+
const NO_BUILD_PROGRESS = {
|
|
2629
|
+
step: () => {},
|
|
2630
|
+
estimate: () => {},
|
|
2631
|
+
miss: () => {},
|
|
2632
|
+
hit: () => {},
|
|
2633
|
+
deviceSetup: () => {},
|
|
2634
|
+
deviceSetupKnown: () => void 0,
|
|
2635
|
+
place: () => {},
|
|
2636
|
+
waitingOn: () => {},
|
|
2637
|
+
waitingFor: () => {},
|
|
2638
|
+
output: () => {},
|
|
2639
|
+
durations: () => ({}),
|
|
2640
|
+
steps: () => null,
|
|
2641
|
+
clear: () => {}
|
|
2642
|
+
};
|
|
2643
|
+
/** `writer`, with each record it writes also passed to `progress.output`. */
|
|
2644
|
+
function tapBuildLog(writer, progress) {
|
|
2645
|
+
if (progress === NO_BUILD_PROGRESS) return writer;
|
|
2646
|
+
return {
|
|
2647
|
+
get file() {
|
|
2648
|
+
return writer.file;
|
|
2649
|
+
},
|
|
2650
|
+
write(record) {
|
|
2651
|
+
progress.output(record);
|
|
2652
|
+
return writer.write(record);
|
|
2653
|
+
},
|
|
2654
|
+
close: () => writer.close(),
|
|
2655
|
+
get written() {
|
|
2656
|
+
return writer.written;
|
|
2657
|
+
},
|
|
2658
|
+
get dropped() {
|
|
2659
|
+
return writer.dropped;
|
|
2660
|
+
},
|
|
2661
|
+
get lastError() {
|
|
2662
|
+
return writer.lastError;
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
const DETAIL_WRITE_MS = 2e3;
|
|
2667
|
+
function toolLine(record) {
|
|
2668
|
+
if (!record || typeof record !== "object") return null;
|
|
2669
|
+
const { src, level, msg } = record;
|
|
2670
|
+
return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
|
|
2671
|
+
}
|
|
2672
|
+
function writeBuildDetail(root, claimId, detail) {
|
|
2673
|
+
const file = workspaceBuildDetailFile(root);
|
|
2674
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
2675
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
2676
|
+
writeFileSync(tmp, JSON.stringify({
|
|
2677
|
+
claimId,
|
|
2678
|
+
detail
|
|
2679
|
+
}));
|
|
2680
|
+
renameSync(tmp, file);
|
|
2681
|
+
}
|
|
2682
|
+
const COLD_PHASES = [
|
|
2683
|
+
"prebuild",
|
|
2684
|
+
"pods",
|
|
2685
|
+
"compile"
|
|
2686
|
+
];
|
|
2687
|
+
const HIT_PHASES = ["install", "launch"];
|
|
2688
|
+
function settledOutcome(phase, entered) {
|
|
2689
|
+
if (COLD_PHASES.includes(phase)) return "cold";
|
|
2690
|
+
if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
|
|
2691
|
+
return HIT_PHASES.includes(phase) ? "hit" : null;
|
|
2692
|
+
}
|
|
2693
|
+
function projectHistory(projectKey) {
|
|
2694
|
+
try {
|
|
2695
|
+
return readStats$1().record?.history?.[projectKey];
|
|
2696
|
+
} catch {
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
|
|
2701
|
+
const startedAt = new Date(now()).toISOString();
|
|
2702
|
+
const record = {
|
|
2703
|
+
platform,
|
|
2704
|
+
slot,
|
|
2705
|
+
startedAt,
|
|
2706
|
+
phase: "prepare",
|
|
2707
|
+
phaseStartedAt: startedAt,
|
|
2708
|
+
phases: [{
|
|
2709
|
+
phase: "prepare",
|
|
2710
|
+
startedAt
|
|
2711
|
+
}],
|
|
2712
|
+
claim: {
|
|
2713
|
+
root: claim.root,
|
|
2714
|
+
path: claim.path,
|
|
2715
|
+
claimId: claim.claimId,
|
|
2716
|
+
pid: claim.owner.pid
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
let warned = false;
|
|
2720
|
+
const guard = (fn) => {
|
|
2721
|
+
try {
|
|
2722
|
+
fn();
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
if (warned) return;
|
|
2725
|
+
warned = true;
|
|
2726
|
+
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
|
|
2730
|
+
...before(state),
|
|
2731
|
+
[ACTIVE_BUILD_KEY]: record
|
|
2732
|
+
})));
|
|
2733
|
+
write(withInterruptedBuild);
|
|
2734
|
+
let projectKey = null;
|
|
2735
|
+
const parser = createBuildDetailParser();
|
|
2736
|
+
let detailWrittenAt = -Infinity;
|
|
2737
|
+
let detailTimer = null;
|
|
2738
|
+
const flushDetail = () => {
|
|
2739
|
+
detailTimer = null;
|
|
2740
|
+
detailWrittenAt = now();
|
|
2741
|
+
const detail = parser.detail(new Date(detailWrittenAt).toISOString());
|
|
2742
|
+
if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
|
|
2743
|
+
};
|
|
2744
|
+
const settle = (outcome) => {
|
|
2745
|
+
record.outcome = outcome;
|
|
2746
|
+
if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
|
|
2747
|
+
};
|
|
2748
|
+
const waits = /* @__PURE__ */ new Map();
|
|
2749
|
+
return {
|
|
2750
|
+
step(phase) {
|
|
2751
|
+
if (phase === record.phase) return;
|
|
2752
|
+
const at = new Date(now()).toISOString();
|
|
2753
|
+
record.phase = phase;
|
|
2754
|
+
record.phaseStartedAt = at;
|
|
2755
|
+
record.phases.push({
|
|
2756
|
+
phase,
|
|
2757
|
+
startedAt: at
|
|
2758
|
+
});
|
|
2759
|
+
if (phase !== "wait") delete record.waitingOn;
|
|
2760
|
+
const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
|
|
2761
|
+
if (outcome) settle(outcome);
|
|
2762
|
+
write();
|
|
2763
|
+
},
|
|
2764
|
+
estimate(key) {
|
|
2765
|
+
if (platform === "macos") return;
|
|
2766
|
+
projectKey = key;
|
|
2767
|
+
record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
2768
|
+
write();
|
|
2769
|
+
},
|
|
2770
|
+
miss(reason, provisional = false) {
|
|
2771
|
+
record.missReason = reason;
|
|
2772
|
+
settle("cold");
|
|
2773
|
+
if (provisional) record.missProvisional = true;
|
|
2774
|
+
else delete record.missProvisional;
|
|
2775
|
+
write();
|
|
2776
|
+
},
|
|
2777
|
+
hit() {
|
|
2778
|
+
delete record.missReason;
|
|
2779
|
+
delete record.missProvisional;
|
|
2780
|
+
settle("hit");
|
|
2781
|
+
write();
|
|
2782
|
+
},
|
|
2783
|
+
deviceSetup(setup) {
|
|
2784
|
+
if (setup === void 0) return;
|
|
2785
|
+
record.deviceSetup = setup;
|
|
2786
|
+
write();
|
|
2787
|
+
},
|
|
2788
|
+
deviceSetupKnown() {
|
|
2789
|
+
return record.deviceSetup;
|
|
2790
|
+
},
|
|
2791
|
+
place(remote) {
|
|
2792
|
+
const current = record.placement;
|
|
2793
|
+
if (!remote) {
|
|
2794
|
+
if (!current) return;
|
|
2795
|
+
delete record.placement;
|
|
2796
|
+
return write();
|
|
2797
|
+
}
|
|
2798
|
+
if (current?.host === remote.host && current.phase === remote.phase) return;
|
|
2799
|
+
const at = new Date(now()).toISOString();
|
|
2800
|
+
record.placement = {
|
|
2801
|
+
host: remote.host,
|
|
2802
|
+
phase: remote.phase,
|
|
2803
|
+
startedAt: current?.host === remote.host ? current.startedAt : at,
|
|
2804
|
+
phaseStartedAt: at
|
|
2805
|
+
};
|
|
2806
|
+
write();
|
|
2807
|
+
},
|
|
2808
|
+
waitingFor(info, kind) {
|
|
2809
|
+
if (info) waits.set(info.kind, info);
|
|
2810
|
+
else if (kind) waits.delete(kind);
|
|
2811
|
+
else waits.clear();
|
|
2812
|
+
const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
|
|
2813
|
+
if (first) record.waitingFor = first;
|
|
2814
|
+
else delete record.waitingFor;
|
|
2815
|
+
write();
|
|
2816
|
+
},
|
|
2817
|
+
waitingOn(holder) {
|
|
2818
|
+
if (holder === null) {
|
|
2819
|
+
if (!record.waitingOn) return;
|
|
2820
|
+
delete record.waitingOn;
|
|
2821
|
+
} else {
|
|
2822
|
+
if (record.waitingOn?.path === holder) return;
|
|
2823
|
+
record.waitingOn = { path: holder };
|
|
2824
|
+
}
|
|
2825
|
+
write();
|
|
2826
|
+
},
|
|
2827
|
+
output(line) {
|
|
2828
|
+
const msg = toolLine(line);
|
|
2829
|
+
if (msg === null || !parser.push(msg) || detailTimer) return;
|
|
2830
|
+
const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
|
|
2831
|
+
if (wait <= 0) return flushDetail();
|
|
2832
|
+
detailTimer = setTimeout(flushDetail, wait);
|
|
2833
|
+
detailTimer.unref?.();
|
|
2834
|
+
},
|
|
2835
|
+
durations() {
|
|
2836
|
+
return phaseDurations(record.phases, now());
|
|
2837
|
+
},
|
|
2838
|
+
steps() {
|
|
2839
|
+
const detail = parser.detail("");
|
|
2840
|
+
return detail?.unit === "steps" ? detail.total : null;
|
|
2841
|
+
},
|
|
2842
|
+
clear() {
|
|
2843
|
+
if (detailTimer) clearTimeout(detailTimer);
|
|
2844
|
+
detailTimer = null;
|
|
2845
|
+
guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
|
|
2846
|
+
guard(() => {
|
|
2847
|
+
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
function phaseDurations(phases, now) {
|
|
2853
|
+
const out = {};
|
|
2854
|
+
phases.forEach(({ phase, startedAt }, index) => {
|
|
2855
|
+
const start = Date.parse(startedAt);
|
|
2856
|
+
const next = phases[index + 1];
|
|
2857
|
+
const end = next ? Date.parse(next.startedAt) : now;
|
|
2858
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
2859
|
+
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
2860
|
+
});
|
|
2861
|
+
return out;
|
|
2862
|
+
}
|
|
2863
|
+
function completedPhaseDurations(phases) {
|
|
2864
|
+
if (phases.length < 2) return void 0;
|
|
2865
|
+
const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
|
|
2866
|
+
return Object.keys(durations).length ? durations : void 0;
|
|
2867
|
+
}
|
|
2868
|
+
const HISTORY_FIELDS = [
|
|
2869
|
+
"devicePlacement",
|
|
2870
|
+
"platform",
|
|
2871
|
+
"status",
|
|
2872
|
+
"configuration",
|
|
2873
|
+
"fingerprint",
|
|
2874
|
+
"cacheKey",
|
|
2875
|
+
"cacheHit",
|
|
2876
|
+
"cacheSkipped",
|
|
2877
|
+
"durationMs",
|
|
2878
|
+
"startedAt",
|
|
2879
|
+
"errorCode",
|
|
2880
|
+
"missReason",
|
|
2881
|
+
"buildMachine",
|
|
2882
|
+
"builtOn",
|
|
2883
|
+
"offloadedTo",
|
|
2884
|
+
"offloadFallback",
|
|
2885
|
+
"diagnostics",
|
|
2886
|
+
"compileSteps"
|
|
2887
|
+
];
|
|
2888
|
+
function withHistoryEntry(state, platform, entry) {
|
|
2889
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2890
|
+
const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
|
|
2891
|
+
const list = Array.isArray(history[platform]) ? history[platform] : [];
|
|
2892
|
+
return {
|
|
2893
|
+
...state,
|
|
2894
|
+
[BUILD_HISTORY_KEY]: {
|
|
2895
|
+
...history,
|
|
2896
|
+
[platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
function finishedResult(record) {
|
|
2901
|
+
if (record.status === "ok") return "succeeded";
|
|
2902
|
+
return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
|
|
2903
|
+
}
|
|
2904
|
+
function recordFinishedBuild(root, record, { update = updateWorkspaceState, now = Date.now } = {}) {
|
|
2905
|
+
const platform = record.platform;
|
|
2906
|
+
update(root, (state) => {
|
|
2907
|
+
const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2908
|
+
const own = active?.platform === platform ? active : null;
|
|
2909
|
+
const entry = {
|
|
2910
|
+
...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
|
|
2911
|
+
result: finishedResult(record),
|
|
2912
|
+
slot: own?.slot ?? "default",
|
|
2913
|
+
phases: own ? phaseDurations(own.phases, now()) : {}
|
|
2914
|
+
};
|
|
2915
|
+
const slot = own?.slot ?? "default";
|
|
2916
|
+
const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
|
|
2917
|
+
const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
|
|
2918
|
+
const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
|
|
2919
|
+
if (platform !== "macos" && (record.devicePlacement || isJsonObject(savedDevice))) {
|
|
2920
|
+
const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
|
|
2921
|
+
if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
|
|
2922
|
+
else delete device.devicePlacement;
|
|
2923
|
+
state = slot === "default" ? {
|
|
2924
|
+
...state,
|
|
2925
|
+
[platform]: device
|
|
2926
|
+
} : {
|
|
2927
|
+
...state,
|
|
2928
|
+
deviceSlots: {
|
|
2929
|
+
...slots,
|
|
2930
|
+
[slot]: {
|
|
2931
|
+
...savedSlot,
|
|
2932
|
+
[platform]: device
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
if (platform === "macos") return withHistoryEntry(state, platform, entry);
|
|
2938
|
+
return withHistoryEntry({
|
|
2939
|
+
...state,
|
|
2940
|
+
lastBuild: record,
|
|
2941
|
+
[LAST_BUILD_KEYS[platform]]: record
|
|
2942
|
+
}, platform, entry);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
/** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
|
|
2946
|
+
function recordBuildPhase(root, platform, startedAt, phase, ms) {
|
|
2947
|
+
updateWorkspaceState(root, (state) => {
|
|
2948
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2949
|
+
const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
|
|
2950
|
+
const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
|
|
2951
|
+
const entry = list[index];
|
|
2952
|
+
if (!isJsonObject(entry)) return state;
|
|
2953
|
+
const phases = isJsonObject(entry.phases) ? entry.phases : {};
|
|
2954
|
+
const next = [...list];
|
|
2955
|
+
next[index] = {
|
|
2956
|
+
...entry,
|
|
2957
|
+
phases: {
|
|
2958
|
+
...phases,
|
|
2959
|
+
[phase]: Math.round(ms)
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
return {
|
|
2963
|
+
...state,
|
|
2964
|
+
[BUILD_HISTORY_KEY]: {
|
|
2965
|
+
...saved,
|
|
2966
|
+
[platform]: next
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
function withInterruptedBuild(state) {
|
|
2972
|
+
const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2973
|
+
if (!left) return state;
|
|
2974
|
+
const last = left.platform === "macos" ? state[BUILD_HISTORY_KEY]?.macos?.[0] : state[LAST_BUILD_KEYS[left.platform]];
|
|
2975
|
+
if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
|
|
2976
|
+
return withHistoryEntry(state, left.platform, {
|
|
2977
|
+
platform: left.platform,
|
|
2978
|
+
status: "failed",
|
|
2979
|
+
result: "interrupted",
|
|
2980
|
+
slot: left.slot,
|
|
2981
|
+
configuration: null,
|
|
2982
|
+
fingerprint: null,
|
|
2983
|
+
cacheKey: null,
|
|
2984
|
+
cacheHit: false,
|
|
2985
|
+
cacheSkipped: false,
|
|
2986
|
+
durationMs: null,
|
|
2987
|
+
startedAt: left.startedAt,
|
|
2988
|
+
phases: {
|
|
2989
|
+
...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
|
|
2990
|
+
[left.phase]: 0
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
function parseActiveBuild(value) {
|
|
2995
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2996
|
+
const record = value;
|
|
2997
|
+
const claim = record.claim;
|
|
2998
|
+
if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
|
|
2999
|
+
if (!isPhase(record.phase)) return null;
|
|
3000
|
+
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
3001
|
+
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
3002
|
+
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
3003
|
+
const missReason = parseMissReason(record.missReason);
|
|
3004
|
+
const placement = parsePlacement(record.placement);
|
|
3005
|
+
const waitingOn = parseWaitingOn(record.waitingOn);
|
|
3006
|
+
const waitingFor = parseWaitingFor(record.waitingFor);
|
|
3007
|
+
const estimate = parseEstimate(record.estimate);
|
|
3008
|
+
return {
|
|
3009
|
+
platform: record.platform,
|
|
3010
|
+
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
3011
|
+
startedAt: record.startedAt,
|
|
3012
|
+
phase: record.phase,
|
|
3013
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
3014
|
+
phases,
|
|
3015
|
+
...missReason ? { missReason } : {},
|
|
3016
|
+
...missReason && record.missProvisional === true ? { missProvisional: true } : {},
|
|
3017
|
+
...placement ? { placement } : {},
|
|
3018
|
+
...waitingOn ? { waitingOn } : {},
|
|
3019
|
+
...waitingFor ? { waitingFor } : {},
|
|
3020
|
+
...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
|
|
3021
|
+
...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
|
|
3022
|
+
...estimate ? { estimate } : {},
|
|
3023
|
+
claim: {
|
|
3024
|
+
root: claim.root,
|
|
3025
|
+
path: typeof claim.path === "string" ? claim.path : "",
|
|
3026
|
+
claimId: claim.claimId,
|
|
3027
|
+
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
3028
|
+
}
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function parsePlacement(value) {
|
|
3032
|
+
if (!value || typeof value !== "object") return null;
|
|
3033
|
+
const { host, phase, startedAt, phaseStartedAt } = value;
|
|
3034
|
+
if ([
|
|
3035
|
+
host,
|
|
3036
|
+
phase,
|
|
3037
|
+
startedAt,
|
|
3038
|
+
phaseStartedAt
|
|
3039
|
+
].some((field) => typeof field !== "string")) return null;
|
|
3040
|
+
return {
|
|
3041
|
+
host,
|
|
3042
|
+
phase,
|
|
3043
|
+
startedAt,
|
|
3044
|
+
phaseStartedAt
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
function parseWaitingFor(value) {
|
|
3048
|
+
if (!value || typeof value !== "object") return null;
|
|
3049
|
+
const { kind, inUse, max, since } = value;
|
|
3050
|
+
if (kind !== "build-slot" && kind !== "device-slot") return null;
|
|
3051
|
+
if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
|
|
3052
|
+
if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
|
|
3053
|
+
return {
|
|
3054
|
+
kind,
|
|
3055
|
+
inUse,
|
|
3056
|
+
max,
|
|
3057
|
+
since
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
function parseWaitingOn(value) {
|
|
3061
|
+
const path = value?.path;
|
|
3062
|
+
return typeof path === "string" && path ? { path } : null;
|
|
3063
|
+
}
|
|
3064
|
+
function parseEstimate(value) {
|
|
3065
|
+
if (!value || typeof value !== "object") return null;
|
|
3066
|
+
const { outcome, expectedMs, basis, phaseMs, planned } = value;
|
|
3067
|
+
if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
|
|
3068
|
+
if (expectedMs !== null && typeof expectedMs !== "number") return null;
|
|
3069
|
+
if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
|
|
3070
|
+
const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
|
|
3071
|
+
return {
|
|
3072
|
+
outcome,
|
|
3073
|
+
expectedMs,
|
|
3074
|
+
basis,
|
|
3075
|
+
phaseMs: Object.fromEntries(durations),
|
|
3076
|
+
planned: planned.filter(isPhase)
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
function isPhase(value) {
|
|
3080
|
+
return BUILD_PHASES.includes(value);
|
|
3081
|
+
}
|
|
3082
|
+
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
3083
|
+
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
3084
|
+
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
3085
|
+
return survey.unresolved.length ? "unknown" : "stale";
|
|
3086
|
+
}
|
|
3087
|
+
function median(values) {
|
|
3088
|
+
if (!values.length) return null;
|
|
3089
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
3090
|
+
const mid = Math.floor(sorted.length / 2);
|
|
3091
|
+
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
3092
|
+
}
|
|
3093
|
+
function estimateBuild(history, platform, known, deviceSetup) {
|
|
3094
|
+
const lists = platform === "macos" ? void 0 : history?.[platform];
|
|
3095
|
+
const outcome = known ?? latestOutcome(lists);
|
|
3096
|
+
const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
|
|
3097
|
+
const phaseMs = {};
|
|
3098
|
+
const planned = [];
|
|
3099
|
+
for (const phase of BUILD_PHASES) {
|
|
3100
|
+
const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
3101
|
+
const ms = median(durations);
|
|
3102
|
+
if (ms === null) continue;
|
|
3103
|
+
phaseMs[phase] = ms;
|
|
3104
|
+
if (durations.length * 2 >= samples.length) planned.push(phase);
|
|
3105
|
+
}
|
|
3106
|
+
return {
|
|
3107
|
+
outcome,
|
|
3108
|
+
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
3109
|
+
basis: samples.length,
|
|
3110
|
+
phaseMs,
|
|
3111
|
+
planned
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
const MIN_TAGGED_SAMPLES = 3;
|
|
3115
|
+
function matchDeviceSetup(samples, deviceSetup) {
|
|
3116
|
+
if (deviceSetup === void 0) return samples;
|
|
3117
|
+
const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
|
|
3118
|
+
if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
|
|
3119
|
+
return samples.filter((sample) => sample.deviceSetup !== true);
|
|
3120
|
+
}
|
|
3121
|
+
function latestOutcome(lists) {
|
|
3122
|
+
let latest = null;
|
|
3123
|
+
for (const outcome of ["hit", "cold"]) {
|
|
3124
|
+
const last = lists?.[outcome]?.at(-1);
|
|
3125
|
+
const at = last ? Date.parse(last.at) : NaN;
|
|
3126
|
+
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
3127
|
+
outcome,
|
|
3128
|
+
at
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
return latest?.outcome ?? null;
|
|
3132
|
+
}
|
|
3133
|
+
function buildReport(record, { state, history }) {
|
|
3134
|
+
const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
3135
|
+
const plannedPhases = estimate.planned.flatMap((phase) => {
|
|
3136
|
+
const expectedMs = estimate.phaseMs[phase];
|
|
3137
|
+
return expectedMs === void 0 ? [] : [{
|
|
3138
|
+
phase,
|
|
3139
|
+
expectedMs
|
|
3140
|
+
}];
|
|
3141
|
+
});
|
|
3142
|
+
const completedPhaseMs = completedPhaseDurations(record.phases);
|
|
3143
|
+
return {
|
|
3144
|
+
platform: record.platform,
|
|
3145
|
+
slot: record.slot,
|
|
3146
|
+
state,
|
|
3147
|
+
phase: record.phase,
|
|
3148
|
+
startedAt: record.startedAt,
|
|
3149
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
3150
|
+
outcome: estimate.outcome,
|
|
3151
|
+
outcomeKnown: record.outcome !== void 0,
|
|
3152
|
+
...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
|
|
3153
|
+
expectedMs: estimate.expectedMs,
|
|
3154
|
+
expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
|
|
3155
|
+
...completedPhaseMs ? { completedPhaseMs } : {},
|
|
3156
|
+
basis: estimate.basis,
|
|
3157
|
+
plannedPhases: plannedPhases.length ? plannedPhases : null,
|
|
3158
|
+
...record.missReason ? { missReason: record.missReason } : {},
|
|
3159
|
+
...record.missReason && record.missProvisional ? { missProvisional: true } : {},
|
|
3160
|
+
placement: record.placement ?? "local",
|
|
3161
|
+
...record.waitingOn ? { waitingOn: record.waitingOn } : {},
|
|
3162
|
+
...record.waitingFor ? { waitingFor: record.waitingFor } : {}
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
function remainingText(remainingMs) {
|
|
3166
|
+
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
3167
|
+
}
|
|
3168
|
+
function buildStatusLine(report, now) {
|
|
3169
|
+
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
3170
|
+
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
3171
|
+
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
3172
|
+
const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
|
|
3173
|
+
const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
|
|
3174
|
+
const head = `build: ${report.platform}${slot} ${report.phase}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
3175
|
+
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
3176
|
+
if (report.expectedMs === null) return head;
|
|
3177
|
+
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
3178
|
+
const remainingMs = report.expectedMs - elapsedMs;
|
|
3179
|
+
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
3180
|
+
}
|
|
3181
|
+
//#endregion
|
|
3182
|
+
//#region src/commands/gc/idle.ts
|
|
3183
|
+
function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
|
|
3184
|
+
const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
|
|
3185
|
+
const dead = new Set(deadProjects);
|
|
3186
|
+
const found = [];
|
|
3187
|
+
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
3188
|
+
if (dead.has(project)) continue;
|
|
3189
|
+
for (const { slot, platforms } of projectDeviceSlots(record)) {
|
|
3190
|
+
const ios = platforms.ios;
|
|
3191
|
+
if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
|
|
3192
|
+
kind: "ios",
|
|
3193
|
+
id: ios.deviceUdid,
|
|
3194
|
+
name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
|
|
3195
|
+
project,
|
|
3196
|
+
slot,
|
|
3197
|
+
activity: readActivity({
|
|
3198
|
+
platform: "ios",
|
|
3199
|
+
id: ios.deviceUdid,
|
|
3200
|
+
slot,
|
|
3201
|
+
workspace: project
|
|
3202
|
+
})
|
|
3203
|
+
});
|
|
3204
|
+
const android = platforms.android;
|
|
3205
|
+
const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
|
|
3206
|
+
if (android?.avdName && serial) found.push({
|
|
3207
|
+
kind: "android",
|
|
3208
|
+
id: android.avdName,
|
|
3209
|
+
name: android.avdName,
|
|
3210
|
+
project,
|
|
3211
|
+
slot,
|
|
3212
|
+
activity: readActivity({
|
|
3213
|
+
platform: "android",
|
|
3214
|
+
id: serial,
|
|
3215
|
+
slot,
|
|
3216
|
+
workspace: project
|
|
3217
|
+
})
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
return found;
|
|
3222
|
+
}
|
|
3223
|
+
function findIdleDevices({ buildInProgress, now, ...inputs }) {
|
|
3224
|
+
return findOwnedDeviceActivity(inputs).filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
|
|
3225
|
+
kind,
|
|
3226
|
+
id,
|
|
3227
|
+
name,
|
|
3228
|
+
project,
|
|
3229
|
+
slot,
|
|
3230
|
+
lastActivityAt: activity.lastActivityAt ?? null,
|
|
3231
|
+
idleForMs: idleForMs(activity, now),
|
|
3232
|
+
buildInProgress: buildInProgress(project)
|
|
3233
|
+
}));
|
|
3234
|
+
}
|
|
3235
|
+
function idleShutdownCandidates(devices, idleMs) {
|
|
3236
|
+
return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
|
|
3237
|
+
}
|
|
3238
|
+
function parseIdleDuration(value) {
|
|
3239
|
+
const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
|
|
3240
|
+
if (!match) return null;
|
|
3241
|
+
const unit = {
|
|
3242
|
+
m: 6e4,
|
|
3243
|
+
h: 36e5,
|
|
3244
|
+
d: 864e5
|
|
3245
|
+
}[match[2]];
|
|
3246
|
+
return Number(match[1]) * unit;
|
|
3247
|
+
}
|
|
3248
|
+
function workspaceBuildInProgress(project) {
|
|
3249
|
+
const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
|
|
3250
|
+
if (!record) return false;
|
|
3251
|
+
try {
|
|
3252
|
+
return activeBuildState(record.claim) !== "stale";
|
|
3253
|
+
} catch {
|
|
3254
|
+
return true;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
function deviceActivityInputs(config, sims, now) {
|
|
3258
|
+
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
3259
|
+
return {
|
|
3260
|
+
config,
|
|
3261
|
+
sims,
|
|
3262
|
+
readActivity: createActivityReader({ now }),
|
|
3263
|
+
androidSerial: (avdName) => {
|
|
3264
|
+
try {
|
|
3265
|
+
return resolve(avdName).serial ?? null;
|
|
3266
|
+
} catch {
|
|
3267
|
+
return null;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function collectOwnedDeviceActivity(config, sims, now = Date.now()) {
|
|
3273
|
+
return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now));
|
|
3274
|
+
}
|
|
3275
|
+
function collectIdleDevices(config, sims, deadProjects, now = Date.now()) {
|
|
3276
|
+
return findIdleDevices({
|
|
3277
|
+
...deviceActivityInputs(config, sims, now),
|
|
3278
|
+
deadProjects,
|
|
3279
|
+
now,
|
|
3280
|
+
buildInProgress: workspaceBuildInProgress
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
function idleDeviceLines(devices) {
|
|
3284
|
+
if (devices.length === 0) return [];
|
|
3285
|
+
return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
|
|
3286
|
+
const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
|
|
3287
|
+
const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
|
|
3288
|
+
const build = d.buildInProgress ? ", build in progress" : "";
|
|
3289
|
+
return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
|
|
3290
|
+
})];
|
|
3291
|
+
}
|
|
3292
|
+
function shutDownIdleDevices(devices, idleMs, recollect) {
|
|
3293
|
+
const candidates = idleShutdownCandidates(devices, idleMs);
|
|
3294
|
+
const shutDown = [];
|
|
3295
|
+
if (candidates.length === 0) {
|
|
3296
|
+
console.log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
|
|
3297
|
+
return {
|
|
3298
|
+
failures: 0,
|
|
3299
|
+
shutDown
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
const fresh = idleShutdownCandidates(recollect(), idleMs);
|
|
3303
|
+
let failures = 0;
|
|
3304
|
+
for (const device of candidates) {
|
|
3305
|
+
const what = `${device.kind} ${device.name}`;
|
|
3306
|
+
const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
|
|
3307
|
+
if (!current) {
|
|
3308
|
+
console.log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
|
|
3309
|
+
recordGcResult("idleDevice", "kept", device.name, {
|
|
3310
|
+
id: device.id,
|
|
3311
|
+
detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
|
|
3312
|
+
});
|
|
3313
|
+
continue;
|
|
3314
|
+
}
|
|
3315
|
+
const outcome = device.kind === "ios" ? teardownOwnedIosSim(device.id, { label: device.name }) : teardownOwnedAvd(device.id, { owner: {
|
|
3316
|
+
projectPath: device.project,
|
|
3317
|
+
slot: device.slot
|
|
3318
|
+
} });
|
|
3319
|
+
if (outcome.status === "torn-down") {
|
|
3320
|
+
shutDown.push(device);
|
|
3321
|
+
console.log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
|
|
3322
|
+
recordGcResult("idleDevice", "done", device.name, {
|
|
3323
|
+
id: device.id,
|
|
3324
|
+
detail: `idle ${formatLongDuration(current.idleForMs)}`
|
|
3325
|
+
});
|
|
3326
|
+
} else if (outcome.status === "missing") console.log(chalk.dim(`${what} is already gone; nothing to shut down.`));
|
|
3327
|
+
else if (outcome.status === "skipped") {
|
|
3328
|
+
console.log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
|
|
3329
|
+
recordGcResult("idleDevice", "kept", device.name, {
|
|
3330
|
+
id: device.id,
|
|
3331
|
+
detail: outcome.reason
|
|
3332
|
+
});
|
|
3333
|
+
} else {
|
|
3334
|
+
failures++;
|
|
3335
|
+
console.log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
|
|
3336
|
+
recordGcResult("idleDevice", "failed", device.name, {
|
|
3337
|
+
id: device.id,
|
|
3338
|
+
detail: outcome.reason
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return {
|
|
3343
|
+
failures,
|
|
3344
|
+
shutDown
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
//#endregion
|
|
3348
|
+
//#region src/supervisor/stop-cause.ts
|
|
3349
|
+
function requestDevServerStop(root, processToken, requester) {
|
|
3350
|
+
if (!processToken) return;
|
|
3351
|
+
try {
|
|
3352
|
+
writeWorkspaceState(root, { [DEV_SERVER_STOP_REQUEST_KEY]: {
|
|
3353
|
+
processToken,
|
|
3354
|
+
by: requester.by,
|
|
3355
|
+
pid: process.pid,
|
|
3356
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3357
|
+
...requester.workspace ? { workspace: requester.workspace } : {}
|
|
3358
|
+
} });
|
|
3359
|
+
} catch {}
|
|
3360
|
+
}
|
|
3361
|
+
function withdrawDevServerStopRequest(root, processToken) {
|
|
3362
|
+
if (!processToken) return;
|
|
3363
|
+
try {
|
|
3364
|
+
clearWorkspaceStateKey(root, DEV_SERVER_STOP_REQUEST_KEY, (value) => {
|
|
3365
|
+
return readDevServerStopRequest({ [DEV_SERVER_STOP_REQUEST_KEY]: value })?.processToken === processToken;
|
|
3366
|
+
});
|
|
3367
|
+
} catch {}
|
|
3368
|
+
}
|
|
3369
|
+
function devServerStopRecord(trigger, request, at) {
|
|
3370
|
+
if (request) return {
|
|
3371
|
+
reason: "requested",
|
|
3372
|
+
at,
|
|
3373
|
+
by: request.by,
|
|
3374
|
+
byPid: request.pid,
|
|
3375
|
+
...request.workspace ? { byWorkspace: request.workspace } : {}
|
|
3376
|
+
};
|
|
3377
|
+
return trigger.kind === "signal" ? {
|
|
3378
|
+
reason: "signal",
|
|
3379
|
+
at,
|
|
3380
|
+
signal: trigger.signal
|
|
3381
|
+
} : {
|
|
3382
|
+
reason: "server-exited",
|
|
3383
|
+
at,
|
|
3384
|
+
mode: trigger.mode,
|
|
3385
|
+
code: trigger.code,
|
|
3386
|
+
signal: trigger.signal
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
function describeDevServerStop(record) {
|
|
3390
|
+
if (record.reason === "idle") return `idle for ${record.idleMinutes} minutes (metro.idleStopMinutes)`;
|
|
3391
|
+
if (record.reason === "requested") return `stop requested by ${record.by} (pid ${record.byPid}${record.byWorkspace ? ` in ${record.byWorkspace}` : ""})`;
|
|
3392
|
+
if (record.reason === "signal") return `received ${record.signal} with no Stim stop request recorded, so it likely came from outside Stim`;
|
|
3393
|
+
const detail = record.signal ? `signal ${record.signal}` : `exit code ${record.code ?? "unknown"}`;
|
|
3394
|
+
const expoSignal = record.mode === "expo-child" && record.code === 0 ? "; Expo CLI also exits 0 on SIGTERM, and no Stim stop request was recorded" : "";
|
|
3395
|
+
return `the ${record.mode} dev server exited unexpectedly (${detail})${expoSignal}`;
|
|
3396
|
+
}
|
|
3397
|
+
function devServerStopLevel(record) {
|
|
3398
|
+
if (record.reason === "server-exited") return "error";
|
|
3399
|
+
return record.reason === "signal" ? "warn" : "info";
|
|
3400
|
+
}
|
|
3401
|
+
/** The log line for a recorded supervisor whose process is proven gone, or null while it may still run. */
|
|
3402
|
+
function vanishedSupervisorMessage(record) {
|
|
3403
|
+
if (!record || typeof record.pid !== "number") return null;
|
|
3404
|
+
const identity = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
3405
|
+
if (identity !== "gone" && identity !== "different") return null;
|
|
3406
|
+
const started = typeof record.startedAt === "string" ? ` (started ${record.startedAt})` : "";
|
|
3407
|
+
return `supervisor pid ${record.pid}${started} ${VANISHED}`;
|
|
3408
|
+
}
|
|
3409
|
+
const VANISHED = "is gone and recorded no cause: usually SIGKILL, a crash or a machine restart";
|
|
3410
|
+
function logVanishedSupervisor(root, record) {
|
|
3411
|
+
const msg = vanishedSupervisorMessage(record);
|
|
3412
|
+
if (!msg) return;
|
|
3413
|
+
const writer = createNdjsonWriter(join(workspaceLogsDir$1(root), "metro.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
|
|
3414
|
+
writer.write({
|
|
3415
|
+
src: "metro",
|
|
3416
|
+
level: "warn",
|
|
3417
|
+
event: "supervisor_vanished",
|
|
3418
|
+
msg
|
|
3419
|
+
});
|
|
3420
|
+
writer.close();
|
|
3421
|
+
}
|
|
3422
|
+
/** The recorded stop, else a vanished supervisor when status proved the recorded one gone. */
|
|
3423
|
+
function metroLastStop(state, supervisor) {
|
|
3424
|
+
const recorded = readDevServerStop(state);
|
|
3425
|
+
if (recorded) return recorded;
|
|
3426
|
+
if (supervisor?.status !== "stale" || typeof supervisor.pid !== "number") return null;
|
|
3427
|
+
const startedAt = typeof supervisor.startedAt === "string" ? supervisor.startedAt : null;
|
|
3428
|
+
return {
|
|
3429
|
+
reason: "vanished",
|
|
3430
|
+
pid: supervisor.pid,
|
|
3431
|
+
startedAt
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
function describeMetroLastStop(stop) {
|
|
3435
|
+
return stop.reason === "vanished" ? `supervisor pid ${stop.pid} ${VANISHED}` : describeDevServerStop(stop);
|
|
3436
|
+
}
|
|
3437
|
+
//#endregion
|
|
2115
3438
|
//#region src/supervisor/cache-reset.ts
|
|
2116
3439
|
async function stopOwnedMetro(root, requester) {
|
|
2117
3440
|
const project = getProject(root);
|
|
@@ -2163,7 +3486,6 @@ async function stopOwnedMetroForReset(root) {
|
|
|
2163
3486
|
const MB = 1048576;
|
|
2164
3487
|
const GB_IN_MB = 1024;
|
|
2165
3488
|
const COMMITTED_MEMORY_SHARE = .6;
|
|
2166
|
-
const STALE_CACHE_DAYS = 14;
|
|
2167
3489
|
const DEVICE_LIST_TIMEOUT_MS = 1e4;
|
|
2168
3490
|
const USAGE_TIMEOUT_MS = 1e4;
|
|
2169
3491
|
const BUDGET_SETTING_REMEDY = "Fix the value with `stim settings set <key> <value>`, or unset the environment variable. Run `stim guide settings` for the budget keys.";
|
|
@@ -2207,7 +3529,7 @@ function machineNumber(key, config, env) {
|
|
|
2207
3529
|
error: null
|
|
2208
3530
|
};
|
|
2209
3531
|
}
|
|
2210
|
-
function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemoryMb = totalmem
|
|
3532
|
+
function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemoryMb = totalmem() / MB } = {}) {
|
|
2211
3533
|
const minFree = machineNumber("budget.minFreeDiskGb", config, env);
|
|
2212
3534
|
const hardFloor = machineNumber("budget.hardFloorDiskGb", config, env);
|
|
2213
3535
|
const memory = machineNumber("budget.maxCommittedMemoryGb", config, env);
|
|
@@ -2280,6 +3602,13 @@ function listSims() {
|
|
|
2280
3602
|
return null;
|
|
2281
3603
|
}
|
|
2282
3604
|
}
|
|
3605
|
+
function snapshotSims(snapshot) {
|
|
3606
|
+
try {
|
|
3607
|
+
return snapshot.read().filter((sim) => sim.available);
|
|
3608
|
+
} catch {
|
|
3609
|
+
return null;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
2283
3612
|
function runningSupervisorPid(path, project) {
|
|
2284
3613
|
const target = resolveSupervisorTarget({
|
|
2285
3614
|
state: readWorkspaceState$1(path)?.supervisor,
|
|
@@ -2288,11 +3617,11 @@ function runningSupervisorPid(path, project) {
|
|
|
2288
3617
|
});
|
|
2289
3618
|
return target.status === "ours" ? target.pid ?? null : null;
|
|
2290
3619
|
}
|
|
2291
|
-
function readMemoryUse() {
|
|
3620
|
+
function readMemoryUse(simSnapshot) {
|
|
2292
3621
|
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
2293
3622
|
return estimateMemoryUse({
|
|
2294
3623
|
config: loadConfig$1(),
|
|
2295
|
-
sims: listSims(),
|
|
3624
|
+
sims: simSnapshot ? snapshotSims(simSnapshot) : listSims(),
|
|
2296
3625
|
androidSerial: (avdName) => {
|
|
2297
3626
|
try {
|
|
2298
3627
|
return resolve(avdName).serial ?? null;
|
|
@@ -2475,14 +3804,17 @@ async function reclaimWorkspaceOutputs({ root, dryRun, now, diskRecovered }) {
|
|
|
2475
3804
|
function cacheLabel(cache) {
|
|
2476
3805
|
return `${cache.name} ${cache.dir}`;
|
|
2477
3806
|
}
|
|
3807
|
+
function trimmableCaches() {
|
|
3808
|
+
return planCacheEmptying(discoverCaches(), false).filter((cache) => cache.prune !== "report-only" && cache.prune !== "atomic" && !cache.machineGlobal);
|
|
3809
|
+
}
|
|
2478
3810
|
function reclaimStaleCacheEntries({ dryRun }) {
|
|
2479
|
-
const caches =
|
|
3811
|
+
const caches = trimmableCaches();
|
|
2480
3812
|
if (dryRun) return {
|
|
2481
3813
|
targets: caches.map(cacheLabel),
|
|
2482
3814
|
failures: 0
|
|
2483
3815
|
};
|
|
2484
3816
|
return {
|
|
2485
|
-
targets: trimCaches(caches,
|
|
3817
|
+
targets: trimCaches(caches, 14).map(cacheLabel),
|
|
2486
3818
|
failures: 0
|
|
2487
3819
|
};
|
|
2488
3820
|
}
|
|
@@ -2496,7 +3828,7 @@ const STEP_ACTIONS = {
|
|
|
2496
3828
|
"idle-devices": "shut down idle owned devices",
|
|
2497
3829
|
"idle-dev-servers": "stop idle dev servers",
|
|
2498
3830
|
"workspace-outputs": "clear build outputs of idle workspaces",
|
|
2499
|
-
"stale-cache-entries": `trim cache entries unused for
|
|
3831
|
+
"stale-cache-entries": `trim cache entries unused for 14 days`
|
|
2500
3832
|
};
|
|
2501
3833
|
async function runStep(step, context) {
|
|
2502
3834
|
const log = console.log;
|
|
@@ -2520,7 +3852,7 @@ async function runStep(step, context) {
|
|
|
2520
3852
|
}
|
|
2521
3853
|
}
|
|
2522
3854
|
function spaceUsers() {
|
|
2523
|
-
const home = homedir
|
|
3855
|
+
const home = homedir();
|
|
2524
3856
|
const candidates = [
|
|
2525
3857
|
["Stim state", getConfigDir()],
|
|
2526
3858
|
["shared build cache", sharedBuildCache()],
|
|
@@ -2558,7 +3890,7 @@ function describeShortfall(budget, measure, short) {
|
|
|
2558
3890
|
...short.workspaces && measure.memory ? [`${measure.memory.liveWorkspaces} active workspaces, over the limit of ${budget.maxLiveWorkspaces}`] : []
|
|
2559
3891
|
];
|
|
2560
3892
|
}
|
|
2561
|
-
async function enforceBudget({ root, note, dryRun = false, budget: resolved }, overrides = {}) {
|
|
3893
|
+
async function enforceBudget({ root, note, dryRun = false, budget: resolved, simSnapshot }, overrides = {}) {
|
|
2562
3894
|
const deps = {
|
|
2563
3895
|
...DEFAULT_DEPS,
|
|
2564
3896
|
...overrides
|
|
@@ -2579,7 +3911,7 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved }, o
|
|
|
2579
3911
|
const paths = [root, getConfigDir()];
|
|
2580
3912
|
const measureNow = () => ({
|
|
2581
3913
|
volumes: budget.minFreeDiskMb > 0 || budget.hardFloorDiskMb > 0 ? deps.volumes(paths) : [],
|
|
2582
|
-
memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? deps.memory() : null
|
|
3914
|
+
memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? deps.memory(simSnapshot) : null
|
|
2583
3915
|
});
|
|
2584
3916
|
let measure = measureNow();
|
|
2585
3917
|
let short = budgetShortfalls(budget, measure);
|
|
@@ -2600,6 +3932,7 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved }, o
|
|
|
2600
3932
|
}).disk.length === 0
|
|
2601
3933
|
});
|
|
2602
3934
|
if (!dryRun) {
|
|
3935
|
+
simSnapshot?.invalidate();
|
|
2603
3936
|
measure = measureNow();
|
|
2604
3937
|
short = budgetShortfalls(budget, measure);
|
|
2605
3938
|
}
|
|
@@ -2734,4 +4067,4 @@ function budgetLine(report) {
|
|
|
2734
4067
|
return `${report.volumes.length ? `${report.volumes.map((v) => `${v.freeGb} GB free on ${v.volume}`).join(", ")} (reclaims below ${Math.max(budget.minFreeDiskGb, budget.hardFloorDiskGb)} GB, ${budget.hardFloorDiskGb ? `refuses below ${budget.hardFloorDiskGb} GB` : "never refuses"})` : "disk budget off"}; ${report.memory ? `roughly ${report.memory.committedGb} GB of a ${budget.maxCommittedMemoryGb} GB memory budget committed` : "memory budget off"}`;
|
|
2735
4068
|
}
|
|
2736
4069
|
//#endregion
|
|
2737
|
-
export {
|
|
4070
|
+
export { describeUnverifiableDevices as $, buildReport as A, phase as At, readStats$1 as B, idleDeviceLines as C, memoryCacheKind as Ct, ACTIVE_BUILD_KEY as D, memoryWorkPending as Dt, workspaceBuildInProgress as E, memorySweepIsScoped as Et, recordFinishedBuild as F, sizeCaches as Ft, DEVICE_LIST_TIMEOUT_MS$1 as G, recordCapacityWait as H, startBuildProgress as I, collectParkedSims as J, collectDeviceLeases as K, tapBuildLog as L, estimateBuild as M, readFootprints as Mt, parseActiveBuild as N, discoverCaches as Nt, NO_BUILD_PROGRESS as O, reclaimMemory as Ot, recordBuildPhase as P, pruneCache as Pt, describeParkedSims as Q, createRunRecorder as R, collectOwnedDeviceActivity as S, inspectWatchmanMemory as St, shutDownIdleDevices as T, memoryScope as Tt, recordRunStats as U, recordCapacityRefusal as V, statsProjectKey$1 as W, deleteParkedSims as X, deleteParkedAvds as Y, deleteProjectDevices as Z, metroLastStop as _, trimCaches as _t, inspectBudget as a, unverifiedDeviceCommand as at, withdrawDevServerStopRequest as b, WATCHMAN_KIND as bt, readVolumeSpace as c, includesArchives as ct, stopOwnedMetroForReset as d, includesWorkspaceOutputs as dt, deviceSweepIsScoped as et, describeDevServerStop as f, isWorkspacesScope as ft, logVanishedSupervisor as g, selectsBeyondCaches as gt, devServerStopRecord as h, selectCaches as ht, enforceBudget as i, findStaleProjectDevices as it, buildStatusLine as j, attributeMachineUsage as jt, activeBuildState as k, scopedMemoryReport as kt, resolveBudget as l, includesParkedDevices as lt, devServerStopLevel as m, previewScopedEmptying as mt, budgetLine as n, findOrphanedDevices as nt, machineNumber as o, withAndroidAvdSizes as ot, describeMetroLastStop as p, planCacheEmptying as pt, collectParkedAvds as q, cacheLabel as r, findStaleDeviceRecords as rt, peekBudget as s, emptyCaches as st, budgetGate as t, eraseParkedDevices as tt, trimmableCaches as u, includesRecordings as ut, requestDevServerStop as v, parseArchiveSelector as vt, parseIdleDuration as w, memoryLines as wt, collectIdleDevices as x, collectMemoryReport as xt, vanishedSupervisorMessage as y, GRADLE_DAEMONS_KIND as yt, readRunEstimates as z };
|