stim 1.26.0 → 1.27.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-BKN2Yuhg.mjs} +2 -2
- package/dist/{act-C-BqJEpu.mjs → act-ROviTF_t.mjs} +297 -122
- package/dist/{activity-Lnb_EoRY.mjs → activity-Cd-ysFes.mjs} +6 -6
- package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-C5nCKWp9.mjs} +5 -5
- package/dist/{android-CSbLRD53.mjs → android--HBgpvpW.mjs} +5 -5
- package/dist/{android-CiNQw3kq.mjs → android-Cd6w7_qw.mjs} +96 -43
- package/dist/{android-B5P81yI-.mjs → android-Deb9X3_Y.mjs} +37 -15
- package/dist/{android-DTAcRwBC.mjs → android-DriNoAVy.mjs} +23 -18
- package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-D5e6ecUg.mjs} +4 -4
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api.d.mts +13 -3
- package/dist/api.mjs +39 -29
- package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-BHQHx3Rm.mjs} +2 -2
- package/dist/{artifact-BxREhAaV.mjs → artifact-0AGJYB9O.mjs} +18 -23
- package/dist/{artifact-CsAP6AGK.mjs → artifact-faemIZv5.mjs} +19 -24
- package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
- package/dist/auto-placement-z-H5pJza.mjs +350 -0
- package/dist/{browser-web-rYPyqhTf.mjs → browser-web-slD8AvLp.mjs} +16 -16
- package/dist/{build-cache-DrXNCy3T.mjs → build-cache-gBJ-WKzA.mjs} +43 -21
- package/dist/{build-DPhApsjV.mjs → build-hS0QyFlQ.mjs} +35 -31
- package/dist/{build-plan-DrMMdayj.mjs → build-plan-CBqYbzYM.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-BWTze1Ls.mjs} +560 -1603
- 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-ftXDLto1.mjs} +24 -23
- package/dist/cli.mjs +1 -1
- package/dist/{client-D9CaRks-.mjs → client-xsdxrhWA.mjs} +48 -62
- package/dist/collector-run.d.mts +10 -9
- package/dist/collector-run.mjs +42 -27
- package/dist/{command-output-BF5AR2dL.mjs → command-output-Dj-WT4BP.mjs} +1 -1
- package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
- package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-BGokZ-Ng.mjs} +108 -60
- package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-C3OtgG94.mjs} +14 -7
- package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
- package/dist/{dev-client-DXR_tPVY.mjs → dev-client-BMca82xo.mjs} +2 -2
- package/dist/{device-BcYeHSFu.mjs → device-BJm0irrq.mjs} +16 -16
- package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-6LP7QsaA.mjs} +101 -16
- package/dist/device-host-worker.mjs +19 -19
- package/dist/{device-ios-BLGfXQeK.mjs → device-ios-534c14fz.mjs} +251 -23
- package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-DwsGfjUN.mjs} +5 -5
- package/dist/{device-pool-Dluix4H3.mjs → device-pool-BnQqvs6o.mjs} +3 -3
- package/dist/{device-BK-zwyd_.mjs → device-tM41-TUq.mjs} +25 -24
- package/dist/{doctor-Cp-BhzTS.mjs → doctor-Dbx1ZpD2.mjs} +1 -1
- package/dist/{doctor-Bhg1ii70.mjs → doctor-DyqE9n1J.mjs} +28 -16
- package/dist/{doctor-OnwP94ne.mjs → doctor-F-3VUUyN.mjs} +76 -32
- 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-CxDbyrrt.mjs} +5 -5
- package/dist/{eas-build-Dsj93iM9.mjs → eas-build-CjNSWmjo.mjs} +4 -4
- package/dist/eas-fallback-9fqb46kW.mjs +87 -0
- package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-DrP4OuuM.mjs} +6 -6
- package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-snej9k4j.mjs} +51 -25
- package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
- package/dist/{gc-C_79zvz4.mjs → gc-BRwAY3qT.mjs} +2 -2
- package/dist/{gc-ldgr-hXU.mjs → gc-CGZ1Yg88.mjs} +160 -45
- package/dist/{guide-D7K0X6KG.mjs → guide-_rfEh_zM.mjs} +580 -183
- package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-BMKzUNnY.mjs} +4 -4
- package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-D7Pk_Oo1.mjs} +12 -8
- package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-B1gqQuva.mjs} +8 -8
- package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-Dg3qrZmV.mjs} +5 -5
- package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B9w4H8hc.mjs} +6 -6
- package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CdgaiuyP.mjs} +17 -16
- package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-Cbvo5KEh.mjs} +7 -8
- package/dist/{ios-ftxdQbJS.mjs → ios-B5HlVMhQ.mjs} +92 -37
- package/dist/{ios-B7-lToDu.mjs → ios-Bk4mWbtS.mjs} +8 -8
- package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
- package/dist/{ios-WxpDJ5T1.mjs → ios-E26qSpkb.mjs} +27 -11
- package/dist/{ios-device-CsyryvpT.mjs → ios-device-C_QkIf1Z.mjs} +3 -3
- package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
- package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-D8Jcy_br.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-BZ03NFgF.mjs} +1 -1
- package/dist/{js-swap-BMhXYVDS.mjs → js-swap-Dg2a945i.mjs} +8 -7
- package/dist/{launch-DOp8RjDz.mjs → launch-CRyBeNsA.mjs} +42 -38
- package/dist/lightened-BCsdAGj_.mjs +171 -0
- package/dist/{logs-dQDki6Tt.mjs → logs-CC_-ZzEE.mjs} +7 -7
- package/dist/{logs-s1dRIG1E.mjs → logs-aJQb-RDg.mjs} +46 -21
- package/dist/{macos-B0VEV3Gf.mjs → macos-BGmTpnv9.mjs} +1 -1
- package/dist/{macos-DBBPNnIA.mjs → macos-DlCBhMw5.mjs} +73 -38
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +7 -7
- package/dist/{metro-CjC10t6E.mjs → metro-CvyD-xwe.mjs} +4 -4
- package/dist/{metro-gateway-B6_PgD0L.mjs → metro-gateway-BnHdqMO_.mjs} +3 -3
- package/dist/{named-ports-BG0j1X4g.mjs → named-ports-BCyMWA6b.mjs} +3 -3
- package/dist/{native-android-CL2NGiik.mjs → native-android-DlpxCyOP.mjs} +17 -18
- package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-C13qT7R_.mjs} +5 -5
- package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DqaIBO5w.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-B1W8SrxB.mjs} +3 -3
- package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-QPP0Rm68.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-Bx-0cku-.mjs} +3 -3
- package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
- package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-C9yAaKEv.mjs} +18 -4
- package/dist/{placement-log-DIFPQAXp.mjs → placement-log-BXUbiZM6.mjs} +3 -3
- package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-UxnQjv5Z.mjs} +36 -10
- package/dist/{ports-C2_w1Z0Z.mjs → ports-uUrghqWN.mjs} +3 -3
- package/dist/{prebuild-gmouvmJZ.mjs → prebuild-BDpToDgR.mjs} +16 -17
- package/dist/{project-BUu6Is4k.mjs → project-DYj-HVx6.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-DDBccgJw.mjs} +32 -26
- package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-DaK2IFyP.mjs} +9 -9
- package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-B-Mw6DcR.mjs} +6 -6
- package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-DjYwUF9U.mjs} +23 -17
- package/dist/{recordings-BldzVKq0.mjs → recordings-C20iUZfp.mjs} +1 -1
- package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
- package/dist/{reload-C9OduiNc.mjs → reload-DJBeF_rC.mjs} +1 -1
- package/dist/{reload-CCR7KC_l.mjs → reload-fUdkH2_p.mjs} +16 -16
- package/dist/{remote-C8S7FlPF.mjs → remote-y6n99WMW.mjs} +22 -9
- package/dist/{result-7F5hEwQJ.mjs → result-CTqBw2Te.mjs} +32 -21
- package/dist/{run-BqB0v3pt.mjs → run-DaCeNKRm.mjs} +3 -3
- package/dist/{selection-BNPBZ6T5.mjs → selection-Bs8phwhN.mjs} +37 -29
- package/dist/{server-bare-A75rDIdD.mjs → server-bare-BOh-iZEj.mjs} +2 -2
- package/dist/{server-command-CssKFOD7.mjs → server-command-8CuA51Cv.mjs} +6 -6
- package/dist/{server-expo-eQfOdydb.mjs → server-expo-ABQs2XFA.mjs} +4 -4
- package/dist/server-expo-Ba_Y21vB.mjs +2 -0
- package/dist/{settings-CosW5X-S.mjs → settings-BDVI8Ivc.mjs} +331 -62
- package/dist/{settings-OaGQF-ri.mjs → settings-CMHhQNwI.mjs} +24 -3
- package/dist/settings-payload-cm60Zhib.mjs +257 -0
- package/dist/settings-write-BdNRLlBn.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-0l7RqSpr.mjs} +6 -6
- package/dist/{stage-jKQ7CP_B.mjs → stage-F4cAfJGi.mjs} +2 -2
- package/dist/{start-C_wefNKB.mjs → start-DcqnF9p8.mjs} +1 -1
- package/dist/{start-_BdJ32cZ.mjs → start-DrPwER_2.mjs} +27 -22
- package/dist/{state-C1i2nMOP.mjs → state-BX1y5syV.mjs} +4 -4
- package/dist/{state-C4OdpOTF.mjs → state-CZxLyetD.mjs} +2 -2
- package/dist/{stats-tEpEEH-u.mjs → stats-GxuCbles.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-BfnK9pUd.mjs} +25 -25
- package/dist/{status-B9M7x4CZ.mjs → status-Bj7ZYvvc.mjs} +1 -1
- package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BfabQn03.mjs} +5 -5
- package/dist/status-worker.mjs +1 -1
- package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
- package/dist/{stop-ySNQHXMG.mjs → stop-CH79S3IZ.mjs} +5 -5
- package/dist/{stop-DRqQVqDJ.mjs → stop-D-EAr25B.mjs} +21 -20
- package/dist/{stop-CmdNhGlC.mjs → stop-x1Yu3He4.mjs} +2 -2
- package/dist/supervisor-run.mjs +17 -17
- package/dist/{support-CVOCp0Ft.mjs → support-Be0UeVZU.mjs} +10 -10
- package/dist/{support-BGSCBoib.mjs → support-CNg_YsOk.mjs} +6 -6
- package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-uPqYvGBv.mjs} +4 -4
- package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-Ch4YjY_A.mjs} +11 -12
- package/dist/{toolchain-J4J8MrAr.mjs → toolchain-CbCG6C5Z.mjs} +321 -15
- package/dist/{toolchain-DoavrvIA.mjs → toolchain-JYAOquPg.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-C0RsbngN.mjs} +731 -727
- package/dist/{web-Bihbf7-u.mjs → web-Cq_AQ4DK.mjs} +9 -9
- package/dist/web-Je6abcpz.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-BjqdjnJR.mjs} +3 -3
- package/dist/{workspaces-g2lhdBXF.mjs → workspaces-DFbj07jp.mjs} +1795 -70
- package/dist/{worktree-BKuklFh_.mjs → worktree-COAJ3-OD.mjs} +934 -514
- package/dist/{worktree-Bm4cb-nU.mjs → worktree-JGUd_53L.mjs} +1 -1
- 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
|
@@ -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-BjqdjnJR.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 {
|
|
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-Dj-WT4BP.mjs";
|
|
6
|
+
import { c as gitQuiet, l as gitQuietAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
|
|
7
|
+
import { Lt as repoRootAsync, pt as resolveTailscaleBinary } from "./settings-BDVI8Ivc.mjs";
|
|
8
|
+
import { a as macosRuntimeClaim, n as macosDir } from "./state-CZxLyetD.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-DriNoAVy.mjs";
|
|
10
|
+
import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-CvyD-xwe.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-BX1y5syV.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-Cd-ysFes.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-E26qSpkb.mjs";
|
|
15
|
+
import { r as releaseBrowserPort } from "./named-ports-BCyMWA6b.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, hostedNativeRecords, isJsonObject, lastNativeUseFrom, 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 = [];
|
|
@@ -349,6 +382,10 @@ function deviceClaims(device) {
|
|
|
349
382
|
workspace: record.workspace && canonical(record.workspace)
|
|
350
383
|
}));
|
|
351
384
|
}
|
|
385
|
+
var AgentDeviceRefusal = class extends Error {
|
|
386
|
+
reason;
|
|
387
|
+
stateDir;
|
|
388
|
+
};
|
|
352
389
|
function explainFailure(error) {
|
|
353
390
|
const stdout = error?.stdout;
|
|
354
391
|
let failure;
|
|
@@ -358,13 +395,17 @@ function explainFailure(error) {
|
|
|
358
395
|
failure = void 0;
|
|
359
396
|
}
|
|
360
397
|
if (typeof failure?.message !== "string" || !failure.message) return error;
|
|
361
|
-
const reason = typeof failure.details?.reason === "string" ?
|
|
362
|
-
|
|
398
|
+
const reason = typeof failure.details?.reason === "string" ? failure.details.reason : void 0;
|
|
399
|
+
const stateDir = typeof failure.details?.stateDir === "string" ? failure.details.stateDir : void 0;
|
|
400
|
+
const refusal = new AgentDeviceRefusal(`agent-device refused: ${failure.message}${reason ? ` (${reason})` : ""}`);
|
|
401
|
+
refusal.reason = reason;
|
|
402
|
+
refusal.stateDir = stateDir;
|
|
403
|
+
return refusal;
|
|
363
404
|
}
|
|
364
405
|
function report(message) {
|
|
365
406
|
ambientWrite("stderr", `${phaseLine("device", message)}\n`);
|
|
366
407
|
}
|
|
367
|
-
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation) {
|
|
408
|
+
async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation, reportedBusy = /* @__PURE__ */ new Set()) {
|
|
368
409
|
const exec = getExecutor();
|
|
369
410
|
try {
|
|
370
411
|
operation?.signal.throwIfAborted();
|
|
@@ -378,6 +419,7 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
|
|
|
378
419
|
};
|
|
379
420
|
const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
|
|
380
421
|
const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
|
|
422
|
+
const busy = /* @__PURE__ */ new Set();
|
|
381
423
|
for (const dir of stateDirs) {
|
|
382
424
|
const deadline = Date.now() + 15e3;
|
|
383
425
|
const run = async (args) => {
|
|
@@ -431,9 +473,16 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
|
|
|
431
473
|
} catch (error) {
|
|
432
474
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
433
475
|
operation?.signal.throwIfAborted();
|
|
434
|
-
|
|
476
|
+
if (error instanceof AgentDeviceRefusal && error.reason === "daemon-busy") busy.add(error.stateDir ?? dir ?? "");
|
|
477
|
+
else report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
|
|
435
478
|
}
|
|
436
479
|
}
|
|
480
|
+
const unreported = [...busy].filter((dir) => !reportedBusy.has(dir));
|
|
481
|
+
if (unreported.length > 0) {
|
|
482
|
+
for (const dir of unreported) reportedBusy.add(dir);
|
|
483
|
+
const stop = unreported.map((dir) => `agent-device daemon stop${dir ? ` --state-dir ${quotedPath$1(dir)}` : ""}`);
|
|
484
|
+
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 ")}`);
|
|
485
|
+
}
|
|
437
486
|
} catch (error) {
|
|
438
487
|
if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
|
|
439
488
|
operation?.signal.throwIfAborted();
|
|
@@ -652,6 +701,7 @@ async function parkOwnedIosSim(settled, udid, park, operation) {
|
|
|
652
701
|
}
|
|
653
702
|
async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {}, operation } = {}) {
|
|
654
703
|
let parkFallback;
|
|
704
|
+
const reportedBusy = /* @__PURE__ */ new Set();
|
|
655
705
|
try {
|
|
656
706
|
const resolved = await resolveOwnedIosSimNow(udid, operation);
|
|
657
707
|
if (resolved.notOwned) return {
|
|
@@ -663,7 +713,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
|
|
|
663
713
|
await closeOwnedDeviceSessions({
|
|
664
714
|
platform: "ios",
|
|
665
715
|
id: udid
|
|
666
|
-
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation);
|
|
716
|
+
}, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation, reportedBusy);
|
|
667
717
|
const current = await resolveOwnedIosSimNow(udid, operation);
|
|
668
718
|
if (current.missing) return { status: "missing" };
|
|
669
719
|
if (current.notOwned) return {
|
|
@@ -721,7 +771,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
|
|
|
721
771
|
if (root && settled.sim) await closeOwnedDeviceSessions({
|
|
722
772
|
platform: "ios",
|
|
723
773
|
id: udid
|
|
724
|
-
}, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation);
|
|
774
|
+
}, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation, reportedBusy);
|
|
725
775
|
return {
|
|
726
776
|
status: "torn-down",
|
|
727
777
|
label: label ?? sim.name ?? udid
|
|
@@ -849,6 +899,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
849
899
|
signal: operation?.signal
|
|
850
900
|
}) }) {
|
|
851
901
|
let parkFallback;
|
|
902
|
+
const reportedBusy = /* @__PURE__ */ new Set();
|
|
852
903
|
const resolveAvd = async (name) => {
|
|
853
904
|
operation?.signal.throwIfAborted();
|
|
854
905
|
const result = await resolve(name);
|
|
@@ -890,7 +941,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
890
941
|
platform: "android",
|
|
891
942
|
id: serial,
|
|
892
943
|
avdName
|
|
893
|
-
}, stillOwned, workspace, void 0, operation);
|
|
944
|
+
}, stillOwned, workspace, void 0, operation, reportedBusy);
|
|
894
945
|
if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
|
|
895
946
|
await waitForShutdown(avdName, async (timeoutMs) => {
|
|
896
947
|
if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
|
|
@@ -911,7 +962,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
|
|
|
911
962
|
platform: "android",
|
|
912
963
|
id: serial,
|
|
913
964
|
avdName
|
|
914
|
-
}, stillStopped, workspace, root, operation);
|
|
965
|
+
}, stillStopped, workspace, root, operation, reportedBusy);
|
|
915
966
|
}
|
|
916
967
|
} else {
|
|
917
968
|
await closeOwnedDeviceSessions({
|
|
@@ -1149,6 +1200,1561 @@ async function teardownBrowserHeld(root, { deleteProfile = false, operation } =
|
|
|
1149
1200
|
}
|
|
1150
1201
|
}
|
|
1151
1202
|
//#endregion
|
|
1203
|
+
//#region src/engine/stats.ts
|
|
1204
|
+
const PLACEMENT_MAX_AGE_MS = 6048e5;
|
|
1205
|
+
function emptyStats() {
|
|
1206
|
+
return {
|
|
1207
|
+
version: STATS_VERSION,
|
|
1208
|
+
machine: {},
|
|
1209
|
+
projects: {}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function updateStats(record, run, now) {
|
|
1213
|
+
const at = new Date(now).toISOString();
|
|
1214
|
+
const durationMs = wholeMs$1(run.durationMs);
|
|
1215
|
+
const phases = {
|
|
1216
|
+
coldBuildMs: wholeMs$1(run.coldBuildMs),
|
|
1217
|
+
podsMs: wholeMs$1(run.podsMs)
|
|
1218
|
+
};
|
|
1219
|
+
const projects = { ...record.projects };
|
|
1220
|
+
const scope = { ...projects[run.projectKey] };
|
|
1221
|
+
const machine = { ...record.machine };
|
|
1222
|
+
const before = scope[run.platform] ?? null;
|
|
1223
|
+
const credit = creditMs(before, run, durationMs);
|
|
1224
|
+
scope[run.platform] = applyRun(before, run, {
|
|
1225
|
+
at,
|
|
1226
|
+
durationMs,
|
|
1227
|
+
credit,
|
|
1228
|
+
phases
|
|
1229
|
+
});
|
|
1230
|
+
projects[run.projectKey] = scope;
|
|
1231
|
+
machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
|
|
1232
|
+
at,
|
|
1233
|
+
durationMs,
|
|
1234
|
+
credit,
|
|
1235
|
+
phases
|
|
1236
|
+
});
|
|
1237
|
+
const history = { ...record.history };
|
|
1238
|
+
if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
|
|
1239
|
+
const outcome = isHit(run.cacheHit) ? "hit" : "cold";
|
|
1240
|
+
const project = { ...history[run.projectKey] };
|
|
1241
|
+
const lists = { ...project[run.platform] };
|
|
1242
|
+
const sample = {
|
|
1243
|
+
at,
|
|
1244
|
+
durationMs,
|
|
1245
|
+
phases: wholePhases(run.phases),
|
|
1246
|
+
...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
|
|
1247
|
+
};
|
|
1248
|
+
lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
|
|
1249
|
+
project[run.platform] = lists;
|
|
1250
|
+
history[run.projectKey] = project;
|
|
1251
|
+
}
|
|
1252
|
+
let placements = record.placements;
|
|
1253
|
+
let buildMachines = record.buildMachines;
|
|
1254
|
+
if (run.placement) {
|
|
1255
|
+
const placement = buildPlacement(run, before, at);
|
|
1256
|
+
placements = trimPlacements([...placements ?? [], placement], now);
|
|
1257
|
+
if (placement.machine && placement.decision !== "here") buildMachines = {
|
|
1258
|
+
...buildMachines,
|
|
1259
|
+
[placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
|
|
1263
|
+
const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
|
|
1264
|
+
return {
|
|
1265
|
+
version: STATS_VERSION,
|
|
1266
|
+
machine,
|
|
1267
|
+
projects,
|
|
1268
|
+
...Object.keys(history).length ? { history } : {},
|
|
1269
|
+
...placements?.length ? { placements } : {},
|
|
1270
|
+
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
1271
|
+
...capacityWaits.length ? { capacityWaits } : {},
|
|
1272
|
+
...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function buildPlacement(run, before, at) {
|
|
1276
|
+
const placement = run.placement;
|
|
1277
|
+
const buildMs = wholeMs$1(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
|
|
1278
|
+
const localEstimateMs = wholeMs$1(before?.lastColdBuildMs);
|
|
1279
|
+
const slotWaitMs = wholeMs$1(placement.slotWaitMs);
|
|
1280
|
+
const deviceSlotWaitMs = wholeMs$1(placement.deviceSlotWaitMs);
|
|
1281
|
+
return {
|
|
1282
|
+
at,
|
|
1283
|
+
project: run.projectKey,
|
|
1284
|
+
platform: run.platform,
|
|
1285
|
+
decision: placement.decision,
|
|
1286
|
+
reason: placement.reason,
|
|
1287
|
+
...placement.machine ? { machine: placement.machine } : {},
|
|
1288
|
+
...buildMs > 0 ? { buildMs } : {},
|
|
1289
|
+
...slotWaitMs > 0 ? { slotWaitMs } : {},
|
|
1290
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
|
|
1291
|
+
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
1292
|
+
...run.failed ? { failed: true } : {}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function applyPlacement(totals, placement) {
|
|
1296
|
+
const next = totals ? { ...totals } : {
|
|
1297
|
+
offloaded: 0,
|
|
1298
|
+
offloadedMs: 0,
|
|
1299
|
+
savedMs: 0,
|
|
1300
|
+
fallbacks: 0
|
|
1301
|
+
};
|
|
1302
|
+
if (placement.decision === "fell-back") {
|
|
1303
|
+
next.fallbacks += 1;
|
|
1304
|
+
next.lastFallbackAt = placement.at;
|
|
1305
|
+
return next;
|
|
1306
|
+
}
|
|
1307
|
+
next.offloaded += 1;
|
|
1308
|
+
next.offloadedMs += placement.buildMs ?? 0;
|
|
1309
|
+
if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
|
|
1310
|
+
next.lastOffloadAt = placement.at;
|
|
1311
|
+
return next;
|
|
1312
|
+
}
|
|
1313
|
+
function trimPlacements(placements, now) {
|
|
1314
|
+
return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
|
|
1315
|
+
}
|
|
1316
|
+
function recordRunStats(run, now) {
|
|
1317
|
+
return withConfigLock$1(() => {
|
|
1318
|
+
const path = statsFile();
|
|
1319
|
+
const loaded = loadForUpdate(path, now);
|
|
1320
|
+
if (loaded.newerVersion !== null) return {
|
|
1321
|
+
recorded: false,
|
|
1322
|
+
note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
|
|
1323
|
+
};
|
|
1324
|
+
writeStats(path, updateStats(loaded.record, run, now));
|
|
1325
|
+
return {
|
|
1326
|
+
recorded: true,
|
|
1327
|
+
note: loaded.note
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function recordCapacityRefusal(event, now) {
|
|
1332
|
+
try {
|
|
1333
|
+
withConfigLock$1(() => {
|
|
1334
|
+
const path = statsFile();
|
|
1335
|
+
const loaded = loadForUpdate(path, now, false);
|
|
1336
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
1337
|
+
const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
|
|
1338
|
+
...event,
|
|
1339
|
+
at: new Date(now).toISOString(),
|
|
1340
|
+
kind: "device"
|
|
1341
|
+
}], now);
|
|
1342
|
+
writeStats(path, {
|
|
1343
|
+
...loaded.record,
|
|
1344
|
+
capacityRefusals
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
} catch {}
|
|
1348
|
+
}
|
|
1349
|
+
function recordCapacityWait(event, now) {
|
|
1350
|
+
try {
|
|
1351
|
+
withConfigLock$1(() => {
|
|
1352
|
+
const path = statsFile();
|
|
1353
|
+
const loaded = loadForUpdate(path, now, false);
|
|
1354
|
+
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
1355
|
+
const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
|
|
1356
|
+
platform: event.platform,
|
|
1357
|
+
max: event.max,
|
|
1358
|
+
workspace: event.workspace,
|
|
1359
|
+
ms: wholeMs$1(event.ms),
|
|
1360
|
+
...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
|
|
1361
|
+
at: new Date(now).toISOString(),
|
|
1362
|
+
kind: "device-wait"
|
|
1363
|
+
}], now);
|
|
1364
|
+
writeStats(path, {
|
|
1365
|
+
...loaded.record,
|
|
1366
|
+
capacityWaits
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
} catch {}
|
|
1370
|
+
}
|
|
1371
|
+
function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
|
|
1372
|
+
let projectKey = null;
|
|
1373
|
+
let cacheKey = null;
|
|
1374
|
+
let coldBuildMs = 0;
|
|
1375
|
+
let podsMs = 0;
|
|
1376
|
+
let placement = null;
|
|
1377
|
+
let deviceSlotWaitMs = 0;
|
|
1378
|
+
let recorded = false;
|
|
1379
|
+
return {
|
|
1380
|
+
setProject(key) {
|
|
1381
|
+
projectKey = key;
|
|
1382
|
+
},
|
|
1383
|
+
setCacheKey(key) {
|
|
1384
|
+
cacheKey = key;
|
|
1385
|
+
},
|
|
1386
|
+
setBuildMs(ms) {
|
|
1387
|
+
coldBuildMs = wholeMs$1(ms);
|
|
1388
|
+
},
|
|
1389
|
+
setPodsMs(ms) {
|
|
1390
|
+
podsMs = wholeMs$1(ms);
|
|
1391
|
+
},
|
|
1392
|
+
addDeviceSlotWaitMs(ms) {
|
|
1393
|
+
deviceSlotWaitMs += wholeMs$1(ms);
|
|
1394
|
+
},
|
|
1395
|
+
deviceSlotWaitMs() {
|
|
1396
|
+
return deviceSlotWaitMs || void 0;
|
|
1397
|
+
},
|
|
1398
|
+
setPlacement(next) {
|
|
1399
|
+
placement = next;
|
|
1400
|
+
},
|
|
1401
|
+
record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
|
|
1402
|
+
if (!projectKey || !cacheKey || recorded) return;
|
|
1403
|
+
recorded = true;
|
|
1404
|
+
const ran = failed ? {} : phases?.() ?? {};
|
|
1405
|
+
try {
|
|
1406
|
+
const outcome = write({
|
|
1407
|
+
platform,
|
|
1408
|
+
projectKey,
|
|
1409
|
+
failed,
|
|
1410
|
+
cacheHit,
|
|
1411
|
+
waitedForBuild: Boolean(waited),
|
|
1412
|
+
durationMs,
|
|
1413
|
+
...offloadedTo ? { offloadedTo } : {},
|
|
1414
|
+
...coldBuildMs > 0 ? { coldBuildMs } : {},
|
|
1415
|
+
...podsMs > 0 ? { podsMs } : {},
|
|
1416
|
+
...Object.keys(ran).length ? { phases: ran } : {},
|
|
1417
|
+
...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
|
|
1418
|
+
...placement ? { placement: {
|
|
1419
|
+
...placement,
|
|
1420
|
+
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
|
|
1421
|
+
} } : {}
|
|
1422
|
+
}, now());
|
|
1423
|
+
if (outcome?.note) note(outcome.note);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
note(`Run statistics could not be recorded: ${error?.message || error}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function loadForUpdate(path, now, recoverCorrupt = true) {
|
|
1431
|
+
let text;
|
|
1432
|
+
try {
|
|
1433
|
+
text = readFileSync(path, "utf-8");
|
|
1434
|
+
} catch {
|
|
1435
|
+
return {
|
|
1436
|
+
record: emptyStats(),
|
|
1437
|
+
note: null,
|
|
1438
|
+
newerVersion: null
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const parsed = decodeStats(text);
|
|
1442
|
+
if (!parsed.record && parsed.newerVersion === null) {
|
|
1443
|
+
if (!recoverCorrupt) return {
|
|
1444
|
+
record: null,
|
|
1445
|
+
note: null,
|
|
1446
|
+
newerVersion: null
|
|
1447
|
+
};
|
|
1448
|
+
const aside = `${path}.corrupt-${now}`;
|
|
1449
|
+
renameSync(path, aside);
|
|
1450
|
+
return {
|
|
1451
|
+
record: emptyStats(),
|
|
1452
|
+
note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
|
|
1453
|
+
newerVersion: null
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
if (parsed.newerVersion !== null) return {
|
|
1457
|
+
record: emptyStats(),
|
|
1458
|
+
note: null,
|
|
1459
|
+
newerVersion: parsed.newerVersion
|
|
1460
|
+
};
|
|
1461
|
+
return {
|
|
1462
|
+
record: parsed.record,
|
|
1463
|
+
note: null,
|
|
1464
|
+
newerVersion: null
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function writeStats(path, record) {
|
|
1468
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1469
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`);
|
|
1470
|
+
try {
|
|
1471
|
+
renameSync(tmp, path);
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
rmSync(tmp, { force: true });
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function applyRun(bucket, run, { at, durationMs, credit, phases }) {
|
|
1478
|
+
const next = bucket ? { ...bucket } : {
|
|
1479
|
+
runs: 0,
|
|
1480
|
+
failed: 0,
|
|
1481
|
+
hits: 0,
|
|
1482
|
+
misses: 0,
|
|
1483
|
+
coldRuns: 0,
|
|
1484
|
+
coldRunMs: 0,
|
|
1485
|
+
hitRuns: 0,
|
|
1486
|
+
hitRunMs: 0,
|
|
1487
|
+
timeSavedMs: 0,
|
|
1488
|
+
firstRunAt: at,
|
|
1489
|
+
lastRunAt: at
|
|
1490
|
+
};
|
|
1491
|
+
next.runs += 1;
|
|
1492
|
+
next.lastRunAt = at;
|
|
1493
|
+
if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
|
|
1494
|
+
if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
|
|
1495
|
+
if (run.failed) {
|
|
1496
|
+
next.failed += 1;
|
|
1497
|
+
return next;
|
|
1498
|
+
}
|
|
1499
|
+
if (isHit(run.cacheHit)) {
|
|
1500
|
+
next.hits += 1;
|
|
1501
|
+
if (!run.waitedForBuild) {
|
|
1502
|
+
next.hitRuns += 1;
|
|
1503
|
+
next.hitRunMs += durationMs;
|
|
1504
|
+
next.timeSavedMs += credit;
|
|
1505
|
+
}
|
|
1506
|
+
return next;
|
|
1507
|
+
}
|
|
1508
|
+
next.misses += 1;
|
|
1509
|
+
if (run.offloadedTo) {
|
|
1510
|
+
next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
|
|
1511
|
+
next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
|
|
1512
|
+
next.lastOffloadHost = run.offloadedTo;
|
|
1513
|
+
return next;
|
|
1514
|
+
}
|
|
1515
|
+
next.coldRuns += 1;
|
|
1516
|
+
next.coldRunMs += durationMs;
|
|
1517
|
+
return next;
|
|
1518
|
+
}
|
|
1519
|
+
function creditMs(bucket, run, durationMs) {
|
|
1520
|
+
if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
|
|
1521
|
+
if (!bucket || bucket.coldRuns <= 0) return 0;
|
|
1522
|
+
return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
|
|
1523
|
+
}
|
|
1524
|
+
function isHit(cacheHit) {
|
|
1525
|
+
return cacheHit === "local" || cacheHit === "remote";
|
|
1526
|
+
}
|
|
1527
|
+
function wholeMs$1(value) {
|
|
1528
|
+
const ms = Number(value);
|
|
1529
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region src/engine/build-detail.ts
|
|
1533
|
+
const LINE_MAX = 160;
|
|
1534
|
+
const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
|
|
1535
|
+
const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
|
|
1536
|
+
const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
|
|
1537
|
+
const GRADLE_TASK = /^> Task (:\S+)/;
|
|
1538
|
+
/**
|
|
1539
|
+
* xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
|
|
1540
|
+
* that touches nothing still signs framework products last. Targets that end any other way are not counted.
|
|
1541
|
+
*/
|
|
1542
|
+
const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
|
|
1543
|
+
const GRADLE_CONFIGURE = /^> Configure project /;
|
|
1544
|
+
/**
|
|
1545
|
+
* SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
|
|
1546
|
+
* output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
|
|
1547
|
+
* `<product>-product` is the link of a product.
|
|
1548
|
+
*/
|
|
1549
|
+
const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
|
|
1550
|
+
const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
|
|
1551
|
+
const XCODE_STEPS = [
|
|
1552
|
+
[/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
|
|
1553
|
+
[/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
|
|
1554
|
+
[/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
|
|
1555
|
+
[/^PhaseScriptExecution$/, "script"],
|
|
1556
|
+
[/^CodeSign$/, "sign"]
|
|
1557
|
+
];
|
|
1558
|
+
const GRADLE_STEPS = [
|
|
1559
|
+
[/[Dd]ex/, "dex"],
|
|
1560
|
+
[/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
|
|
1561
|
+
[/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
|
|
1562
|
+
[/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
|
|
1563
|
+
[/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
|
|
1564
|
+
];
|
|
1565
|
+
function stepOf(name, table) {
|
|
1566
|
+
return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
1567
|
+
}
|
|
1568
|
+
function clip(text) {
|
|
1569
|
+
return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
|
|
1570
|
+
}
|
|
1571
|
+
function unescape(token) {
|
|
1572
|
+
return token.replace(/\\(.)/g, "$1");
|
|
1573
|
+
}
|
|
1574
|
+
function baseName(path) {
|
|
1575
|
+
return path.split("/").findLast(Boolean) ?? path;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* An xcodebuild action line in short form: the action, the file it works on and its target, as in
|
|
1579
|
+
* `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
|
|
1580
|
+
* as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
|
|
1581
|
+
* shows its name instead.
|
|
1582
|
+
*/
|
|
1583
|
+
function shortXcodeAction(action, args, target) {
|
|
1584
|
+
const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
|
|
1585
|
+
const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
|
|
1586
|
+
return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
|
|
1587
|
+
}
|
|
1588
|
+
function swiftpmStep(text) {
|
|
1589
|
+
return /^Linking\b|-product$/.test(text) ? "link" : "compile";
|
|
1590
|
+
}
|
|
1591
|
+
/** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
|
|
1592
|
+
function parseBuildToolLine(msg) {
|
|
1593
|
+
const counted = SWIFTPM_PROGRESS.exec(msg);
|
|
1594
|
+
if (counted) {
|
|
1595
|
+
const text = counted[3]?.trim() ?? "";
|
|
1596
|
+
return {
|
|
1597
|
+
kind: "count",
|
|
1598
|
+
done: Number(counted[1]),
|
|
1599
|
+
total: Number(counted[2]),
|
|
1600
|
+
step: swiftpmStep(text),
|
|
1601
|
+
line: text ? clip(text) : null
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
1605
|
+
const graph = TARGET_GRAPH.exec(msg);
|
|
1606
|
+
if (graph) return {
|
|
1607
|
+
kind: "total",
|
|
1608
|
+
unit: "targets",
|
|
1609
|
+
total: Number(graph[1])
|
|
1610
|
+
};
|
|
1611
|
+
if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
1612
|
+
const action = XCODE_ACTION.exec(msg);
|
|
1613
|
+
if (action) {
|
|
1614
|
+
const [, name, args, target, project] = action;
|
|
1615
|
+
const step = stepOf(name, XCODE_STEPS);
|
|
1616
|
+
return {
|
|
1617
|
+
kind: "unit",
|
|
1618
|
+
unit: "targets",
|
|
1619
|
+
id: `${project}/${target}`,
|
|
1620
|
+
step,
|
|
1621
|
+
line: step ? shortXcodeAction(name, args, target) : null,
|
|
1622
|
+
finished: XCODE_TARGET_DONE.test(name)
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const task = GRADLE_TASK.exec(msg);
|
|
1626
|
+
if (task) {
|
|
1627
|
+
const path = task[1];
|
|
1628
|
+
return {
|
|
1629
|
+
kind: "unit",
|
|
1630
|
+
unit: "tasks",
|
|
1631
|
+
id: path,
|
|
1632
|
+
step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
|
|
1633
|
+
line: clip(`> Task ${path}`),
|
|
1634
|
+
finished: false
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
|
|
1641
|
+
* total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
|
|
1642
|
+
* its own `[done / total]` counts, which the parser passes through.
|
|
1643
|
+
*/
|
|
1644
|
+
function createBuildDetailParser() {
|
|
1645
|
+
let seen = false;
|
|
1646
|
+
let step = null;
|
|
1647
|
+
let unit = null;
|
|
1648
|
+
let total = null;
|
|
1649
|
+
let line = null;
|
|
1650
|
+
let tasks = 0;
|
|
1651
|
+
let counted = null;
|
|
1652
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1653
|
+
return {
|
|
1654
|
+
push(msg) {
|
|
1655
|
+
const parsed = parseBuildToolLine(msg);
|
|
1656
|
+
if (!parsed) return false;
|
|
1657
|
+
seen = true;
|
|
1658
|
+
if (parsed.kind === "total") {
|
|
1659
|
+
unit = "targets";
|
|
1660
|
+
total = parsed.total;
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
if (parsed.kind === "configure") {
|
|
1664
|
+
step = "configure";
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
if (parsed.kind === "count") {
|
|
1668
|
+
unit = "steps";
|
|
1669
|
+
counted = {
|
|
1670
|
+
done: parsed.done,
|
|
1671
|
+
total: parsed.total
|
|
1672
|
+
};
|
|
1673
|
+
step = parsed.step;
|
|
1674
|
+
if (parsed.line) line = parsed.line;
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
unit = parsed.unit;
|
|
1678
|
+
if (parsed.unit === "tasks") tasks += 1;
|
|
1679
|
+
else if (parsed.finished) targets.add(parsed.id);
|
|
1680
|
+
if (parsed.step) step = parsed.step;
|
|
1681
|
+
if (parsed.line) line = parsed.line;
|
|
1682
|
+
return true;
|
|
1683
|
+
},
|
|
1684
|
+
detail(updatedAt) {
|
|
1685
|
+
if (!seen) return null;
|
|
1686
|
+
if (unit === "steps" && counted) return {
|
|
1687
|
+
step,
|
|
1688
|
+
unit,
|
|
1689
|
+
done: Math.min(counted.done, counted.total),
|
|
1690
|
+
total: counted.total,
|
|
1691
|
+
line,
|
|
1692
|
+
updatedAt
|
|
1693
|
+
};
|
|
1694
|
+
const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
|
|
1695
|
+
return {
|
|
1696
|
+
step,
|
|
1697
|
+
unit,
|
|
1698
|
+
done: done !== null && total !== null ? Math.min(done, total) : done,
|
|
1699
|
+
total: unit === "tasks" ? null : total,
|
|
1700
|
+
line,
|
|
1701
|
+
updatedAt
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/stim-version.ts
|
|
1708
|
+
const STIM_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1709
|
+
//#endregion
|
|
1710
|
+
//#region src/engine/build-ledger.ts
|
|
1711
|
+
const LOCK_WAIT_MS = 2e3;
|
|
1712
|
+
const GIT_TIMEOUT_MS = 2e3;
|
|
1713
|
+
function text(value) {
|
|
1714
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
1715
|
+
}
|
|
1716
|
+
function wholeMs(value) {
|
|
1717
|
+
const ms = Number(value);
|
|
1718
|
+
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
1719
|
+
}
|
|
1720
|
+
function wholePhaseMs(phases) {
|
|
1721
|
+
const out = {};
|
|
1722
|
+
for (const [name, ms] of Object.entries(phases ?? {})) if (wholeMs(ms) > 0) out[name] = wholeMs(ms);
|
|
1723
|
+
return out;
|
|
1724
|
+
}
|
|
1725
|
+
/** The remedy a thrown error carries, when it carries one. */
|
|
1726
|
+
function errorRemedy(error) {
|
|
1727
|
+
const remedy = typeof error === "object" && error !== null && "remedy" in error ? error.remedy : void 0;
|
|
1728
|
+
return typeof remedy === "string" && remedy !== "" ? remedy : void 0;
|
|
1729
|
+
}
|
|
1730
|
+
function buildLedgerRecord(run) {
|
|
1731
|
+
const { record, extras, outcome } = run;
|
|
1732
|
+
const platform = record.platform === "ios" || record.platform === "android" ? record.platform : "macos";
|
|
1733
|
+
const configuration = text(record.configuration);
|
|
1734
|
+
const durationMs = wholeMs(record.durationMs);
|
|
1735
|
+
const startedAt = Date.parse(String(record.startedAt));
|
|
1736
|
+
const fallback = text(record.offloadFallback);
|
|
1737
|
+
const ranHere = fallback !== null || record.builtOn === "here";
|
|
1738
|
+
const machine = text(record.offloadedTo) ?? (ranHere ? null : run.offload?.host) ?? null;
|
|
1739
|
+
const placement = machine ? {
|
|
1740
|
+
where: "offloaded",
|
|
1741
|
+
machine
|
|
1742
|
+
} : fallback ? {
|
|
1743
|
+
where: "local",
|
|
1744
|
+
fellBack: true,
|
|
1745
|
+
reason: fallback
|
|
1746
|
+
} : { where: "local" };
|
|
1747
|
+
const hit = record.cacheHit === "local" || record.cacheHit === "remote" ? record.cacheHit : null;
|
|
1748
|
+
const missReason = text(record.missReason);
|
|
1749
|
+
const cache = {
|
|
1750
|
+
result: record.cacheSkipped === true ? "bypassed" : hit ? "hit" : "miss",
|
|
1751
|
+
...hit && record.cacheSkipped !== true ? { source: hit } : {},
|
|
1752
|
+
key: text(record.cacheKey),
|
|
1753
|
+
fingerprint: text(record.fingerprint),
|
|
1754
|
+
...missReason && !hit ? { missReason } : {}
|
|
1755
|
+
};
|
|
1756
|
+
const offloadPhases = wholePhaseMs(run.offload?.phaseMs);
|
|
1757
|
+
const scheme = text(extras.scheme);
|
|
1758
|
+
const device = text(extras.device?.name);
|
|
1759
|
+
const runtime = text(extras.device?.runtime);
|
|
1760
|
+
const failure = outcome === "succeeded" ? void 0 : {
|
|
1761
|
+
code: text(record.errorCode),
|
|
1762
|
+
message: text(extras.message),
|
|
1763
|
+
remedy: text(extras.remedy),
|
|
1764
|
+
phase: run.phase
|
|
1765
|
+
};
|
|
1766
|
+
const where = placement.machine ? `on ${placement.machine}` : "here";
|
|
1767
|
+
const cacheText = cache.result === "hit" ? `cache hit${cache.source === "remote" ? " (remote)" : ""}` : cache.result;
|
|
1768
|
+
const summary = `${platform} build ${outcome} ${where} in ${formatElapsed(durationMs)} (${cacheText})`;
|
|
1769
|
+
const why = failure ? `: ${[failure.code, failure.message].filter(Boolean).join(": ") || "no reason recorded"}` : "";
|
|
1770
|
+
return {
|
|
1771
|
+
ts: run.now,
|
|
1772
|
+
src: "builds",
|
|
1773
|
+
level: outcome === "succeeded" ? "info" : outcome === "cancelled" ? "warn" : "error",
|
|
1774
|
+
msg: `${summary}${why}`,
|
|
1775
|
+
event: "build",
|
|
1776
|
+
id: run.id,
|
|
1777
|
+
startedAt: Number.isFinite(startedAt) ? startedAt : run.now - durationMs,
|
|
1778
|
+
project: run.root,
|
|
1779
|
+
workspace: run.workspace,
|
|
1780
|
+
branch: run.branch,
|
|
1781
|
+
platform,
|
|
1782
|
+
...scheme ? { scheme } : {},
|
|
1783
|
+
...platform === "android" && configuration ? { variant: configuration } : {},
|
|
1784
|
+
configuration,
|
|
1785
|
+
placement,
|
|
1786
|
+
cache,
|
|
1787
|
+
durationMs,
|
|
1788
|
+
phases: wholePhaseMs(run.phases),
|
|
1789
|
+
...Object.keys(offloadPhases).length ? { offloadPhases } : {},
|
|
1790
|
+
outcome,
|
|
1791
|
+
...failure ? { failure } : {},
|
|
1792
|
+
...device ? { device: {
|
|
1793
|
+
name: device,
|
|
1794
|
+
...runtime ? { runtime } : {}
|
|
1795
|
+
} } : {},
|
|
1796
|
+
stimVersion: run.version
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function lineId(line) {
|
|
1800
|
+
try {
|
|
1801
|
+
const parsed = JSON.parse(line);
|
|
1802
|
+
return typeof parsed === "object" && parsed !== null && "id" in parsed ? parsed.id : void 0;
|
|
1803
|
+
} catch {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* The ledger file's text after appending `line`, keeping only the newest `cap` complete records. A run that records
|
|
1809
|
+
* twice (an Android launch that fails after the build was recorded) replaces its earlier record.
|
|
1810
|
+
*/
|
|
1811
|
+
function appendCapped(existing, line, cap = BUILD_LEDGER_CAP) {
|
|
1812
|
+
const id = lineId(line);
|
|
1813
|
+
const lines = existing.split("\n").filter((each) => each.trim() !== "" && (id === void 0 || lineId(each) !== id));
|
|
1814
|
+
lines.push(line);
|
|
1815
|
+
return `${lines.slice(-cap).join("\n")}\n`;
|
|
1816
|
+
}
|
|
1817
|
+
function appendBuildLedger(record, file = buildLedgerFile()) {
|
|
1818
|
+
withDirLock(buildLedgerLock(), () => {
|
|
1819
|
+
let existing = "";
|
|
1820
|
+
try {
|
|
1821
|
+
existing = readFileSync(file, "utf-8");
|
|
1822
|
+
} catch {}
|
|
1823
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1824
|
+
writeFileSync(tmp, appendCapped(existing, JSON.stringify(record)));
|
|
1825
|
+
try {
|
|
1826
|
+
renameSync(tmp, file);
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
rmSync(tmp, { force: true });
|
|
1829
|
+
throw error;
|
|
1830
|
+
}
|
|
1831
|
+
}, {
|
|
1832
|
+
waitMs: LOCK_WAIT_MS,
|
|
1833
|
+
ensureParent: () => {
|
|
1834
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1835
|
+
mkdirSync(dirname(buildLedgerLock()), { recursive: true });
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
/** Records one finished build in the ledger. A failure to do so never changes the run, so it is ignored. */
|
|
1840
|
+
function writeBuildLedger(root, run) {
|
|
1841
|
+
try {
|
|
1842
|
+
const branch = gitQuiet(root, [
|
|
1843
|
+
"symbolic-ref",
|
|
1844
|
+
"--quiet",
|
|
1845
|
+
"--short",
|
|
1846
|
+
"HEAD"
|
|
1847
|
+
], { timeoutMs: GIT_TIMEOUT_MS })?.trim() || null;
|
|
1848
|
+
appendBuildLedger(buildLedgerRecord({
|
|
1849
|
+
...run,
|
|
1850
|
+
id: run.id ?? randomUUID(),
|
|
1851
|
+
root,
|
|
1852
|
+
workspace: workspaceName(root),
|
|
1853
|
+
branch,
|
|
1854
|
+
version: STIM_VERSION
|
|
1855
|
+
}));
|
|
1856
|
+
} catch {}
|
|
1857
|
+
}
|
|
1858
|
+
//#endregion
|
|
1859
|
+
//#region src/engine/build-progress.ts
|
|
1860
|
+
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
1861
|
+
const NO_BUILD_PROGRESS = {
|
|
1862
|
+
step: () => {},
|
|
1863
|
+
activity: () => {},
|
|
1864
|
+
estimate: () => {},
|
|
1865
|
+
miss: () => {},
|
|
1866
|
+
hit: () => {},
|
|
1867
|
+
deviceSetup: () => {},
|
|
1868
|
+
deviceSetupKnown: () => void 0,
|
|
1869
|
+
place: () => {},
|
|
1870
|
+
waitingOn: () => {},
|
|
1871
|
+
waitingFor: () => {},
|
|
1872
|
+
output: () => {},
|
|
1873
|
+
durations: () => ({}),
|
|
1874
|
+
phase: () => null,
|
|
1875
|
+
steps: () => null,
|
|
1876
|
+
clear: () => {}
|
|
1877
|
+
};
|
|
1878
|
+
/** `writer`, with each record it writes also passed to `progress.output`. */
|
|
1879
|
+
function tapBuildLog(writer, progress) {
|
|
1880
|
+
if (progress === NO_BUILD_PROGRESS) return writer;
|
|
1881
|
+
return {
|
|
1882
|
+
get file() {
|
|
1883
|
+
return writer.file;
|
|
1884
|
+
},
|
|
1885
|
+
write(record) {
|
|
1886
|
+
progress.output(record);
|
|
1887
|
+
return writer.write(record);
|
|
1888
|
+
},
|
|
1889
|
+
close: () => writer.close(),
|
|
1890
|
+
get written() {
|
|
1891
|
+
return writer.written;
|
|
1892
|
+
},
|
|
1893
|
+
get dropped() {
|
|
1894
|
+
return writer.dropped;
|
|
1895
|
+
},
|
|
1896
|
+
get lastError() {
|
|
1897
|
+
return writer.lastError;
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function runFailureRecord({ code, message, phase }) {
|
|
1902
|
+
return {
|
|
1903
|
+
src: "build",
|
|
1904
|
+
level: "error",
|
|
1905
|
+
event: "run_failed",
|
|
1906
|
+
...code ? { code } : {},
|
|
1907
|
+
...phase ? { phase } : {},
|
|
1908
|
+
msg: [code, message].filter(Boolean).join(": ") || "the run failed"
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function thrownRunFailureRecord(error, phase) {
|
|
1912
|
+
return runFailureRecord({
|
|
1913
|
+
code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0,
|
|
1914
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1915
|
+
phase
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const DETAIL_WRITE_MS = 2e3;
|
|
1919
|
+
function toolLine(record) {
|
|
1920
|
+
if (!record || typeof record !== "object") return null;
|
|
1921
|
+
const { src, level, msg } = record;
|
|
1922
|
+
return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
|
|
1923
|
+
}
|
|
1924
|
+
function writeBuildDetail(root, claimId, detail) {
|
|
1925
|
+
const file = workspaceBuildDetailFile(root);
|
|
1926
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
1927
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1928
|
+
writeFileSync(tmp, JSON.stringify({
|
|
1929
|
+
claimId,
|
|
1930
|
+
detail
|
|
1931
|
+
}));
|
|
1932
|
+
renameSync(tmp, file);
|
|
1933
|
+
}
|
|
1934
|
+
const COLD_PHASES = [
|
|
1935
|
+
"prebuild",
|
|
1936
|
+
"pods",
|
|
1937
|
+
"compile"
|
|
1938
|
+
];
|
|
1939
|
+
const HIT_PHASES = ["install", "launch"];
|
|
1940
|
+
function settledOutcome(phase, entered) {
|
|
1941
|
+
if (COLD_PHASES.includes(phase)) return "cold";
|
|
1942
|
+
if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
|
|
1943
|
+
return HIT_PHASES.includes(phase) ? "hit" : null;
|
|
1944
|
+
}
|
|
1945
|
+
function projectHistory(projectKey) {
|
|
1946
|
+
try {
|
|
1947
|
+
return readStats$1().record?.history?.[projectKey];
|
|
1948
|
+
} catch {
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
|
|
1953
|
+
const startedAt = new Date(now()).toISOString();
|
|
1954
|
+
const record = {
|
|
1955
|
+
platform,
|
|
1956
|
+
slot,
|
|
1957
|
+
startedAt,
|
|
1958
|
+
phase: "prepare",
|
|
1959
|
+
phaseStartedAt: startedAt,
|
|
1960
|
+
phases: [{
|
|
1961
|
+
phase: "prepare",
|
|
1962
|
+
startedAt
|
|
1963
|
+
}],
|
|
1964
|
+
claim: {
|
|
1965
|
+
root: claim.root,
|
|
1966
|
+
path: claim.path,
|
|
1967
|
+
claimId: claim.claimId,
|
|
1968
|
+
pid: claim.owner.pid
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
let warned = false;
|
|
1972
|
+
const guard = (fn) => {
|
|
1973
|
+
try {
|
|
1974
|
+
fn();
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
if (warned) return;
|
|
1977
|
+
warned = true;
|
|
1978
|
+
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
|
|
1982
|
+
...before(state),
|
|
1983
|
+
[ACTIVE_BUILD_KEY]: record
|
|
1984
|
+
})));
|
|
1985
|
+
write(withInterruptedBuild);
|
|
1986
|
+
const phases = trace.phases(`${platform} build`);
|
|
1987
|
+
phases.enter("prepare");
|
|
1988
|
+
let projectKey = null;
|
|
1989
|
+
const parser = createBuildDetailParser();
|
|
1990
|
+
let detailWrittenAt = -Infinity;
|
|
1991
|
+
let detailTimer = null;
|
|
1992
|
+
const flushDetail = () => {
|
|
1993
|
+
detailTimer = null;
|
|
1994
|
+
detailWrittenAt = now();
|
|
1995
|
+
const detail = parser.detail(new Date(detailWrittenAt).toISOString());
|
|
1996
|
+
if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
|
|
1997
|
+
};
|
|
1998
|
+
const settle = (outcome) => {
|
|
1999
|
+
record.outcome = outcome;
|
|
2000
|
+
if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
|
|
2001
|
+
};
|
|
2002
|
+
const waits = /* @__PURE__ */ new Map();
|
|
2003
|
+
return {
|
|
2004
|
+
step(phase) {
|
|
2005
|
+
if (phase === record.phase) return;
|
|
2006
|
+
const at = new Date(now()).toISOString();
|
|
2007
|
+
record.phase = phase;
|
|
2008
|
+
record.phaseStartedAt = at;
|
|
2009
|
+
record.phases.push({
|
|
2010
|
+
phase,
|
|
2011
|
+
startedAt: at
|
|
2012
|
+
});
|
|
2013
|
+
phases.enter(phase);
|
|
2014
|
+
if (phase !== "wait") delete record.waitingOn;
|
|
2015
|
+
delete record.activity;
|
|
2016
|
+
const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
|
|
2017
|
+
if (outcome) settle(outcome);
|
|
2018
|
+
write();
|
|
2019
|
+
},
|
|
2020
|
+
activity(name, at) {
|
|
2021
|
+
if ((record.activity?.name ?? null) === name) return;
|
|
2022
|
+
if (name) record.activity = {
|
|
2023
|
+
name,
|
|
2024
|
+
startedAt: new Date(at ?? now()).toISOString()
|
|
2025
|
+
};
|
|
2026
|
+
else delete record.activity;
|
|
2027
|
+
write();
|
|
2028
|
+
},
|
|
2029
|
+
estimate(key) {
|
|
2030
|
+
if (platform === "macos") return;
|
|
2031
|
+
projectKey = key;
|
|
2032
|
+
record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
2033
|
+
write();
|
|
2034
|
+
},
|
|
2035
|
+
miss(reason, provisional = false) {
|
|
2036
|
+
record.missReason = reason;
|
|
2037
|
+
settle("cold");
|
|
2038
|
+
if (provisional) record.missProvisional = true;
|
|
2039
|
+
else delete record.missProvisional;
|
|
2040
|
+
write();
|
|
2041
|
+
},
|
|
2042
|
+
hit() {
|
|
2043
|
+
delete record.missReason;
|
|
2044
|
+
delete record.missProvisional;
|
|
2045
|
+
settle("hit");
|
|
2046
|
+
write();
|
|
2047
|
+
},
|
|
2048
|
+
deviceSetup(setup) {
|
|
2049
|
+
if (setup === void 0) return;
|
|
2050
|
+
record.deviceSetup = setup;
|
|
2051
|
+
write();
|
|
2052
|
+
},
|
|
2053
|
+
deviceSetupKnown() {
|
|
2054
|
+
return record.deviceSetup;
|
|
2055
|
+
},
|
|
2056
|
+
place(remote) {
|
|
2057
|
+
const current = record.placement;
|
|
2058
|
+
if (remote && current?.host === remote.host && current.phase === remote.phase) return;
|
|
2059
|
+
if (current) record.offload = withOffloadPhase(record.offload, current, now());
|
|
2060
|
+
if (!remote) {
|
|
2061
|
+
if (!current) return;
|
|
2062
|
+
delete record.placement;
|
|
2063
|
+
return write();
|
|
2064
|
+
}
|
|
2065
|
+
const at = new Date(now()).toISOString();
|
|
2066
|
+
record.placement = {
|
|
2067
|
+
host: remote.host,
|
|
2068
|
+
phase: remote.phase,
|
|
2069
|
+
startedAt: current?.host === remote.host ? current.startedAt : at,
|
|
2070
|
+
phaseStartedAt: at
|
|
2071
|
+
};
|
|
2072
|
+
write();
|
|
2073
|
+
},
|
|
2074
|
+
waitingFor(info, kind) {
|
|
2075
|
+
if (info) waits.set(info.kind, info);
|
|
2076
|
+
else if (kind) waits.delete(kind);
|
|
2077
|
+
else waits.clear();
|
|
2078
|
+
const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
|
|
2079
|
+
if (first) record.waitingFor = first;
|
|
2080
|
+
else delete record.waitingFor;
|
|
2081
|
+
write();
|
|
2082
|
+
},
|
|
2083
|
+
waitingOn(holder) {
|
|
2084
|
+
if (holder === null) {
|
|
2085
|
+
if (!record.waitingOn) return;
|
|
2086
|
+
delete record.waitingOn;
|
|
2087
|
+
} else {
|
|
2088
|
+
if (record.waitingOn?.path === holder) return;
|
|
2089
|
+
record.waitingOn = { path: holder };
|
|
2090
|
+
}
|
|
2091
|
+
write();
|
|
2092
|
+
},
|
|
2093
|
+
output(line) {
|
|
2094
|
+
const msg = toolLine(line);
|
|
2095
|
+
if (msg === null || !parser.push(msg) || detailTimer) return;
|
|
2096
|
+
const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
|
|
2097
|
+
if (wait <= 0) return flushDetail();
|
|
2098
|
+
detailTimer = setTimeout(flushDetail, wait);
|
|
2099
|
+
detailTimer.unref?.();
|
|
2100
|
+
},
|
|
2101
|
+
durations() {
|
|
2102
|
+
return phaseDurations(record.phases, now());
|
|
2103
|
+
},
|
|
2104
|
+
phase() {
|
|
2105
|
+
return record.phase;
|
|
2106
|
+
},
|
|
2107
|
+
steps() {
|
|
2108
|
+
const detail = parser.detail("");
|
|
2109
|
+
return detail?.unit === "steps" ? detail.total : null;
|
|
2110
|
+
},
|
|
2111
|
+
clear() {
|
|
2112
|
+
phases.end();
|
|
2113
|
+
if (detailTimer) clearTimeout(detailTimer);
|
|
2114
|
+
detailTimer = null;
|
|
2115
|
+
guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
|
|
2116
|
+
guard(() => {
|
|
2117
|
+
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
/** `offload` with the time `placement` has spent in its current phase as of `now` added to that phase. */
|
|
2123
|
+
function withOffloadPhase(offload, placement, now) {
|
|
2124
|
+
const phaseMs = { ...offload?.phaseMs };
|
|
2125
|
+
const ms = Math.max(0, now - Date.parse(placement.phaseStartedAt));
|
|
2126
|
+
if (Number.isFinite(ms)) phaseMs[placement.phase] = (phaseMs[placement.phase] ?? 0) + ms;
|
|
2127
|
+
return {
|
|
2128
|
+
host: placement.host,
|
|
2129
|
+
phaseMs
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function phaseDurations(phases, now) {
|
|
2133
|
+
const out = {};
|
|
2134
|
+
phases.forEach(({ phase, startedAt }, index) => {
|
|
2135
|
+
const start = Date.parse(startedAt);
|
|
2136
|
+
const next = phases[index + 1];
|
|
2137
|
+
const end = next ? Date.parse(next.startedAt) : now;
|
|
2138
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
2139
|
+
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
2140
|
+
});
|
|
2141
|
+
return out;
|
|
2142
|
+
}
|
|
2143
|
+
function completedPhaseDurations(phases) {
|
|
2144
|
+
if (phases.length < 2) return void 0;
|
|
2145
|
+
const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
|
|
2146
|
+
return Object.keys(durations).length ? durations : void 0;
|
|
2147
|
+
}
|
|
2148
|
+
const HISTORY_FIELDS = [
|
|
2149
|
+
"devicePlacement",
|
|
2150
|
+
"platform",
|
|
2151
|
+
"status",
|
|
2152
|
+
"configuration",
|
|
2153
|
+
"fingerprint",
|
|
2154
|
+
"cacheKey",
|
|
2155
|
+
"cacheHit",
|
|
2156
|
+
"cacheSkipped",
|
|
2157
|
+
"durationMs",
|
|
2158
|
+
"startedAt",
|
|
2159
|
+
"errorCode",
|
|
2160
|
+
"missReason",
|
|
2161
|
+
"buildMachine",
|
|
2162
|
+
"builtOn",
|
|
2163
|
+
"offloadedTo",
|
|
2164
|
+
"offloadFallback",
|
|
2165
|
+
"diagnostics",
|
|
2166
|
+
"compileSteps"
|
|
2167
|
+
];
|
|
2168
|
+
function withHistoryEntry(state, platform, entry) {
|
|
2169
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2170
|
+
const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
|
|
2171
|
+
const list = Array.isArray(history[platform]) ? history[platform] : [];
|
|
2172
|
+
return {
|
|
2173
|
+
...state,
|
|
2174
|
+
[BUILD_HISTORY_KEY]: {
|
|
2175
|
+
...history,
|
|
2176
|
+
[platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function finishedResult(record) {
|
|
2181
|
+
if (record.status === "ok") return "succeeded";
|
|
2182
|
+
return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
|
|
2183
|
+
}
|
|
2184
|
+
function recordFinishedBuild(root, recorded, { update = updateWorkspaceState, now = Date.now, artifactOnly = false, ledger = writeBuildLedger } = {}) {
|
|
2185
|
+
const { ledger: extras, ...record } = recorded;
|
|
2186
|
+
const ran = {};
|
|
2187
|
+
try {
|
|
2188
|
+
updateFinishedBuild(root, record, ran, {
|
|
2189
|
+
update,
|
|
2190
|
+
now,
|
|
2191
|
+
artifactOnly
|
|
2192
|
+
});
|
|
2193
|
+
} finally {
|
|
2194
|
+
ledger(root, {
|
|
2195
|
+
now: now(),
|
|
2196
|
+
outcome: finishedResult(record),
|
|
2197
|
+
record,
|
|
2198
|
+
extras: isJsonObject(extras) ? extras : {},
|
|
2199
|
+
phases: {},
|
|
2200
|
+
phase: null,
|
|
2201
|
+
...ran
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function updateFinishedBuild(root, record, ran, { update, now, artifactOnly }) {
|
|
2206
|
+
const platform = record.platform;
|
|
2207
|
+
update(root, (state) => {
|
|
2208
|
+
const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2209
|
+
const own = active?.platform === platform ? active : null;
|
|
2210
|
+
const phases = own ? phaseDurations(own.phases, now()) : {};
|
|
2211
|
+
if (own) {
|
|
2212
|
+
const offload = own.placement ? withOffloadPhase(own.offload, own.placement, now()) : own.offload;
|
|
2213
|
+
Object.assign(ran, {
|
|
2214
|
+
id: own.claim.claimId,
|
|
2215
|
+
phases,
|
|
2216
|
+
phase: own.phase,
|
|
2217
|
+
...offload ? { offload } : {}
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
const entry = {
|
|
2221
|
+
...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
|
|
2222
|
+
result: finishedResult(record),
|
|
2223
|
+
slot: own?.slot ?? "default",
|
|
2224
|
+
phases
|
|
2225
|
+
};
|
|
2226
|
+
if (artifactOnly || platform === "macos") return withHistoryEntry(state, platform, entry);
|
|
2227
|
+
const slot = own?.slot ?? "default";
|
|
2228
|
+
const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
|
|
2229
|
+
const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
|
|
2230
|
+
const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
|
|
2231
|
+
if (record.devicePlacement || isJsonObject(savedDevice)) {
|
|
2232
|
+
const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
|
|
2233
|
+
if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
|
|
2234
|
+
else delete device.devicePlacement;
|
|
2235
|
+
state = slot === "default" ? {
|
|
2236
|
+
...state,
|
|
2237
|
+
[platform]: device
|
|
2238
|
+
} : {
|
|
2239
|
+
...state,
|
|
2240
|
+
deviceSlots: {
|
|
2241
|
+
...slots,
|
|
2242
|
+
[slot]: {
|
|
2243
|
+
...savedSlot,
|
|
2244
|
+
[platform]: device
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
return withHistoryEntry({
|
|
2250
|
+
...state,
|
|
2251
|
+
lastBuild: record,
|
|
2252
|
+
[LAST_BUILD_KEYS[platform]]: record
|
|
2253
|
+
}, platform, entry);
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
/** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
|
|
2257
|
+
function recordBuildPhase(root, platform, startedAt, phase, ms) {
|
|
2258
|
+
updateWorkspaceState(root, (state) => {
|
|
2259
|
+
const saved = state[BUILD_HISTORY_KEY];
|
|
2260
|
+
const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
|
|
2261
|
+
const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
|
|
2262
|
+
const entry = list[index];
|
|
2263
|
+
if (!isJsonObject(entry)) return state;
|
|
2264
|
+
const phases = isJsonObject(entry.phases) ? entry.phases : {};
|
|
2265
|
+
const next = [...list];
|
|
2266
|
+
next[index] = {
|
|
2267
|
+
...entry,
|
|
2268
|
+
phases: {
|
|
2269
|
+
...phases,
|
|
2270
|
+
[phase]: Math.round(ms)
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
return {
|
|
2274
|
+
...state,
|
|
2275
|
+
[BUILD_HISTORY_KEY]: {
|
|
2276
|
+
...saved,
|
|
2277
|
+
[platform]: next
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
function withInterruptedBuild(state) {
|
|
2283
|
+
const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
2284
|
+
if (!left) return state;
|
|
2285
|
+
const last = state[BUILD_HISTORY_KEY]?.[left.platform]?.[0];
|
|
2286
|
+
if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
|
|
2287
|
+
return withHistoryEntry(state, left.platform, {
|
|
2288
|
+
platform: left.platform,
|
|
2289
|
+
status: "failed",
|
|
2290
|
+
result: "interrupted",
|
|
2291
|
+
slot: left.slot,
|
|
2292
|
+
configuration: null,
|
|
2293
|
+
fingerprint: null,
|
|
2294
|
+
cacheKey: null,
|
|
2295
|
+
cacheHit: false,
|
|
2296
|
+
cacheSkipped: false,
|
|
2297
|
+
durationMs: null,
|
|
2298
|
+
startedAt: left.startedAt,
|
|
2299
|
+
phases: {
|
|
2300
|
+
...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
|
|
2301
|
+
[left.phase]: 0
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
function parseActiveBuild(value) {
|
|
2306
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2307
|
+
const record = value;
|
|
2308
|
+
const claim = record.claim;
|
|
2309
|
+
if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
|
|
2310
|
+
if (!isPhase(record.phase)) return null;
|
|
2311
|
+
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
2312
|
+
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
2313
|
+
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
2314
|
+
const missReason = parseMissReason(record.missReason);
|
|
2315
|
+
const placement = parsePlacement(record.placement);
|
|
2316
|
+
const waitingOn = parseWaitingOn(record.waitingOn);
|
|
2317
|
+
const waitingFor = parseWaitingFor(record.waitingFor);
|
|
2318
|
+
const estimate = parseEstimate(record.estimate);
|
|
2319
|
+
const activity = parseActivity(record.activity);
|
|
2320
|
+
const offload = parseOffload(record.offload);
|
|
2321
|
+
return {
|
|
2322
|
+
platform: record.platform,
|
|
2323
|
+
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
2324
|
+
startedAt: record.startedAt,
|
|
2325
|
+
phase: record.phase,
|
|
2326
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
2327
|
+
phases,
|
|
2328
|
+
...missReason ? { missReason } : {},
|
|
2329
|
+
...missReason && record.missProvisional === true ? { missProvisional: true } : {},
|
|
2330
|
+
...placement ? { placement } : {},
|
|
2331
|
+
...offload ? { offload } : {},
|
|
2332
|
+
...waitingOn ? { waitingOn } : {},
|
|
2333
|
+
...waitingFor ? { waitingFor } : {},
|
|
2334
|
+
...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
|
|
2335
|
+
...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
|
|
2336
|
+
...estimate ? { estimate } : {},
|
|
2337
|
+
...activity ? { activity } : {},
|
|
2338
|
+
claim: {
|
|
2339
|
+
root: claim.root,
|
|
2340
|
+
path: typeof claim.path === "string" ? claim.path : "",
|
|
2341
|
+
claimId: claim.claimId,
|
|
2342
|
+
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function parseOffload(value) {
|
|
2347
|
+
if (!isJsonObject(value) || typeof value.host !== "string" || !isJsonObject(value.phaseMs)) return null;
|
|
2348
|
+
const phaseMs = {};
|
|
2349
|
+
for (const [phase, ms] of Object.entries(value.phaseMs)) if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) phaseMs[phase] = ms;
|
|
2350
|
+
return {
|
|
2351
|
+
host: value.host,
|
|
2352
|
+
phaseMs
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
function parsePlacement(value) {
|
|
2356
|
+
if (!value || typeof value !== "object") return null;
|
|
2357
|
+
const { host, phase, startedAt, phaseStartedAt } = value;
|
|
2358
|
+
if ([
|
|
2359
|
+
host,
|
|
2360
|
+
phase,
|
|
2361
|
+
startedAt,
|
|
2362
|
+
phaseStartedAt
|
|
2363
|
+
].some((field) => typeof field !== "string")) return null;
|
|
2364
|
+
return {
|
|
2365
|
+
host,
|
|
2366
|
+
phase,
|
|
2367
|
+
startedAt,
|
|
2368
|
+
phaseStartedAt
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function parseWaitingFor(value) {
|
|
2372
|
+
if (!value || typeof value !== "object") return null;
|
|
2373
|
+
const { kind, inUse, max, since } = value;
|
|
2374
|
+
if (kind !== "build-slot" && kind !== "device-slot") return null;
|
|
2375
|
+
if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
|
|
2376
|
+
if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
|
|
2377
|
+
return {
|
|
2378
|
+
kind,
|
|
2379
|
+
inUse,
|
|
2380
|
+
max,
|
|
2381
|
+
since
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function parseActivity(value) {
|
|
2385
|
+
if (!value || typeof value !== "object") return null;
|
|
2386
|
+
const { name, startedAt } = value;
|
|
2387
|
+
if (name !== "booting" && name !== "bundling" && name !== "waiting-ready") return null;
|
|
2388
|
+
return typeof startedAt === "string" ? {
|
|
2389
|
+
name,
|
|
2390
|
+
startedAt
|
|
2391
|
+
} : null;
|
|
2392
|
+
}
|
|
2393
|
+
function parseWaitingOn(value) {
|
|
2394
|
+
const path = value?.path;
|
|
2395
|
+
return typeof path === "string" && path ? { path } : null;
|
|
2396
|
+
}
|
|
2397
|
+
function parseEstimate(value) {
|
|
2398
|
+
if (!value || typeof value !== "object") return null;
|
|
2399
|
+
const { outcome, expectedMs, basis, phaseMs, planned } = value;
|
|
2400
|
+
if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
|
|
2401
|
+
if (expectedMs !== null && typeof expectedMs !== "number") return null;
|
|
2402
|
+
if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
|
|
2403
|
+
const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
|
|
2404
|
+
return {
|
|
2405
|
+
outcome,
|
|
2406
|
+
expectedMs,
|
|
2407
|
+
basis,
|
|
2408
|
+
phaseMs: Object.fromEntries(durations),
|
|
2409
|
+
planned: planned.filter(isPhase)
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function isPhase(value) {
|
|
2413
|
+
return BUILD_PHASES.includes(value);
|
|
2414
|
+
}
|
|
2415
|
+
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
2416
|
+
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
2417
|
+
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
2418
|
+
return survey.unresolved.length ? "unknown" : "stale";
|
|
2419
|
+
}
|
|
2420
|
+
function median(values) {
|
|
2421
|
+
if (!values.length) return null;
|
|
2422
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
2423
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2424
|
+
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
2425
|
+
}
|
|
2426
|
+
function estimateBuild(history, platform, known, deviceSetup) {
|
|
2427
|
+
const lists = platform === "macos" ? void 0 : history?.[platform];
|
|
2428
|
+
const outcome = known ?? latestOutcome(lists);
|
|
2429
|
+
const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
|
|
2430
|
+
const phaseMs = {};
|
|
2431
|
+
const planned = [];
|
|
2432
|
+
for (const phase of BUILD_PHASES) {
|
|
2433
|
+
const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
2434
|
+
const ms = median(durations);
|
|
2435
|
+
if (ms === null) continue;
|
|
2436
|
+
phaseMs[phase] = ms;
|
|
2437
|
+
if (durations.length * 2 >= samples.length) planned.push(phase);
|
|
2438
|
+
}
|
|
2439
|
+
return {
|
|
2440
|
+
outcome,
|
|
2441
|
+
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
2442
|
+
basis: samples.length,
|
|
2443
|
+
phaseMs,
|
|
2444
|
+
planned
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
const MIN_TAGGED_SAMPLES = 3;
|
|
2448
|
+
function matchDeviceSetup(samples, deviceSetup) {
|
|
2449
|
+
if (deviceSetup === void 0) return samples;
|
|
2450
|
+
const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
|
|
2451
|
+
if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
|
|
2452
|
+
return samples.filter((sample) => sample.deviceSetup !== true);
|
|
2453
|
+
}
|
|
2454
|
+
function latestOutcome(lists) {
|
|
2455
|
+
let latest = null;
|
|
2456
|
+
for (const outcome of ["hit", "cold"]) {
|
|
2457
|
+
const last = lists?.[outcome]?.at(-1);
|
|
2458
|
+
const at = last ? Date.parse(last.at) : NaN;
|
|
2459
|
+
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
2460
|
+
outcome,
|
|
2461
|
+
at
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
return latest?.outcome ?? null;
|
|
2465
|
+
}
|
|
2466
|
+
function buildReport(record, { state, history }) {
|
|
2467
|
+
const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
2468
|
+
const plannedPhases = estimate.planned.flatMap((phase) => {
|
|
2469
|
+
const expectedMs = estimate.phaseMs[phase];
|
|
2470
|
+
return expectedMs === void 0 ? [] : [{
|
|
2471
|
+
phase,
|
|
2472
|
+
expectedMs
|
|
2473
|
+
}];
|
|
2474
|
+
});
|
|
2475
|
+
const completedPhaseMs = completedPhaseDurations(record.phases);
|
|
2476
|
+
return {
|
|
2477
|
+
platform: record.platform,
|
|
2478
|
+
slot: record.slot,
|
|
2479
|
+
state,
|
|
2480
|
+
phase: record.phase,
|
|
2481
|
+
startedAt: record.startedAt,
|
|
2482
|
+
phaseStartedAt: record.phaseStartedAt,
|
|
2483
|
+
outcome: estimate.outcome,
|
|
2484
|
+
outcomeKnown: record.outcome !== void 0,
|
|
2485
|
+
...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
|
|
2486
|
+
expectedMs: estimate.expectedMs,
|
|
2487
|
+
expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
|
|
2488
|
+
...completedPhaseMs ? { completedPhaseMs } : {},
|
|
2489
|
+
basis: estimate.basis,
|
|
2490
|
+
plannedPhases: plannedPhases.length ? plannedPhases : null,
|
|
2491
|
+
...record.missReason ? { missReason: record.missReason } : {},
|
|
2492
|
+
...record.missReason && record.missProvisional ? { missProvisional: true } : {},
|
|
2493
|
+
placement: record.placement ?? "local",
|
|
2494
|
+
...record.waitingOn ? { waitingOn: record.waitingOn } : {},
|
|
2495
|
+
...record.waitingFor ? { waitingFor: record.waitingFor } : {},
|
|
2496
|
+
...record.activity ? { activity: { ...record.activity } } : {}
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* The run's `bundling` activity with Metro's percent, when the request Metro reports in flight is the one the activity
|
|
2501
|
+
* started on: same platform and same start, since both come from the request's `bundle_response_started` record.
|
|
2502
|
+
*/
|
|
2503
|
+
function bundlingActivity(activity, platform, bundle) {
|
|
2504
|
+
if (activity?.name !== "bundling" || !bundle?.bundling || bundle.percent === void 0) return activity;
|
|
2505
|
+
if (bundle.platform !== platform || bundle.startedAt !== activity.startedAt) return activity;
|
|
2506
|
+
return {
|
|
2507
|
+
...activity,
|
|
2508
|
+
percent: bundle.percent
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function remainingText(remainingMs) {
|
|
2512
|
+
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
2513
|
+
}
|
|
2514
|
+
function activityText({ name, percent, startedAt }, now) {
|
|
2515
|
+
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)))}`;
|
|
2516
|
+
}
|
|
2517
|
+
function buildStatusLine(report, now) {
|
|
2518
|
+
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
2519
|
+
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
2520
|
+
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
2521
|
+
const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
|
|
2522
|
+
const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
|
|
2523
|
+
const activity = report.activity ? ` (${activityText(report.activity, now)})` : "";
|
|
2524
|
+
const head = `build: ${report.platform}${slot} ${report.phase}${activity}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
2525
|
+
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
2526
|
+
if (report.expectedMs === null) return head;
|
|
2527
|
+
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
2528
|
+
const remainingMs = report.expectedMs - elapsedMs;
|
|
2529
|
+
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
2530
|
+
}
|
|
2531
|
+
//#endregion
|
|
2532
|
+
//#region src/commands/gc/idle.ts
|
|
2533
|
+
function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
|
|
2534
|
+
const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
|
|
2535
|
+
const dead = new Set(deadProjects);
|
|
2536
|
+
const found = [];
|
|
2537
|
+
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
2538
|
+
if (dead.has(project)) continue;
|
|
2539
|
+
for (const { slot, platforms } of projectDeviceSlots(record)) {
|
|
2540
|
+
const ios = platforms.ios;
|
|
2541
|
+
if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
|
|
2542
|
+
kind: "ios",
|
|
2543
|
+
id: ios.deviceUdid,
|
|
2544
|
+
name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
|
|
2545
|
+
project,
|
|
2546
|
+
slot,
|
|
2547
|
+
activity: readActivity({
|
|
2548
|
+
platform: "ios",
|
|
2549
|
+
id: ios.deviceUdid,
|
|
2550
|
+
slot,
|
|
2551
|
+
workspace: project
|
|
2552
|
+
})
|
|
2553
|
+
});
|
|
2554
|
+
const android = platforms.android;
|
|
2555
|
+
const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
|
|
2556
|
+
if (android?.avdName && serial) found.push({
|
|
2557
|
+
kind: "android",
|
|
2558
|
+
id: android.avdName,
|
|
2559
|
+
name: android.avdName,
|
|
2560
|
+
project,
|
|
2561
|
+
slot,
|
|
2562
|
+
activity: readActivity({
|
|
2563
|
+
platform: "android",
|
|
2564
|
+
id: serial,
|
|
2565
|
+
slot,
|
|
2566
|
+
workspace: project
|
|
2567
|
+
})
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return found;
|
|
2572
|
+
}
|
|
2573
|
+
function findIdleDevices({ buildInProgress, now, ...inputs }) {
|
|
2574
|
+
return idleDevicesFromActivity(findOwnedDeviceActivity(inputs), buildInProgress, now);
|
|
2575
|
+
}
|
|
2576
|
+
function idleDevicesFromActivity(devices, buildInProgress, now) {
|
|
2577
|
+
return devices.filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
|
|
2578
|
+
kind,
|
|
2579
|
+
id,
|
|
2580
|
+
name,
|
|
2581
|
+
project,
|
|
2582
|
+
slot,
|
|
2583
|
+
lastActivityAt: activity.lastActivityAt ?? null,
|
|
2584
|
+
idleForMs: idleForMs(activity, now),
|
|
2585
|
+
buildInProgress: buildInProgress(project)
|
|
2586
|
+
}));
|
|
2587
|
+
}
|
|
2588
|
+
function idleShutdownCandidates(devices, idleMs) {
|
|
2589
|
+
return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
|
|
2590
|
+
}
|
|
2591
|
+
function parseIdleDuration(value) {
|
|
2592
|
+
const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
|
|
2593
|
+
if (!match) return null;
|
|
2594
|
+
const unit = {
|
|
2595
|
+
m: 6e4,
|
|
2596
|
+
h: 36e5,
|
|
2597
|
+
d: 864e5
|
|
2598
|
+
}[match[2]];
|
|
2599
|
+
return Number(match[1]) * unit;
|
|
2600
|
+
}
|
|
2601
|
+
function workspaceBuildInProgress(project) {
|
|
2602
|
+
const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
|
|
2603
|
+
if (!record) return false;
|
|
2604
|
+
try {
|
|
2605
|
+
return activeBuildState(record.claim) !== "stale";
|
|
2606
|
+
} catch {
|
|
2607
|
+
return true;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
function deviceActivityInputs(config, sims, now, env) {
|
|
2611
|
+
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
2612
|
+
return {
|
|
2613
|
+
config,
|
|
2614
|
+
sims,
|
|
2615
|
+
readActivity: createActivityReader({
|
|
2616
|
+
now,
|
|
2617
|
+
env
|
|
2618
|
+
}),
|
|
2619
|
+
androidSerial: (avdName) => {
|
|
2620
|
+
try {
|
|
2621
|
+
return resolve(avdName).serial ?? null;
|
|
2622
|
+
} catch {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function collectOwnedDeviceActivity(config, sims, now = Date.now(), env) {
|
|
2629
|
+
return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now, env));
|
|
2630
|
+
}
|
|
2631
|
+
function collectIdleDevices(config, sims, deadProjects, now = Date.now(), env) {
|
|
2632
|
+
return findIdleDevices({
|
|
2633
|
+
...deviceActivityInputs(config, sims, now, env),
|
|
2634
|
+
deadProjects,
|
|
2635
|
+
now,
|
|
2636
|
+
buildInProgress: workspaceBuildInProgress
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = [], signal, operation) {
|
|
2640
|
+
const resolve = ownedAvdSerialResolverAsync({
|
|
2641
|
+
timeoutMs: 5e3,
|
|
2642
|
+
signal,
|
|
2643
|
+
operation
|
|
2644
|
+
});
|
|
2645
|
+
const serials = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
2647
|
+
signal?.throwIfAborted();
|
|
2648
|
+
if (deadProjects.includes(project)) continue;
|
|
2649
|
+
for (const { platforms } of projectDeviceSlots(record)) {
|
|
2650
|
+
const android = platforms.android;
|
|
2651
|
+
if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
|
|
2652
|
+
try {
|
|
2653
|
+
serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
2656
|
+
signal?.throwIfAborted();
|
|
2657
|
+
serials.set(android.avdName, null);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
config,
|
|
2663
|
+
sims,
|
|
2664
|
+
readActivity: createActivityReaderAsync({
|
|
2665
|
+
now,
|
|
2666
|
+
env,
|
|
2667
|
+
signal,
|
|
2668
|
+
operation
|
|
2669
|
+
}),
|
|
2670
|
+
androidSerial: (name) => serials.get(name) ?? null
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env, signal, operation) {
|
|
2674
|
+
const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env, [], signal, operation));
|
|
2675
|
+
return Promise.all(devices.map(async (device) => Object.assign({}, device, { activity: await device.activity })));
|
|
2676
|
+
}
|
|
2677
|
+
async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env, signal, operation) {
|
|
2678
|
+
const pending = findOwnedDeviceActivity({
|
|
2679
|
+
...await deviceActivityInputsAsync(config, sims, now, env, deadProjects, signal, operation),
|
|
2680
|
+
deadProjects
|
|
2681
|
+
});
|
|
2682
|
+
return idleDevicesFromActivity(await Promise.all(pending.map(async (device) => Object.assign({}, device, { activity: await device.activity }))), workspaceBuildInProgress, now);
|
|
2683
|
+
}
|
|
2684
|
+
function idleDeviceLines(devices) {
|
|
2685
|
+
if (devices.length === 0) return [];
|
|
2686
|
+
return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
|
|
2687
|
+
const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
|
|
2688
|
+
const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
|
|
2689
|
+
const build = d.buildInProgress ? ", build in progress" : "";
|
|
2690
|
+
return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
|
|
2691
|
+
})];
|
|
2692
|
+
}
|
|
2693
|
+
async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT, signal, operation) {
|
|
2694
|
+
signal?.throwIfAborted();
|
|
2695
|
+
const candidates = idleShutdownCandidates(devices, idleMs);
|
|
2696
|
+
const shutDown = [];
|
|
2697
|
+
if (candidates.length === 0) {
|
|
2698
|
+
log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
|
|
2699
|
+
return {
|
|
2700
|
+
failures: 0,
|
|
2701
|
+
shutDown
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
const fresh = idleShutdownCandidates(await recollect(), idleMs);
|
|
2705
|
+
signal?.throwIfAborted();
|
|
2706
|
+
let failures = 0;
|
|
2707
|
+
for (const device of candidates) {
|
|
2708
|
+
signal?.throwIfAborted();
|
|
2709
|
+
const what = `${device.kind} ${device.name}`;
|
|
2710
|
+
const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
|
|
2711
|
+
if (!current) {
|
|
2712
|
+
log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
|
|
2713
|
+
record("idleDevice", "kept", device.name, {
|
|
2714
|
+
id: device.id,
|
|
2715
|
+
detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
|
|
2716
|
+
});
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, {
|
|
2720
|
+
label: device.name,
|
|
2721
|
+
operation
|
|
2722
|
+
}) : await teardownOwnedAvd(device.id, {
|
|
2723
|
+
owner: {
|
|
2724
|
+
projectPath: device.project,
|
|
2725
|
+
slot: device.slot
|
|
2726
|
+
},
|
|
2727
|
+
operation
|
|
2728
|
+
});
|
|
2729
|
+
if (outcome.status === "torn-down") {
|
|
2730
|
+
shutDown.push(device);
|
|
2731
|
+
log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
|
|
2732
|
+
record("idleDevice", "done", device.name, {
|
|
2733
|
+
id: device.id,
|
|
2734
|
+
detail: `idle ${formatLongDuration(current.idleForMs)}`
|
|
2735
|
+
});
|
|
2736
|
+
} else if (outcome.status === "missing") log(chalk.dim(`${what} is already gone; nothing to shut down.`));
|
|
2737
|
+
else if (outcome.status === "skipped") {
|
|
2738
|
+
log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
|
|
2739
|
+
record("idleDevice", "kept", device.name, {
|
|
2740
|
+
id: device.id,
|
|
2741
|
+
detail: outcome.reason
|
|
2742
|
+
});
|
|
2743
|
+
} else {
|
|
2744
|
+
failures++;
|
|
2745
|
+
log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
|
|
2746
|
+
record("idleDevice", "failed", device.name, {
|
|
2747
|
+
id: device.id,
|
|
2748
|
+
detail: outcome.reason
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
failures,
|
|
2754
|
+
shutDown
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
//#endregion
|
|
1152
2758
|
//#region src/macos/instances.ts
|
|
1153
2759
|
const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
|
|
1154
2760
|
function bundleDirs(root) {
|
|
@@ -1853,9 +3459,9 @@ function numberOrNull(v) {
|
|
|
1853
3459
|
//#region src/workspace/in-use.ts
|
|
1854
3460
|
const NATIVE_RUN = "native-run";
|
|
1855
3461
|
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) {
|
|
3462
|
+
function workspaceInUse(root, { supervisor = true, devServer = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
|
|
1857
3463
|
const reasons = [];
|
|
1858
|
-
if (supervisor) {
|
|
3464
|
+
if (supervisor && devServer) {
|
|
1859
3465
|
const project = getProject(root);
|
|
1860
3466
|
const target = resolveSupervisorTarget({
|
|
1861
3467
|
state: readWorkspaceState$1(root)?.supervisor,
|
|
@@ -1864,6 +3470,11 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1864
3470
|
});
|
|
1865
3471
|
if (target.status === "ours") reasons.push(`its dev server supervisor (pid ${target.pid}) is running`);
|
|
1866
3472
|
else if (target.status === "unverified") reasons.push(`its dev server supervisor cannot be verified: ${target.reason ?? "unknown identity"}`);
|
|
3473
|
+
const browser = webFacts(readWebRecord(root));
|
|
3474
|
+
if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
|
|
3475
|
+
else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
|
|
3476
|
+
}
|
|
3477
|
+
if (supervisor) {
|
|
1867
3478
|
const macos = macosAppState(readMacosRecord(root));
|
|
1868
3479
|
if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
|
|
1869
3480
|
else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
|
|
@@ -1877,9 +3488,6 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
|
|
|
1877
3488
|
const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
|
|
1878
3489
|
if (runtime.live.length) reasons.push("its macOS runtime claim is held");
|
|
1879
3490
|
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
3491
|
}
|
|
1884
3492
|
if (nativeRun) {
|
|
1885
3493
|
const claims = (0, ownership_claim_exports.readClaimSet)(workspaceProcessLockPath(workspaceDir(root), NATIVE_RUN, true));
|
|
@@ -1974,6 +3582,63 @@ async function withIdleWorkspace(root, fn, { purpose, operation, signal = operat
|
|
|
1974
3582
|
}
|
|
1975
3583
|
}
|
|
1976
3584
|
//#endregion
|
|
3585
|
+
//#region src/workspace/regenerable-dirs.ts
|
|
3586
|
+
/**
|
|
3587
|
+
* Directories that hold only what a build or an install regenerates, relative to an app or package root. ios/build is
|
|
3588
|
+
* not one: `pod install` writes the codegen output (build/generated/ios) and, on React Native 0.88, the podspec
|
|
3589
|
+
* facades (scripts/cocoapods/rncore_facades.rb) there, the Pods project references them, and only `pod install`
|
|
3590
|
+
* recreates them.
|
|
3591
|
+
*/
|
|
3592
|
+
const ARTIFACT_DIRS = {
|
|
3593
|
+
build: [
|
|
3594
|
+
"android/app/build",
|
|
3595
|
+
"android/build",
|
|
3596
|
+
"android/.gradle",
|
|
3597
|
+
".build"
|
|
3598
|
+
],
|
|
3599
|
+
dependencies: ["node_modules", "ios/Pods"]
|
|
3600
|
+
};
|
|
3601
|
+
/** The existing real directories among `dirs` under `root`, never through a link. */
|
|
3602
|
+
function existingDirsOf(root, dirs) {
|
|
3603
|
+
return dirs.map((dir) => join(root, dir)).filter((path) => {
|
|
3604
|
+
try {
|
|
3605
|
+
return lstatSync(path).isDirectory();
|
|
3606
|
+
} catch {
|
|
3607
|
+
return false;
|
|
3608
|
+
}
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Why `path`, inside the checkout at `root`, must not be deleted as a regenerable directory, or null when it is a real
|
|
3613
|
+
* directory inside `root` whose every file git ignores.
|
|
3614
|
+
*/
|
|
3615
|
+
async function regenerableDirProblem(root, path, signal) {
|
|
3616
|
+
const rel = relative(root, path);
|
|
3617
|
+
try {
|
|
3618
|
+
if (!lstatSync(path).isDirectory()) return "it is not a directory";
|
|
3619
|
+
} catch {
|
|
3620
|
+
return "it is gone";
|
|
3621
|
+
}
|
|
3622
|
+
if (canonicalPath(path) !== join(canonicalPath(root), rel)) return "it or a directory above it is a link, or it lies outside its project";
|
|
3623
|
+
const tracked = await gitQuietAsync(root, [
|
|
3624
|
+
"ls-files",
|
|
3625
|
+
"--",
|
|
3626
|
+
rel
|
|
3627
|
+
], { signal });
|
|
3628
|
+
if (tracked === null) return "git cannot tell whether it holds tracked files";
|
|
3629
|
+
if (tracked.trim()) return "it holds files git tracks";
|
|
3630
|
+
const unignored = await gitQuietAsync(root, [
|
|
3631
|
+
"ls-files",
|
|
3632
|
+
"--others",
|
|
3633
|
+
"--exclude-standard",
|
|
3634
|
+
"--",
|
|
3635
|
+
rel
|
|
3636
|
+
], { signal });
|
|
3637
|
+
if (unignored === null) return "git cannot tell whether it ignores every file in it";
|
|
3638
|
+
if (unignored.trim()) return "it holds files git does not ignore";
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3641
|
+
//#endregion
|
|
1977
3642
|
//#region src/commands/gc/workspaces.ts
|
|
1978
3643
|
function workspacesRoot() {
|
|
1979
3644
|
return join$1(getConfigDir(), "workspaces");
|
|
@@ -2091,7 +3756,7 @@ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse
|
|
|
2091
3756
|
continue;
|
|
2092
3757
|
}
|
|
2093
3758
|
if (present) continue;
|
|
2094
|
-
if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
|
|
3759
|
+
if (registryKeys.some((key) => isPathPrefix$1(projectRoot, key))) continue;
|
|
2095
3760
|
if (!isMounted(projectRoot)) {
|
|
2096
3761
|
skipped.push({
|
|
2097
3762
|
dir,
|
|
@@ -2153,39 +3818,68 @@ async function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure
|
|
|
2153
3818
|
}
|
|
2154
3819
|
}
|
|
2155
3820
|
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
3821
|
return {
|
|
2170
|
-
orphaned,
|
|
3822
|
+
orphaned: await mapLimited(classified.orphaned, sizeConcurrency(operation), async (entry) => {
|
|
3823
|
+
operation?.signal.throwIfAborted();
|
|
3824
|
+
const bytes = await measuredDirectorySizeAsync(entry.dir, {
|
|
3825
|
+
timeoutMs: SIZE_TIMEOUT_MS,
|
|
3826
|
+
operation,
|
|
3827
|
+
signal: operation?.signal
|
|
3828
|
+
});
|
|
3829
|
+
return {
|
|
3830
|
+
...entry,
|
|
3831
|
+
bytes
|
|
3832
|
+
};
|
|
3833
|
+
}),
|
|
2171
3834
|
skipped: classified.skipped
|
|
2172
3835
|
};
|
|
2173
3836
|
}
|
|
2174
3837
|
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.";
|
|
3838
|
+
/** How long after its last ios, android or macos run a workspace keeps its build outputs. */
|
|
3839
|
+
const LIGHTEN_AFTER_MS = 9e5;
|
|
3840
|
+
const BUILD_CHECKS = {
|
|
3841
|
+
devServer: false,
|
|
3842
|
+
hosted: false,
|
|
3843
|
+
managedLocks: false
|
|
3844
|
+
};
|
|
3845
|
+
const BUILD_IN_PROGRESS = "a build is in progress";
|
|
3846
|
+
/** Why the build outputs of the workspace at `root` are in use: it is building or running a macOS app. */
|
|
3847
|
+
function workspaceBuildReasons(root, { nativeRun = true } = {}) {
|
|
3848
|
+
const reasons = workspaceInUse(root, {
|
|
3849
|
+
...BUILD_CHECKS,
|
|
3850
|
+
nativeRun
|
|
3851
|
+
});
|
|
3852
|
+
return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
|
|
3853
|
+
}
|
|
3854
|
+
async function workspaceBuildReasonsAsync(root, signal, operation) {
|
|
3855
|
+
const reasons = await workspaceInUseAsync(root, BUILD_CHECKS, signal, operation);
|
|
3856
|
+
return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
|
|
3857
|
+
}
|
|
3858
|
+
async function inTreeBuildDirs(root, signal) {
|
|
3859
|
+
const dirs = [];
|
|
3860
|
+
for (const path of existingDirsOf(root, ARTIFACT_DIRS.build)) if (!await regenerableDirProblem(root, path, signal)) dirs.push(path);
|
|
3861
|
+
return dirs;
|
|
3862
|
+
}
|
|
2175
3863
|
const WORKSPACE_OUTPUT_DIRS = [
|
|
2176
3864
|
"derived-data",
|
|
2177
3865
|
"gradle-build",
|
|
2178
3866
|
"android-cas",
|
|
2179
3867
|
"cache-provider"
|
|
2180
3868
|
];
|
|
3869
|
+
/** Why a workspace whose last ios, android or macos run was at `nativeUsed` keeps its build outputs, or null. */
|
|
3870
|
+
function recentNativeUseReason(nativeUsed, now) {
|
|
3871
|
+
return now - nativeUsed < 9e5 ? `ran ios, android or macos within the last ${LIGHTEN_AFTER_MS / 6e4} minutes` : null;
|
|
3872
|
+
}
|
|
2181
3873
|
const DAY_MS = 864e5;
|
|
2182
3874
|
function planWorkspaceOutputs(entries, { olderThan, now }) {
|
|
2183
|
-
return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
|
|
3875
|
+
return entries.map(({ dir, projectRoot, problem, inTreeDirs = [], bytes, lastUsed, nativeUsed, inUse }) => {
|
|
3876
|
+
const recentNative = recentNativeUseReason(nativeUsed, now);
|
|
2184
3877
|
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];
|
|
3878
|
+
const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : 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
3879
|
return {
|
|
2187
3880
|
dir,
|
|
2188
3881
|
projectRoot,
|
|
3882
|
+
inTreeDirs,
|
|
2189
3883
|
bytes,
|
|
2190
3884
|
idleDays,
|
|
2191
3885
|
willClear: keptCode === null,
|
|
@@ -2204,48 +3898,68 @@ function outputPaths(dir) {
|
|
|
2204
3898
|
} catch {}
|
|
2205
3899
|
return paths;
|
|
2206
3900
|
}
|
|
2207
|
-
async function outputBytes(
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
3901
|
+
async function outputBytes(entries, timeoutMs, signal, operation) {
|
|
3902
|
+
const sizes = await mapLimited(entries.flatMap((paths, index) => paths.map((path) => ({
|
|
3903
|
+
path,
|
|
3904
|
+
index
|
|
3905
|
+
}))), sizeConcurrency(operation), async ({ path, index }) => ({
|
|
3906
|
+
index,
|
|
3907
|
+
size: await measuredDirectorySizeAsync(path, {
|
|
2211
3908
|
timeoutMs,
|
|
2212
3909
|
signal,
|
|
2213
3910
|
operation
|
|
2214
|
-
})
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
3911
|
+
})
|
|
3912
|
+
}));
|
|
3913
|
+
return entries.map((_, index) => {
|
|
3914
|
+
const own = sizes.filter((size) => size.index === index).map((size) => size.size);
|
|
3915
|
+
return own.includes(null) ? null : own.reduce((total, size) => total + (size ?? 0), 0);
|
|
3916
|
+
});
|
|
2219
3917
|
}
|
|
2220
|
-
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation }) {
|
|
3918
|
+
function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation, lighten = false }) {
|
|
2221
3919
|
const mountedVolumes = listMountedVolumes();
|
|
2222
|
-
const outputEntries = (entries) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) =>
|
|
3920
|
+
const outputEntries = (entries, inTree = /* @__PURE__ */ new Map()) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => {
|
|
3921
|
+
const inTreeDirs = inTree.get(entry.dir) ?? [];
|
|
3922
|
+
return Object.assign({}, entry, {
|
|
3923
|
+
inTreeDirs,
|
|
3924
|
+
paths: [...outputPaths(entry.dir), ...inTreeDirs]
|
|
3925
|
+
});
|
|
3926
|
+
}).filter((entry) => entry.paths.length > 0);
|
|
2223
3927
|
const report = (sizes) => ({
|
|
2224
3928
|
root: workspacesRoot(),
|
|
2225
|
-
workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
|
|
3929
|
+
workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
|
|
3930
|
+
lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
|
|
3931
|
+
nativeUsed: lighten && entry.projectRoot !== null ? lastNativeUseFrom(readWorkspaceState$1(entry.projectRoot)) : NaN
|
|
3932
|
+
})), {
|
|
2226
3933
|
olderThan,
|
|
2227
3934
|
now
|
|
2228
3935
|
})
|
|
2229
3936
|
});
|
|
3937
|
+
const inUseOf = (root) => lighten ? workspaceBuildReasons(root) : workspaceInUse(root);
|
|
2230
3938
|
if (!measure && !signal) return report(outputEntries(listWorkspaceDirs()).map(({ paths: _paths, ...entry }) => Object.assign({}, entry, {
|
|
2231
3939
|
bytes: null,
|
|
2232
|
-
inUse: entry.projectRoot === null ? [] :
|
|
3940
|
+
inUse: entry.projectRoot === null ? [] : inUseOf(entry.projectRoot)
|
|
2233
3941
|
})));
|
|
2234
3942
|
return (async () => {
|
|
2235
3943
|
const sizes = [];
|
|
2236
|
-
const
|
|
2237
|
-
|
|
3944
|
+
const listed = await listWorkspaceDirsAsync(signal);
|
|
3945
|
+
const inTree = /* @__PURE__ */ new Map();
|
|
3946
|
+
if (lighten) {
|
|
3947
|
+
for (const { dir, projectRoot } of listed) if (projectRoot !== null && !exclude.includes(dir)) inTree.set(dir, await inTreeBuildDirs(projectRoot, signal));
|
|
3948
|
+
}
|
|
3949
|
+
const entries = outputEntries(listed, inTree);
|
|
3950
|
+
const bytes = measure ? await outputBytes(entries.map((entry) => entry.paths), sizeTimeoutMs, signal, operation) : [];
|
|
3951
|
+
for (const [index, { paths: _paths, ...entry }] of entries.entries()) {
|
|
2238
3952
|
signal?.throwIfAborted();
|
|
2239
3953
|
sizes.push({
|
|
2240
3954
|
...entry,
|
|
2241
|
-
bytes:
|
|
2242
|
-
inUse: entry.projectRoot === null ? [] : signal ? await
|
|
3955
|
+
bytes: bytes[index] ?? null,
|
|
3956
|
+
inUse: entry.projectRoot === null ? [] : !signal ? inUseOf(entry.projectRoot) : lighten ? await workspaceBuildReasonsAsync(entry.projectRoot, signal, operation) : await workspaceInUseAsync(entry.projectRoot, {}, signal, operation)
|
|
2243
3957
|
});
|
|
2244
3958
|
}
|
|
2245
3959
|
return report(sizes);
|
|
2246
3960
|
})();
|
|
2247
3961
|
}
|
|
2248
|
-
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal }, { log, record } = GC_OUTPUT) {
|
|
3962
|
+
async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal, lighten = false }, { log, record } = GC_OUTPUT) {
|
|
2249
3963
|
let failures = 0;
|
|
2250
3964
|
let cleared = 0;
|
|
2251
3965
|
const clearedRoots = [];
|
|
@@ -2269,13 +3983,19 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2269
3983
|
...entry,
|
|
2270
3984
|
problem: null,
|
|
2271
3985
|
lastUsed: workspaceLastUsed(root),
|
|
2272
|
-
|
|
3986
|
+
nativeUsed: lighten ? lastNativeUseFrom(readWorkspaceState$1(root)) : NaN,
|
|
3987
|
+
inUse: lighten && workspaceBuildInProgress(root) ? [BUILD_IN_PROGRESS] : []
|
|
2273
3988
|
}], {
|
|
2274
3989
|
olderThan,
|
|
2275
3990
|
now
|
|
2276
3991
|
});
|
|
2277
3992
|
if (again?.keptReason) return again.keptReason;
|
|
2278
|
-
|
|
3993
|
+
const inTree = [];
|
|
3994
|
+
for (const path of lighten ? existingDirsOf(root, ARTIFACT_DIRS.build) : []) inTree.push({
|
|
3995
|
+
path,
|
|
3996
|
+
problem: await regenerableDirProblem(root, path, signal)
|
|
3997
|
+
});
|
|
3998
|
+
for (const path of [...outputPaths(entry.dir), ...inTree.filter((dir) => !dir.problem).map((dir) => dir.path)]) {
|
|
2279
3999
|
signal?.throwIfAborted();
|
|
2280
4000
|
await rm(path, {
|
|
2281
4001
|
recursive: true,
|
|
@@ -2283,11 +4003,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2283
4003
|
});
|
|
2284
4004
|
}
|
|
2285
4005
|
signal?.throwIfAborted();
|
|
4006
|
+
for (const { path, problem } of inTree) if (problem) log(chalk.yellow(`Kept ${path}: ${problem}`));
|
|
2286
4007
|
return null;
|
|
2287
4008
|
}, {
|
|
2288
4009
|
purpose: "gc",
|
|
2289
4010
|
signal,
|
|
2290
|
-
operation
|
|
4011
|
+
operation,
|
|
4012
|
+
...lighten ? BUILD_CHECKS : {}
|
|
2291
4013
|
});
|
|
2292
4014
|
} catch (error) {
|
|
2293
4015
|
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
@@ -2311,10 +4033,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2311
4033
|
}
|
|
2312
4034
|
cleared += entry.bytes ?? 0;
|
|
2313
4035
|
clearedRoots.push(root);
|
|
2314
|
-
log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
4036
|
+
log(chalk.green(lighten ? `Lightened ${root}: freed ${sizeText(entry.bytes)} of build outputs` : `Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
|
|
2315
4037
|
record("workspaceOutputs", "done", root, { bytes: entry.bytes });
|
|
2316
4038
|
}
|
|
2317
|
-
if (cleared)
|
|
4039
|
+
if (cleared) {
|
|
4040
|
+
const count = `${clearedRoots.length} workspace${clearedRoots.length === 1 ? "" : "s"}`;
|
|
4041
|
+
log(chalk.dim(`${lighten ? `Lightened ${count}, freeing` : "Cleared"} ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
|
|
4042
|
+
}
|
|
2318
4043
|
return {
|
|
2319
4044
|
failures,
|
|
2320
4045
|
cleared: clearedRoots
|
|
@@ -2322,7 +4047,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
|
|
|
2322
4047
|
}
|
|
2323
4048
|
function stillOrphaned({ dir, projectRoot }) {
|
|
2324
4049
|
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));
|
|
4050
|
+
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix$1(projectRoot, key));
|
|
2326
4051
|
}
|
|
2327
4052
|
async function deleteOrphanedWorkspaces(orphaned, output = GC_OUTPUT, operation) {
|
|
2328
4053
|
let failures = 0;
|
|
@@ -2381,4 +4106,4 @@ function sizeText(bytes) {
|
|
|
2381
4106
|
return bytes === null ? "size unknown" : formatBytes(bytes);
|
|
2382
4107
|
}
|
|
2383
4108
|
//#endregion
|
|
2384
|
-
export {
|
|
4109
|
+
export { STIM_VERSION as $, collectIdleDevicesAsync as A, activeBuildState as B, terminateChild as C, createGcResults as Ct, withManagedTunnelRemovalLock as D, readConcurrency as Dt, withManagedTunnelLock as E, mapLimited as Et, parseIdleDuration as F, parseActiveBuild as G, buildStatusLine as H, shutDownIdleDevices as I, runFailureRecord as J, recordBuildPhase as K, workspaceBuildInProgress as L, collectOwnedDeviceActivityAsync as M, idleDeviceLines as N, runningBundleInstancesAsync as O, sizeConcurrency as Ot, idleShutdownCandidates as P, errorRemedy as Q, ACTIVE_BUILD_KEY as R, stopTunnel as S, GC_OUTPUT as St, withManagedRemoteWorktreeRemovalLock as T, eachLimited as Tt, bundlingActivity as U, buildReport as V, estimateBuild as W, tapBuildLog as X, startBuildProgress as Y, thrownRunFailureRecord as Z, withIdleWorkspace as _, measureDu as _t, collectOrphanedWorkspaces as a, recordRunStats as at, resolveSupervisorTarget as b, listBuildSlots as bt, isInsideWorkspaces as c, eraseParkedIosSim as ct, rootPresence as d, teardownOwnedBrowser as dt, createRunRecorder as et, sizeText as f, teardownOwnedIosSim as ft, emptyWorkspaceDir as g, canonical$1 as gt, ARTIFACT_DIRS as h, probePublicHttp as ht, clearWorkspaceOutputs as i, recordCapacityWait as it, collectOwnedDeviceActivity as j, collectIdleDevices as k, listWorkspaceDirs as l, teardownBrowserHeld as lt, workspaceDirRemoved as m, teardownParkedIosSim as mt, REBUILD_COST as n, readStats$1 as nt, collectWorkspaceOutputs as o, statsProjectKey$1 as ot, workspaceBuildReasons as p, teardownParkedAvd as pt, recordFinishedBuild as q, WORKSPACE_OUTPUT_DIRS as r, recordCapacityRefusal as rt, deleteOrphanedWorkspaces as s, eraseParkedAvd as st, LIGHTEN_AFTER_MS as t, readRunEstimates as tt, recentNativeUseReason as u, teardownOwnedAvd as ut, workspaceInUse as v, writeUsageCache as vt, withManagedRemoteWorktreeLock as w, takeGcResults as wt, startTunnelSequence as x, releaseBuildSlot as xt, workspaceInUseAsync as y, acquireBuildSlot as yt, NO_BUILD_PROGRESS as z };
|