stim 1.26.0 → 1.28.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/dist/{acquire-artifact-DVsvSwf7.mjs → acquire-artifact-CKByBnTD.mjs} +2 -2
- package/dist/{act-C-BqJEpu.mjs → act-8iswyfRa.mjs} +319 -126
- package/dist/{activity-Lnb_EoRY.mjs → activity-DRMg4_mD.mjs} +6 -6
- package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-tE90fDdo.mjs} +5 -5
- package/dist/{android-CiNQw3kq.mjs → android-CHmSU4B4.mjs} +101 -45
- package/dist/{android-DTAcRwBC.mjs → android-CsPs-7Mg.mjs} +23 -18
- package/dist/{android-B5P81yI-.mjs → android-D577ApKG.mjs} +37 -15
- package/dist/{android-CSbLRD53.mjs → android-YWCbmkVK.mjs} +5 -5
- package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-_aDifWW0.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api.d.mts +14 -4
- package/dist/api.mjs +39 -29
- package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-CQbwM0qO.mjs} +2 -2
- package/dist/{artifact-CsAP6AGK.mjs → artifact-DYtHjxsn.mjs} +19 -24
- package/dist/{artifact-BxREhAaV.mjs → artifact-bFkfD3vf.mjs} +18 -23
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/auto-placement-BVqs8AJi.mjs +350 -0
- package/dist/{browser-web-rYPyqhTf.mjs → browser-web-BUaLcafY.mjs} +16 -16
- package/dist/{build-DPhApsjV.mjs → build-cQURy_KW.mjs} +35 -31
- package/dist/{build-cache-DrXNCy3T.mjs → build-cache-CZULpXVl.mjs} +43 -21
- package/dist/{build-plan-DrMMdayj.mjs → build-plan-CxoYfxsc.mjs} +7 -6
- package/dist/{cache-manifest-BEZ9Bpl1.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{budget-k2mZGvvk.mjs → ccache-Bm_A1xX9.mjs} +562 -1604
- package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
- package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
- package/dist/{cli-BBKFGOFl.mjs → cli-CpbaLDa7.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-D9CaRks-.mjs → client-YxCer6ab.mjs} +84 -65
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BF5AR2dL.mjs → command-output-C8R1KIvi.mjs} +1 -1
- package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
- package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-DKeMzwXY.mjs} +108 -60
- package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-CojA2o0t.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-DXR_tPVY.mjs → dev-client-D-BW5uiq.mjs} +2 -2
- package/dist/{device-BK-zwyd_.mjs → device-CgXfbcz5.mjs} +25 -24
- package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-Cd8lMSn3.mjs} +175 -22
- package/dist/{device-BcYeHSFu.mjs → device-eeK95Lir.mjs} +16 -16
- package/dist/device-host-worker.mjs +22 -20
- package/dist/{device-ios-BLGfXQeK.mjs → device-ios-Cc9ulihb.mjs} +251 -23
- package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-Mtzp4THX.mjs} +5 -5
- package/dist/{device-pool-Dluix4H3.mjs → device-pool-eRz6DtCg.mjs} +3 -3
- package/dist/{doctor-Bhg1ii70.mjs → doctor-Bz_ouYAW.mjs} +28 -16
- package/dist/{doctor-Cp-BhzTS.mjs → doctor-C2QyhejJ.mjs} +1 -1
- package/dist/{doctor-fingerprint-D3a04Mhu.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +1 -1
- package/dist/doctor-fingerprint.mjs +1 -1
- package/dist/{doctor-watchman-BStK05nt.mjs → doctor-watchman-BTW82PDC.mjs} +5 -5
- package/dist/{doctor-OnwP94ne.mjs → doctor-xf7ht-5n.mjs} +76 -32
- package/dist/{eas-build-Dsj93iM9.mjs → eas-build-Dm6q68QY.mjs} +4 -4
- package/dist/eas-fallback-DajNpxf-.mjs +87 -0
- package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-CBFo_Uo1.mjs} +37 -8
- package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-D1QfyJzQ.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-ldgr-hXU.mjs → gc-DM5oMm-J.mjs} +161 -45
- package/dist/{gc-C_79zvz4.mjs → gc-PLFwb0k4.mjs} +2 -2
- package/dist/{guide-D7K0X6KG.mjs → guide-C4I46XAk.mjs} +674 -213
- package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-DlaKLbW0.mjs} +6 -7
- package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-DDRm9hPJ.mjs} +51 -20
- package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-bSebOKbz.mjs} +8 -8
- package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-B0c53W_c.mjs} +5 -5
- package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B7-8Pv4H.mjs} +6 -6
- package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CDe33CSr.mjs} +22 -18
- package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-CQ0QwU9J.mjs} +7 -8
- package/dist/{ios-WxpDJ5T1.mjs → ios-B1DYuOjh.mjs} +27 -11
- package/dist/{ios-B7-lToDu.mjs → ios-B1HvafEG.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-device-CsyryvpT.mjs → ios-device-B4elVFKi.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-CR4mZ7CH.mjs} +22 -9
- package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
- package/dist/{ios-state-Cw7-WQf2.mjs → ios-state-BPHqMDdZ.mjs} +1 -1
- package/dist/{ios-ftxdQbJS.mjs → ios-tg4tp3q1.mjs} +94 -37
- package/dist/{js-swap-BMhXYVDS.mjs → js-swap-CVpSW1fn.mjs} +8 -7
- package/dist/{launch-DOp8RjDz.mjs → launch-DoySmhjz.mjs} +42 -38
- package/dist/lightened-DFAJgfIk.mjs +171 -0
- package/dist/{logs-s1dRIG1E.mjs → logs-BGsW7iPe.mjs} +46 -21
- package/dist/{logs-dQDki6Tt.mjs → logs-DI5ILkYB.mjs} +7 -7
- package/dist/{macos-DBBPNnIA.mjs → macos-Bo9st9Di.mjs} +73 -38
- package/dist/{macos-B0VEV3Gf.mjs → macos-C12RMJQF.mjs} +1 -1
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +7 -7
- package/dist/{metro-CjC10t6E.mjs → metro-BnhXV2Mj.mjs} +4 -4
- package/dist/{metro-gateway-B6_PgD0L.mjs → metro-request-DXyKjVDF.mjs} +26 -47
- package/dist/{named-ports-BG0j1X4g.mjs → named-ports-Phoi2POD.mjs} +3 -3
- package/dist/{native-android-CL2NGiik.mjs → native-android-vxgtLD7w.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-BN96IVw8.mjs} +5 -5
- package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DG4Rodzj.mjs} +9 -8
- package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
- package/dist/{native-xcode-inputs-BW7H8JGu.mjs → native-xcode-inputs-BKarCoKg.mjs} +3 -3
- package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-8A-efGa5.mjs} +17 -17
- package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
- package/dist/offload-worker.mjs +23 -24
- package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
- package/dist/{ownership-BjYvMNX_.mjs → ownership-DJCq48IJ.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-BzTa8g1F.mjs} +18 -4
- package/dist/{placement-log-DIFPQAXp.mjs → placement-log-Cvhta5ib.mjs} +3 -3
- package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-WkZdccuN.mjs} +36 -10
- package/dist/{ports-C2_w1Z0Z.mjs → ports-UV4mJgyC.mjs} +3 -3
- package/dist/{prebuild-gmouvmJZ.mjs → prebuild-k8d_tqOq.mjs} +16 -17
- package/dist/{project-BUu6Is4k.mjs → project-Sui52ywT.mjs} +17 -17
- package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
- package/dist/pull-requests.mjs +1 -1
- package/dist/{react-native-android-Cgg99REN.mjs → react-native-android-DANlGGQf.mjs} +32 -26
- package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-jUHnIfZ9.mjs} +9 -9
- package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-DCWRjS7K.mjs} +6 -6
- package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-BwMsFMXi.mjs} +23 -17
- package/dist/{recordings-BldzVKq0.mjs → recordings-iy0mkm7b.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-C9OduiNc.mjs → reload-Bj7QyuDy.mjs} +1 -1
- package/dist/{reload-CCR7KC_l.mjs → reload-c_uEyc_X.mjs} +16 -16
- package/dist/{remote-C8S7FlPF.mjs → remote-DFlpAqdV.mjs} +23 -10
- package/dist/{result-7F5hEwQJ.mjs → result-BIcXREyC.mjs} +32 -21
- package/dist/{run-BqB0v3pt.mjs → run-y2HwyNvg.mjs} +3 -3
- package/dist/{selection-BNPBZ6T5.mjs → selection-CXgM_VK_.mjs} +37 -29
- package/dist/{server-bare-A75rDIdD.mjs → server-bare-2N4vSRZG.mjs} +2 -2
- package/dist/{server-command-CssKFOD7.mjs → server-command-qneHPcRI.mjs} +6 -6
- package/dist/{server-expo-eQfOdydb.mjs → server-expo-Ccm9zB0o.mjs} +4 -4
- package/dist/server-expo-WulkyAwT.mjs +2 -0
- package/dist/{settings-CosW5X-S.mjs → settings-CY5JZlNN.mjs} +375 -62
- package/dist/{settings-OaGQF-ri.mjs → settings-Cfd_Q6ys.mjs} +24 -3
- package/dist/settings-payload-DwQiEMCn.mjs +257 -0
- package/dist/settings-write-CJpjkeAd.mjs +185 -0
- package/dist/settings.schema.json +51 -33
- package/dist/{simslim-Bax1qcxu.mjs → simslim-DCQwzoPy.mjs} +2 -2
- package/dist/{slot-launch-CYCEhfBY.mjs → slot-launch-BpmanOnk.mjs} +6 -6
- package/dist/{stage-jKQ7CP_B.mjs → stage-CmugrSG5.mjs} +2 -2
- package/dist/{start-C_wefNKB.mjs → start-CHH3zgIU.mjs} +1 -1
- package/dist/{start-_BdJ32cZ.mjs → start-DYLk6GXn.mjs} +27 -22
- package/dist/{state-C1i2nMOP.mjs → state-B2QJbDwz.mjs} +4 -4
- package/dist/{state-C4OdpOTF.mjs → state-BXC1Lfxw.mjs} +2 -2
- package/dist/{stats-tEpEEH-u.mjs → stats-DxKqhcGp.mjs} +5 -5
- package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
- package/dist/{status-CRXz-Gtj.mjs → status-7ULDEJ1e.mjs} +1 -1
- package/dist/{status-DQrqodLj.mjs → status-BHCkOZga.mjs} +51 -28
- package/dist/{status-B9M7x4CZ.mjs → status-D_Ljc89p.mjs} +1 -1
- package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BmsPqYSU.mjs} +18 -7
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-DRqQVqDJ.mjs → stop-D-dGE9gh.mjs} +21 -20
- package/dist/{stop-ySNQHXMG.mjs → stop-DQvE-P76.mjs} +10 -6
- package/dist/{stop-CmdNhGlC.mjs → stop-b2pouxAO.mjs} +2 -2
- package/dist/supervisor-run.mjs +237 -63
- package/dist/{support-BGSCBoib.mjs → support-04IhBUTT.mjs} +6 -6
- package/dist/{support-CVOCp0Ft.mjs → support-BtlJKjyQ.mjs} +12 -11
- package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-Cj2UpmPN.mjs} +4 -4
- package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-BhF2stxY.mjs} +11 -12
- package/dist/{toolchain-J4J8MrAr.mjs → toolchain-mLA_km-b.mjs} +321 -15
- package/dist/{toolchain-DoavrvIA.mjs → toolchain-qktiV8xc.mjs} +1 -1
- package/dist/{trigger-KVaWWNyn.mjs → trigger-DMou-X7j.mjs} +2 -2
- package/dist/trigger-qp2ji5wE.mjs +2 -0
- package/dist/{warm-progress-DvhTX_eu.mjs → warm-progress-B0Vpuvpl.mjs} +731 -727
- package/dist/{web-Bihbf7-u.mjs → web-BYmK0TjL.mjs} +9 -9
- package/dist/web-CrVLVQZY.mjs +2 -0
- package/dist/web-run.mjs +9 -9
- package/dist/windows-report.mjs +1 -1
- package/dist/{workspace-state-pUg3_RRA.mjs → workspace-state-kIZrdlSg.mjs} +3 -3
- package/dist/{workspaces-g2lhdBXF.mjs → workspaces-CmOcceDN.mjs} +1879 -74
- package/dist/{worktree-BKuklFh_.mjs → worktree-2dk5edza.mjs} +933 -534
- package/dist/worktree-nliAoHxB.mjs +2 -0
- package/package.json +7 -7
- package/dist/ccache-Bx5TbxwG.mjs +0 -143
- package/dist/eas-fallback-FwkeqqGi.mjs +0 -381
- package/dist/hosted-ios-D55QhYaD.mjs +0 -15
- package/dist/native-run-BfIDFKLH.mjs +0 -275
- package/dist/server-expo-wvcj5tiZ.mjs +0 -2
- package/dist/settings-write-QjYfSrjF.mjs +0 -360
- package/dist/trigger-DzJu208Q.mjs +0 -2
- package/dist/web-BFLueSh-.mjs +0 -2
- package/dist/worktree-Bm4cb-nU.mjs +0 -2
|
@@ -1,31 +1,64 @@
|
|
|
1
|
-
import { D as ownership_claim_exports, S as process_identity_exports, c as throwIfCancelled, d as workspaceProcessLockError, f as workspaceProcessLockPath, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, o as cancellableSleep, t as getExecutor, u as withWorkspaceProcessLock, v as stopDeclaringSpawnsOn } from "./exec-
|
|
2
|
-
import { A as workspaceName$1, E as workspaceDir, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, u as workspaceLastUsed, w as workspaceAgentDeviceDir } from "./workspace-state-
|
|
3
|
-
import { F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, I as listMountedVolumes, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-
|
|
1
|
+
import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, c as throwIfCancelled, d as workspaceProcessLockError, f as workspaceProcessLockPath, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, o as cancellableSleep, t as getExecutor, u as withWorkspaceProcessLock, v as stopDeclaringSpawnsOn } from "./exec-aULwtNly.mjs";
|
|
2
|
+
import { A as workspaceName$1, E as workspaceDir, l as withWorkspaceStateLockAsync, o as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey, u as workspaceLastUsed, w as workspaceAgentDeviceDir } from "./workspace-state-kIZrdlSg.mjs";
|
|
3
|
+
import { D as withConfigLock$1, F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, I as listMountedVolumes, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix$1, j as acquireAvdClaim, p as getProject } from "./config-Cn94Jc0g.mjs";
|
|
4
4
|
import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
|
|
5
|
-
import { a as formatElapsed, l as phaseLine } from "./command-output-
|
|
6
|
-
import { t as runOperationFile } from "./operation-exec-
|
|
7
|
-
import { Pt as repoRootAsync,
|
|
8
|
-
import { a as macosRuntimeClaim, n as macosDir } from "./state-
|
|
9
|
-
import { Ct as forgetCreatedDeviceAsync, H as ownedAvdMatchesConfiguration, K as ownedAvdSystemImage, L as listOrphanedAvdDirectories, S as deleteAvd, St as forgetCreatedDevice, Tt as recordCreatedDevice, V as ownedAvdDirectory, _t as parkSim, bt as removeParkedAfter, g as avdPathExists, ht as eraseParkedAfter, it as shutdownAndroidEmulator, j as listAdbDevicesAsync, lt as wipeAvdUserData, nt as resolveOwnedAvdSerialAsync, p as assertOwnedAvdStoppedAsync, st as waitForAndroidEmulatorShutdown, ut as isStimOwnedAvd, v as avdStorageRoots, wt as readCreatedDevices$1, yt as readParked } from "./android-
|
|
10
|
-
import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-
|
|
11
|
-
import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-
|
|
5
|
+
import { a as formatElapsed, d as quotedPath$1, l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-C8R1KIvi.mjs";
|
|
6
|
+
import { c as gitQuiet, l as gitQuietAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
|
|
7
|
+
import { Pt as lockReason, Vt as repoRootAsync, Xt as worktreeLockAsync, Yt as worktreeLock, pt as resolveTailscaleBinary } from "./settings-CY5JZlNN.mjs";
|
|
8
|
+
import { a as macosRuntimeClaim, n as macosDir } from "./state-BXC1Lfxw.mjs";
|
|
9
|
+
import { Ct as forgetCreatedDeviceAsync, G as ownedAvdSerialResolverAsync, H as ownedAvdMatchesConfiguration, K as ownedAvdSystemImage, L as listOrphanedAvdDirectories, S as deleteAvd, St as forgetCreatedDevice, Tt as recordCreatedDevice, V as ownedAvdDirectory, W as ownedAvdSerialResolver, _t as parkSim, bt as removeParkedAfter, g as avdPathExists, ht as eraseParkedAfter, it as shutdownAndroidEmulator, j as listAdbDevicesAsync, lt as wipeAvdUserData, nt as resolveOwnedAvdSerialAsync, p as assertOwnedAvdStoppedAsync, st as waitForAndroidEmulatorShutdown, ut as isStimOwnedAvd, v as avdStorageRoots, wt as readCreatedDevices$1, yt as readParked } from "./android-CsPs-7Mg.mjs";
|
|
10
|
+
import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-BnhXV2Mj.mjs";
|
|
11
|
+
import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-B2QJbDwz.mjs";
|
|
12
12
|
import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
|
|
13
|
-
import { N as listBuildLocks, d as readAgentDeviceRecords, t as agentDeviceDaemon } from "./activity-
|
|
14
|
-
import { C as renameIosSim, D as resolveOwnedIosSim, c as eraseIosSim, f as listAllIosSimsAsync, k as shutdownIosSim, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-
|
|
15
|
-
import { r as releaseBrowserPort } from "./named-ports-
|
|
13
|
+
import { N as listBuildLocks, d as readAgentDeviceRecords, i as createActivityReaderAsync, r as createActivityReader, s as idleForMs, t as agentDeviceDaemon } from "./activity-DRMg4_mD.mjs";
|
|
14
|
+
import { C as renameIosSim, D as resolveOwnedIosSim, c as eraseIosSim, f as listAllIosSimsAsync, k as shutdownIosSim, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-B1DYuOjh.mjs";
|
|
15
|
+
import { r as releaseBrowserPort } from "./named-ports-Phoi2POD.mjs";
|
|
16
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
17
17
|
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
18
18
|
import { isDeepStrictEqual } from "node:util";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
|
-
import { ambientContext, ambientEnv, ambientWrite } from "@stim-cli/core";
|
|
22
|
-
import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
|
|
21
|
+
import { ambientContext, ambientEnv, ambientWrite, withDirLock, workspaceName } from "@stim-cli/core";
|
|
22
|
+
import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_LEDGER_CAP, BUILD_PHASES, LAST_BUILD_KEYS, STATS_VERSION, buildLedgerFile, buildLedgerLock, buildSlotsDir, decodeStats, hostedAppAttempt, hostedNativeRecords, isJsonObject, lastNativeUseFrom, loadPerCore, macosAppState, parseHostedNativePlacement, parseMissReason, readJsonFile, readMacosRecord, readRunEstimates, readStats as readStats$1, requireAutomaticMachine, statsFile, statsProjectKey as statsProjectKey$1, trace, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedNative, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
|
|
23
23
|
import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1 } from "path";
|
|
24
24
|
import { request } from "node:https";
|
|
25
25
|
import chalk from "chalk";
|
|
26
26
|
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
|
|
27
27
|
import { lstat, readFile, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
28
28
|
import { resolve4 } from "node:dns/promises";
|
|
29
|
+
//#region src/commands/gc/limited.ts
|
|
30
|
+
const isCleanupFailure = (error) => error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error;
|
|
31
|
+
async function eachLimited(items, limit, run) {
|
|
32
|
+
const queue = items[Symbol.iterator]();
|
|
33
|
+
let failed = false;
|
|
34
|
+
const worker = async () => {
|
|
35
|
+
for (const item of queue) {
|
|
36
|
+
if (failed) return;
|
|
37
|
+
try {
|
|
38
|
+
await run(item);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
failed = true;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const errors = (await Promise.allSettled(Array.from({ length: Math.min(limit, items.length) }, worker))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
46
|
+
if (errors.length) throw errors.find(isCleanupFailure) ?? errors[0];
|
|
47
|
+
}
|
|
48
|
+
function readConcurrency(operation) {
|
|
49
|
+
return operationState(operation).declaring.size ? 1 : 6;
|
|
50
|
+
}
|
|
51
|
+
function sizeConcurrency(operation) {
|
|
52
|
+
return operationState(operation).declaring.size ? 1 : 4;
|
|
53
|
+
}
|
|
54
|
+
async function mapLimited(items, limit, run) {
|
|
55
|
+
const done = [];
|
|
56
|
+
await eachLimited([...items.entries()], limit, async ([index, item]) => {
|
|
57
|
+
done.push([index, await run(item)]);
|
|
58
|
+
});
|
|
59
|
+
return done.toSorted(([a], [b]) => a - b).map(([, result]) => result);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
29
62
|
//#region src/commands/gc/results.ts
|
|
30
63
|
function createGcResults(observe) {
|
|
31
64
|
const results = [];
|
|
@@ -71,7 +104,8 @@ function toRecord(holder) {
|
|
|
71
104
|
index: typeof details.index === "number" ? details.index : null,
|
|
72
105
|
projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
|
|
73
106
|
startedAt: holder.startedAt || null,
|
|
74
|
-
logFile: typeof details.logFile === "string" ? details.logFile : null
|
|
107
|
+
logFile: typeof details.logFile === "string" ? details.logFile : null,
|
|
108
|
+
client: details.offloaded === true && typeof details.client === "string" ? details.client : null
|
|
75
109
|
};
|
|
76
110
|
}
|
|
77
111
|
function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
@@ -97,7 +131,8 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
|
|
|
97
131
|
index: got.index,
|
|
98
132
|
projectRoot: root,
|
|
99
133
|
startedAt: got.claim.startedAt,
|
|
100
|
-
logFile
|
|
134
|
+
logFile,
|
|
135
|
+
client: null
|
|
101
136
|
},
|
|
102
137
|
claim: got.claim
|
|
103
138
|
};
|
|
@@ -191,12 +226,37 @@ function listBuildSlots() {
|
|
|
191
226
|
projectRoot: info?.projectRoot ?? null,
|
|
192
227
|
startedAt: info?.startedAt ?? null,
|
|
193
228
|
logFile: info?.logFile ?? null,
|
|
229
|
+
client: info?.client ?? null,
|
|
194
230
|
alive: survey.live.length > 0,
|
|
195
231
|
unresolved: survey.unresolved.length > 0
|
|
196
232
|
});
|
|
197
233
|
}
|
|
198
234
|
return slots;
|
|
199
235
|
}
|
|
236
|
+
/** The `concurrency.maxBuilds` slots a new build could take, `0` to `max - 1`, and who holds each busy one. */
|
|
237
|
+
function buildSlotUsage(max, slots = listBuildSlots()) {
|
|
238
|
+
const holders = slots.flatMap(({ index, alive, unresolved, projectRoot, client, startedAt }) => {
|
|
239
|
+
if (index === null || index >= max) return [];
|
|
240
|
+
if (alive) return [{
|
|
241
|
+
slot: index,
|
|
242
|
+
workspace: projectRoot,
|
|
243
|
+
client,
|
|
244
|
+
since: startedAt
|
|
245
|
+
}];
|
|
246
|
+
return unresolved ? [{
|
|
247
|
+
slot: index,
|
|
248
|
+
workspace: null,
|
|
249
|
+
client: null,
|
|
250
|
+
since: null,
|
|
251
|
+
unresolved: true
|
|
252
|
+
}] : [];
|
|
253
|
+
}).toSorted((a, b) => a.slot - b.slot);
|
|
254
|
+
return {
|
|
255
|
+
max,
|
|
256
|
+
inUse: holders.length,
|
|
257
|
+
holders
|
|
258
|
+
};
|
|
259
|
+
}
|
|
200
260
|
//#endregion
|
|
201
261
|
//#region src/devices/report-only-usage.ts
|
|
202
262
|
function canonical$1(path) {
|
|
@@ -349,6 +409,10 @@ function deviceClaims(device) {
|
|
|
349
409
|
workspace: record.workspace && canonical(record.workspace)
|
|
350
410
|
}));
|
|
351
411
|
}
|
|
412
|
+
var AgentDeviceRefusal = class extends Error {
|
|
413
|
+
reason;
|
|
414
|
+
stateDir;
|
|
415
|
+
};
|
|
352
416
|
function explainFailure(error) {
|
|
353
417
|
const stdout = error?.stdout;
|
|
354
418
|
let failure;
|
|
@@ -358,13 +422,17 @@ function explainFailure(error) {
|
|
|
358
422
|
failure = void 0;
|
|
359
423
|
}
|
|
360
424
|
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
361
|
-
const reason = typeof failure.details?.reason === "string" ?
|
|
362
|
-
|
|
425
|
+
const reason = typeof failure.details?.reason === "string" ? failure.details.reason : void 0;
|
|
426
|
+
const stateDir = typeof failure.details?.stateDir === "string" ? failure.details.stateDir : void 0;
|
|
427
|
+
const refusal = new AgentDeviceRefusal(`agent-device refused: ${failure.message}${reason ? ` (${reason})` : ""}`);
|
|
428
|
+
refusal.reason = reason;
|
|
429
|
+
refusal.stateDir = stateDir;
|
|
430
|
+
return refusal;
|
|
363
431
|
}
|
|
364
432
|
function report(message) {
|
|
365
433
|
ambientWrite("stderr", `${phaseLine("device", message)}\n`);
|
|
366
434
|
}
|
|
367
|
-
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation) {
|
|
435
|
+
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation, reportedBusy = /* @__PURE__ */ new Set()) {
|
|
368
436
|
const exec = getExecutor();
|
|
369
437
|
try {
|
|
370
438
|
operation?.signal.throwIfAborted();
|
|
@@ -378,6 +446,7 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
|
|
|
378
446
|
};
|
|
379
447
|
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
380
448
|
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
449
|
+
const busy = /* @__PURE__ */ new Set();
|
|
381
450
|
for (const dir of stateDirs) {
|
|
382
451
|
const deadline = Date.now() + 15e3;
|
|
383
452
|
const run = async (args) => {
|
|
@@ -431,9 +500,16 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
|
|
|
431
500
|
} catch (error) {
|
|
432
501
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
433
502
|
operation?.signal.throwIfAborted();
|
|
434
|
-
|
|
503
|
+
if (error instanceof AgentDeviceRefusal && error.reason === "daemon-busy") busy.add(error.stateDir ?? dir ?? "");
|
|
504
|
+
else report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
435
505
|
}
|
|
436
506
|
}
|
|
507
|
+
const unreported = [...busy].filter((dir) => !reportedBusy.has(dir));
|
|
508
|
+
if (unreported.length > 0) {
|
|
509
|
+
for (const dir of unreported) reportedBusy.add(dir);
|
|
510
|
+
const stop = unreported.map((dir) => `agent-device daemon stop${dir ? ` --state-dir ${quotedPath$1(dir)}` : ""}`);
|
|
511
|
+
report(`agent-device daemon is busy, so Stim did not check it for sessions on ${device.platform === "ios" ? device.id : device.id ?? device.avdName}; any that name the device stay open until the daemon is idle or, if it is yours, stopped with ${stop.join(" and ")}`);
|
|
512
|
+
}
|
|
437
513
|
} catch (error) {
|
|
438
514
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
439
515
|
operation?.signal.throwIfAborted();
|
|
@@ -652,6 +728,7 @@ async function parkOwnedIosSim(settled, udid, park, operation) {
|
|
|
652
728
|
}
|
|
653
729
|
async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {}, operation } = {}) {
|
|
654
730
|
let parkFallback;
|
|
731
|
+
const reportedBusy = /* @__PURE__ */ new Set();
|
|
655
732
|
try {
|
|
656
733
|
const resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
657
734
|
if (resolved.notOwned) return {
|
|
@@ -663,7 +740,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
|
|
|
663
740
|
await closeOwnedDeviceSessions({
|
|
664
741
|
platform: "ios",
|
|
665
742
|
id: udid
|
|
666
|
-
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation);
|
|
743
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation, reportedBusy);
|
|
667
744
|
const current = await resolveOwnedIosSimNow(udid, operation);
|
|
668
745
|
if (current.missing) return { status: "missing" };
|
|
669
746
|
if (current.notOwned) return {
|
|
@@ -721,7 +798,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
|
|
|
721
798
|
if (root && settled.sim) await closeOwnedDeviceSessions({
|
|
722
799
|
platform: "ios",
|
|
723
800
|
id: udid
|
|
724
|
-
}, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation);
|
|
801
|
+
}, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation, reportedBusy);
|
|
725
802
|
return {
|
|
726
803
|
status: "torn-down",
|
|
727
804
|
label: label ?? sim.name ?? udid
|
|
@@ -849,6 +926,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
849
926
|
signal: operation?.signal
|
|
850
927
|
}) }) {
|
|
851
928
|
let parkFallback;
|
|
929
|
+
const reportedBusy = /* @__PURE__ */ new Set();
|
|
852
930
|
const resolveAvd = async (name) => {
|
|
853
931
|
operation?.signal.throwIfAborted();
|
|
854
932
|
const result = await resolve(name);
|
|
@@ -890,7 +968,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
890
968
|
platform: "android",
|
|
891
969
|
id: serial,
|
|
892
970
|
avdName
|
|
893
|
-
}, stillOwned, workspace, void 0, operation);
|
|
971
|
+
}, stillOwned, workspace, void 0, operation, reportedBusy);
|
|
894
972
|
if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
895
973
|
await waitForShutdown(avdName, async (timeoutMs) => {
|
|
896
974
|
if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
@@ -911,7 +989,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
911
989
|
platform: "android",
|
|
912
990
|
id: serial,
|
|
913
991
|
avdName
|
|
914
|
-
}, stillStopped, workspace, root, operation);
|
|
992
|
+
}, stillStopped, workspace, root, operation, reportedBusy);
|
|
915
993
|
}
|
|
916
994
|
} else {
|
|
917
995
|
await closeOwnedDeviceSessions({
|
|
@@ -1149,6 +1227,1592 @@ async function teardownBrowserHeld(root, { deleteProfile = false, operation } =
|
|
|
1149
1227
|
}
|
|
1150
1228
|
}
|
|
1151
1229
|
//#endregion
|
|
1230
|
+
//#region src/engine/stats.ts
|
|
1231
|
+
const PLACEMENT_MAX_AGE_MS = 6048e5;
|
|
1232
|
+
function emptyStats() {
|
|
1233
|
+
return {
|
|
1234
|
+
version: STATS_VERSION,
|
|
1235
|
+
machine: {},
|
|
1236
|
+
projects: {}
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function updateStats(record, run, now) {
|
|
1240
|
+
const at = new Date(now).toISOString();
|
|
1241
|
+
const durationMs = wholeMs$1(run.durationMs);
|
|
1242
|
+
const phases = {
|
|
1243
|
+
coldBuildMs: wholeMs$1(run.coldBuildMs),
|
|
1244
|
+
podsMs: wholeMs$1(run.podsMs)
|
|
1245
|
+
};
|
|
1246
|
+
const projects = { ...record.projects };
|
|
1247
|
+
const scope = { ...projects[run.projectKey] };
|
|
1248
|
+
const machine = { ...record.machine };
|
|
1249
|
+
const before = scope[run.platform] ?? null;
|
|
1250
|
+
const credit = creditMs(before, run, durationMs);
|
|
1251
|
+
scope[run.platform] = applyRun(before, run, {
|
|
1252
|
+
at,
|
|
1253
|
+
durationMs,
|
|
1254
|
+
credit,
|
|
1255
|
+
phases
|
|
1256
|
+
});
|
|
1257
|
+
projects[run.projectKey] = scope;
|
|
1258
|
+
machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
|
|
1259
|
+
at,
|
|
1260
|
+
durationMs,
|
|
1261
|
+
credit,
|
|
1262
|
+
phases
|
|
1263
|
+
});
|
|
1264
|
+
const history = { ...record.history };
|
|
1265
|
+
if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
|
|
1266
|
+
const outcome = isHit(run.cacheHit) ? "hit" : "cold";
|
|
1267
|
+
const project = { ...history[run.projectKey] };
|
|
1268
|
+
const lists = { ...project[run.platform] };
|
|
1269
|
+
const sample = {
|
|
1270
|
+
at,
|
|
1271
|
+
durationMs,
|
|
1272
|
+
phases: wholePhases(run.phases),
|
|
1273
|
+
...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
|
|
1274
|
+
};
|
|
1275
|
+
lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
|
|
1276
|
+
project[run.platform] = lists;
|
|
1277
|
+
history[run.projectKey] = project;
|
|
1278
|
+
}
|
|
1279
|
+
let placements = record.placements;
|
|
1280
|
+
let buildMachines = record.buildMachines;
|
|
1281
|
+
if (run.placement) {
|
|
1282
|
+
const placement = buildPlacement(run, before, at);
|
|
1283
|
+
placements = trimPlacements([...placements ?? [], placement], now);
|
|
1284
|
+
if (placement.machine && placement.decision !== "here") buildMachines = {
|
|
1285
|
+
...buildMachines,
|
|
1286
|
+
[placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
|
|
1290
|
+
const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
|
|
1291
|
+
return {
|
|
1292
|
+
version: STATS_VERSION,
|
|
1293
|
+
machine,
|
|
1294
|
+
projects,
|
|
1295
|
+
...Object.keys(history).length ? { history } : {},
|
|
1296
|
+
...placements?.length ? { placements } : {},
|
|
1297
|
+
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
1298
|
+
...capacityWaits.length ? { capacityWaits } : {},
|
|
1299
|
+
...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function buildPlacement(run, before, at) {
|
|
1303
|
+
const placement = run.placement;
|
|
1304
|
+
const buildMs = wholeMs$1(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
|
|
1305
|
+
const localEstimateMs = wholeMs$1(before?.lastColdBuildMs);
|
|
1306
|
+
const slotWaitMs = wholeMs$1(placement.slotWaitMs);
|
|
1307
|
+
const deviceSlotWaitMs = wholeMs$1(placement.deviceSlotWaitMs);
|
|
1308
|
+
return {
|
|
1309
|
+
at,
|
|
1310
|
+
project: run.projectKey,
|
|
1311
|
+
platform: run.platform,
|
|
1312
|
+
decision: placement.decision,
|
|
1313
|
+
reason: placement.reason,
|
|
1314
|
+
...placement.machine ? { machine: placement.machine } : {},
|
|
1315
|
+
...buildMs > 0 ? { buildMs } : {},
|
|
1316
|
+
...slotWaitMs > 0 ? { slotWaitMs } : {},
|
|
1317
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
|
|
1318
|
+
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
1319
|
+
...run.failed ? { failed: true } : {}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function applyPlacement(totals, placement) {
|
|
1323
|
+
const next = totals ? { ...totals } : {
|
|
1324
|
+
offloaded: 0,
|
|
1325
|
+
offloadedMs: 0,
|
|
1326
|
+
savedMs: 0,
|
|
1327
|
+
fallbacks: 0
|
|
1328
|
+
};
|
|
1329
|
+
if (placement.decision === "fell-back") {
|
|
1330
|
+
next.fallbacks += 1;
|
|
1331
|
+
next.lastFallbackAt = placement.at;
|
|
1332
|
+
return next;
|
|
1333
|
+
}
|
|
1334
|
+
next.offloaded += 1;
|
|
1335
|
+
next.offloadedMs += placement.buildMs ?? 0;
|
|
1336
|
+
if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
|
|
1337
|
+
next.lastOffloadAt = placement.at;
|
|
1338
|
+
return next;
|
|
1339
|
+
}
|
|
1340
|
+
function trimPlacements(placements, now) {
|
|
1341
|
+
return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
|
|
1342
|
+
}
|
|
1343
|
+
function recordRunStats(run, now) {
|
|
1344
|
+
return withConfigLock$1(() => {
|
|
1345
|
+
const path = statsFile();
|
|
1346
|
+
const loaded = loadForUpdate(path, now);
|
|
1347
|
+
if (loaded.newerVersion !== null) return {
|
|
1348
|
+
recorded: false,
|
|
1349
|
+
note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
|
|
1350
|
+
};
|
|
1351
|
+
writeStats(path, updateStats(loaded.record, run, now));
|
|
1352
|
+
return {
|
|
1353
|
+
recorded: true,
|
|
1354
|
+
note: loaded.note
|
|
1355
|
+
};
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function recordCapacityRefusal(event, now) {
|
|
1359
|
+
try {
|
|
1360
|
+
withConfigLock$1(() => {
|
|
1361
|
+
const path = statsFile();
|
|
1362
|
+
const loaded = loadForUpdate(path, now, false);
|
|
1363
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
1364
|
+
const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
|
|
1365
|
+
...event,
|
|
1366
|
+
at: new Date(now).toISOString(),
|
|
1367
|
+
kind: "device"
|
|
1368
|
+
}], now);
|
|
1369
|
+
writeStats(path, {
|
|
1370
|
+
...loaded.record,
|
|
1371
|
+
capacityRefusals
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
} catch {}
|
|
1375
|
+
}
|
|
1376
|
+
function recordCapacityWait(event, now) {
|
|
1377
|
+
try {
|
|
1378
|
+
withConfigLock$1(() => {
|
|
1379
|
+
const path = statsFile();
|
|
1380
|
+
const loaded = loadForUpdate(path, now, false);
|
|
1381
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
1382
|
+
const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
|
|
1383
|
+
platform: event.platform,
|
|
1384
|
+
max: event.max,
|
|
1385
|
+
workspace: event.workspace,
|
|
1386
|
+
ms: wholeMs$1(event.ms),
|
|
1387
|
+
...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
|
|
1388
|
+
at: new Date(now).toISOString(),
|
|
1389
|
+
kind: "device-wait"
|
|
1390
|
+
}], now);
|
|
1391
|
+
writeStats(path, {
|
|
1392
|
+
...loaded.record,
|
|
1393
|
+
capacityWaits
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
} catch {}
|
|
1397
|
+
}
|
|
1398
|
+
function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
|
|
1399
|
+
let projectKey = null;
|
|
1400
|
+
let cacheKey = null;
|
|
1401
|
+
let coldBuildMs = 0;
|
|
1402
|
+
let podsMs = 0;
|
|
1403
|
+
let placement = null;
|
|
1404
|
+
let deviceSlotWaitMs = 0;
|
|
1405
|
+
let recorded = false;
|
|
1406
|
+
return {
|
|
1407
|
+
setProject(key) {
|
|
1408
|
+
projectKey = key;
|
|
1409
|
+
},
|
|
1410
|
+
setCacheKey(key) {
|
|
1411
|
+
cacheKey = key;
|
|
1412
|
+
},
|
|
1413
|
+
setBuildMs(ms) {
|
|
1414
|
+
coldBuildMs = wholeMs$1(ms);
|
|
1415
|
+
},
|
|
1416
|
+
setPodsMs(ms) {
|
|
1417
|
+
podsMs = wholeMs$1(ms);
|
|
1418
|
+
},
|
|
1419
|
+
addDeviceSlotWaitMs(ms) {
|
|
1420
|
+
deviceSlotWaitMs += wholeMs$1(ms);
|
|
1421
|
+
},
|
|
1422
|
+
deviceSlotWaitMs() {
|
|
1423
|
+
return deviceSlotWaitMs || void 0;
|
|
1424
|
+
},
|
|
1425
|
+
setPlacement(next) {
|
|
1426
|
+
placement = next;
|
|
1427
|
+
},
|
|
1428
|
+
record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
|
|
1429
|
+
if (!projectKey || !cacheKey || recorded) return;
|
|
1430
|
+
recorded = true;
|
|
1431
|
+
const ran = failed ? {} : phases?.() ?? {};
|
|
1432
|
+
try {
|
|
1433
|
+
const outcome = write({
|
|
1434
|
+
platform,
|
|
1435
|
+
projectKey,
|
|
1436
|
+
failed,
|
|
1437
|
+
cacheHit,
|
|
1438
|
+
waitedForBuild: Boolean(waited),
|
|
1439
|
+
durationMs,
|
|
1440
|
+
...offloadedTo ? { offloadedTo } : {},
|
|
1441
|
+
...coldBuildMs > 0 ? { coldBuildMs } : {},
|
|
1442
|
+
...podsMs > 0 ? { podsMs } : {},
|
|
1443
|
+
...Object.keys(ran).length ? { phases: ran } : {},
|
|
1444
|
+
...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
|
|
1445
|
+
...placement ? { placement: {
|
|
1446
|
+
...placement,
|
|
1447
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
|
|
1448
|
+
} } : {}
|
|
1449
|
+
}, now());
|
|
1450
|
+
if (outcome?.note) note(outcome.note);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
note(`Run statistics could not be recorded: ${error?.message || error}`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function loadForUpdate(path, now, recoverCorrupt = true) {
|
|
1458
|
+
let text;
|
|
1459
|
+
try {
|
|
1460
|
+
text = readFileSync(path, "utf-8");
|
|
1461
|
+
} catch {
|
|
1462
|
+
return {
|
|
1463
|
+
record: emptyStats(),
|
|
1464
|
+
note: null,
|
|
1465
|
+
newerVersion: null
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
const parsed = decodeStats(text);
|
|
1469
|
+
if (!parsed.record && parsed.newerVersion === null) {
|
|
1470
|
+
if (!recoverCorrupt) return {
|
|
1471
|
+
record: null,
|
|
1472
|
+
note: null,
|
|
1473
|
+
newerVersion: null
|
|
1474
|
+
};
|
|
1475
|
+
const aside = `${path}.corrupt-${now}`;
|
|
1476
|
+
renameSync(path, aside);
|
|
1477
|
+
return {
|
|
1478
|
+
record: emptyStats(),
|
|
1479
|
+
note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
|
|
1480
|
+
newerVersion: null
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
if (parsed.newerVersion !== null) return {
|
|
1484
|
+
record: emptyStats(),
|
|
1485
|
+
note: null,
|
|
1486
|
+
newerVersion: parsed.newerVersion
|
|
1487
|
+
};
|
|
1488
|
+
return {
|
|
1489
|
+
record: parsed.record,
|
|
1490
|
+
note: null,
|
|
1491
|
+
newerVersion: null
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function writeStats(path, record) {
|
|
1495
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1496
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`);
|
|
1497
|
+
try {
|
|
1498
|
+
renameSync(tmp, path);
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
rmSync(tmp, { force: true });
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
function applyRun(bucket, run, { at, durationMs, credit, phases }) {
|
|
1505
|
+
const next = bucket ? { ...bucket } : {
|
|
1506
|
+
runs: 0,
|
|
1507
|
+
failed: 0,
|
|
1508
|
+
hits: 0,
|
|
1509
|
+
misses: 0,
|
|
1510
|
+
coldRuns: 0,
|
|
1511
|
+
coldRunMs: 0,
|
|
1512
|
+
hitRuns: 0,
|
|
1513
|
+
hitRunMs: 0,
|
|
1514
|
+
timeSavedMs: 0,
|
|
1515
|
+
firstRunAt: at,
|
|
1516
|
+
lastRunAt: at
|
|
1517
|
+
};
|
|
1518
|
+
next.runs += 1;
|
|
1519
|
+
next.lastRunAt = at;
|
|
1520
|
+
if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
|
|
1521
|
+
if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
|
|
1522
|
+
if (run.failed) {
|
|
1523
|
+
next.failed += 1;
|
|
1524
|
+
return next;
|
|
1525
|
+
}
|
|
1526
|
+
if (isHit(run.cacheHit)) {
|
|
1527
|
+
next.hits += 1;
|
|
1528
|
+
if (!run.waitedForBuild) {
|
|
1529
|
+
next.hitRuns += 1;
|
|
1530
|
+
next.hitRunMs += durationMs;
|
|
1531
|
+
next.timeSavedMs += credit;
|
|
1532
|
+
}
|
|
1533
|
+
return next;
|
|
1534
|
+
}
|
|
1535
|
+
next.misses += 1;
|
|
1536
|
+
if (run.offloadedTo) {
|
|
1537
|
+
next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
|
|
1538
|
+
next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
|
|
1539
|
+
next.lastOffloadHost = run.offloadedTo;
|
|
1540
|
+
return next;
|
|
1541
|
+
}
|
|
1542
|
+
next.coldRuns += 1;
|
|
1543
|
+
next.coldRunMs += durationMs;
|
|
1544
|
+
return next;
|
|
1545
|
+
}
|
|
1546
|
+
function creditMs(bucket, run, durationMs) {
|
|
1547
|
+
if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
|
|
1548
|
+
if (!bucket || bucket.coldRuns <= 0) return 0;
|
|
1549
|
+
return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
|
|
1550
|
+
}
|
|
1551
|
+
function isHit(cacheHit) {
|
|
1552
|
+
return cacheHit === "local" || cacheHit === "remote";
|
|
1553
|
+
}
|
|
1554
|
+
function wholeMs$1(value) {
|
|
1555
|
+
const ms = Number(value);
|
|
1556
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
1557
|
+
}
|
|
1558
|
+
//#endregion
|
|
1559
|
+
//#region src/engine/build-detail.ts
|
|
1560
|
+
const LINE_MAX = 160;
|
|
1561
|
+
const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
|
|
1562
|
+
const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
|
|
1563
|
+
const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
|
|
1564
|
+
const GRADLE_TASK = /^> Task (:\S+)/;
|
|
1565
|
+
/**
|
|
1566
|
+
* xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
|
|
1567
|
+
* that touches nothing still signs framework products last. Targets that end any other way are not counted.
|
|
1568
|
+
*/
|
|
1569
|
+
const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
|
|
1570
|
+
const GRADLE_CONFIGURE = /^> Configure project /;
|
|
1571
|
+
/**
|
|
1572
|
+
* SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
|
|
1573
|
+
* output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
|
|
1574
|
+
* `<product>-product` is the link of a product.
|
|
1575
|
+
*/
|
|
1576
|
+
const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
|
|
1577
|
+
const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
|
|
1578
|
+
const XCODE_STEPS = [
|
|
1579
|
+
[/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
|
|
1580
|
+
[/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
|
|
1581
|
+
[/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
|
|
1582
|
+
[/^PhaseScriptExecution$/, "script"],
|
|
1583
|
+
[/^CodeSign$/, "sign"]
|
|
1584
|
+
];
|
|
1585
|
+
const GRADLE_STEPS = [
|
|
1586
|
+
[/[Dd]ex/, "dex"],
|
|
1587
|
+
[/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
|
|
1588
|
+
[/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
|
|
1589
|
+
[/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
|
|
1590
|
+
[/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
|
|
1591
|
+
];
|
|
1592
|
+
function stepOf(name, table) {
|
|
1593
|
+
return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
1594
|
+
}
|
|
1595
|
+
function clip(text) {
|
|
1596
|
+
return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
|
|
1597
|
+
}
|
|
1598
|
+
function unescape(token) {
|
|
1599
|
+
return token.replace(/\\(.)/g, "$1");
|
|
1600
|
+
}
|
|
1601
|
+
function baseName(path) {
|
|
1602
|
+
return path.split("/").findLast(Boolean) ?? path;
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* An xcodebuild action line in short form: the action, the file it works on and its target, as in
|
|
1606
|
+
* `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
|
|
1607
|
+
* as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
|
|
1608
|
+
* shows its name instead.
|
|
1609
|
+
*/
|
|
1610
|
+
function shortXcodeAction(action, args, target) {
|
|
1611
|
+
const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
|
|
1612
|
+
const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
|
|
1613
|
+
return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
|
|
1614
|
+
}
|
|
1615
|
+
function swiftpmStep(text) {
|
|
1616
|
+
return /^Linking\b|-product$/.test(text) ? "link" : "compile";
|
|
1617
|
+
}
|
|
1618
|
+
/** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
|
|
1619
|
+
function parseBuildToolLine(msg) {
|
|
1620
|
+
const counted = SWIFTPM_PROGRESS.exec(msg);
|
|
1621
|
+
if (counted) {
|
|
1622
|
+
const text = counted[3]?.trim() ?? "";
|
|
1623
|
+
return {
|
|
1624
|
+
kind: "count",
|
|
1625
|
+
done: Number(counted[1]),
|
|
1626
|
+
total: Number(counted[2]),
|
|
1627
|
+
step: swiftpmStep(text),
|
|
1628
|
+
line: text ? clip(text) : null
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
1632
|
+
const graph = TARGET_GRAPH.exec(msg);
|
|
1633
|
+
if (graph) return {
|
|
1634
|
+
kind: "total",
|
|
1635
|
+
unit: "targets",
|
|
1636
|
+
total: Number(graph[1])
|
|
1637
|
+
};
|
|
1638
|
+
if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
1639
|
+
const action = XCODE_ACTION.exec(msg);
|
|
1640
|
+
if (action) {
|
|
1641
|
+
const [, name, args, target, project] = action;
|
|
1642
|
+
const step = stepOf(name, XCODE_STEPS);
|
|
1643
|
+
return {
|
|
1644
|
+
kind: "unit",
|
|
1645
|
+
unit: "targets",
|
|
1646
|
+
id: `${project}/${target}`,
|
|
1647
|
+
step,
|
|
1648
|
+
line: step ? shortXcodeAction(name, args, target) : null,
|
|
1649
|
+
finished: XCODE_TARGET_DONE.test(name)
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const task = GRADLE_TASK.exec(msg);
|
|
1653
|
+
if (task) {
|
|
1654
|
+
const path = task[1];
|
|
1655
|
+
return {
|
|
1656
|
+
kind: "unit",
|
|
1657
|
+
unit: "tasks",
|
|
1658
|
+
id: path,
|
|
1659
|
+
step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
|
|
1660
|
+
line: clip(`> Task ${path}`),
|
|
1661
|
+
finished: false
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
|
|
1668
|
+
* total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
|
|
1669
|
+
* its own `[done / total]` counts, which the parser passes through.
|
|
1670
|
+
*/
|
|
1671
|
+
function createBuildDetailParser() {
|
|
1672
|
+
let seen = false;
|
|
1673
|
+
let step = null;
|
|
1674
|
+
let unit = null;
|
|
1675
|
+
let total = null;
|
|
1676
|
+
let line = null;
|
|
1677
|
+
let tasks = 0;
|
|
1678
|
+
let counted = null;
|
|
1679
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1680
|
+
return {
|
|
1681
|
+
push(msg) {
|
|
1682
|
+
const parsed = parseBuildToolLine(msg);
|
|
1683
|
+
if (!parsed) return false;
|
|
1684
|
+
seen = true;
|
|
1685
|
+
if (parsed.kind === "total") {
|
|
1686
|
+
unit = "targets";
|
|
1687
|
+
total = parsed.total;
|
|
1688
|
+
return true;
|
|
1689
|
+
}
|
|
1690
|
+
if (parsed.kind === "configure") {
|
|
1691
|
+
step = "configure";
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
if (parsed.kind === "count") {
|
|
1695
|
+
unit = "steps";
|
|
1696
|
+
counted = {
|
|
1697
|
+
done: parsed.done,
|
|
1698
|
+
total: parsed.total
|
|
1699
|
+
};
|
|
1700
|
+
step = parsed.step;
|
|
1701
|
+
if (parsed.line) line = parsed.line;
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
unit = parsed.unit;
|
|
1705
|
+
if (parsed.unit === "tasks") tasks += 1;
|
|
1706
|
+
else if (parsed.finished) targets.add(parsed.id);
|
|
1707
|
+
if (parsed.step) step = parsed.step;
|
|
1708
|
+
if (parsed.line) line = parsed.line;
|
|
1709
|
+
return true;
|
|
1710
|
+
},
|
|
1711
|
+
detail(updatedAt) {
|
|
1712
|
+
if (!seen) return null;
|
|
1713
|
+
if (unit === "steps" && counted) return {
|
|
1714
|
+
step,
|
|
1715
|
+
unit,
|
|
1716
|
+
done: Math.min(counted.done, counted.total),
|
|
1717
|
+
total: counted.total,
|
|
1718
|
+
line,
|
|
1719
|
+
updatedAt
|
|
1720
|
+
};
|
|
1721
|
+
const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
|
|
1722
|
+
return {
|
|
1723
|
+
step,
|
|
1724
|
+
unit,
|
|
1725
|
+
done: done !== null && total !== null ? Math.min(done, total) : done,
|
|
1726
|
+
total: unit === "tasks" ? null : total,
|
|
1727
|
+
line,
|
|
1728
|
+
updatedAt
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
//#endregion
|
|
1734
|
+
//#region src/stim-version.ts
|
|
1735
|
+
const STIM_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1736
|
+
//#endregion
|
|
1737
|
+
//#region src/engine/build-ledger.ts
|
|
1738
|
+
const LOCK_WAIT_MS = 2e3;
|
|
1739
|
+
const GIT_TIMEOUT_MS = 2e3;
|
|
1740
|
+
function text(value) {
|
|
1741
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
1742
|
+
}
|
|
1743
|
+
function wholeMs(value) {
|
|
1744
|
+
const ms = Number(value);
|
|
1745
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
1746
|
+
}
|
|
1747
|
+
function wholePhaseMs(phases) {
|
|
1748
|
+
const out = {};
|
|
1749
|
+
for (const [name, ms] of Object.entries(phases ?? {})) if (wholeMs(ms) > 0) out[name] = wholeMs(ms);
|
|
1750
|
+
return out;
|
|
1751
|
+
}
|
|
1752
|
+
/** The remedy a thrown error carries, when it carries one. */
|
|
1753
|
+
function errorRemedy(error) {
|
|
1754
|
+
const remedy = typeof error === "object" && error !== null && "remedy" in error ? error.remedy : void 0;
|
|
1755
|
+
return typeof remedy === "string" && remedy !== "" ? remedy : void 0;
|
|
1756
|
+
}
|
|
1757
|
+
function buildLedgerRecord(run) {
|
|
1758
|
+
const { record, extras, outcome } = run;
|
|
1759
|
+
const platform = record.platform === "ios" || record.platform === "android" ? record.platform : "macos";
|
|
1760
|
+
const configuration = text(record.configuration);
|
|
1761
|
+
const durationMs = wholeMs(record.durationMs);
|
|
1762
|
+
const startedAt = Date.parse(String(record.startedAt));
|
|
1763
|
+
const fallback = text(record.offloadFallback);
|
|
1764
|
+
const ranHere = fallback !== null || record.builtOn === "here";
|
|
1765
|
+
const machine = text(record.offloadedTo) ?? (ranHere ? null : run.offload?.host) ?? null;
|
|
1766
|
+
const placement = machine ? {
|
|
1767
|
+
where: "offloaded",
|
|
1768
|
+
machine
|
|
1769
|
+
} : fallback ? {
|
|
1770
|
+
where: "local",
|
|
1771
|
+
fellBack: true,
|
|
1772
|
+
reason: fallback
|
|
1773
|
+
} : { where: "local" };
|
|
1774
|
+
const hit = record.cacheHit === "local" || record.cacheHit === "remote" ? record.cacheHit : null;
|
|
1775
|
+
const missReason = text(record.missReason);
|
|
1776
|
+
const cache = {
|
|
1777
|
+
result: record.cacheSkipped === true ? "bypassed" : hit ? "hit" : "miss",
|
|
1778
|
+
...hit && record.cacheSkipped !== true ? { source: hit } : {},
|
|
1779
|
+
key: text(record.cacheKey),
|
|
1780
|
+
fingerprint: text(record.fingerprint),
|
|
1781
|
+
...missReason && !hit ? { missReason } : {}
|
|
1782
|
+
};
|
|
1783
|
+
const offloadPhases = wholePhaseMs(run.offload?.phaseMs);
|
|
1784
|
+
const scheme = text(extras.scheme);
|
|
1785
|
+
const device = text(extras.device?.name);
|
|
1786
|
+
const runtime = text(extras.device?.runtime);
|
|
1787
|
+
const failure = outcome === "succeeded" ? void 0 : {
|
|
1788
|
+
code: text(record.errorCode),
|
|
1789
|
+
message: text(extras.message),
|
|
1790
|
+
remedy: text(extras.remedy),
|
|
1791
|
+
phase: run.phase
|
|
1792
|
+
};
|
|
1793
|
+
const bootStartedAt = run.boot ? Date.parse(run.boot.startedAt) : NaN;
|
|
1794
|
+
const where = placement.machine ? `on ${placement.machine}` : "here";
|
|
1795
|
+
const cacheText = cache.result === "hit" ? `cache hit${cache.source === "remote" ? " (remote)" : ""}` : cache.result;
|
|
1796
|
+
const summary = `${platform} build ${outcome} ${where} in ${formatElapsed(durationMs)} (${cacheText})`;
|
|
1797
|
+
const why = failure ? `: ${[failure.code, failure.message].filter(Boolean).join(": ") || "no reason recorded"}` : "";
|
|
1798
|
+
return {
|
|
1799
|
+
ts: run.now,
|
|
1800
|
+
src: "builds",
|
|
1801
|
+
level: outcome === "succeeded" ? "info" : outcome === "cancelled" ? "warn" : "error",
|
|
1802
|
+
msg: `${summary}${why}`,
|
|
1803
|
+
event: "build",
|
|
1804
|
+
id: run.id,
|
|
1805
|
+
startedAt: Number.isFinite(startedAt) ? startedAt : run.now - durationMs,
|
|
1806
|
+
project: run.root,
|
|
1807
|
+
workspace: run.workspace,
|
|
1808
|
+
branch: run.branch,
|
|
1809
|
+
platform,
|
|
1810
|
+
...scheme ? { scheme } : {},
|
|
1811
|
+
...platform === "android" && configuration ? { variant: configuration } : {},
|
|
1812
|
+
configuration,
|
|
1813
|
+
placement,
|
|
1814
|
+
cache,
|
|
1815
|
+
durationMs,
|
|
1816
|
+
phases: wholePhaseMs(run.phases),
|
|
1817
|
+
...Object.keys(offloadPhases).length ? { offloadPhases } : {},
|
|
1818
|
+
outcome,
|
|
1819
|
+
...failure ? { failure } : {},
|
|
1820
|
+
...device ? { device: {
|
|
1821
|
+
name: device,
|
|
1822
|
+
...runtime ? { runtime } : {}
|
|
1823
|
+
} } : {},
|
|
1824
|
+
...run.loadPerCore === void 0 ? {} : { loadPerCore: run.loadPerCore },
|
|
1825
|
+
...run.boot && Number.isFinite(bootStartedAt) ? { boot: {
|
|
1826
|
+
startedAt: bootStartedAt,
|
|
1827
|
+
loadPerCore: run.boot.loadPerCore
|
|
1828
|
+
} } : {},
|
|
1829
|
+
stimVersion: run.version
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function lineId(line) {
|
|
1833
|
+
try {
|
|
1834
|
+
const parsed = JSON.parse(line);
|
|
1835
|
+
return typeof parsed === "object" && parsed !== null && "id" in parsed ? parsed.id : void 0;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The ledger file's text after appending `line`, keeping only the newest `cap` complete records. A run that records
|
|
1842
|
+
* twice (an Android launch that fails after the build was recorded) replaces its earlier record.
|
|
1843
|
+
*/
|
|
1844
|
+
function appendCapped(existing, line, cap = BUILD_LEDGER_CAP) {
|
|
1845
|
+
const id = lineId(line);
|
|
1846
|
+
const lines = existing.split("\n").filter((each) => each.trim() !== "" && (id === void 0 || lineId(each) !== id));
|
|
1847
|
+
lines.push(line);
|
|
1848
|
+
return `${lines.slice(-cap).join("\n")}\n`;
|
|
1849
|
+
}
|
|
1850
|
+
function appendBuildLedger(record, file = buildLedgerFile()) {
|
|
1851
|
+
withDirLock(buildLedgerLock(), () => {
|
|
1852
|
+
let existing = "";
|
|
1853
|
+
try {
|
|
1854
|
+
existing = readFileSync(file, "utf-8");
|
|
1855
|
+
} catch {}
|
|
1856
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1857
|
+
writeFileSync(tmp, appendCapped(existing, JSON.stringify(record)));
|
|
1858
|
+
try {
|
|
1859
|
+
renameSync(tmp, file);
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
rmSync(tmp, { force: true });
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}, {
|
|
1865
|
+
waitMs: LOCK_WAIT_MS,
|
|
1866
|
+
ensureParent: () => {
|
|
1867
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1868
|
+
mkdirSync(dirname(buildLedgerLock()), { recursive: true });
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
/** Records one finished build in the ledger. A failure to do so never changes the run, so it is ignored. */
|
|
1873
|
+
function writeBuildLedger(root, run) {
|
|
1874
|
+
try {
|
|
1875
|
+
const branch = gitQuiet(root, [
|
|
1876
|
+
"symbolic-ref",
|
|
1877
|
+
"--quiet",
|
|
1878
|
+
"--short",
|
|
1879
|
+
"HEAD"
|
|
1880
|
+
], { timeoutMs: GIT_TIMEOUT_MS })?.trim() || null;
|
|
1881
|
+
appendBuildLedger(buildLedgerRecord({
|
|
1882
|
+
...run,
|
|
1883
|
+
id: run.id ?? randomUUID(),
|
|
1884
|
+
root,
|
|
1885
|
+
workspace: workspaceName(root),
|
|
1886
|
+
branch,
|
|
1887
|
+
version: STIM_VERSION
|
|
1888
|
+
}));
|
|
1889
|
+
} catch {}
|
|
1890
|
+
}
|
|
1891
|
+
//#endregion
|
|
1892
|
+
//#region src/engine/build-progress.ts
|
|
1893
|
+
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
1894
|
+
const NO_BUILD_PROGRESS = {
|
|
1895
|
+
step: () => {},
|
|
1896
|
+
activity: () => {},
|
|
1897
|
+
estimate: () => {},
|
|
1898
|
+
miss: () => {},
|
|
1899
|
+
hit: () => {},
|
|
1900
|
+
deviceSetup: () => {},
|
|
1901
|
+
deviceSetupKnown: () => void 0,
|
|
1902
|
+
place: () => {},
|
|
1903
|
+
waitingOn: () => {},
|
|
1904
|
+
waitingFor: () => {},
|
|
1905
|
+
bootStarted: () => {},
|
|
1906
|
+
output: () => {},
|
|
1907
|
+
durations: () => ({}),
|
|
1908
|
+
phase: () => null,
|
|
1909
|
+
steps: () => null,
|
|
1910
|
+
clear: () => {}
|
|
1911
|
+
};
|
|
1912
|
+
/** `writer`, with each record it writes also passed to `progress.output`. */
|
|
1913
|
+
function tapBuildLog(writer, progress) {
|
|
1914
|
+
if (progress === NO_BUILD_PROGRESS) return writer;
|
|
1915
|
+
return {
|
|
1916
|
+
get file() {
|
|
1917
|
+
return writer.file;
|
|
1918
|
+
},
|
|
1919
|
+
write(record) {
|
|
1920
|
+
progress.output(record);
|
|
1921
|
+
return writer.write(record);
|
|
1922
|
+
},
|
|
1923
|
+
close: () => writer.close(),
|
|
1924
|
+
get written() {
|
|
1925
|
+
return writer.written;
|
|
1926
|
+
},
|
|
1927
|
+
get dropped() {
|
|
1928
|
+
return writer.dropped;
|
|
1929
|
+
},
|
|
1930
|
+
get lastError() {
|
|
1931
|
+
return writer.lastError;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function runFailureRecord({ code, message, phase }) {
|
|
1936
|
+
return {
|
|
1937
|
+
src: "build",
|
|
1938
|
+
level: "error",
|
|
1939
|
+
event: "run_failed",
|
|
1940
|
+
...code ? { code } : {},
|
|
1941
|
+
...phase ? { phase } : {},
|
|
1942
|
+
msg: [code, message].filter(Boolean).join(": ") || "the run failed"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function thrownRunFailureRecord(error, phase) {
|
|
1946
|
+
return runFailureRecord({
|
|
1947
|
+
code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0,
|
|
1948
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1949
|
+
phase
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
const DETAIL_WRITE_MS = 2e3;
|
|
1953
|
+
function toolLine(record) {
|
|
1954
|
+
if (!record || typeof record !== "object") return null;
|
|
1955
|
+
const { src, level, msg } = record;
|
|
1956
|
+
return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
|
|
1957
|
+
}
|
|
1958
|
+
function writeBuildDetail(root, claimId, detail) {
|
|
1959
|
+
const file = workspaceBuildDetailFile(root);
|
|
1960
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
1961
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1962
|
+
writeFileSync(tmp, JSON.stringify({
|
|
1963
|
+
claimId,
|
|
1964
|
+
detail
|
|
1965
|
+
}));
|
|
1966
|
+
renameSync(tmp, file);
|
|
1967
|
+
}
|
|
1968
|
+
const COLD_PHASES = [
|
|
1969
|
+
"prebuild",
|
|
1970
|
+
"pods",
|
|
1971
|
+
"compile"
|
|
1972
|
+
];
|
|
1973
|
+
const HIT_PHASES = ["install", "launch"];
|
|
1974
|
+
function settledOutcome(phase, entered) {
|
|
1975
|
+
if (COLD_PHASES.includes(phase)) return "cold";
|
|
1976
|
+
if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
|
|
1977
|
+
return HIT_PHASES.includes(phase) ? "hit" : null;
|
|
1978
|
+
}
|
|
1979
|
+
function projectHistory(projectKey) {
|
|
1980
|
+
try {
|
|
1981
|
+
return readStats$1().record?.history?.[projectKey];
|
|
1982
|
+
} catch {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function startBuildProgress({ root, platform, slot, claim, now = Date.now, load = () => loadPerCore().loadPerCore, note = () => {} }) {
|
|
1987
|
+
const startedAt = new Date(now()).toISOString();
|
|
1988
|
+
const record = {
|
|
1989
|
+
platform,
|
|
1990
|
+
slot,
|
|
1991
|
+
startedAt,
|
|
1992
|
+
phase: "prepare",
|
|
1993
|
+
phaseStartedAt: startedAt,
|
|
1994
|
+
phases: [{
|
|
1995
|
+
phase: "prepare",
|
|
1996
|
+
startedAt
|
|
1997
|
+
}],
|
|
1998
|
+
claim: {
|
|
1999
|
+
root: claim.root,
|
|
2000
|
+
path: claim.path,
|
|
2001
|
+
claimId: claim.claimId,
|
|
2002
|
+
pid: claim.owner.pid
|
|
2003
|
+
},
|
|
2004
|
+
loadPerCore: load()
|
|
2005
|
+
};
|
|
2006
|
+
let warned = false;
|
|
2007
|
+
const guard = (fn) => {
|
|
2008
|
+
try {
|
|
2009
|
+
fn();
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
if (warned) return;
|
|
2012
|
+
warned = true;
|
|
2013
|
+
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
|
|
2017
|
+
...before(state),
|
|
2018
|
+
[ACTIVE_BUILD_KEY]: record
|
|
2019
|
+
})));
|
|
2020
|
+
write(withInterruptedBuild);
|
|
2021
|
+
const phases = trace.phases(`${platform} build`);
|
|
2022
|
+
phases.enter("prepare");
|
|
2023
|
+
let projectKey = null;
|
|
2024
|
+
const parser = createBuildDetailParser();
|
|
2025
|
+
let detailWrittenAt = -Infinity;
|
|
2026
|
+
let detailTimer = null;
|
|
2027
|
+
const flushDetail = () => {
|
|
2028
|
+
detailTimer = null;
|
|
2029
|
+
detailWrittenAt = now();
|
|
2030
|
+
const detail = parser.detail(new Date(detailWrittenAt).toISOString());
|
|
2031
|
+
if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
|
|
2032
|
+
};
|
|
2033
|
+
const settle = (outcome) => {
|
|
2034
|
+
record.outcome = outcome;
|
|
2035
|
+
if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
|
|
2036
|
+
};
|
|
2037
|
+
const waits = /* @__PURE__ */ new Map();
|
|
2038
|
+
return {
|
|
2039
|
+
step(phase) {
|
|
2040
|
+
if (phase === record.phase) return;
|
|
2041
|
+
const at = new Date(now()).toISOString();
|
|
2042
|
+
record.phase = phase;
|
|
2043
|
+
record.phaseStartedAt = at;
|
|
2044
|
+
record.phases.push({
|
|
2045
|
+
phase,
|
|
2046
|
+
startedAt: at
|
|
2047
|
+
});
|
|
2048
|
+
phases.enter(phase);
|
|
2049
|
+
if (phase !== "wait") delete record.waitingOn;
|
|
2050
|
+
delete record.activity;
|
|
2051
|
+
const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
|
|
2052
|
+
if (outcome) settle(outcome);
|
|
2053
|
+
write();
|
|
2054
|
+
},
|
|
2055
|
+
activity(name, at) {
|
|
2056
|
+
if ((record.activity?.name ?? null) === name) return;
|
|
2057
|
+
if (name) record.activity = {
|
|
2058
|
+
name,
|
|
2059
|
+
startedAt: new Date(at ?? now()).toISOString()
|
|
2060
|
+
};
|
|
2061
|
+
else delete record.activity;
|
|
2062
|
+
write();
|
|
2063
|
+
},
|
|
2064
|
+
estimate(key) {
|
|
2065
|
+
if (platform === "macos") return;
|
|
2066
|
+
projectKey = key;
|
|
2067
|
+
record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
2068
|
+
write();
|
|
2069
|
+
},
|
|
2070
|
+
miss(reason, provisional = false) {
|
|
2071
|
+
record.missReason = reason;
|
|
2072
|
+
settle("cold");
|
|
2073
|
+
if (provisional) record.missProvisional = true;
|
|
2074
|
+
else delete record.missProvisional;
|
|
2075
|
+
write();
|
|
2076
|
+
},
|
|
2077
|
+
hit() {
|
|
2078
|
+
delete record.missReason;
|
|
2079
|
+
delete record.missProvisional;
|
|
2080
|
+
settle("hit");
|
|
2081
|
+
write();
|
|
2082
|
+
},
|
|
2083
|
+
deviceSetup(setup) {
|
|
2084
|
+
if (setup === void 0) return;
|
|
2085
|
+
record.deviceSetup = setup;
|
|
2086
|
+
write();
|
|
2087
|
+
},
|
|
2088
|
+
deviceSetupKnown() {
|
|
2089
|
+
return record.deviceSetup;
|
|
2090
|
+
},
|
|
2091
|
+
place(remote) {
|
|
2092
|
+
const current = record.placement;
|
|
2093
|
+
if (remote && current?.host === remote.host && current.phase === remote.phase) return;
|
|
2094
|
+
if (current) record.offload = withOffloadPhase(record.offload, current, now());
|
|
2095
|
+
if (!remote) {
|
|
2096
|
+
if (!current) return;
|
|
2097
|
+
delete record.placement;
|
|
2098
|
+
return write();
|
|
2099
|
+
}
|
|
2100
|
+
const at = new Date(now()).toISOString();
|
|
2101
|
+
record.placement = {
|
|
2102
|
+
host: remote.host,
|
|
2103
|
+
phase: remote.phase,
|
|
2104
|
+
startedAt: current?.host === remote.host ? current.startedAt : at,
|
|
2105
|
+
phaseStartedAt: at
|
|
2106
|
+
};
|
|
2107
|
+
write();
|
|
2108
|
+
},
|
|
2109
|
+
waitingFor(info, kind) {
|
|
2110
|
+
if (info) waits.set(info.kind, info);
|
|
2111
|
+
else if (kind) waits.delete(kind);
|
|
2112
|
+
else waits.clear();
|
|
2113
|
+
const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
|
|
2114
|
+
if (first) record.waitingFor = first;
|
|
2115
|
+
else delete record.waitingFor;
|
|
2116
|
+
write();
|
|
2117
|
+
},
|
|
2118
|
+
bootStarted() {
|
|
2119
|
+
if (record.boot) return;
|
|
2120
|
+
record.boot = {
|
|
2121
|
+
startedAt: new Date(now()).toISOString(),
|
|
2122
|
+
loadPerCore: load()
|
|
2123
|
+
};
|
|
2124
|
+
write();
|
|
2125
|
+
},
|
|
2126
|
+
waitingOn(holder) {
|
|
2127
|
+
if (holder === null) {
|
|
2128
|
+
if (!record.waitingOn) return;
|
|
2129
|
+
delete record.waitingOn;
|
|
2130
|
+
} else {
|
|
2131
|
+
if (record.waitingOn?.path === holder) return;
|
|
2132
|
+
record.waitingOn = { path: holder };
|
|
2133
|
+
}
|
|
2134
|
+
write();
|
|
2135
|
+
},
|
|
2136
|
+
output(line) {
|
|
2137
|
+
const msg = toolLine(line);
|
|
2138
|
+
if (msg === null || !parser.push(msg) || detailTimer) return;
|
|
2139
|
+
const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
|
|
2140
|
+
if (wait <= 0) return flushDetail();
|
|
2141
|
+
detailTimer = setTimeout(flushDetail, wait);
|
|
2142
|
+
detailTimer.unref?.();
|
|
2143
|
+
},
|
|
2144
|
+
durations() {
|
|
2145
|
+
return phaseDurations(record.phases, now());
|
|
2146
|
+
},
|
|
2147
|
+
phase() {
|
|
2148
|
+
return record.phase;
|
|
2149
|
+
},
|
|
2150
|
+
steps() {
|
|
2151
|
+
const detail = parser.detail("");
|
|
2152
|
+
return detail?.unit === "steps" ? detail.total : null;
|
|
2153
|
+
},
|
|
2154
|
+
clear() {
|
|
2155
|
+
phases.end();
|
|
2156
|
+
if (detailTimer) clearTimeout(detailTimer);
|
|
2157
|
+
detailTimer = null;
|
|
2158
|
+
guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
|
|
2159
|
+
guard(() => {
|
|
2160
|
+
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
/** `offload` with the time `placement` has spent in its current phase as of `now` added to that phase. */
|
|
2166
|
+
function withOffloadPhase(offload, placement, now) {
|
|
2167
|
+
const phaseMs = { ...offload?.phaseMs };
|
|
2168
|
+
const ms = Math.max(0, now - Date.parse(placement.phaseStartedAt));
|
|
2169
|
+
if (Number.isFinite(ms)) phaseMs[placement.phase] = (phaseMs[placement.phase] ?? 0) + ms;
|
|
2170
|
+
return {
|
|
2171
|
+
host: placement.host,
|
|
2172
|
+
phaseMs
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function phaseDurations(phases, now) {
|
|
2176
|
+
const out = {};
|
|
2177
|
+
phases.forEach(({ phase, startedAt }, index) => {
|
|
2178
|
+
const start = Date.parse(startedAt);
|
|
2179
|
+
const next = phases[index + 1];
|
|
2180
|
+
const end = next ? Date.parse(next.startedAt) : now;
|
|
2181
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
2182
|
+
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
2183
|
+
});
|
|
2184
|
+
return out;
|
|
2185
|
+
}
|
|
2186
|
+
function completedPhaseDurations(phases) {
|
|
2187
|
+
if (phases.length < 2) return void 0;
|
|
2188
|
+
const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
|
|
2189
|
+
return Object.keys(durations).length ? durations : void 0;
|
|
2190
|
+
}
|
|
2191
|
+
const HISTORY_FIELDS = [
|
|
2192
|
+
"devicePlacement",
|
|
2193
|
+
"platform",
|
|
2194
|
+
"status",
|
|
2195
|
+
"configuration",
|
|
2196
|
+
"fingerprint",
|
|
2197
|
+
"cacheKey",
|
|
2198
|
+
"cacheHit",
|
|
2199
|
+
"cacheSkipped",
|
|
2200
|
+
"durationMs",
|
|
2201
|
+
"startedAt",
|
|
2202
|
+
"errorCode",
|
|
2203
|
+
"missReason",
|
|
2204
|
+
"buildMachine",
|
|
2205
|
+
"builtOn",
|
|
2206
|
+
"offloadedTo",
|
|
2207
|
+
"offloadFallback",
|
|
2208
|
+
"diagnostics",
|
|
2209
|
+
"compileSteps"
|
|
2210
|
+
];
|
|
2211
|
+
function withHistoryEntry(state, platform, entry) {
|
|
2212
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2213
|
+
const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
|
|
2214
|
+
const list = Array.isArray(history[platform]) ? history[platform] : [];
|
|
2215
|
+
return {
|
|
2216
|
+
...state,
|
|
2217
|
+
[BUILD_HISTORY_KEY]: {
|
|
2218
|
+
...history,
|
|
2219
|
+
[platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
function finishedResult(record) {
|
|
2224
|
+
if (record.status === "ok") return "succeeded";
|
|
2225
|
+
return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
|
|
2226
|
+
}
|
|
2227
|
+
function recordFinishedBuild(root, recorded, { update = updateWorkspaceState, now = Date.now, artifactOnly = false, ledger = writeBuildLedger } = {}) {
|
|
2228
|
+
const { ledger: extras, ...record } = recorded;
|
|
2229
|
+
const ran = {};
|
|
2230
|
+
try {
|
|
2231
|
+
updateFinishedBuild(root, record, ran, {
|
|
2232
|
+
update,
|
|
2233
|
+
now,
|
|
2234
|
+
artifactOnly
|
|
2235
|
+
});
|
|
2236
|
+
} finally {
|
|
2237
|
+
ledger(root, {
|
|
2238
|
+
now: now(),
|
|
2239
|
+
outcome: finishedResult(record),
|
|
2240
|
+
record,
|
|
2241
|
+
extras: isJsonObject(extras) ? extras : {},
|
|
2242
|
+
phases: {},
|
|
2243
|
+
phase: null,
|
|
2244
|
+
...ran
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
function updateFinishedBuild(root, record, ran, { update, now, artifactOnly }) {
|
|
2249
|
+
const platform = record.platform;
|
|
2250
|
+
update(root, (state) => {
|
|
2251
|
+
const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2252
|
+
const own = active?.platform === platform ? active : null;
|
|
2253
|
+
const phases = own ? phaseDurations(own.phases, now()) : {};
|
|
2254
|
+
if (own) {
|
|
2255
|
+
const offload = own.placement ? withOffloadPhase(own.offload, own.placement, now()) : own.offload;
|
|
2256
|
+
Object.assign(ran, {
|
|
2257
|
+
id: own.claim.claimId,
|
|
2258
|
+
phases,
|
|
2259
|
+
phase: own.phase,
|
|
2260
|
+
...offload ? { offload } : {},
|
|
2261
|
+
...own.loadPerCore === void 0 ? {} : { loadPerCore: own.loadPerCore },
|
|
2262
|
+
...own.boot ? { boot: own.boot } : {}
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
const entry = {
|
|
2266
|
+
...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
|
|
2267
|
+
result: finishedResult(record),
|
|
2268
|
+
slot: own?.slot ?? "default",
|
|
2269
|
+
phases
|
|
2270
|
+
};
|
|
2271
|
+
if (artifactOnly || platform === "macos") return withHistoryEntry(state, platform, entry);
|
|
2272
|
+
const slot = own?.slot ?? "default";
|
|
2273
|
+
const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
|
|
2274
|
+
const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
|
|
2275
|
+
const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
|
|
2276
|
+
if (record.devicePlacement || isJsonObject(savedDevice)) {
|
|
2277
|
+
const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
|
|
2278
|
+
if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
|
|
2279
|
+
else delete device.devicePlacement;
|
|
2280
|
+
state = slot === "default" ? {
|
|
2281
|
+
...state,
|
|
2282
|
+
[platform]: device
|
|
2283
|
+
} : {
|
|
2284
|
+
...state,
|
|
2285
|
+
deviceSlots: {
|
|
2286
|
+
...slots,
|
|
2287
|
+
[slot]: {
|
|
2288
|
+
...savedSlot,
|
|
2289
|
+
[platform]: device
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
return withHistoryEntry({
|
|
2295
|
+
...state,
|
|
2296
|
+
lastBuild: record,
|
|
2297
|
+
[LAST_BUILD_KEYS[platform]]: record
|
|
2298
|
+
}, platform, entry);
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
/** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
|
|
2302
|
+
function recordBuildPhase(root, platform, startedAt, phase, ms) {
|
|
2303
|
+
updateWorkspaceState(root, (state) => {
|
|
2304
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2305
|
+
const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
|
|
2306
|
+
const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
|
|
2307
|
+
const entry = list[index];
|
|
2308
|
+
if (!isJsonObject(entry)) return state;
|
|
2309
|
+
const phases = isJsonObject(entry.phases) ? entry.phases : {};
|
|
2310
|
+
const next = [...list];
|
|
2311
|
+
next[index] = {
|
|
2312
|
+
...entry,
|
|
2313
|
+
phases: {
|
|
2314
|
+
...phases,
|
|
2315
|
+
[phase]: Math.round(ms)
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
return {
|
|
2319
|
+
...state,
|
|
2320
|
+
[BUILD_HISTORY_KEY]: {
|
|
2321
|
+
...saved,
|
|
2322
|
+
[platform]: next
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
function withInterruptedBuild(state) {
|
|
2328
|
+
const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2329
|
+
if (!left) return state;
|
|
2330
|
+
const last = state[BUILD_HISTORY_KEY]?.[left.platform]?.[0];
|
|
2331
|
+
if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
|
|
2332
|
+
return withHistoryEntry(state, left.platform, {
|
|
2333
|
+
platform: left.platform,
|
|
2334
|
+
status: "failed",
|
|
2335
|
+
result: "interrupted",
|
|
2336
|
+
slot: left.slot,
|
|
2337
|
+
configuration: null,
|
|
2338
|
+
fingerprint: null,
|
|
2339
|
+
cacheKey: null,
|
|
2340
|
+
cacheHit: false,
|
|
2341
|
+
cacheSkipped: false,
|
|
2342
|
+
durationMs: null,
|
|
2343
|
+
startedAt: left.startedAt,
|
|
2344
|
+
phases: {
|
|
2345
|
+
...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
|
|
2346
|
+
[left.phase]: 0
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
function parseActiveBuild(value) {
|
|
2351
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2352
|
+
const record = value;
|
|
2353
|
+
const claim = record.claim;
|
|
2354
|
+
if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
|
|
2355
|
+
if (!isPhase(record.phase)) return null;
|
|
2356
|
+
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
2357
|
+
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
2358
|
+
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
2359
|
+
const missReason = parseMissReason(record.missReason);
|
|
2360
|
+
const placement = parsePlacement(record.placement);
|
|
2361
|
+
const waitingOn = parseWaitingOn(record.waitingOn);
|
|
2362
|
+
const waitingFor = parseWaitingFor(record.waitingFor);
|
|
2363
|
+
const estimate = parseEstimate(record.estimate);
|
|
2364
|
+
const activity = parseActivity(record.activity);
|
|
2365
|
+
const offload = parseOffload(record.offload);
|
|
2366
|
+
const boot = parseBoot(record.boot);
|
|
2367
|
+
return {
|
|
2368
|
+
platform: record.platform,
|
|
2369
|
+
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
2370
|
+
startedAt: record.startedAt,
|
|
2371
|
+
phase: record.phase,
|
|
2372
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
2373
|
+
phases,
|
|
2374
|
+
...missReason ? { missReason } : {},
|
|
2375
|
+
...missReason && record.missProvisional === true ? { missProvisional: true } : {},
|
|
2376
|
+
...placement ? { placement } : {},
|
|
2377
|
+
...offload ? { offload } : {},
|
|
2378
|
+
...waitingOn ? { waitingOn } : {},
|
|
2379
|
+
...waitingFor ? { waitingFor } : {},
|
|
2380
|
+
...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
|
|
2381
|
+
...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
|
|
2382
|
+
...estimate ? { estimate } : {},
|
|
2383
|
+
...activity ? { activity } : {},
|
|
2384
|
+
...isLoad(record.loadPerCore) ? { loadPerCore: record.loadPerCore } : {},
|
|
2385
|
+
...boot ? { boot } : {},
|
|
2386
|
+
claim: {
|
|
2387
|
+
root: claim.root,
|
|
2388
|
+
path: typeof claim.path === "string" ? claim.path : "",
|
|
2389
|
+
claimId: claim.claimId,
|
|
2390
|
+
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function isLoad(value) {
|
|
2395
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
2396
|
+
}
|
|
2397
|
+
function parseBoot(value) {
|
|
2398
|
+
if (!isJsonObject(value) || typeof value.startedAt !== "string" || !isLoad(value.loadPerCore)) return null;
|
|
2399
|
+
return {
|
|
2400
|
+
startedAt: value.startedAt,
|
|
2401
|
+
loadPerCore: value.loadPerCore
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
function parseOffload(value) {
|
|
2405
|
+
if (!isJsonObject(value) || typeof value.host !== "string" || !isJsonObject(value.phaseMs)) return null;
|
|
2406
|
+
const phaseMs = {};
|
|
2407
|
+
for (const [phase, ms] of Object.entries(value.phaseMs)) if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) phaseMs[phase] = ms;
|
|
2408
|
+
return {
|
|
2409
|
+
host: value.host,
|
|
2410
|
+
phaseMs
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function parsePlacement(value) {
|
|
2414
|
+
if (!value || typeof value !== "object") return null;
|
|
2415
|
+
const { host, phase, startedAt, phaseStartedAt } = value;
|
|
2416
|
+
if ([
|
|
2417
|
+
host,
|
|
2418
|
+
phase,
|
|
2419
|
+
startedAt,
|
|
2420
|
+
phaseStartedAt
|
|
2421
|
+
].some((field) => typeof field !== "string")) return null;
|
|
2422
|
+
return {
|
|
2423
|
+
host,
|
|
2424
|
+
phase,
|
|
2425
|
+
startedAt,
|
|
2426
|
+
phaseStartedAt
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
function parseWaitingFor(value) {
|
|
2430
|
+
if (!value || typeof value !== "object") return null;
|
|
2431
|
+
const { kind, inUse, max, since } = value;
|
|
2432
|
+
if (kind !== "build-slot" && kind !== "device-slot") return null;
|
|
2433
|
+
if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
|
|
2434
|
+
if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
|
|
2435
|
+
return {
|
|
2436
|
+
kind,
|
|
2437
|
+
inUse,
|
|
2438
|
+
max,
|
|
2439
|
+
since
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function parseActivity(value) {
|
|
2443
|
+
if (!value || typeof value !== "object") return null;
|
|
2444
|
+
const { name, startedAt } = value;
|
|
2445
|
+
if (name !== "booting" && name !== "bundling" && name !== "waiting-ready") return null;
|
|
2446
|
+
return typeof startedAt === "string" ? {
|
|
2447
|
+
name,
|
|
2448
|
+
startedAt
|
|
2449
|
+
} : null;
|
|
2450
|
+
}
|
|
2451
|
+
function parseWaitingOn(value) {
|
|
2452
|
+
const path = value?.path;
|
|
2453
|
+
return typeof path === "string" && path ? { path } : null;
|
|
2454
|
+
}
|
|
2455
|
+
function parseEstimate(value) {
|
|
2456
|
+
if (!value || typeof value !== "object") return null;
|
|
2457
|
+
const { outcome, expectedMs, basis, phaseMs, planned } = value;
|
|
2458
|
+
if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
|
|
2459
|
+
if (expectedMs !== null && typeof expectedMs !== "number") return null;
|
|
2460
|
+
if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
|
|
2461
|
+
const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
|
|
2462
|
+
return {
|
|
2463
|
+
outcome,
|
|
2464
|
+
expectedMs,
|
|
2465
|
+
basis,
|
|
2466
|
+
phaseMs: Object.fromEntries(durations),
|
|
2467
|
+
planned: planned.filter(isPhase)
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function isPhase(value) {
|
|
2471
|
+
return BUILD_PHASES.includes(value);
|
|
2472
|
+
}
|
|
2473
|
+
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
2474
|
+
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
2475
|
+
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
2476
|
+
return survey.unresolved.length ? "unknown" : "stale";
|
|
2477
|
+
}
|
|
2478
|
+
function median(values) {
|
|
2479
|
+
if (!values.length) return null;
|
|
2480
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
2481
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2482
|
+
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
2483
|
+
}
|
|
2484
|
+
function estimateBuild(history, platform, known, deviceSetup) {
|
|
2485
|
+
const lists = platform === "macos" ? void 0 : history?.[platform];
|
|
2486
|
+
const outcome = known ?? latestOutcome(lists);
|
|
2487
|
+
const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
|
|
2488
|
+
const phaseMs = {};
|
|
2489
|
+
const planned = [];
|
|
2490
|
+
for (const phase of BUILD_PHASES) {
|
|
2491
|
+
const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
2492
|
+
const ms = median(durations);
|
|
2493
|
+
if (ms === null) continue;
|
|
2494
|
+
phaseMs[phase] = ms;
|
|
2495
|
+
if (durations.length * 2 >= samples.length) planned.push(phase);
|
|
2496
|
+
}
|
|
2497
|
+
return {
|
|
2498
|
+
outcome,
|
|
2499
|
+
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
2500
|
+
basis: samples.length,
|
|
2501
|
+
phaseMs,
|
|
2502
|
+
planned
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
const MIN_TAGGED_SAMPLES = 3;
|
|
2506
|
+
function matchDeviceSetup(samples, deviceSetup) {
|
|
2507
|
+
if (deviceSetup === void 0) return samples;
|
|
2508
|
+
const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
|
|
2509
|
+
if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
|
|
2510
|
+
return samples.filter((sample) => sample.deviceSetup !== true);
|
|
2511
|
+
}
|
|
2512
|
+
function latestOutcome(lists) {
|
|
2513
|
+
let latest = null;
|
|
2514
|
+
for (const outcome of ["hit", "cold"]) {
|
|
2515
|
+
const last = lists?.[outcome]?.at(-1);
|
|
2516
|
+
const at = last ? Date.parse(last.at) : NaN;
|
|
2517
|
+
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
2518
|
+
outcome,
|
|
2519
|
+
at
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
return latest?.outcome ?? null;
|
|
2523
|
+
}
|
|
2524
|
+
function buildReport(record, { state, history }) {
|
|
2525
|
+
const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
2526
|
+
const plannedPhases = estimate.planned.flatMap((phase) => {
|
|
2527
|
+
const expectedMs = estimate.phaseMs[phase];
|
|
2528
|
+
return expectedMs === void 0 ? [] : [{
|
|
2529
|
+
phase,
|
|
2530
|
+
expectedMs
|
|
2531
|
+
}];
|
|
2532
|
+
});
|
|
2533
|
+
const completedPhaseMs = completedPhaseDurations(record.phases);
|
|
2534
|
+
return {
|
|
2535
|
+
platform: record.platform,
|
|
2536
|
+
slot: record.slot,
|
|
2537
|
+
state,
|
|
2538
|
+
phase: record.phase,
|
|
2539
|
+
startedAt: record.startedAt,
|
|
2540
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
2541
|
+
outcome: estimate.outcome,
|
|
2542
|
+
outcomeKnown: record.outcome !== void 0,
|
|
2543
|
+
...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
|
|
2544
|
+
expectedMs: estimate.expectedMs,
|
|
2545
|
+
expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
|
|
2546
|
+
...completedPhaseMs ? { completedPhaseMs } : {},
|
|
2547
|
+
basis: estimate.basis,
|
|
2548
|
+
plannedPhases: plannedPhases.length ? plannedPhases : null,
|
|
2549
|
+
...record.missReason ? { missReason: record.missReason } : {},
|
|
2550
|
+
...record.missReason && record.missProvisional ? { missProvisional: true } : {},
|
|
2551
|
+
placement: record.placement ?? "local",
|
|
2552
|
+
...record.waitingOn ? { waitingOn: record.waitingOn } : {},
|
|
2553
|
+
...record.waitingFor ? { waitingFor: record.waitingFor } : {},
|
|
2554
|
+
...record.activity ? { activity: { ...record.activity } } : {}
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* The run's `bundling` activity with Metro's percent, when the request Metro reports in flight is the one the activity
|
|
2559
|
+
* started on: same platform and same start, since both come from the request's `bundle_response_started` record.
|
|
2560
|
+
*/
|
|
2561
|
+
function bundlingActivity(activity, platform, bundle) {
|
|
2562
|
+
if (activity?.name !== "bundling" || !bundle?.bundling || bundle.percent === void 0) return activity;
|
|
2563
|
+
if (bundle.platform !== platform || bundle.startedAt !== activity.startedAt) return activity;
|
|
2564
|
+
return {
|
|
2565
|
+
...activity,
|
|
2566
|
+
percent: bundle.percent
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
function remainingText(remainingMs) {
|
|
2570
|
+
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
2571
|
+
}
|
|
2572
|
+
function activityText({ name, percent, startedAt }, now) {
|
|
2573
|
+
return `${name === "booting" ? "booting" : name === "waiting-ready" ? "waiting for app ready" : percent === void 0 ? "bundling JS" : `bundling JS ${percent}%`}, ${formatElapsed(Math.max(0, now - Date.parse(startedAt)))}`;
|
|
2574
|
+
}
|
|
2575
|
+
function buildStatusLine(report, now) {
|
|
2576
|
+
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
2577
|
+
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
2578
|
+
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
2579
|
+
const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
|
|
2580
|
+
const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
|
|
2581
|
+
const activity = report.activity ? ` (${activityText(report.activity, now)})` : "";
|
|
2582
|
+
const head = `build: ${report.platform}${slot} ${report.phase}${activity}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
2583
|
+
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
2584
|
+
if (report.expectedMs === null) return head;
|
|
2585
|
+
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
2586
|
+
const remainingMs = report.expectedMs - elapsedMs;
|
|
2587
|
+
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
2588
|
+
}
|
|
2589
|
+
//#endregion
|
|
2590
|
+
//#region src/commands/gc/idle.ts
|
|
2591
|
+
function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
|
|
2592
|
+
const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
|
|
2593
|
+
const dead = new Set(deadProjects);
|
|
2594
|
+
const found = [];
|
|
2595
|
+
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
2596
|
+
if (dead.has(project)) continue;
|
|
2597
|
+
for (const { slot, platforms } of projectDeviceSlots(record)) {
|
|
2598
|
+
const ios = platforms.ios;
|
|
2599
|
+
if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
|
|
2600
|
+
kind: "ios",
|
|
2601
|
+
id: ios.deviceUdid,
|
|
2602
|
+
name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
|
|
2603
|
+
project,
|
|
2604
|
+
slot,
|
|
2605
|
+
activity: readActivity({
|
|
2606
|
+
platform: "ios",
|
|
2607
|
+
id: ios.deviceUdid,
|
|
2608
|
+
slot,
|
|
2609
|
+
workspace: project
|
|
2610
|
+
})
|
|
2611
|
+
});
|
|
2612
|
+
const android = platforms.android;
|
|
2613
|
+
const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
|
|
2614
|
+
if (android?.avdName && serial) found.push({
|
|
2615
|
+
kind: "android",
|
|
2616
|
+
id: android.avdName,
|
|
2617
|
+
name: android.avdName,
|
|
2618
|
+
project,
|
|
2619
|
+
slot,
|
|
2620
|
+
activity: readActivity({
|
|
2621
|
+
platform: "android",
|
|
2622
|
+
id: serial,
|
|
2623
|
+
slot,
|
|
2624
|
+
workspace: project
|
|
2625
|
+
})
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
return found;
|
|
2630
|
+
}
|
|
2631
|
+
function findIdleDevices({ buildInProgress, now, ...inputs }) {
|
|
2632
|
+
return idleDevicesFromActivity(findOwnedDeviceActivity(inputs), buildInProgress, now);
|
|
2633
|
+
}
|
|
2634
|
+
function idleDevicesFromActivity(devices, buildInProgress, now) {
|
|
2635
|
+
return devices.filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
|
|
2636
|
+
kind,
|
|
2637
|
+
id,
|
|
2638
|
+
name,
|
|
2639
|
+
project,
|
|
2640
|
+
slot,
|
|
2641
|
+
lastActivityAt: activity.lastActivityAt ?? null,
|
|
2642
|
+
idleForMs: idleForMs(activity, now),
|
|
2643
|
+
buildInProgress: buildInProgress(project)
|
|
2644
|
+
}));
|
|
2645
|
+
}
|
|
2646
|
+
function idleShutdownCandidates(devices, idleMs) {
|
|
2647
|
+
return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
|
|
2648
|
+
}
|
|
2649
|
+
function parseIdleDuration(value) {
|
|
2650
|
+
const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
|
|
2651
|
+
if (!match) return null;
|
|
2652
|
+
const unit = {
|
|
2653
|
+
m: 6e4,
|
|
2654
|
+
h: 36e5,
|
|
2655
|
+
d: 864e5
|
|
2656
|
+
}[match[2]];
|
|
2657
|
+
return Number(match[1]) * unit;
|
|
2658
|
+
}
|
|
2659
|
+
function workspaceBuildInProgress(project) {
|
|
2660
|
+
const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
|
|
2661
|
+
if (!record) return false;
|
|
2662
|
+
try {
|
|
2663
|
+
return activeBuildState(record.claim) !== "stale";
|
|
2664
|
+
} catch {
|
|
2665
|
+
return true;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function deviceActivityInputs(config, sims, now, env) {
|
|
2669
|
+
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
2670
|
+
return {
|
|
2671
|
+
config,
|
|
2672
|
+
sims,
|
|
2673
|
+
readActivity: createActivityReader({
|
|
2674
|
+
now,
|
|
2675
|
+
env
|
|
2676
|
+
}),
|
|
2677
|
+
androidSerial: (avdName) => {
|
|
2678
|
+
try {
|
|
2679
|
+
return resolve(avdName).serial ?? null;
|
|
2680
|
+
} catch {
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
function collectOwnedDeviceActivity(config, sims, now = Date.now(), env) {
|
|
2687
|
+
return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now, env));
|
|
2688
|
+
}
|
|
2689
|
+
function collectIdleDevices(config, sims, deadProjects, now = Date.now(), env) {
|
|
2690
|
+
return findIdleDevices({
|
|
2691
|
+
...deviceActivityInputs(config, sims, now, env),
|
|
2692
|
+
deadProjects,
|
|
2693
|
+
now,
|
|
2694
|
+
buildInProgress: workspaceBuildInProgress
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = [], signal, operation) {
|
|
2698
|
+
const resolve = ownedAvdSerialResolverAsync({
|
|
2699
|
+
timeoutMs: 5e3,
|
|
2700
|
+
signal,
|
|
2701
|
+
operation
|
|
2702
|
+
});
|
|
2703
|
+
const serials = /* @__PURE__ */ new Map();
|
|
2704
|
+
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
2705
|
+
signal?.throwIfAborted();
|
|
2706
|
+
if (deadProjects.includes(project)) continue;
|
|
2707
|
+
for (const { platforms } of projectDeviceSlots(record)) {
|
|
2708
|
+
const android = platforms.android;
|
|
2709
|
+
if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
|
|
2710
|
+
try {
|
|
2711
|
+
serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2714
|
+
signal?.throwIfAborted();
|
|
2715
|
+
serials.set(android.avdName, null);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return {
|
|
2720
|
+
config,
|
|
2721
|
+
sims,
|
|
2722
|
+
readActivity: createActivityReaderAsync({
|
|
2723
|
+
now,
|
|
2724
|
+
env,
|
|
2725
|
+
signal,
|
|
2726
|
+
operation
|
|
2727
|
+
}),
|
|
2728
|
+
androidSerial: (name) => serials.get(name) ?? null
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env, signal, operation) {
|
|
2732
|
+
const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env, [], signal, operation));
|
|
2733
|
+
return Promise.all(devices.map(async (device) => Object.assign({}, device, { activity: await device.activity })));
|
|
2734
|
+
}
|
|
2735
|
+
async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env, signal, operation) {
|
|
2736
|
+
const pending = findOwnedDeviceActivity({
|
|
2737
|
+
...await deviceActivityInputsAsync(config, sims, now, env, deadProjects, signal, operation),
|
|
2738
|
+
deadProjects
|
|
2739
|
+
});
|
|
2740
|
+
return idleDevicesFromActivity(await Promise.all(pending.map(async (device) => Object.assign({}, device, { activity: await device.activity }))), workspaceBuildInProgress, now);
|
|
2741
|
+
}
|
|
2742
|
+
function idleDeviceLines(devices) {
|
|
2743
|
+
if (devices.length === 0) return [];
|
|
2744
|
+
return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
|
|
2745
|
+
const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
|
|
2746
|
+
const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
|
|
2747
|
+
const build = d.buildInProgress ? ", build in progress" : "";
|
|
2748
|
+
return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
|
|
2749
|
+
})];
|
|
2750
|
+
}
|
|
2751
|
+
async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT, signal, operation) {
|
|
2752
|
+
signal?.throwIfAborted();
|
|
2753
|
+
const candidates = idleShutdownCandidates(devices, idleMs);
|
|
2754
|
+
const shutDown = [];
|
|
2755
|
+
if (candidates.length === 0) {
|
|
2756
|
+
log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
|
|
2757
|
+
return {
|
|
2758
|
+
failures: 0,
|
|
2759
|
+
shutDown
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
const fresh = idleShutdownCandidates(await recollect(), idleMs);
|
|
2763
|
+
signal?.throwIfAborted();
|
|
2764
|
+
let failures = 0;
|
|
2765
|
+
for (const device of candidates) {
|
|
2766
|
+
signal?.throwIfAborted();
|
|
2767
|
+
const what = `${device.kind} ${device.name}`;
|
|
2768
|
+
const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
|
|
2769
|
+
if (!current) {
|
|
2770
|
+
log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
|
|
2771
|
+
record("idleDevice", "kept", device.name, {
|
|
2772
|
+
id: device.id,
|
|
2773
|
+
detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
|
|
2774
|
+
});
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, {
|
|
2778
|
+
label: device.name,
|
|
2779
|
+
operation
|
|
2780
|
+
}) : await teardownOwnedAvd(device.id, {
|
|
2781
|
+
owner: {
|
|
2782
|
+
projectPath: device.project,
|
|
2783
|
+
slot: device.slot
|
|
2784
|
+
},
|
|
2785
|
+
operation
|
|
2786
|
+
});
|
|
2787
|
+
if (outcome.status === "torn-down") {
|
|
2788
|
+
shutDown.push(device);
|
|
2789
|
+
log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
|
|
2790
|
+
record("idleDevice", "done", device.name, {
|
|
2791
|
+
id: device.id,
|
|
2792
|
+
detail: `idle ${formatLongDuration(current.idleForMs)}`
|
|
2793
|
+
});
|
|
2794
|
+
} else if (outcome.status === "missing") log(chalk.dim(`${what} is already gone; nothing to shut down.`));
|
|
2795
|
+
else if (outcome.status === "skipped") {
|
|
2796
|
+
log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
|
|
2797
|
+
record("idleDevice", "kept", device.name, {
|
|
2798
|
+
id: device.id,
|
|
2799
|
+
detail: outcome.reason
|
|
2800
|
+
});
|
|
2801
|
+
} else {
|
|
2802
|
+
failures++;
|
|
2803
|
+
log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
|
|
2804
|
+
record("idleDevice", "failed", device.name, {
|
|
2805
|
+
id: device.id,
|
|
2806
|
+
detail: outcome.reason
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
failures,
|
|
2812
|
+
shutDown
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
//#endregion
|
|
1152
2816
|
//#region src/macos/instances.ts
|
|
1153
2817
|
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1154
2818
|
function bundleDirs(root) {
|
|
@@ -1221,10 +2885,20 @@ function failedBundleListing(record, error) {
|
|
|
1221
2885
|
if (!open.trim()) throw unverified(record.pid, `lsof failed (${error.message.split("\n")[0]})`);
|
|
1222
2886
|
return open;
|
|
1223
2887
|
}
|
|
2888
|
+
/** The attempt directories under a hosted session's `apps`, each of which holds one `App.app`. */
|
|
2889
|
+
function hostedBundleDirs(area) {
|
|
2890
|
+
const apps = join(area, "apps");
|
|
2891
|
+
if (process.platform !== "darwin" || !existsSync(apps)) return [];
|
|
2892
|
+
if (realpathSync(apps) !== apps) throw new Error("Hosted app data resolves outside its private area.");
|
|
2893
|
+
return readdirSync(apps).filter((attempt) => hostedAppAttempt(attempt)).map((attempt) => join(apps, attempt)).filter((dir) => lstatSync(dir, { throwIfNoEntry: false })?.isDirectory());
|
|
2894
|
+
}
|
|
1224
2895
|
/** Verifies owned bundle instances without blocking on process and executable probes. */
|
|
1225
|
-
|
|
2896
|
+
function runningBundleInstancesAsync(root, operation) {
|
|
2897
|
+
return runningInstancesInAsync(bundleDirs(root), operation);
|
|
2898
|
+
}
|
|
2899
|
+
/** Verifies the running copies of app bundles directly inside `dirs`. */
|
|
2900
|
+
async function runningInstancesInAsync(dirs, operation) {
|
|
1226
2901
|
operation?.signal.throwIfAborted();
|
|
1227
|
-
const dirs = bundleDirs(root);
|
|
1228
2902
|
if (!dirs.length) return [];
|
|
1229
2903
|
const listing = await runOperationFile(operation, getExecutor(), "ps", [
|
|
1230
2904
|
"-axww",
|
|
@@ -1853,9 +3527,9 @@ function numberOrNull(v) {
|
|
|
1853
3527
|
//#region src/workspace/in-use.ts
|
|
1854
3528
|
const NATIVE_RUN = "native-run";
|
|
1855
3529
|
const NATIVE_RUN_HELD = "a stim ios, android or stop run holds its native-run.lock";
|
|
1856
|
-
function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
|
|
3530
|
+
function workspaceInUse(root, { supervisor = true, devServer = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
|
|
1857
3531
|
const reasons = [];
|
|
1858
|
-
if (supervisor) {
|
|
3532
|
+
if (supervisor && devServer) {
|
|
1859
3533
|
const project = getProject(root);
|
|
1860
3534
|
const target = resolveSupervisorTarget({
|
|
1861
3535
|
state: readWorkspaceState$1(root)?.supervisor,
|
|
@@ -1864,6 +3538,11 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1864
3538
|
});
|
|
1865
3539
|
if (target.status === "ours") reasons.push(`its dev server supervisor (pid ${target.pid}) is running`);
|
|
1866
3540
|
else if (target.status === "unverified") reasons.push(`its dev server supervisor cannot be verified: ${target.reason ?? "unknown identity"}`);
|
|
3541
|
+
const browser = webFacts(readWebRecord(root));
|
|
3542
|
+
if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
|
|
3543
|
+
else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
|
|
3544
|
+
}
|
|
3545
|
+
if (supervisor) {
|
|
1867
3546
|
const macos = macosAppState(readMacosRecord(root));
|
|
1868
3547
|
if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
|
|
1869
3548
|
else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
|
|
@@ -1877,9 +3556,6 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1877
3556
|
const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
|
|
1878
3557
|
if (runtime.live.length) reasons.push("its macOS runtime claim is held");
|
|
1879
3558
|
else if (runtime.unresolved[0]) reasons.push(`its macOS runtime cannot be verified: ${runtime.unresolved[0].reason}`);
|
|
1880
|
-
const browser = webFacts(readWebRecord(root));
|
|
1881
|
-
if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
|
|
1882
|
-
else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
|
|
1883
3559
|
}
|
|
1884
3560
|
if (nativeRun) {
|
|
1885
3561
|
const claims = (0, ownership_claim_exports.readClaimSet)(workspaceProcessLockPath(workspaceDir(root), NATIVE_RUN, true));
|
|
@@ -1974,6 +3650,63 @@ async function withIdleWorkspace(root, fn, { purpose, operation, signal = operat
|
|
|
1974
3650
|
}
|
|
1975
3651
|
}
|
|
1976
3652
|
//#endregion
|
|
3653
|
+
//#region src/workspace/regenerable-dirs.ts
|
|
3654
|
+
/**
|
|
3655
|
+
* Directories that hold only what a build or an install regenerates, relative to an app or package root. ios/build is
|
|
3656
|
+
* not one: `pod install` writes the codegen output (build/generated/ios) and, on React Native 0.88, the podspec
|
|
3657
|
+
* facades (scripts/cocoapods/rncore_facades.rb) there, the Pods project references them, and only `pod install`
|
|
3658
|
+
* recreates them.
|
|
3659
|
+
*/
|
|
3660
|
+
const ARTIFACT_DIRS = {
|
|
3661
|
+
build: [
|
|
3662
|
+
"android/app/build",
|
|
3663
|
+
"android/build",
|
|
3664
|
+
"android/.gradle",
|
|
3665
|
+
".build"
|
|
3666
|
+
],
|
|
3667
|
+
dependencies: ["node_modules", "ios/Pods"]
|
|
3668
|
+
};
|
|
3669
|
+
/** The existing real directories among `dirs` under `root`, never through a link. */
|
|
3670
|
+
function existingDirsOf(root, dirs) {
|
|
3671
|
+
return dirs.map((dir) => join(root, dir)).filter((path) => {
|
|
3672
|
+
try {
|
|
3673
|
+
return lstatSync(path).isDirectory();
|
|
3674
|
+
} catch {
|
|
3675
|
+
return false;
|
|
3676
|
+
}
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Why `path`, inside the checkout at `root`, must not be deleted as a regenerable directory, or null when it is a real
|
|
3681
|
+
* directory inside `root` whose every file git ignores.
|
|
3682
|
+
*/
|
|
3683
|
+
async function regenerableDirProblem(root, path, signal) {
|
|
3684
|
+
const rel = relative(root, path);
|
|
3685
|
+
try {
|
|
3686
|
+
if (!lstatSync(path).isDirectory()) return "it is not a directory";
|
|
3687
|
+
} catch {
|
|
3688
|
+
return "it is gone";
|
|
3689
|
+
}
|
|
3690
|
+
if (canonicalPath(path) !== join(canonicalPath(root), rel)) return "it or a directory above it is a link, or it lies outside its project";
|
|
3691
|
+
const tracked = await gitQuietAsync(root, [
|
|
3692
|
+
"ls-files",
|
|
3693
|
+
"--",
|
|
3694
|
+
rel
|
|
3695
|
+
], { signal });
|
|
3696
|
+
if (tracked === null) return "git cannot tell whether it holds tracked files";
|
|
3697
|
+
if (tracked.trim()) return "it holds files git tracks";
|
|
3698
|
+
const unignored = await gitQuietAsync(root, [
|
|
3699
|
+
"ls-files",
|
|
3700
|
+
"--others",
|
|
3701
|
+
"--exclude-standard",
|
|
3702
|
+
"--",
|
|
3703
|
+
rel
|
|
3704
|
+
], { signal });
|
|
3705
|
+
if (unignored === null) return "git cannot tell whether it ignores every file in it";
|
|
3706
|
+
if (unignored.trim()) return "it holds files git does not ignore";
|
|
3707
|
+
return null;
|
|
3708
|
+
}
|
|
3709
|
+
//#endregion
|
|
1977
3710
|
//#region src/commands/gc/workspaces.ts
|
|
1978
3711
|
function workspacesRoot() {
|
|
1979
3712
|
return join$1(getConfigDir(), "workspaces");
|
|
@@ -2091,7 +3824,7 @@ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse
|
|
|
2091
3824
|
continue;
|
|
2092
3825
|
}
|
|
2093
3826
|
if (present) continue;
|
|
2094
|
-
if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
|
|
3827
|
+
if (registryKeys.some((key) => isPathPrefix$1(projectRoot, key))) continue;
|
|
2095
3828
|
if (!isMounted(projectRoot)) {
|
|
2096
3829
|
skipped.push({
|
|
2097
3830
|
dir,
|
|
@@ -2153,39 +3886,71 @@ async function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure
|
|
|
2153
3886
|
}
|
|
2154
3887
|
}
|
|
2155
3888
|
if (!measure) return classified;
|
|
2156
|
-
const orphaned = [];
|
|
2157
|
-
for (const entry of classified.orphaned) {
|
|
2158
|
-
operation?.signal.throwIfAborted();
|
|
2159
|
-
const bytes = await measuredDirectorySizeAsync(entry.dir, {
|
|
2160
|
-
timeoutMs: SIZE_TIMEOUT_MS,
|
|
2161
|
-
operation,
|
|
2162
|
-
signal: operation?.signal
|
|
2163
|
-
});
|
|
2164
|
-
orphaned.push({
|
|
2165
|
-
...entry,
|
|
2166
|
-
bytes
|
|
2167
|
-
});
|
|
2168
|
-
}
|
|
2169
3889
|
return {
|
|
2170
|
-
orphaned,
|
|
3890
|
+
orphaned: await mapLimited(classified.orphaned, sizeConcurrency(operation), async (entry) => {
|
|
3891
|
+
operation?.signal.throwIfAborted();
|
|
3892
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, {
|
|
3893
|
+
timeoutMs: SIZE_TIMEOUT_MS,
|
|
3894
|
+
operation,
|
|
3895
|
+
signal: operation?.signal
|
|
3896
|
+
});
|
|
3897
|
+
return {
|
|
3898
|
+
...entry,
|
|
3899
|
+
bytes
|
|
3900
|
+
};
|
|
3901
|
+
}),
|
|
2171
3902
|
skipped: classified.skipped
|
|
2172
3903
|
};
|
|
2173
3904
|
}
|
|
2174
3905
|
const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
|
|
3906
|
+
/** How long after its last ios, android or macos run a workspace keeps its build outputs. */
|
|
3907
|
+
const LIGHTEN_AFTER_MS = 9e5;
|
|
3908
|
+
const BUILD_CHECKS = {
|
|
3909
|
+
devServer: false,
|
|
3910
|
+
hosted: false,
|
|
3911
|
+
managedLocks: false
|
|
3912
|
+
};
|
|
3913
|
+
const BUILD_IN_PROGRESS = "a build is in progress";
|
|
3914
|
+
/** Why the build outputs of the workspace at `root` are in use: it is building or running a macOS app. */
|
|
3915
|
+
function workspaceBuildReasons(root, { nativeRun = true } = {}) {
|
|
3916
|
+
const reasons = workspaceInUse(root, {
|
|
3917
|
+
...BUILD_CHECKS,
|
|
3918
|
+
nativeRun
|
|
3919
|
+
});
|
|
3920
|
+
return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
|
|
3921
|
+
}
|
|
3922
|
+
async function workspaceBuildReasonsAsync(root, signal, operation) {
|
|
3923
|
+
const reasons = await workspaceInUseAsync(root, BUILD_CHECKS, signal, operation);
|
|
3924
|
+
return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
|
|
3925
|
+
}
|
|
3926
|
+
async function inTreeBuildDirs(root, signal) {
|
|
3927
|
+
const dirs = [];
|
|
3928
|
+
for (const path of existingDirsOf(root, ARTIFACT_DIRS.build)) if (!await regenerableDirProblem(root, path, signal)) dirs.push(path);
|
|
3929
|
+
return dirs;
|
|
3930
|
+
}
|
|
2175
3931
|
const WORKSPACE_OUTPUT_DIRS = [
|
|
2176
3932
|
"derived-data",
|
|
2177
3933
|
"gradle-build",
|
|
2178
3934
|
"android-cas",
|
|
2179
3935
|
"cache-provider"
|
|
2180
3936
|
];
|
|
3937
|
+
/** Why a workspace whose last ios, android or macos run was at `nativeUsed` keeps its build outputs, or null. */
|
|
3938
|
+
function recentNativeUseReason(nativeUsed, now) {
|
|
3939
|
+
return now - nativeUsed < 9e5 ? `ran ios, android or macos within the last ${LIGHTEN_AFTER_MS / 6e4} minutes` : null;
|
|
3940
|
+
}
|
|
2181
3941
|
const DAY_MS = 864e5;
|
|
3942
|
+
function keptByLock(entry) {
|
|
3943
|
+
return entry.keptCode === "locked" || entry.keptCode === "lock-unknown";
|
|
3944
|
+
}
|
|
2182
3945
|
function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
2183
|
-
return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
|
|
3946
|
+
return entries.map(({ dir, projectRoot, problem, inTreeDirs = [], bytes, lastUsed, nativeUsed, inUse, lock }) => {
|
|
3947
|
+
const recentNative = recentNativeUseReason(nativeUsed, now);
|
|
2184
3948
|
const idleDays = Number.isFinite(lastUsed) ? Math.max(0, Math.floor((now - lastUsed) / DAY_MS)) : null;
|
|
2185
|
-
const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
|
|
3949
|
+
const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : lock ? [lock === "locked" ? "locked" : "lock-unknown", lockReason(lock)] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : recentNative ? ["recently-used", recentNative] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
|
|
2186
3950
|
return {
|
|
2187
3951
|
dir,
|
|
2188
3952
|
projectRoot,
|
|
3953
|
+
inTreeDirs,
|
|
2189
3954
|
bytes,
|
|
2190
3955
|
idleDays,
|
|
2191
3956
|
willClear: keptCode === null,
|
|
@@ -2195,6 +3960,11 @@ function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
|
2195
3960
|
});
|
|
2196
3961
|
}
|
|
2197
3962
|
const SIZE_TIMEOUT_MS = 6e4;
|
|
3963
|
+
const gitRead = (operation, signal) => ({
|
|
3964
|
+
env: operation?.env,
|
|
3965
|
+
signal: signal ?? operation?.signal,
|
|
3966
|
+
declareFor: operation
|
|
3967
|
+
});
|
|
2198
3968
|
function outputPaths(dir) {
|
|
2199
3969
|
const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
|
|
2200
3970
|
const macos = join$1(dir, "macos");
|
|
@@ -2204,48 +3974,71 @@ function outputPaths(dir) {
|
|
|
2204
3974
|
} catch {}
|
|
2205
3975
|
return paths;
|
|
2206
3976
|
}
|
|
2207
|
-
async function outputBytes(
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
3977
|
+
async function outputBytes(entries, timeoutMs, signal, operation) {
|
|
3978
|
+
const sizes = await mapLimited(entries.flatMap((paths, index) => paths.map((path) => ({
|
|
3979
|
+
path,
|
|
3980
|
+
index
|
|
3981
|
+
}))), sizeConcurrency(operation), async ({ path, index }) => ({
|
|
3982
|
+
index,
|
|
3983
|
+
size: await measuredDirectorySizeAsync(path, {
|
|
2211
3984
|
timeoutMs,
|
|
2212
3985
|
signal,
|
|
2213
3986
|
operation
|
|
2214
|
-
})
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
3987
|
+
})
|
|
3988
|
+
}));
|
|
3989
|
+
return entries.map((_, index) => {
|
|
3990
|
+
const own = sizes.filter((size) => size.index === index).map((size) => size.size);
|
|
3991
|
+
return own.includes(null) ? null : own.reduce((total, size) => total + (size ?? 0), 0);
|
|
3992
|
+
});
|
|
2219
3993
|
}
|
|
2220
|
-
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation }) {
|
|
3994
|
+
function collectWorkspaceOutputs({ olderThan, now, exclude = [], only, measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation, lighten = false }) {
|
|
2221
3995
|
const mountedVolumes = listMountedVolumes();
|
|
2222
|
-
const outputEntries = (entries) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot
|
|
3996
|
+
const outputEntries = (entries, inTree = /* @__PURE__ */ new Map()) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => only === void 0 || entry.projectRoot !== null && canonicalPath(entry.projectRoot) === only).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => {
|
|
3997
|
+
const inTreeDirs = inTree.get(entry.dir) ?? [];
|
|
3998
|
+
return Object.assign({}, entry, {
|
|
3999
|
+
inTreeDirs,
|
|
4000
|
+
paths: [...outputPaths(entry.dir), ...inTreeDirs]
|
|
4001
|
+
});
|
|
4002
|
+
}).filter((entry) => entry.paths.length > 0);
|
|
2223
4003
|
const report = (sizes) => ({
|
|
2224
4004
|
root: workspacesRoot(),
|
|
2225
|
-
workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
|
|
4005
|
+
workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
|
|
4006
|
+
lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
|
|
4007
|
+
nativeUsed: lighten && entry.projectRoot !== null ? lastNativeUseFrom(readWorkspaceState$1(entry.projectRoot)) : NaN
|
|
4008
|
+
})), {
|
|
2226
4009
|
olderThan,
|
|
2227
4010
|
now
|
|
2228
4011
|
})
|
|
2229
4012
|
});
|
|
4013
|
+
const inUseOf = (root) => lighten ? workspaceBuildReasons(root) : workspaceInUse(root);
|
|
2230
4014
|
if (!measure && !signal) return report(outputEntries(listWorkspaceDirs()).map(({ paths: _paths, ...entry }) => Object.assign({}, entry, {
|
|
2231
4015
|
bytes: null,
|
|
2232
|
-
inUse: entry.projectRoot === null ? [] :
|
|
4016
|
+
inUse: entry.projectRoot === null ? [] : inUseOf(entry.projectRoot),
|
|
4017
|
+
lock: entry.projectRoot === null ? null : worktreeLock(entry.projectRoot)
|
|
2233
4018
|
})));
|
|
2234
4019
|
return (async () => {
|
|
2235
4020
|
const sizes = [];
|
|
2236
|
-
const
|
|
2237
|
-
|
|
4021
|
+
const listed = await listWorkspaceDirsAsync(signal);
|
|
4022
|
+
const inTree = /* @__PURE__ */ new Map();
|
|
4023
|
+
if (lighten) {
|
|
4024
|
+
for (const { dir, projectRoot } of listed) if (projectRoot !== null && !exclude.includes(dir)) inTree.set(dir, await inTreeBuildDirs(projectRoot, signal));
|
|
4025
|
+
}
|
|
4026
|
+
const entries = outputEntries(listed, inTree);
|
|
4027
|
+
const bytes = measure ? await outputBytes(entries.map((entry) => entry.paths), sizeTimeoutMs, signal, operation) : [];
|
|
4028
|
+
const locks = await mapLimited(entries, sizeConcurrency(operation), async ({ projectRoot }) => projectRoot === null ? null : worktreeLockAsync(projectRoot, gitRead(operation, signal)));
|
|
4029
|
+
for (const [index, { paths: _paths, ...entry }] of entries.entries()) {
|
|
2238
4030
|
signal?.throwIfAborted();
|
|
2239
4031
|
sizes.push({
|
|
2240
4032
|
...entry,
|
|
2241
|
-
bytes:
|
|
2242
|
-
|
|
4033
|
+
bytes: bytes[index] ?? null,
|
|
4034
|
+
lock: locks[index] ?? null,
|
|
4035
|
+
inUse: entry.projectRoot === null ? [] : !signal ? inUseOf(entry.projectRoot) : lighten ? await workspaceBuildReasonsAsync(entry.projectRoot, signal, operation) : await workspaceInUseAsync(entry.projectRoot, {}, signal, operation)
|
|
2243
4036
|
});
|
|
2244
4037
|
}
|
|
2245
4038
|
return report(sizes);
|
|
2246
4039
|
})();
|
|
2247
4040
|
}
|
|
2248
|
-
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal }, { log, record } = GC_OUTPUT) {
|
|
4041
|
+
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal, lighten = false }, { log, record } = GC_OUTPUT) {
|
|
2249
4042
|
let failures = 0;
|
|
2250
4043
|
let cleared = 0;
|
|
2251
4044
|
const clearedRoots = [];
|
|
@@ -2269,13 +4062,20 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2269
4062
|
...entry,
|
|
2270
4063
|
problem: null,
|
|
2271
4064
|
lastUsed: workspaceLastUsed(root),
|
|
2272
|
-
|
|
4065
|
+
nativeUsed: lighten ? lastNativeUseFrom(readWorkspaceState$1(root)) : NaN,
|
|
4066
|
+
inUse: lighten && workspaceBuildInProgress(root) ? [BUILD_IN_PROGRESS] : [],
|
|
4067
|
+
lock: await worktreeLockAsync(root, gitRead(operation, signal))
|
|
2273
4068
|
}], {
|
|
2274
4069
|
olderThan,
|
|
2275
4070
|
now
|
|
2276
4071
|
});
|
|
2277
4072
|
if (again?.keptReason) return again.keptReason;
|
|
2278
|
-
|
|
4073
|
+
const inTree = [];
|
|
4074
|
+
for (const path of lighten ? existingDirsOf(root, ARTIFACT_DIRS.build) : []) inTree.push({
|
|
4075
|
+
path,
|
|
4076
|
+
problem: await regenerableDirProblem(root, path, signal)
|
|
4077
|
+
});
|
|
4078
|
+
for (const path of [...outputPaths(entry.dir), ...inTree.filter((dir) => !dir.problem).map((dir) => dir.path)]) {
|
|
2279
4079
|
signal?.throwIfAborted();
|
|
2280
4080
|
await rm(path, {
|
|
2281
4081
|
recursive: true,
|
|
@@ -2283,11 +4083,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2283
4083
|
});
|
|
2284
4084
|
}
|
|
2285
4085
|
signal?.throwIfAborted();
|
|
4086
|
+
for (const { path, problem } of inTree) if (problem) log(chalk.yellow(`Kept ${path}: ${problem}`));
|
|
2286
4087
|
return null;
|
|
2287
4088
|
}, {
|
|
2288
4089
|
purpose: "gc",
|
|
2289
4090
|
signal,
|
|
2290
|
-
operation
|
|
4091
|
+
operation,
|
|
4092
|
+
...lighten ? BUILD_CHECKS : {}
|
|
2291
4093
|
});
|
|
2292
4094
|
} catch (error) {
|
|
2293
4095
|
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
@@ -2311,10 +4113,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2311
4113
|
}
|
|
2312
4114
|
cleared += entry.bytes ?? 0;
|
|
2313
4115
|
clearedRoots.push(root);
|
|
2314
|
-
log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
4116
|
+
log(chalk.green(lighten ? `Lightened ${root}: freed ${sizeText(entry.bytes)} of build outputs` : `Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
2315
4117
|
record("workspaceOutputs", "done", root, { bytes: entry.bytes });
|
|
2316
4118
|
}
|
|
2317
|
-
if (cleared)
|
|
4119
|
+
if (cleared) {
|
|
4120
|
+
const count = `${clearedRoots.length} workspace${clearedRoots.length === 1 ? "" : "s"}`;
|
|
4121
|
+
log(chalk.dim(`${lighten ? `Lightened ${count}, freeing` : "Cleared"} ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
|
|
4122
|
+
}
|
|
2318
4123
|
return {
|
|
2319
4124
|
failures,
|
|
2320
4125
|
cleared: clearedRoots
|
|
@@ -2322,7 +4127,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2322
4127
|
}
|
|
2323
4128
|
function stillOrphaned({ dir, projectRoot }) {
|
|
2324
4129
|
if (readRecordedRoot(dir, basename$1(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
|
|
2325
|
-
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
|
|
4130
|
+
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix$1(projectRoot, key));
|
|
2326
4131
|
}
|
|
2327
4132
|
async function deleteOrphanedWorkspaces(orphaned, output = GC_OUTPUT, operation) {
|
|
2328
4133
|
let failures = 0;
|
|
@@ -2381,4 +4186,4 @@ function sizeText(bytes) {
|
|
|
2381
4186
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
2382
4187
|
}
|
|
2383
4188
|
//#endregion
|
|
2384
|
-
export {
|
|
4189
|
+
export { tapBuildLog as $, runningBundleInstancesAsync as A, mapLimited as At, workspaceBuildInProgress as B, stopTunnel as C, buildSlotUsage as Ct, withManagedTunnelLock as D, createGcResults as Dt, withManagedRemoteWorktreeRemovalLock as E, GC_OUTPUT as Et, collectOwnedDeviceActivityAsync as F, buildStatusLine as G, NO_BUILD_PROGRESS as H, idleDeviceLines as I, parseActiveBuild as J, bundlingActivity as K, idleShutdownCandidates as L, collectIdleDevices as M, sizeConcurrency as Mt, collectIdleDevicesAsync as N, withManagedTunnelRemovalLock as O, takeGcResults as Ot, collectOwnedDeviceActivity as P, startBuildProgress as Q, parseIdleDuration as R, startTunnelSequence as S, acquireBuildSlot as St, withManagedRemoteWorktreeLock as T, releaseBuildSlot as Tt, activeBuildState as U, ACTIVE_BUILD_KEY as V, buildReport as W, recordFinishedBuild as X, recordBuildPhase as Y, runFailureRecord as Z, emptyWorkspaceDir as _, teardownParkedIosSim as _t, collectOrphanedWorkspaces as a, readStats$1 as at, workspaceInUseAsync as b, measureDu as bt, isInsideWorkspaces as c, recordRunStats as ct, recentNativeUseReason as d, eraseParkedIosSim as dt, thrownRunFailureRecord as et, rootPresence as f, teardownBrowserHeld as ft, ARTIFACT_DIRS as g, teardownParkedAvd as gt, workspaceDirRemoved as h, teardownOwnedIosSim as ht, clearWorkspaceOutputs as i, readRunEstimates as it, runningInstancesInAsync as j, readConcurrency as jt, hostedBundleDirs as k, eachLimited as kt, keptByLock as l, statsProjectKey$1 as lt, workspaceBuildReasons as m, teardownOwnedBrowser as mt, REBUILD_COST as n, STIM_VERSION as nt, collectWorkspaceOutputs as o, recordCapacityRefusal as ot, sizeText as p, teardownOwnedAvd as pt, estimateBuild as q, WORKSPACE_OUTPUT_DIRS as r, createRunRecorder as rt, deleteOrphanedWorkspaces as s, recordCapacityWait as st, LIGHTEN_AFTER_MS as t, errorRemedy as tt, listWorkspaceDirs as u, eraseParkedAvd as ut, withIdleWorkspace as v, probePublicHttp as vt, terminateChild as w, listBuildSlots as wt, resolveSupervisorTarget as x, writeUsageCache as xt, workspaceInUse as y, canonical$1 as yt, shutDownIdleDevices as z };
|