stim 1.20.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -8
- package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
- package/dist/act-DsuqEkiM.mjs +2801 -0
- package/dist/activity-CMLmzp7o.mjs +1397 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
- package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
- package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
- package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/android-project-DVWyCbWU.d.mts +391 -0
- package/dist/api.d.mts +810 -64
- package/dist/api.mjs +782 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
- package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
- package/dist/budget-Cvwn54qV.mjs +9285 -0
- package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
- package/dist/build-cache-Dv7RN200.mjs +602 -0
- package/dist/build-plan-DUC8Ka9y.mjs +248 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-OyRoEuSr.mjs +1577 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/config-DlrsduwG.mjs +955 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
- package/dist/device-DklWZLqb.mjs +1068 -0
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
- package/dist/device-host-worker.mjs +76 -79
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
- package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
- package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/doctor-thjWAV0z.mjs +2 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
- package/dist/exec-DEoAhcze.mjs +858 -0
- package/dist/exec-DnYc4HGH.d.mts +78 -0
- package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
- package/dist/idle-C4cZ757K.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
- package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
- package/dist/ios-signing-BtAffm0A.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
- package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
- package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
- package/dist/logs-BfwwadsZ.mjs +479 -0
- package/dist/logs-jTS_lHSo.mjs +248 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/macos-NfV2D7dO.mjs +694 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +123 -248
- package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
- package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
- package/dist/native-android-CqgnOgsT.mjs +645 -0
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-NwUuxbkg.mjs +276 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +59 -41
- package/dist/offload-worker.mjs +478 -115
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
- package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/prebuild-D4JoEAEs.mjs +484 -0
- package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/pull-request-eODyIqZF.d.mts +124 -0
- package/dist/pull-requests.d.mts +2 -120
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
- package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
- package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
- package/dist/reload-D7AVlb8N.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
- package/dist/result-D2Ykz_iD.mjs +435 -0
- package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
- package/dist/selection-DSMbWEX2.mjs +620 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
- package/dist/settings-DpR-1Wnn.mjs +114 -0
- package/dist/settings-write-CzD60rOD.mjs +360 -0
- package/dist/settings.schema.json +176 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-Bps5oius.mjs +200 -0
- package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/start-EojN4WFz.mjs +1364 -0
- package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
- package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
- package/dist/stats-report-BzaPrRm6.mjs +16 -0
- package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
- package/dist/status-EvrK72L2.mjs +2 -0
- package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
- package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +56 -28
- package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
- package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
- package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
- package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
- package/dist/toolchain-BY8X8HXx.mjs +2 -0
- package/dist/toolchain-DpJMHq0n.mjs +876 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
- package/dist/web-run.mjs +13 -12
- package/dist/windows-report.d.mts +1 -0
- package/dist/windows-report.mjs +18 -0
- package/dist/windows-report.ps1 +43 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/xcode-BXn9-xYx.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- package/shim/android-optimizations.gradle +7 -5
- package/dist/activity-BSV745Kz.mjs +0 -629
- package/dist/api-qvOzWyfO.mjs +0 -154
- package/dist/api-run.mjs +0 -294
- package/dist/app-install-BIKtMfPq.mjs +0 -896
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
- package/dist/budget-f7516Zk0.mjs +0 -4106
- package/dist/build-facts-DcLv2HSd.d.mts +0 -111
- package/dist/cancellation-C_22VQTH.mjs +0 -24
- package/dist/client-qQAaXSUs.mjs +0 -1771
- package/dist/config-BYDxlMY3.mjs +0 -562
- package/dist/device-DuiPJQSn.mjs +0 -290
- package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
- package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
- package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
- package/dist/exec-DqKo9GKJ.mjs +0 -268
- package/dist/git-DDfFzkMm.mjs +0 -45
- package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
- package/dist/logs-BGrEUaEs.mjs +0 -383
- package/dist/macos-Ch5VJmcS.mjs +0 -584
- package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
- package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
- package/dist/plan-C4TjdPsR.mjs +0 -260
- package/dist/prebuild-DYbucPXy.mjs +0 -212
- package/dist/preview-78a9kR_-.mjs +0 -1011
- package/dist/react-native-build-CtCccJtb.mjs +0 -239
- package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
- package/dist/remote-cache-OyOyVIS3.mjs +0 -526
- package/dist/result-CaJg5Iat.mjs +0 -186
- package/dist/server-expo-Bigriskl.mjs +0 -2
- package/dist/settings-BORNaaCO.mjs +0 -402
- package/dist/settings-vk3mJGSm.d.mts +0 -13
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/start-BWqzODpb.mjs +0 -1116
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
- package/dist/tailnet-Dh-qiYzG.mjs +0 -265
- package/dist/toolchain-BFkAcstd.mjs +0 -2
- package/dist/toolchain-DxSsNssu.mjs +0 -1657
- package/dist/trigger-9_USgmxz.mjs +0 -2
- package/dist/web-Dq-i3wzJ.mjs +0 -2
- package/dist/worktree-DpLdCHga.mjs +0 -2
- package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
- package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
package/dist/budget-f7516Zk0.mjs
DELETED
|
@@ -1,4106 +0,0 @@
|
|
|
1
|
-
import { D as withConfigLock$1, I as measuredDirectorySize, L as volumeRootFor, M as directorySize, N as formatBytes, P as isOnMountedVolume, V as deviceSlotPlatforms, W as projectDeviceSlots, _ as loadConfig$1, a as clearDevice, d as getConfigDir, j as ownership_claim_exports, k as process_identity_exports, o as clearSupervisor, p as getProject } from "./config-BYDxlMY3.mjs";
|
|
2
|
-
import { t as getExecutor } from "./exec-DqKo9GKJ.mjs";
|
|
3
|
-
import { i as registeredCaches } from "./cache-manifest-XH-fvDUj.mjs";
|
|
4
|
-
import { h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-BX1sJQ2E.mjs";
|
|
5
|
-
import { a as updateWorkspaceState, c as writeWorkspaceState, m as sharedBuildCache, r as readWorkspaceState$1, t as clearWorkspaceStateKey, w as workspaceLogsDir$1, x as workspaceDir } from "./workspace-state-DJt_MMvf.mjs";
|
|
6
|
-
import { a as formatElapsed, l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-BTy8hZ0F.mjs";
|
|
7
|
-
import { A as clearWorkspaceSupervisor, M as readPidFile } from "./project-_phNIJfs.mjs";
|
|
8
|
-
import { A as listAvds, I as ownedAvdDirectory, ft as readParked, it as isStimOwnedSim, rt as isStimOwnedAvd, st as dropParked, z as ownedAvdSerialResolver } from "./android-Mz_Inn3w.mjs";
|
|
9
|
-
import { r as killMetroTree, u as resolveProjectMetro } from "./metro-DnCEjCxs.mjs";
|
|
10
|
-
import { C as recordGcResult, I as listBuildSlots, P as resolveSupervisorTarget, S as teardownParkedIosSim, T as canonicalPath, a as collectWorkspaceOutputs, b as teardownOwnedIosSim, f as withIdleWorkspace, g as eraseParkedIosSim, h as eraseParkedAvd, r as clearWorkspaceOutputs, v as teardownOwnedAvd, x as teardownParkedAvd } from "./workspaces-C36s2jb9.mjs";
|
|
11
|
-
import { a as withWorkspaceProcessLock, o as workspaceProcessLockError } from "./named-ports-4WopRPg-.mjs";
|
|
12
|
-
import { r as createNdjsonWriter } from "./ndjson-GOnbyvso.mjs";
|
|
13
|
-
import { n as supervisorError } from "./errors-DzkMYh8Q.mjs";
|
|
14
|
-
import { a as parseWatchmanPid, d as capacity, g as formatSpace, h as environmentState, i as parseWatchmanClients, n as parseTriggerCount, r as parseWatchRoots, t as parseSubscriberNames } from "./watchman-CPQFZLXY.mjs";
|
|
15
|
-
import { i as createDeviceProcessTables, o as idleForMs, p as workspaceActivity, r as createActivityReader } from "./activity-BSV745Kz.mjs";
|
|
16
|
-
import { d as listAllIosSims, p as listIosDeviceTypes, y as parseRuntimeVersion } from "./ios-RkW4pnQf.mjs";
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
19
|
-
import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
|
|
20
|
-
import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, DEV_SERVER_STOP_REQUEST_KEY, LAST_BUILD_KEYS, MACHINE_OWNER_KINDS, STATS_VERSION, buildWorkerRoot, decodeStats, isJsonObject, loadConfig, parseMissReason, readDevServerStop, readDevServerStopRequest, readRunEstimates, readStats as readStats$1, settingDefinition, settingValueError, statsFile, statsProjectKey as statsProjectKey$1, trimCapacityRefusals, trimCapacityWaits, trimSamples, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { existsSync as existsSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
23
|
-
import { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, cacheDirRefusal, compiledHelper, configDir } from "@stim-cli/core";
|
|
24
|
-
import { homedir, tmpdir } from "os";
|
|
25
|
-
import { homedir as homedir$1, platform, totalmem as totalmem$1 } from "node:os";
|
|
26
|
-
import chalk from "chalk";
|
|
27
|
-
//#region src/engine/stats.ts
|
|
28
|
-
const PLACEMENT_MAX_AGE_MS = 6048e5;
|
|
29
|
-
function emptyStats() {
|
|
30
|
-
return {
|
|
31
|
-
version: STATS_VERSION,
|
|
32
|
-
machine: {},
|
|
33
|
-
projects: {}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function updateStats(record, run, now) {
|
|
37
|
-
const at = new Date(now).toISOString();
|
|
38
|
-
const durationMs = wholeMs(run.durationMs);
|
|
39
|
-
const phases = {
|
|
40
|
-
coldBuildMs: wholeMs(run.coldBuildMs),
|
|
41
|
-
podsMs: wholeMs(run.podsMs)
|
|
42
|
-
};
|
|
43
|
-
const projects = { ...record.projects };
|
|
44
|
-
const scope = { ...projects[run.projectKey] };
|
|
45
|
-
const machine = { ...record.machine };
|
|
46
|
-
const before = scope[run.platform] ?? null;
|
|
47
|
-
const credit = creditMs(before, run, durationMs);
|
|
48
|
-
scope[run.platform] = applyRun(before, run, {
|
|
49
|
-
at,
|
|
50
|
-
durationMs,
|
|
51
|
-
credit,
|
|
52
|
-
phases
|
|
53
|
-
});
|
|
54
|
-
projects[run.projectKey] = scope;
|
|
55
|
-
machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
|
|
56
|
-
at,
|
|
57
|
-
durationMs,
|
|
58
|
-
credit,
|
|
59
|
-
phases
|
|
60
|
-
});
|
|
61
|
-
const history = { ...record.history };
|
|
62
|
-
if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
|
|
63
|
-
const outcome = isHit(run.cacheHit) ? "hit" : "cold";
|
|
64
|
-
const project = { ...history[run.projectKey] };
|
|
65
|
-
const lists = { ...project[run.platform] };
|
|
66
|
-
const sample = {
|
|
67
|
-
at,
|
|
68
|
-
durationMs,
|
|
69
|
-
phases: wholePhases(run.phases),
|
|
70
|
-
...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
|
|
71
|
-
};
|
|
72
|
-
lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
|
|
73
|
-
project[run.platform] = lists;
|
|
74
|
-
history[run.projectKey] = project;
|
|
75
|
-
}
|
|
76
|
-
let placements = record.placements;
|
|
77
|
-
let buildMachines = record.buildMachines;
|
|
78
|
-
if (run.placement) {
|
|
79
|
-
const placement = buildPlacement(run, before, at);
|
|
80
|
-
placements = trimPlacements([...placements ?? [], placement], now);
|
|
81
|
-
if (placement.machine && placement.decision !== "here") buildMachines = {
|
|
82
|
-
...buildMachines,
|
|
83
|
-
[placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
|
|
87
|
-
const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
|
|
88
|
-
return {
|
|
89
|
-
version: STATS_VERSION,
|
|
90
|
-
machine,
|
|
91
|
-
projects,
|
|
92
|
-
...Object.keys(history).length ? { history } : {},
|
|
93
|
-
...placements?.length ? { placements } : {},
|
|
94
|
-
...capacityRefusals.length ? { capacityRefusals } : {},
|
|
95
|
-
...capacityWaits.length ? { capacityWaits } : {},
|
|
96
|
-
...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function buildPlacement(run, before, at) {
|
|
100
|
-
const placement = run.placement;
|
|
101
|
-
const buildMs = wholeMs(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
|
|
102
|
-
const localEstimateMs = wholeMs(before?.lastColdBuildMs);
|
|
103
|
-
const slotWaitMs = wholeMs(placement.slotWaitMs);
|
|
104
|
-
const deviceSlotWaitMs = wholeMs(placement.deviceSlotWaitMs);
|
|
105
|
-
return {
|
|
106
|
-
at,
|
|
107
|
-
project: run.projectKey,
|
|
108
|
-
platform: run.platform,
|
|
109
|
-
decision: placement.decision,
|
|
110
|
-
reason: placement.reason,
|
|
111
|
-
...placement.machine ? { machine: placement.machine } : {},
|
|
112
|
-
...buildMs > 0 ? { buildMs } : {},
|
|
113
|
-
...slotWaitMs > 0 ? { slotWaitMs } : {},
|
|
114
|
-
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
|
|
115
|
-
...localEstimateMs > 0 ? { localEstimateMs } : {},
|
|
116
|
-
...run.failed ? { failed: true } : {}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function applyPlacement(totals, placement) {
|
|
120
|
-
const next = totals ? { ...totals } : {
|
|
121
|
-
offloaded: 0,
|
|
122
|
-
offloadedMs: 0,
|
|
123
|
-
savedMs: 0,
|
|
124
|
-
fallbacks: 0
|
|
125
|
-
};
|
|
126
|
-
if (placement.decision === "fell-back") {
|
|
127
|
-
next.fallbacks += 1;
|
|
128
|
-
next.lastFallbackAt = placement.at;
|
|
129
|
-
return next;
|
|
130
|
-
}
|
|
131
|
-
next.offloaded += 1;
|
|
132
|
-
next.offloadedMs += placement.buildMs ?? 0;
|
|
133
|
-
if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
|
|
134
|
-
next.lastOffloadAt = placement.at;
|
|
135
|
-
return next;
|
|
136
|
-
}
|
|
137
|
-
function trimPlacements(placements, now) {
|
|
138
|
-
return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
|
|
139
|
-
}
|
|
140
|
-
function recordRunStats(run, now) {
|
|
141
|
-
return withConfigLock$1(() => {
|
|
142
|
-
const path = statsFile();
|
|
143
|
-
const loaded = loadForUpdate(path, now);
|
|
144
|
-
if (loaded.newerVersion !== null) return {
|
|
145
|
-
recorded: false,
|
|
146
|
-
note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
|
|
147
|
-
};
|
|
148
|
-
writeStats(path, updateStats(loaded.record, run, now));
|
|
149
|
-
return {
|
|
150
|
-
recorded: true,
|
|
151
|
-
note: loaded.note
|
|
152
|
-
};
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
function recordCapacityRefusal(event, now) {
|
|
156
|
-
try {
|
|
157
|
-
withConfigLock$1(() => {
|
|
158
|
-
const path = statsFile();
|
|
159
|
-
const loaded = loadForUpdate(path, now, false);
|
|
160
|
-
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
161
|
-
const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
|
|
162
|
-
...event,
|
|
163
|
-
at: new Date(now).toISOString(),
|
|
164
|
-
kind: "device"
|
|
165
|
-
}], now);
|
|
166
|
-
writeStats(path, {
|
|
167
|
-
...loaded.record,
|
|
168
|
-
capacityRefusals
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
} catch {}
|
|
172
|
-
}
|
|
173
|
-
function recordCapacityWait(event, now) {
|
|
174
|
-
try {
|
|
175
|
-
withConfigLock$1(() => {
|
|
176
|
-
const path = statsFile();
|
|
177
|
-
const loaded = loadForUpdate(path, now, false);
|
|
178
|
-
if (!loaded.record || loaded.newerVersion !== null) return;
|
|
179
|
-
const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
|
|
180
|
-
platform: event.platform,
|
|
181
|
-
max: event.max,
|
|
182
|
-
workspace: event.workspace,
|
|
183
|
-
ms: wholeMs(event.ms),
|
|
184
|
-
...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
|
|
185
|
-
at: new Date(now).toISOString(),
|
|
186
|
-
kind: "device-wait"
|
|
187
|
-
}], now);
|
|
188
|
-
writeStats(path, {
|
|
189
|
-
...loaded.record,
|
|
190
|
-
capacityWaits
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
} catch {}
|
|
194
|
-
}
|
|
195
|
-
function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
|
|
196
|
-
let projectKey = null;
|
|
197
|
-
let cacheKey = null;
|
|
198
|
-
let coldBuildMs = 0;
|
|
199
|
-
let podsMs = 0;
|
|
200
|
-
let placement = null;
|
|
201
|
-
let deviceSlotWaitMs = 0;
|
|
202
|
-
let recorded = false;
|
|
203
|
-
return {
|
|
204
|
-
setProject(key) {
|
|
205
|
-
projectKey = key;
|
|
206
|
-
},
|
|
207
|
-
setCacheKey(key) {
|
|
208
|
-
cacheKey = key;
|
|
209
|
-
},
|
|
210
|
-
setBuildMs(ms) {
|
|
211
|
-
coldBuildMs = wholeMs(ms);
|
|
212
|
-
},
|
|
213
|
-
setPodsMs(ms) {
|
|
214
|
-
podsMs = wholeMs(ms);
|
|
215
|
-
},
|
|
216
|
-
addDeviceSlotWaitMs(ms) {
|
|
217
|
-
deviceSlotWaitMs += wholeMs(ms);
|
|
218
|
-
},
|
|
219
|
-
deviceSlotWaitMs() {
|
|
220
|
-
return deviceSlotWaitMs || void 0;
|
|
221
|
-
},
|
|
222
|
-
setPlacement(next) {
|
|
223
|
-
placement = next;
|
|
224
|
-
},
|
|
225
|
-
record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
|
|
226
|
-
if (!projectKey || !cacheKey || recorded) return;
|
|
227
|
-
recorded = true;
|
|
228
|
-
const ran = failed ? {} : phases?.() ?? {};
|
|
229
|
-
try {
|
|
230
|
-
const outcome = write({
|
|
231
|
-
platform,
|
|
232
|
-
projectKey,
|
|
233
|
-
failed,
|
|
234
|
-
cacheHit,
|
|
235
|
-
waitedForBuild: Boolean(waited),
|
|
236
|
-
durationMs,
|
|
237
|
-
...offloadedTo ? { offloadedTo } : {},
|
|
238
|
-
...coldBuildMs > 0 ? { coldBuildMs } : {},
|
|
239
|
-
...podsMs > 0 ? { podsMs } : {},
|
|
240
|
-
...Object.keys(ran).length ? { phases: ran } : {},
|
|
241
|
-
...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
|
|
242
|
-
...placement ? { placement: {
|
|
243
|
-
...placement,
|
|
244
|
-
...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
|
|
245
|
-
} } : {}
|
|
246
|
-
}, now());
|
|
247
|
-
if (outcome?.note) note(outcome.note);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
note(`Run statistics could not be recorded: ${error?.message || error}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function loadForUpdate(path, now, recoverCorrupt = true) {
|
|
255
|
-
let text;
|
|
256
|
-
try {
|
|
257
|
-
text = readFileSync(path, "utf-8");
|
|
258
|
-
} catch {
|
|
259
|
-
return {
|
|
260
|
-
record: emptyStats(),
|
|
261
|
-
note: null,
|
|
262
|
-
newerVersion: null
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
const parsed = decodeStats(text);
|
|
266
|
-
if (!parsed.record && parsed.newerVersion === null) {
|
|
267
|
-
if (!recoverCorrupt) return {
|
|
268
|
-
record: null,
|
|
269
|
-
note: null,
|
|
270
|
-
newerVersion: null
|
|
271
|
-
};
|
|
272
|
-
const aside = `${path}.corrupt-${now}`;
|
|
273
|
-
renameSync(path, aside);
|
|
274
|
-
return {
|
|
275
|
-
record: emptyStats(),
|
|
276
|
-
note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
|
|
277
|
-
newerVersion: null
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
if (parsed.newerVersion !== null) return {
|
|
281
|
-
record: emptyStats(),
|
|
282
|
-
note: null,
|
|
283
|
-
newerVersion: parsed.newerVersion
|
|
284
|
-
};
|
|
285
|
-
return {
|
|
286
|
-
record: parsed.record,
|
|
287
|
-
note: null,
|
|
288
|
-
newerVersion: null
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function writeStats(path, record) {
|
|
292
|
-
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
293
|
-
writeFileSync(tmp, `${JSON.stringify(record)}\n`);
|
|
294
|
-
try {
|
|
295
|
-
renameSync(tmp, path);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
rmSync(tmp, { force: true });
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function applyRun(bucket, run, { at, durationMs, credit, phases }) {
|
|
302
|
-
const next = bucket ? { ...bucket } : {
|
|
303
|
-
runs: 0,
|
|
304
|
-
failed: 0,
|
|
305
|
-
hits: 0,
|
|
306
|
-
misses: 0,
|
|
307
|
-
coldRuns: 0,
|
|
308
|
-
coldRunMs: 0,
|
|
309
|
-
hitRuns: 0,
|
|
310
|
-
hitRunMs: 0,
|
|
311
|
-
timeSavedMs: 0,
|
|
312
|
-
firstRunAt: at,
|
|
313
|
-
lastRunAt: at
|
|
314
|
-
};
|
|
315
|
-
next.runs += 1;
|
|
316
|
-
next.lastRunAt = at;
|
|
317
|
-
if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
|
|
318
|
-
if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
|
|
319
|
-
if (run.failed) {
|
|
320
|
-
next.failed += 1;
|
|
321
|
-
return next;
|
|
322
|
-
}
|
|
323
|
-
if (isHit(run.cacheHit)) {
|
|
324
|
-
next.hits += 1;
|
|
325
|
-
if (!run.waitedForBuild) {
|
|
326
|
-
next.hitRuns += 1;
|
|
327
|
-
next.hitRunMs += durationMs;
|
|
328
|
-
next.timeSavedMs += credit;
|
|
329
|
-
}
|
|
330
|
-
return next;
|
|
331
|
-
}
|
|
332
|
-
next.misses += 1;
|
|
333
|
-
if (run.offloadedTo) {
|
|
334
|
-
next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
|
|
335
|
-
next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
|
|
336
|
-
next.lastOffloadHost = run.offloadedTo;
|
|
337
|
-
return next;
|
|
338
|
-
}
|
|
339
|
-
next.coldRuns += 1;
|
|
340
|
-
next.coldRunMs += durationMs;
|
|
341
|
-
return next;
|
|
342
|
-
}
|
|
343
|
-
function creditMs(bucket, run, durationMs) {
|
|
344
|
-
if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
|
|
345
|
-
if (!bucket || bucket.coldRuns <= 0) return 0;
|
|
346
|
-
return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
|
|
347
|
-
}
|
|
348
|
-
function isHit(cacheHit) {
|
|
349
|
-
return cacheHit === "local" || cacheHit === "remote";
|
|
350
|
-
}
|
|
351
|
-
function wholeMs(value) {
|
|
352
|
-
const ms = Number(value);
|
|
353
|
-
return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
|
|
354
|
-
}
|
|
355
|
-
//#endregion
|
|
356
|
-
//#region src/engine/build-detail.ts
|
|
357
|
-
const LINE_MAX = 160;
|
|
358
|
-
const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
|
|
359
|
-
const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
|
|
360
|
-
const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
|
|
361
|
-
const GRADLE_TASK = /^> Task (:\S+)/;
|
|
362
|
-
/**
|
|
363
|
-
* xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
|
|
364
|
-
* that touches nothing still signs framework products last. Targets that end any other way are not counted.
|
|
365
|
-
*/
|
|
366
|
-
const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
|
|
367
|
-
const GRADLE_CONFIGURE = /^> Configure project /;
|
|
368
|
-
/**
|
|
369
|
-
* SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
|
|
370
|
-
* output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
|
|
371
|
-
* `<product>-product` is the link of a product.
|
|
372
|
-
*/
|
|
373
|
-
const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
|
|
374
|
-
const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
|
|
375
|
-
const XCODE_STEPS = [
|
|
376
|
-
[/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
|
|
377
|
-
[/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
|
|
378
|
-
[/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
|
|
379
|
-
[/^PhaseScriptExecution$/, "script"],
|
|
380
|
-
[/^CodeSign$/, "sign"]
|
|
381
|
-
];
|
|
382
|
-
const GRADLE_STEPS = [
|
|
383
|
-
[/[Dd]ex/, "dex"],
|
|
384
|
-
[/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
|
|
385
|
-
[/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
|
|
386
|
-
[/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
|
|
387
|
-
[/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
|
|
388
|
-
];
|
|
389
|
-
function stepOf(name, table) {
|
|
390
|
-
return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
|
|
391
|
-
}
|
|
392
|
-
function clip(text) {
|
|
393
|
-
return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
|
|
394
|
-
}
|
|
395
|
-
function unescape(token) {
|
|
396
|
-
return token.replace(/\\(.)/g, "$1");
|
|
397
|
-
}
|
|
398
|
-
function baseName(path) {
|
|
399
|
-
return path.split("/").findLast(Boolean) ?? path;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* An xcodebuild action line in short form: the action, the file it works on and its target, as in
|
|
403
|
-
* `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
|
|
404
|
-
* as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
|
|
405
|
-
* shows its name instead.
|
|
406
|
-
*/
|
|
407
|
-
function shortXcodeAction(action, args, target) {
|
|
408
|
-
const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
|
|
409
|
-
const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
|
|
410
|
-
return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
|
|
411
|
-
}
|
|
412
|
-
function swiftpmStep(text) {
|
|
413
|
-
return /^Linking\b|-product$/.test(text) ? "link" : "compile";
|
|
414
|
-
}
|
|
415
|
-
/** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
|
|
416
|
-
function parseBuildToolLine(msg) {
|
|
417
|
-
const counted = SWIFTPM_PROGRESS.exec(msg);
|
|
418
|
-
if (counted) {
|
|
419
|
-
const text = counted[3]?.trim() ?? "";
|
|
420
|
-
return {
|
|
421
|
-
kind: "count",
|
|
422
|
-
done: Number(counted[1]),
|
|
423
|
-
total: Number(counted[2]),
|
|
424
|
-
step: swiftpmStep(text),
|
|
425
|
-
line: text ? clip(text) : null
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
429
|
-
const graph = TARGET_GRAPH.exec(msg);
|
|
430
|
-
if (graph) return {
|
|
431
|
-
kind: "total",
|
|
432
|
-
unit: "targets",
|
|
433
|
-
total: Number(graph[1])
|
|
434
|
-
};
|
|
435
|
-
if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
|
|
436
|
-
const action = XCODE_ACTION.exec(msg);
|
|
437
|
-
if (action) {
|
|
438
|
-
const [, name, args, target, project] = action;
|
|
439
|
-
const step = stepOf(name, XCODE_STEPS);
|
|
440
|
-
return {
|
|
441
|
-
kind: "unit",
|
|
442
|
-
unit: "targets",
|
|
443
|
-
id: `${project}/${target}`,
|
|
444
|
-
step,
|
|
445
|
-
line: step ? shortXcodeAction(name, args, target) : null,
|
|
446
|
-
finished: XCODE_TARGET_DONE.test(name)
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
const task = GRADLE_TASK.exec(msg);
|
|
450
|
-
if (task) {
|
|
451
|
-
const path = task[1];
|
|
452
|
-
return {
|
|
453
|
-
kind: "unit",
|
|
454
|
-
unit: "tasks",
|
|
455
|
-
id: path,
|
|
456
|
-
step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
|
|
457
|
-
line: clip(`> Task ${path}`),
|
|
458
|
-
finished: false
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
|
|
465
|
-
* total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
|
|
466
|
-
* its own `[done / total]` counts, which the parser passes through.
|
|
467
|
-
*/
|
|
468
|
-
function createBuildDetailParser() {
|
|
469
|
-
let seen = false;
|
|
470
|
-
let step = null;
|
|
471
|
-
let unit = null;
|
|
472
|
-
let total = null;
|
|
473
|
-
let line = null;
|
|
474
|
-
let tasks = 0;
|
|
475
|
-
let counted = null;
|
|
476
|
-
const targets = /* @__PURE__ */ new Set();
|
|
477
|
-
return {
|
|
478
|
-
push(msg) {
|
|
479
|
-
const parsed = parseBuildToolLine(msg);
|
|
480
|
-
if (!parsed) return false;
|
|
481
|
-
seen = true;
|
|
482
|
-
if (parsed.kind === "total") {
|
|
483
|
-
unit = "targets";
|
|
484
|
-
total = parsed.total;
|
|
485
|
-
return true;
|
|
486
|
-
}
|
|
487
|
-
if (parsed.kind === "configure") {
|
|
488
|
-
step = "configure";
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
if (parsed.kind === "count") {
|
|
492
|
-
unit = "steps";
|
|
493
|
-
counted = {
|
|
494
|
-
done: parsed.done,
|
|
495
|
-
total: parsed.total
|
|
496
|
-
};
|
|
497
|
-
step = parsed.step;
|
|
498
|
-
if (parsed.line) line = parsed.line;
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
unit = parsed.unit;
|
|
502
|
-
if (parsed.unit === "tasks") tasks += 1;
|
|
503
|
-
else if (parsed.finished) targets.add(parsed.id);
|
|
504
|
-
if (parsed.step) step = parsed.step;
|
|
505
|
-
if (parsed.line) line = parsed.line;
|
|
506
|
-
return true;
|
|
507
|
-
},
|
|
508
|
-
detail(updatedAt) {
|
|
509
|
-
if (!seen) return null;
|
|
510
|
-
if (unit === "steps" && counted) return {
|
|
511
|
-
step,
|
|
512
|
-
unit,
|
|
513
|
-
done: Math.min(counted.done, counted.total),
|
|
514
|
-
total: counted.total,
|
|
515
|
-
line,
|
|
516
|
-
updatedAt
|
|
517
|
-
};
|
|
518
|
-
const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
|
|
519
|
-
return {
|
|
520
|
-
step,
|
|
521
|
-
unit,
|
|
522
|
-
done: done !== null && total !== null ? Math.min(done, total) : done,
|
|
523
|
-
total: unit === "tasks" ? null : total,
|
|
524
|
-
line,
|
|
525
|
-
updatedAt
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
//#endregion
|
|
531
|
-
//#region src/engine/build-progress.ts
|
|
532
|
-
const ACTIVE_BUILD_KEY = "activeBuild";
|
|
533
|
-
const NO_BUILD_PROGRESS = {
|
|
534
|
-
step: () => {},
|
|
535
|
-
activity: () => {},
|
|
536
|
-
estimate: () => {},
|
|
537
|
-
miss: () => {},
|
|
538
|
-
hit: () => {},
|
|
539
|
-
deviceSetup: () => {},
|
|
540
|
-
deviceSetupKnown: () => void 0,
|
|
541
|
-
place: () => {},
|
|
542
|
-
waitingOn: () => {},
|
|
543
|
-
waitingFor: () => {},
|
|
544
|
-
output: () => {},
|
|
545
|
-
durations: () => ({}),
|
|
546
|
-
steps: () => null,
|
|
547
|
-
clear: () => {}
|
|
548
|
-
};
|
|
549
|
-
/** `writer`, with each record it writes also passed to `progress.output`. */
|
|
550
|
-
function tapBuildLog(writer, progress) {
|
|
551
|
-
if (progress === NO_BUILD_PROGRESS) return writer;
|
|
552
|
-
return {
|
|
553
|
-
get file() {
|
|
554
|
-
return writer.file;
|
|
555
|
-
},
|
|
556
|
-
write(record) {
|
|
557
|
-
progress.output(record);
|
|
558
|
-
return writer.write(record);
|
|
559
|
-
},
|
|
560
|
-
close: () => writer.close(),
|
|
561
|
-
get written() {
|
|
562
|
-
return writer.written;
|
|
563
|
-
},
|
|
564
|
-
get dropped() {
|
|
565
|
-
return writer.dropped;
|
|
566
|
-
},
|
|
567
|
-
get lastError() {
|
|
568
|
-
return writer.lastError;
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
const DETAIL_WRITE_MS = 2e3;
|
|
573
|
-
function toolLine(record) {
|
|
574
|
-
if (!record || typeof record !== "object") return null;
|
|
575
|
-
const { src, level, msg } = record;
|
|
576
|
-
return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
|
|
577
|
-
}
|
|
578
|
-
function writeBuildDetail(root, claimId, detail) {
|
|
579
|
-
const file = workspaceBuildDetailFile(root);
|
|
580
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
581
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
582
|
-
writeFileSync(tmp, JSON.stringify({
|
|
583
|
-
claimId,
|
|
584
|
-
detail
|
|
585
|
-
}));
|
|
586
|
-
renameSync(tmp, file);
|
|
587
|
-
}
|
|
588
|
-
const COLD_PHASES = [
|
|
589
|
-
"prebuild",
|
|
590
|
-
"pods",
|
|
591
|
-
"compile"
|
|
592
|
-
];
|
|
593
|
-
const HIT_PHASES = ["install", "launch"];
|
|
594
|
-
function settledOutcome(phase, entered) {
|
|
595
|
-
if (COLD_PHASES.includes(phase)) return "cold";
|
|
596
|
-
if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
|
|
597
|
-
return HIT_PHASES.includes(phase) ? "hit" : null;
|
|
598
|
-
}
|
|
599
|
-
function projectHistory(projectKey) {
|
|
600
|
-
try {
|
|
601
|
-
return readStats$1().record?.history?.[projectKey];
|
|
602
|
-
} catch {
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
|
|
607
|
-
const startedAt = new Date(now()).toISOString();
|
|
608
|
-
const record = {
|
|
609
|
-
platform,
|
|
610
|
-
slot,
|
|
611
|
-
startedAt,
|
|
612
|
-
phase: "prepare",
|
|
613
|
-
phaseStartedAt: startedAt,
|
|
614
|
-
phases: [{
|
|
615
|
-
phase: "prepare",
|
|
616
|
-
startedAt
|
|
617
|
-
}],
|
|
618
|
-
claim: {
|
|
619
|
-
root: claim.root,
|
|
620
|
-
path: claim.path,
|
|
621
|
-
claimId: claim.claimId,
|
|
622
|
-
pid: claim.owner.pid
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
let warned = false;
|
|
626
|
-
const guard = (fn) => {
|
|
627
|
-
try {
|
|
628
|
-
fn();
|
|
629
|
-
} catch (error) {
|
|
630
|
-
if (warned) return;
|
|
631
|
-
warned = true;
|
|
632
|
-
note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
|
|
636
|
-
...before(state),
|
|
637
|
-
[ACTIVE_BUILD_KEY]: record
|
|
638
|
-
})));
|
|
639
|
-
write(withInterruptedBuild);
|
|
640
|
-
let projectKey = null;
|
|
641
|
-
const parser = createBuildDetailParser();
|
|
642
|
-
let detailWrittenAt = -Infinity;
|
|
643
|
-
let detailTimer = null;
|
|
644
|
-
const flushDetail = () => {
|
|
645
|
-
detailTimer = null;
|
|
646
|
-
detailWrittenAt = now();
|
|
647
|
-
const detail = parser.detail(new Date(detailWrittenAt).toISOString());
|
|
648
|
-
if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
|
|
649
|
-
};
|
|
650
|
-
const settle = (outcome) => {
|
|
651
|
-
record.outcome = outcome;
|
|
652
|
-
if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
|
|
653
|
-
};
|
|
654
|
-
const waits = /* @__PURE__ */ new Map();
|
|
655
|
-
return {
|
|
656
|
-
step(phase) {
|
|
657
|
-
if (phase === record.phase) return;
|
|
658
|
-
const at = new Date(now()).toISOString();
|
|
659
|
-
record.phase = phase;
|
|
660
|
-
record.phaseStartedAt = at;
|
|
661
|
-
record.phases.push({
|
|
662
|
-
phase,
|
|
663
|
-
startedAt: at
|
|
664
|
-
});
|
|
665
|
-
if (phase !== "wait") delete record.waitingOn;
|
|
666
|
-
delete record.activity;
|
|
667
|
-
const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
|
|
668
|
-
if (outcome) settle(outcome);
|
|
669
|
-
write();
|
|
670
|
-
},
|
|
671
|
-
activity(name, at) {
|
|
672
|
-
if ((record.activity?.name ?? null) === name) return;
|
|
673
|
-
if (name) record.activity = {
|
|
674
|
-
name,
|
|
675
|
-
startedAt: new Date(at ?? now()).toISOString()
|
|
676
|
-
};
|
|
677
|
-
else delete record.activity;
|
|
678
|
-
write();
|
|
679
|
-
},
|
|
680
|
-
estimate(key) {
|
|
681
|
-
if (platform === "macos") return;
|
|
682
|
-
projectKey = key;
|
|
683
|
-
record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
684
|
-
write();
|
|
685
|
-
},
|
|
686
|
-
miss(reason, provisional = false) {
|
|
687
|
-
record.missReason = reason;
|
|
688
|
-
settle("cold");
|
|
689
|
-
if (provisional) record.missProvisional = true;
|
|
690
|
-
else delete record.missProvisional;
|
|
691
|
-
write();
|
|
692
|
-
},
|
|
693
|
-
hit() {
|
|
694
|
-
delete record.missReason;
|
|
695
|
-
delete record.missProvisional;
|
|
696
|
-
settle("hit");
|
|
697
|
-
write();
|
|
698
|
-
},
|
|
699
|
-
deviceSetup(setup) {
|
|
700
|
-
if (setup === void 0) return;
|
|
701
|
-
record.deviceSetup = setup;
|
|
702
|
-
write();
|
|
703
|
-
},
|
|
704
|
-
deviceSetupKnown() {
|
|
705
|
-
return record.deviceSetup;
|
|
706
|
-
},
|
|
707
|
-
place(remote) {
|
|
708
|
-
const current = record.placement;
|
|
709
|
-
if (!remote) {
|
|
710
|
-
if (!current) return;
|
|
711
|
-
delete record.placement;
|
|
712
|
-
return write();
|
|
713
|
-
}
|
|
714
|
-
if (current?.host === remote.host && current.phase === remote.phase) return;
|
|
715
|
-
const at = new Date(now()).toISOString();
|
|
716
|
-
record.placement = {
|
|
717
|
-
host: remote.host,
|
|
718
|
-
phase: remote.phase,
|
|
719
|
-
startedAt: current?.host === remote.host ? current.startedAt : at,
|
|
720
|
-
phaseStartedAt: at
|
|
721
|
-
};
|
|
722
|
-
write();
|
|
723
|
-
},
|
|
724
|
-
waitingFor(info, kind) {
|
|
725
|
-
if (info) waits.set(info.kind, info);
|
|
726
|
-
else if (kind) waits.delete(kind);
|
|
727
|
-
else waits.clear();
|
|
728
|
-
const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
|
|
729
|
-
if (first) record.waitingFor = first;
|
|
730
|
-
else delete record.waitingFor;
|
|
731
|
-
write();
|
|
732
|
-
},
|
|
733
|
-
waitingOn(holder) {
|
|
734
|
-
if (holder === null) {
|
|
735
|
-
if (!record.waitingOn) return;
|
|
736
|
-
delete record.waitingOn;
|
|
737
|
-
} else {
|
|
738
|
-
if (record.waitingOn?.path === holder) return;
|
|
739
|
-
record.waitingOn = { path: holder };
|
|
740
|
-
}
|
|
741
|
-
write();
|
|
742
|
-
},
|
|
743
|
-
output(line) {
|
|
744
|
-
const msg = toolLine(line);
|
|
745
|
-
if (msg === null || !parser.push(msg) || detailTimer) return;
|
|
746
|
-
const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
|
|
747
|
-
if (wait <= 0) return flushDetail();
|
|
748
|
-
detailTimer = setTimeout(flushDetail, wait);
|
|
749
|
-
detailTimer.unref?.();
|
|
750
|
-
},
|
|
751
|
-
durations() {
|
|
752
|
-
return phaseDurations(record.phases, now());
|
|
753
|
-
},
|
|
754
|
-
steps() {
|
|
755
|
-
const detail = parser.detail("");
|
|
756
|
-
return detail?.unit === "steps" ? detail.total : null;
|
|
757
|
-
},
|
|
758
|
-
clear() {
|
|
759
|
-
if (detailTimer) clearTimeout(detailTimer);
|
|
760
|
-
detailTimer = null;
|
|
761
|
-
guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
|
|
762
|
-
guard(() => {
|
|
763
|
-
clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
function phaseDurations(phases, now) {
|
|
769
|
-
const out = {};
|
|
770
|
-
phases.forEach(({ phase, startedAt }, index) => {
|
|
771
|
-
const start = Date.parse(startedAt);
|
|
772
|
-
const next = phases[index + 1];
|
|
773
|
-
const end = next ? Date.parse(next.startedAt) : now;
|
|
774
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
775
|
-
out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
|
|
776
|
-
});
|
|
777
|
-
return out;
|
|
778
|
-
}
|
|
779
|
-
function completedPhaseDurations(phases) {
|
|
780
|
-
if (phases.length < 2) return void 0;
|
|
781
|
-
const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
|
|
782
|
-
return Object.keys(durations).length ? durations : void 0;
|
|
783
|
-
}
|
|
784
|
-
const HISTORY_FIELDS = [
|
|
785
|
-
"devicePlacement",
|
|
786
|
-
"platform",
|
|
787
|
-
"status",
|
|
788
|
-
"configuration",
|
|
789
|
-
"fingerprint",
|
|
790
|
-
"cacheKey",
|
|
791
|
-
"cacheHit",
|
|
792
|
-
"cacheSkipped",
|
|
793
|
-
"durationMs",
|
|
794
|
-
"startedAt",
|
|
795
|
-
"errorCode",
|
|
796
|
-
"missReason",
|
|
797
|
-
"buildMachine",
|
|
798
|
-
"builtOn",
|
|
799
|
-
"offloadedTo",
|
|
800
|
-
"offloadFallback",
|
|
801
|
-
"diagnostics",
|
|
802
|
-
"compileSteps"
|
|
803
|
-
];
|
|
804
|
-
function withHistoryEntry(state, platform, entry) {
|
|
805
|
-
const saved = state[BUILD_HISTORY_KEY];
|
|
806
|
-
const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
|
|
807
|
-
const list = Array.isArray(history[platform]) ? history[platform] : [];
|
|
808
|
-
return {
|
|
809
|
-
...state,
|
|
810
|
-
[BUILD_HISTORY_KEY]: {
|
|
811
|
-
...history,
|
|
812
|
-
[platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function finishedResult(record) {
|
|
817
|
-
if (record.status === "ok") return "succeeded";
|
|
818
|
-
return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
|
|
819
|
-
}
|
|
820
|
-
function recordFinishedBuild(root, record, { update = updateWorkspaceState, now = Date.now, artifactOnly = false } = {}) {
|
|
821
|
-
const platform = record.platform;
|
|
822
|
-
update(root, (state) => {
|
|
823
|
-
const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
824
|
-
const own = active?.platform === platform ? active : null;
|
|
825
|
-
const entry = {
|
|
826
|
-
...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
|
|
827
|
-
result: finishedResult(record),
|
|
828
|
-
slot: own?.slot ?? "default",
|
|
829
|
-
phases: own ? phaseDurations(own.phases, now()) : {}
|
|
830
|
-
};
|
|
831
|
-
if (artifactOnly || platform === "macos") return withHistoryEntry(state, platform, entry);
|
|
832
|
-
const slot = own?.slot ?? "default";
|
|
833
|
-
const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
|
|
834
|
-
const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
|
|
835
|
-
const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
|
|
836
|
-
if (record.devicePlacement || isJsonObject(savedDevice)) {
|
|
837
|
-
const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
|
|
838
|
-
if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
|
|
839
|
-
else delete device.devicePlacement;
|
|
840
|
-
state = slot === "default" ? {
|
|
841
|
-
...state,
|
|
842
|
-
[platform]: device
|
|
843
|
-
} : {
|
|
844
|
-
...state,
|
|
845
|
-
deviceSlots: {
|
|
846
|
-
...slots,
|
|
847
|
-
[slot]: {
|
|
848
|
-
...savedSlot,
|
|
849
|
-
[platform]: device
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
return withHistoryEntry({
|
|
855
|
-
...state,
|
|
856
|
-
lastBuild: record,
|
|
857
|
-
[LAST_BUILD_KEYS[platform]]: record
|
|
858
|
-
}, platform, entry);
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
/** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
|
|
862
|
-
function recordBuildPhase(root, platform, startedAt, phase, ms) {
|
|
863
|
-
updateWorkspaceState(root, (state) => {
|
|
864
|
-
const saved = state[BUILD_HISTORY_KEY];
|
|
865
|
-
const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
|
|
866
|
-
const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
|
|
867
|
-
const entry = list[index];
|
|
868
|
-
if (!isJsonObject(entry)) return state;
|
|
869
|
-
const phases = isJsonObject(entry.phases) ? entry.phases : {};
|
|
870
|
-
const next = [...list];
|
|
871
|
-
next[index] = {
|
|
872
|
-
...entry,
|
|
873
|
-
phases: {
|
|
874
|
-
...phases,
|
|
875
|
-
[phase]: Math.round(ms)
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
return {
|
|
879
|
-
...state,
|
|
880
|
-
[BUILD_HISTORY_KEY]: {
|
|
881
|
-
...saved,
|
|
882
|
-
[platform]: next
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
function withInterruptedBuild(state) {
|
|
888
|
-
const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
|
|
889
|
-
if (!left) return state;
|
|
890
|
-
const last = state[BUILD_HISTORY_KEY]?.[left.platform]?.[0];
|
|
891
|
-
if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
|
|
892
|
-
return withHistoryEntry(state, left.platform, {
|
|
893
|
-
platform: left.platform,
|
|
894
|
-
status: "failed",
|
|
895
|
-
result: "interrupted",
|
|
896
|
-
slot: left.slot,
|
|
897
|
-
configuration: null,
|
|
898
|
-
fingerprint: null,
|
|
899
|
-
cacheKey: null,
|
|
900
|
-
cacheHit: false,
|
|
901
|
-
cacheSkipped: false,
|
|
902
|
-
durationMs: null,
|
|
903
|
-
startedAt: left.startedAt,
|
|
904
|
-
phases: {
|
|
905
|
-
...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
|
|
906
|
-
[left.phase]: 0
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
function parseActiveBuild(value) {
|
|
911
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
912
|
-
const record = value;
|
|
913
|
-
const claim = record.claim;
|
|
914
|
-
if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
|
|
915
|
-
if (!isPhase(record.phase)) return null;
|
|
916
|
-
if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
|
|
917
|
-
if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
|
|
918
|
-
const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
|
|
919
|
-
const missReason = parseMissReason(record.missReason);
|
|
920
|
-
const placement = parsePlacement(record.placement);
|
|
921
|
-
const waitingOn = parseWaitingOn(record.waitingOn);
|
|
922
|
-
const waitingFor = parseWaitingFor(record.waitingFor);
|
|
923
|
-
const estimate = parseEstimate(record.estimate);
|
|
924
|
-
const activity = parseActivity(record.activity);
|
|
925
|
-
return {
|
|
926
|
-
platform: record.platform,
|
|
927
|
-
slot: typeof record.slot === "string" ? record.slot : "default",
|
|
928
|
-
startedAt: record.startedAt,
|
|
929
|
-
phase: record.phase,
|
|
930
|
-
phaseStartedAt: record.phaseStartedAt,
|
|
931
|
-
phases,
|
|
932
|
-
...missReason ? { missReason } : {},
|
|
933
|
-
...missReason && record.missProvisional === true ? { missProvisional: true } : {},
|
|
934
|
-
...placement ? { placement } : {},
|
|
935
|
-
...waitingOn ? { waitingOn } : {},
|
|
936
|
-
...waitingFor ? { waitingFor } : {},
|
|
937
|
-
...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
|
|
938
|
-
...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
|
|
939
|
-
...estimate ? { estimate } : {},
|
|
940
|
-
...activity ? { activity } : {},
|
|
941
|
-
claim: {
|
|
942
|
-
root: claim.root,
|
|
943
|
-
path: typeof claim.path === "string" ? claim.path : "",
|
|
944
|
-
claimId: claim.claimId,
|
|
945
|
-
pid: typeof claim.pid === "number" ? claim.pid : 0
|
|
946
|
-
}
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
function parsePlacement(value) {
|
|
950
|
-
if (!value || typeof value !== "object") return null;
|
|
951
|
-
const { host, phase, startedAt, phaseStartedAt } = value;
|
|
952
|
-
if ([
|
|
953
|
-
host,
|
|
954
|
-
phase,
|
|
955
|
-
startedAt,
|
|
956
|
-
phaseStartedAt
|
|
957
|
-
].some((field) => typeof field !== "string")) return null;
|
|
958
|
-
return {
|
|
959
|
-
host,
|
|
960
|
-
phase,
|
|
961
|
-
startedAt,
|
|
962
|
-
phaseStartedAt
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
function parseWaitingFor(value) {
|
|
966
|
-
if (!value || typeof value !== "object") return null;
|
|
967
|
-
const { kind, inUse, max, since } = value;
|
|
968
|
-
if (kind !== "build-slot" && kind !== "device-slot") return null;
|
|
969
|
-
if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
|
|
970
|
-
if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
|
|
971
|
-
return {
|
|
972
|
-
kind,
|
|
973
|
-
inUse,
|
|
974
|
-
max,
|
|
975
|
-
since
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
function parseActivity(value) {
|
|
979
|
-
if (!value || typeof value !== "object") return null;
|
|
980
|
-
const { name, startedAt } = value;
|
|
981
|
-
if (name !== "booting" && name !== "bundling" && name !== "waiting-ready") return null;
|
|
982
|
-
return typeof startedAt === "string" ? {
|
|
983
|
-
name,
|
|
984
|
-
startedAt
|
|
985
|
-
} : null;
|
|
986
|
-
}
|
|
987
|
-
function parseWaitingOn(value) {
|
|
988
|
-
const path = value?.path;
|
|
989
|
-
return typeof path === "string" && path ? { path } : null;
|
|
990
|
-
}
|
|
991
|
-
function parseEstimate(value) {
|
|
992
|
-
if (!value || typeof value !== "object") return null;
|
|
993
|
-
const { outcome, expectedMs, basis, phaseMs, planned } = value;
|
|
994
|
-
if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
|
|
995
|
-
if (expectedMs !== null && typeof expectedMs !== "number") return null;
|
|
996
|
-
if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
|
|
997
|
-
const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
|
|
998
|
-
return {
|
|
999
|
-
outcome,
|
|
1000
|
-
expectedMs,
|
|
1001
|
-
basis,
|
|
1002
|
-
phaseMs: Object.fromEntries(durations),
|
|
1003
|
-
planned: planned.filter(isPhase)
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
function isPhase(value) {
|
|
1007
|
-
return BUILD_PHASES.includes(value);
|
|
1008
|
-
}
|
|
1009
|
-
function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
|
|
1010
|
-
if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
|
|
1011
|
-
if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
|
|
1012
|
-
return survey.unresolved.length ? "unknown" : "stale";
|
|
1013
|
-
}
|
|
1014
|
-
function median(values) {
|
|
1015
|
-
if (!values.length) return null;
|
|
1016
|
-
const sorted = values.toSorted((a, b) => a - b);
|
|
1017
|
-
const mid = Math.floor(sorted.length / 2);
|
|
1018
|
-
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
1019
|
-
}
|
|
1020
|
-
function estimateBuild(history, platform, known, deviceSetup) {
|
|
1021
|
-
const lists = platform === "macos" ? void 0 : history?.[platform];
|
|
1022
|
-
const outcome = known ?? latestOutcome(lists);
|
|
1023
|
-
const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
|
|
1024
|
-
const phaseMs = {};
|
|
1025
|
-
const planned = [];
|
|
1026
|
-
for (const phase of BUILD_PHASES) {
|
|
1027
|
-
const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
|
|
1028
|
-
const ms = median(durations);
|
|
1029
|
-
if (ms === null) continue;
|
|
1030
|
-
phaseMs[phase] = ms;
|
|
1031
|
-
if (durations.length * 2 >= samples.length) planned.push(phase);
|
|
1032
|
-
}
|
|
1033
|
-
return {
|
|
1034
|
-
outcome,
|
|
1035
|
-
expectedMs: median(samples.map((sample) => sample.durationMs)),
|
|
1036
|
-
basis: samples.length,
|
|
1037
|
-
phaseMs,
|
|
1038
|
-
planned
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
const MIN_TAGGED_SAMPLES = 3;
|
|
1042
|
-
function matchDeviceSetup(samples, deviceSetup) {
|
|
1043
|
-
if (deviceSetup === void 0) return samples;
|
|
1044
|
-
const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
|
|
1045
|
-
if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
|
|
1046
|
-
return samples.filter((sample) => sample.deviceSetup !== true);
|
|
1047
|
-
}
|
|
1048
|
-
function latestOutcome(lists) {
|
|
1049
|
-
let latest = null;
|
|
1050
|
-
for (const outcome of ["hit", "cold"]) {
|
|
1051
|
-
const last = lists?.[outcome]?.at(-1);
|
|
1052
|
-
const at = last ? Date.parse(last.at) : NaN;
|
|
1053
|
-
if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
|
|
1054
|
-
outcome,
|
|
1055
|
-
at
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
return latest?.outcome ?? null;
|
|
1059
|
-
}
|
|
1060
|
-
function buildReport(record, { state, history }) {
|
|
1061
|
-
const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
|
|
1062
|
-
const plannedPhases = estimate.planned.flatMap((phase) => {
|
|
1063
|
-
const expectedMs = estimate.phaseMs[phase];
|
|
1064
|
-
return expectedMs === void 0 ? [] : [{
|
|
1065
|
-
phase,
|
|
1066
|
-
expectedMs
|
|
1067
|
-
}];
|
|
1068
|
-
});
|
|
1069
|
-
const completedPhaseMs = completedPhaseDurations(record.phases);
|
|
1070
|
-
return {
|
|
1071
|
-
platform: record.platform,
|
|
1072
|
-
slot: record.slot,
|
|
1073
|
-
state,
|
|
1074
|
-
phase: record.phase,
|
|
1075
|
-
startedAt: record.startedAt,
|
|
1076
|
-
phaseStartedAt: record.phaseStartedAt,
|
|
1077
|
-
outcome: estimate.outcome,
|
|
1078
|
-
outcomeKnown: record.outcome !== void 0,
|
|
1079
|
-
...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
|
|
1080
|
-
expectedMs: estimate.expectedMs,
|
|
1081
|
-
expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
|
|
1082
|
-
...completedPhaseMs ? { completedPhaseMs } : {},
|
|
1083
|
-
basis: estimate.basis,
|
|
1084
|
-
plannedPhases: plannedPhases.length ? plannedPhases : null,
|
|
1085
|
-
...record.missReason ? { missReason: record.missReason } : {},
|
|
1086
|
-
...record.missReason && record.missProvisional ? { missProvisional: true } : {},
|
|
1087
|
-
placement: record.placement ?? "local",
|
|
1088
|
-
...record.waitingOn ? { waitingOn: record.waitingOn } : {},
|
|
1089
|
-
...record.waitingFor ? { waitingFor: record.waitingFor } : {},
|
|
1090
|
-
...record.activity ? { activity: { ...record.activity } } : {}
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* The run's `bundling` activity with Metro's percent, when the request Metro reports in flight is the one the activity
|
|
1095
|
-
* started on: same platform and same start, since both come from the request's `bundle_response_started` record.
|
|
1096
|
-
*/
|
|
1097
|
-
function bundlingActivity(activity, platform, bundle) {
|
|
1098
|
-
if (activity?.name !== "bundling" || !bundle?.bundling || bundle.percent === void 0) return activity;
|
|
1099
|
-
if (bundle.platform !== platform || bundle.startedAt !== activity.startedAt) return activity;
|
|
1100
|
-
return {
|
|
1101
|
-
...activity,
|
|
1102
|
-
percent: bundle.percent
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
function remainingText(remainingMs) {
|
|
1106
|
-
return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
|
|
1107
|
-
}
|
|
1108
|
-
function activityText({ name, percent, startedAt }, now) {
|
|
1109
|
-
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)))}`;
|
|
1110
|
-
}
|
|
1111
|
-
function buildStatusLine(report, now) {
|
|
1112
|
-
const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
|
|
1113
|
-
if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
|
|
1114
|
-
const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
|
|
1115
|
-
const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
|
|
1116
|
-
const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
|
|
1117
|
-
const activity = report.activity ? ` (${activityText(report.activity, now)})` : "";
|
|
1118
|
-
const head = `build: ${report.platform}${slot} ${report.phase}${activity}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
|
|
1119
|
-
if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
|
|
1120
|
-
if (report.expectedMs === null) return head;
|
|
1121
|
-
const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
|
|
1122
|
-
const remainingMs = report.expectedMs - elapsedMs;
|
|
1123
|
-
return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
|
|
1124
|
-
}
|
|
1125
|
-
//#endregion
|
|
1126
|
-
//#region src/supervisor/stop-cause.ts
|
|
1127
|
-
function requestDevServerStop(root, processToken, requester) {
|
|
1128
|
-
if (!processToken) return;
|
|
1129
|
-
try {
|
|
1130
|
-
writeWorkspaceState(root, { [DEV_SERVER_STOP_REQUEST_KEY]: {
|
|
1131
|
-
processToken,
|
|
1132
|
-
by: requester.by,
|
|
1133
|
-
pid: process.pid,
|
|
1134
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1135
|
-
...requester.workspace ? { workspace: requester.workspace } : {}
|
|
1136
|
-
} });
|
|
1137
|
-
} catch {}
|
|
1138
|
-
}
|
|
1139
|
-
function withdrawDevServerStopRequest(root, processToken) {
|
|
1140
|
-
if (!processToken) return;
|
|
1141
|
-
try {
|
|
1142
|
-
clearWorkspaceStateKey(root, DEV_SERVER_STOP_REQUEST_KEY, (value) => {
|
|
1143
|
-
return readDevServerStopRequest({ [DEV_SERVER_STOP_REQUEST_KEY]: value })?.processToken === processToken;
|
|
1144
|
-
});
|
|
1145
|
-
} catch {}
|
|
1146
|
-
}
|
|
1147
|
-
function devServerStopRecord(trigger, request, at) {
|
|
1148
|
-
if (request) return {
|
|
1149
|
-
reason: "requested",
|
|
1150
|
-
at,
|
|
1151
|
-
by: request.by,
|
|
1152
|
-
byPid: request.pid,
|
|
1153
|
-
...request.workspace ? { byWorkspace: request.workspace } : {}
|
|
1154
|
-
};
|
|
1155
|
-
return trigger.kind === "signal" ? {
|
|
1156
|
-
reason: "signal",
|
|
1157
|
-
at,
|
|
1158
|
-
signal: trigger.signal
|
|
1159
|
-
} : {
|
|
1160
|
-
reason: "server-exited",
|
|
1161
|
-
at,
|
|
1162
|
-
mode: trigger.mode,
|
|
1163
|
-
code: trigger.code,
|
|
1164
|
-
signal: trigger.signal
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
function describeDevServerStop(record) {
|
|
1168
|
-
if (record.reason === "idle") return `idle for ${record.idleMinutes} minutes (metro.idleStopMinutes)`;
|
|
1169
|
-
if (record.reason === "requested") return `stop requested by ${record.by} (pid ${record.byPid}${record.byWorkspace ? ` in ${record.byWorkspace}` : ""})`;
|
|
1170
|
-
if (record.reason === "signal") return `received ${record.signal} with no Stim stop request recorded, so it likely came from outside Stim`;
|
|
1171
|
-
const detail = record.signal ? `signal ${record.signal}` : `exit code ${record.code ?? "unknown"}`;
|
|
1172
|
-
const expoSignal = record.mode === "expo-child" && record.code === 0 ? "; Expo CLI also exits 0 on SIGTERM, and no Stim stop request was recorded" : "";
|
|
1173
|
-
return `the ${record.mode} dev server exited unexpectedly (${detail})${expoSignal}`;
|
|
1174
|
-
}
|
|
1175
|
-
function devServerStopLevel(record) {
|
|
1176
|
-
if (record.reason === "server-exited") return "error";
|
|
1177
|
-
return record.reason === "signal" ? "warn" : "info";
|
|
1178
|
-
}
|
|
1179
|
-
/** The log line for a recorded supervisor whose process is proven gone, or null while it may still run. */
|
|
1180
|
-
function vanishedSupervisorMessage(record) {
|
|
1181
|
-
if (!record || typeof record.pid !== "number") return null;
|
|
1182
|
-
const identity = (0, process_identity_exports.inspectProcessIdentity)(record);
|
|
1183
|
-
if (identity !== "gone" && identity !== "different") return null;
|
|
1184
|
-
const started = typeof record.startedAt === "string" ? ` (started ${record.startedAt})` : "";
|
|
1185
|
-
return `supervisor pid ${record.pid}${started} ${VANISHED}`;
|
|
1186
|
-
}
|
|
1187
|
-
const VANISHED = "is gone and recorded no cause: usually SIGKILL, a crash or a machine restart";
|
|
1188
|
-
function logVanishedSupervisor(root, record) {
|
|
1189
|
-
const msg = vanishedSupervisorMessage(record);
|
|
1190
|
-
if (!msg) return;
|
|
1191
|
-
const writer = createNdjsonWriter(join(workspaceLogsDir$1(root), "metro.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
|
|
1192
|
-
writer.write({
|
|
1193
|
-
src: "metro",
|
|
1194
|
-
level: "warn",
|
|
1195
|
-
event: "supervisor_vanished",
|
|
1196
|
-
msg
|
|
1197
|
-
});
|
|
1198
|
-
writer.close();
|
|
1199
|
-
}
|
|
1200
|
-
/** The recorded stop, else a vanished supervisor when status proved the recorded one gone. */
|
|
1201
|
-
function metroLastStop(state, supervisor) {
|
|
1202
|
-
const recorded = readDevServerStop(state);
|
|
1203
|
-
if (recorded) return recorded;
|
|
1204
|
-
if (supervisor?.status !== "stale" || typeof supervisor.pid !== "number") return null;
|
|
1205
|
-
const startedAt = typeof supervisor.startedAt === "string" ? supervisor.startedAt : null;
|
|
1206
|
-
return {
|
|
1207
|
-
reason: "vanished",
|
|
1208
|
-
pid: supervisor.pid,
|
|
1209
|
-
startedAt
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
function describeMetroLastStop(stop) {
|
|
1213
|
-
return stop.reason === "vanished" ? `supervisor pid ${stop.pid} ${VANISHED}` : describeDevServerStop(stop);
|
|
1214
|
-
}
|
|
1215
|
-
//#endregion
|
|
1216
|
-
//#region src/supervisor/cache-reset.ts
|
|
1217
|
-
async function stopOwnedMetro(root, requester) {
|
|
1218
|
-
const project = getProject(root);
|
|
1219
|
-
const port = project?.metroPort;
|
|
1220
|
-
const target = resolveSupervisorTarget({
|
|
1221
|
-
state: readWorkspaceState$1(root)?.supervisor,
|
|
1222
|
-
record: project?.supervisor,
|
|
1223
|
-
reservedPort: port
|
|
1224
|
-
});
|
|
1225
|
-
if (target.status === "unverified") return {
|
|
1226
|
-
status: "unverified",
|
|
1227
|
-
reason: target.reason ?? "unknown identity"
|
|
1228
|
-
};
|
|
1229
|
-
if (port && target.status !== "ours" && !(await resolveProjectMetro(port, root)).missing) return {
|
|
1230
|
-
status: "external",
|
|
1231
|
-
reason: `a dev server Stim did not start answers on port ${port}`
|
|
1232
|
-
};
|
|
1233
|
-
if (target.status !== "ours") return { status: "not-running" };
|
|
1234
|
-
const expected = {
|
|
1235
|
-
pid: target.pid,
|
|
1236
|
-
processToken: target.processToken
|
|
1237
|
-
};
|
|
1238
|
-
requestDevServerStop(root, target.processToken, requester);
|
|
1239
|
-
const signalled = killMetroTree(target.pid, target.processToken);
|
|
1240
|
-
if (!signalled) withdrawDevServerStopRequest(root, target.processToken);
|
|
1241
|
-
if (!signalled || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
|
|
1242
|
-
status: "stuck",
|
|
1243
|
-
reason: `supervisor pid ${target.pid} did not exit`
|
|
1244
|
-
};
|
|
1245
|
-
if (port && !(await resolveProjectMetro(port, root)).missing) return {
|
|
1246
|
-
status: "port-occupied",
|
|
1247
|
-
reason: `port ${port} is still occupied after stopping Metro`
|
|
1248
|
-
};
|
|
1249
|
-
clearWorkspaceSupervisor(root, expected);
|
|
1250
|
-
clearSupervisor(root, expected);
|
|
1251
|
-
return { status: "stopped" };
|
|
1252
|
-
}
|
|
1253
|
-
async function stopOwnedMetroForReset(root) {
|
|
1254
|
-
const stopped = await stopOwnedMetro(root, { by: "stim start --reset-cache" });
|
|
1255
|
-
switch (stopped.status) {
|
|
1256
|
-
case "unverified": throw supervisorError("STIM_SUPERVISOR_EXITED", `Cannot reset Metro: ${stopped.reason}.`, "Stop it with the tool that started it, then retry. Stim leaves unverified processes alone.");
|
|
1257
|
-
case "external": throw supervisorError("STIM_SUPERVISOR_EXITED", "Cannot reset an externally started dev server.", "Stop it with the tool that started it, then retry `stim start --reset-cache`.");
|
|
1258
|
-
case "stuck": throw supervisorError("STIM_SUPERVISOR_EXITED", "The owned Metro supervisor could not be stopped for a cache reset.", "Inspect `stim status` and `stim logs`, then retry. No cache reset was recorded.");
|
|
1259
|
-
case "port-occupied": throw supervisorError("STIM_SUPERVISOR_EXITED", `Port ${getProject(root)?.metroPort} is still occupied after stopping Metro.`, "Inspect the listener before retrying. Stim does not terminate unrecorded processes.");
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
//#endregion
|
|
1263
|
-
//#region src/cache/caches.ts
|
|
1264
|
-
function xcodeDerivedDataRoot() {
|
|
1265
|
-
return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
1266
|
-
}
|
|
1267
|
-
function compilationCache() {
|
|
1268
|
-
const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
1269
|
-
if (!existsSync$1(dir)) return null;
|
|
1270
|
-
return {
|
|
1271
|
-
name: "Xcode compilation cache",
|
|
1272
|
-
dir,
|
|
1273
|
-
prune: "atomic",
|
|
1274
|
-
note: "index-backed, so it can only be emptied whole"
|
|
1275
|
-
};
|
|
1276
|
-
}
|
|
1277
|
-
function gradleBuildCache() {
|
|
1278
|
-
const gradleHome = process.env.GRADLE_USER_HOME || join$1(homedir(), ".gradle");
|
|
1279
|
-
const dir = resolve$1(gradleHome, "caches", "build-cache-1");
|
|
1280
|
-
if (!existsSync$1(dir)) return null;
|
|
1281
|
-
return {
|
|
1282
|
-
name: "Gradle build cache",
|
|
1283
|
-
dir,
|
|
1284
|
-
prune: "report-only",
|
|
1285
|
-
note: "shared with every Gradle build; report only, never pruned or emptied by Stim"
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
function metroFileMaps() {
|
|
1289
|
-
const root = tmpdir();
|
|
1290
|
-
if (!existsSync$1(root)) return null;
|
|
1291
|
-
let names;
|
|
1292
|
-
try {
|
|
1293
|
-
names = readdirSync$1(root).filter((n) => n.startsWith("metro-file-map-"));
|
|
1294
|
-
} catch {
|
|
1295
|
-
return null;
|
|
1296
|
-
}
|
|
1297
|
-
const uid = process.getuid?.();
|
|
1298
|
-
const files = [];
|
|
1299
|
-
let bytes = 0;
|
|
1300
|
-
for (const n of names) try {
|
|
1301
|
-
const st = statSync$1(join$1(root, n));
|
|
1302
|
-
if (uid !== void 0 && st.uid !== uid) continue;
|
|
1303
|
-
files.push(join$1(root, n));
|
|
1304
|
-
bytes += st.size;
|
|
1305
|
-
} catch {}
|
|
1306
|
-
if (!files.length) return null;
|
|
1307
|
-
return {
|
|
1308
|
-
name: "Metro file maps",
|
|
1309
|
-
dir: root,
|
|
1310
|
-
files,
|
|
1311
|
-
bytes,
|
|
1312
|
-
prune: "entries",
|
|
1313
|
-
note: `${files.length} file(s), one per project root Metro has served`
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
function discoverCaches() {
|
|
1317
|
-
const gradle = gradleBuildCache();
|
|
1318
|
-
const gradleDir = gradle ? canonicalCacheDir(gradle.dir) : null;
|
|
1319
|
-
const registered = protectNestedMetroAncestors(suppressLegacyMetroAncestors(registeredCaches().map((c) => Object.assign({}, c, {
|
|
1320
|
-
name: c.name ?? c.dir,
|
|
1321
|
-
note: c.note ?? "registered",
|
|
1322
|
-
prune: c.prune,
|
|
1323
|
-
source: "registered"
|
|
1324
|
-
}))));
|
|
1325
|
-
const detected = [
|
|
1326
|
-
compilationCache(),
|
|
1327
|
-
gradle,
|
|
1328
|
-
metroFileMaps()
|
|
1329
|
-
].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
|
|
1330
|
-
return distinguishSharedNames(mergeCacheDescriptors([...registered, ...detected], gradleDir));
|
|
1331
|
-
}
|
|
1332
|
-
function distinguishSharedNames(caches) {
|
|
1333
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1334
|
-
for (const cache of caches) counts.set(cache.name, (counts.get(cache.name) ?? 0) + 1);
|
|
1335
|
-
const labelled = caches.map((cache) => (counts.get(cache.name) ?? 0) < 2 ? cache : Object.assign({}, cache, { name: `${cache.name}: ${basename$1(cache.dir) || cache.dir}` }));
|
|
1336
|
-
const labels = /* @__PURE__ */ new Map();
|
|
1337
|
-
for (const cache of labelled) labels.set(cache.name, (labels.get(cache.name) ?? 0) + 1);
|
|
1338
|
-
return labelled.map((cache, i) => (labels.get(cache.name) ?? 0) < 2 || cache === caches[i] ? cache : Object.assign({}, cache, { name: `${caches[i].name}: ${cache.dir}` }));
|
|
1339
|
-
}
|
|
1340
|
-
function suppressLegacyMetroAncestors(caches) {
|
|
1341
|
-
const current = caches.filter(isCurrentMetroStore);
|
|
1342
|
-
return caches.filter((cache) => {
|
|
1343
|
-
if (!isLegacyMetroStore(cache)) return true;
|
|
1344
|
-
return !current.some((candidate) => isNamedMetroChild(cache.dir, candidate.dir));
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
function protectNestedMetroAncestors(caches) {
|
|
1348
|
-
const current = caches.filter(isCurrentMetroStore);
|
|
1349
|
-
return caches.map((cache) => {
|
|
1350
|
-
if (!isCurrentMetroStore(cache)) return cache;
|
|
1351
|
-
if (!current.some((candidate) => isNamedMetroChild(cache.dir, candidate.dir))) return cache;
|
|
1352
|
-
return Object.assign({}, cache, {
|
|
1353
|
-
prune: "report-only",
|
|
1354
|
-
note: "named Metro store is also an override parent; report only while its child is registered"
|
|
1355
|
-
});
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
function isLegacyMetroStore(cache) {
|
|
1359
|
-
return isMetroStore(cache) && cache.layout === void 0;
|
|
1360
|
-
}
|
|
1361
|
-
function isCurrentMetroStore(cache) {
|
|
1362
|
-
return isMetroStore(cache) && cache.layout === METRO_NAMED_CACHE_LAYOUT;
|
|
1363
|
-
}
|
|
1364
|
-
function isMetroStore(cache) {
|
|
1365
|
-
return cache.source === "registered" && cache.name === "Metro transform cache" && cache.prune === "entries" && cache.entriesDepth === 2;
|
|
1366
|
-
}
|
|
1367
|
-
function isNamedMetroChild(parent, candidate) {
|
|
1368
|
-
const dir = canonicalCacheDir(parent);
|
|
1369
|
-
const child = canonicalCacheDir(candidate);
|
|
1370
|
-
return dirname$1(child) === dir || canonicalCacheDir(dirname$1(candidate)) === dir;
|
|
1371
|
-
}
|
|
1372
|
-
function mergeCacheDescriptors(caches, gradleDir) {
|
|
1373
|
-
const merged = [];
|
|
1374
|
-
for (const cache of caches) {
|
|
1375
|
-
const descriptor = protectGradleCache(cache, gradleDir);
|
|
1376
|
-
const dir = canonicalCacheDir(descriptor.dir);
|
|
1377
|
-
const exact = merged.findIndex((candidate) => canonicalCacheDir(candidate.dir) === dir);
|
|
1378
|
-
if (exact !== -1) {
|
|
1379
|
-
const current = merged[exact];
|
|
1380
|
-
if (pruneStrength(descriptor.prune) > pruneStrength(current.prune)) merged[exact] = Object.assign({}, current, {
|
|
1381
|
-
prune: descriptor.prune,
|
|
1382
|
-
note: descriptor.note
|
|
1383
|
-
});
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
if (gradleDir && isGradleProtectedDescriptor(descriptor, gradleDir)) {
|
|
1387
|
-
if (merged.some((candidate) => {
|
|
1388
|
-
const candidateDir = canonicalCacheDir(candidate.dir);
|
|
1389
|
-
return isGradleProtectedDescriptor(candidate, gradleDir) && cachePathContains(candidateDir, dir);
|
|
1390
|
-
})) continue;
|
|
1391
|
-
for (let i = merged.length - 1; i >= 0; i--) {
|
|
1392
|
-
const candidate = merged[i];
|
|
1393
|
-
const candidateDir = canonicalCacheDir(candidate.dir);
|
|
1394
|
-
if (isGradleProtectedDescriptor(candidate, gradleDir) && cachePathContains(dir, candidateDir)) merged.splice(i, 1);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
merged.push(descriptor);
|
|
1398
|
-
}
|
|
1399
|
-
return merged;
|
|
1400
|
-
}
|
|
1401
|
-
function protectGradleCache(cache, gradleDir) {
|
|
1402
|
-
if (cache.files) return cache;
|
|
1403
|
-
if (!gradleDir || !cachePathsOverlap(canonicalCacheDir(cache.dir), gradleDir)) return cache;
|
|
1404
|
-
return Object.assign({}, cache, {
|
|
1405
|
-
prune: "report-only",
|
|
1406
|
-
note: "shared with every Gradle build; report only, never pruned or emptied by Stim"
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
function isGradleProtectedDescriptor(cache, gradleDir) {
|
|
1410
|
-
return cache.prune === "report-only" && cachePathsOverlap(canonicalCacheDir(cache.dir), gradleDir);
|
|
1411
|
-
}
|
|
1412
|
-
function pruneStrength(prune) {
|
|
1413
|
-
if (prune === "report-only") return 3;
|
|
1414
|
-
if (prune === "atomic") return 2;
|
|
1415
|
-
return 1;
|
|
1416
|
-
}
|
|
1417
|
-
function cachePathsOverlap(left, right) {
|
|
1418
|
-
return cachePathContains(left, right) || cachePathContains(right, left);
|
|
1419
|
-
}
|
|
1420
|
-
function cachePathContains(parent, child) {
|
|
1421
|
-
const rel = relative$1(parent, child);
|
|
1422
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
1423
|
-
}
|
|
1424
|
-
function canonicalCacheDir(dir) {
|
|
1425
|
-
try {
|
|
1426
|
-
return realpathSync$1(dir);
|
|
1427
|
-
} catch {
|
|
1428
|
-
return resolve$1(dir);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
function sizeCaches(caches) {
|
|
1432
|
-
return caches.map((c) => Object.assign({}, c, { bytes: c.bytes ?? directorySize(c.dir, { timeoutMs: "unbounded" }) }));
|
|
1433
|
-
}
|
|
1434
|
-
function pruneCache(cache, { olderThanDays, now = Date.now(), protect, evictBytes, byMtime = false } = {}) {
|
|
1435
|
-
const cutoff = now - olderThanDays * 24 * 60 * 60 * 1e3;
|
|
1436
|
-
if (cache.prune === "report-only") return {
|
|
1437
|
-
removed: 0,
|
|
1438
|
-
bytes: 0,
|
|
1439
|
-
skipped: "report-only shared cache; Stim never deletes it"
|
|
1440
|
-
};
|
|
1441
|
-
if (cache.prune === "atomic") return {
|
|
1442
|
-
removed: 0,
|
|
1443
|
-
bytes: 0,
|
|
1444
|
-
skipped: "index-backed; empty it whole or not at all"
|
|
1445
|
-
};
|
|
1446
|
-
if (!cache.files && cacheDirRefusal(cache.dir)) return {
|
|
1447
|
-
removed: 0,
|
|
1448
|
-
bytes: 0,
|
|
1449
|
-
skipped: `${cache.dir} is not a directory this cache owns`
|
|
1450
|
-
};
|
|
1451
|
-
const entries = cache.files ?? entriesAtDepth(cache.dir, cache.entriesDepth ?? 1);
|
|
1452
|
-
const candidates = [];
|
|
1453
|
-
let protectedEntries = 0;
|
|
1454
|
-
for (const entry of entries) try {
|
|
1455
|
-
const st = statSync$1(entry);
|
|
1456
|
-
const used = byMtime ? st.mtimeMs : Math.max(st.atimeMs, st.mtimeMs);
|
|
1457
|
-
if (used >= cutoff) continue;
|
|
1458
|
-
if (protect?.(entry)) {
|
|
1459
|
-
protectedEntries++;
|
|
1460
|
-
continue;
|
|
1461
|
-
}
|
|
1462
|
-
candidates.push({
|
|
1463
|
-
entry,
|
|
1464
|
-
used,
|
|
1465
|
-
isDirectory: st.isDirectory(),
|
|
1466
|
-
size: st.size
|
|
1467
|
-
});
|
|
1468
|
-
} catch {}
|
|
1469
|
-
if (evictBytes !== void 0) candidates.sort((a, b) => a.used - b.used);
|
|
1470
|
-
let removed = 0;
|
|
1471
|
-
let bytes = 0;
|
|
1472
|
-
let failed = 0;
|
|
1473
|
-
for (const { entry, isDirectory, size } of candidates) {
|
|
1474
|
-
if (evictBytes !== void 0 && bytes >= evictBytes) break;
|
|
1475
|
-
try {
|
|
1476
|
-
if (!existsSync$1(entry) || statSync$1(entry).mtimeMs >= cutoff) continue;
|
|
1477
|
-
const entryBytes = isDirectory ? directorySize(entry, { timeoutMs: "unbounded" }) : size;
|
|
1478
|
-
rmSync$1(entry, {
|
|
1479
|
-
recursive: true,
|
|
1480
|
-
force: true
|
|
1481
|
-
});
|
|
1482
|
-
removed++;
|
|
1483
|
-
bytes += entryBytes;
|
|
1484
|
-
} catch {
|
|
1485
|
-
failed++;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
return {
|
|
1489
|
-
removed,
|
|
1490
|
-
bytes,
|
|
1491
|
-
failed,
|
|
1492
|
-
skipped: null,
|
|
1493
|
-
protectedEntries
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
function entriesAtDepth(dir, depth) {
|
|
1497
|
-
let level = [dir];
|
|
1498
|
-
for (let i = 0; i < depth; i++) {
|
|
1499
|
-
const next = [];
|
|
1500
|
-
for (const parent of level) {
|
|
1501
|
-
let names;
|
|
1502
|
-
try {
|
|
1503
|
-
names = readdirSync$1(parent);
|
|
1504
|
-
} catch {
|
|
1505
|
-
continue;
|
|
1506
|
-
}
|
|
1507
|
-
for (const name of names) next.push(join$1(parent, name));
|
|
1508
|
-
}
|
|
1509
|
-
level = next;
|
|
1510
|
-
}
|
|
1511
|
-
return level;
|
|
1512
|
-
}
|
|
1513
|
-
//#endregion
|
|
1514
|
-
//#region src/footprint.ts
|
|
1515
|
-
const SOURCE = "stim-footprint.swift";
|
|
1516
|
-
const COMPILE_TIMEOUT_MS = 12e4;
|
|
1517
|
-
const READ_TIMEOUT_MS = 5e3;
|
|
1518
|
-
function helperSource() {
|
|
1519
|
-
const here = fileURLToPath(import.meta.url);
|
|
1520
|
-
if (basename(dirname(here)) === "src") return fileURLToPath(new URL(`../helper/${SOURCE}`, import.meta.url));
|
|
1521
|
-
return join(dirname(here), SOURCE);
|
|
1522
|
-
}
|
|
1523
|
-
async function buildHelper() {
|
|
1524
|
-
const exec = getExecutor();
|
|
1525
|
-
if (!exec.runFileQuiet("xcode-select", ["-p"], { timeoutMs: READ_TIMEOUT_MS })) return null;
|
|
1526
|
-
const source = helperSource();
|
|
1527
|
-
try {
|
|
1528
|
-
return await compiledHelper({
|
|
1529
|
-
dir: join(configDir(), "helpers"),
|
|
1530
|
-
name: "stim-footprint",
|
|
1531
|
-
inputs: [source],
|
|
1532
|
-
version: "",
|
|
1533
|
-
compile: async (output) => {
|
|
1534
|
-
await exec.runFileAsync("xcrun", [
|
|
1535
|
-
"swiftc",
|
|
1536
|
-
"-O",
|
|
1537
|
-
"-swift-version",
|
|
1538
|
-
"5",
|
|
1539
|
-
"-o",
|
|
1540
|
-
output,
|
|
1541
|
-
source
|
|
1542
|
-
], { timeoutMs: COMPILE_TIMEOUT_MS });
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
} catch {
|
|
1546
|
-
return null;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
const RETRY_BACKOFF_INITIAL_MS = 1e4;
|
|
1550
|
-
const RETRY_BACKOFF_MAX_MS = 6e5;
|
|
1551
|
-
let built = null;
|
|
1552
|
-
let failure = null;
|
|
1553
|
-
/** The `stim-footprint` helper, compiled on first use. A success is kept for the process's life; a failure retries after a doubling backoff instead of being pinned forever. */
|
|
1554
|
-
function footprintHelper() {
|
|
1555
|
-
const home = configDir();
|
|
1556
|
-
if (built?.home === home) return built.promise;
|
|
1557
|
-
if (failure?.home === home && Date.now() < failure.nextRetryAt) return Promise.resolve(null);
|
|
1558
|
-
const promise = buildHelper().then((helper) => {
|
|
1559
|
-
if (helper === null) {
|
|
1560
|
-
const backoffMs = failure?.home === home ? Math.min(failure.backoffMs * 2, RETRY_BACKOFF_MAX_MS) : RETRY_BACKOFF_INITIAL_MS;
|
|
1561
|
-
failure = {
|
|
1562
|
-
home,
|
|
1563
|
-
nextRetryAt: Date.now() + backoffMs,
|
|
1564
|
-
backoffMs
|
|
1565
|
-
};
|
|
1566
|
-
if (built?.promise === promise) built = null;
|
|
1567
|
-
return null;
|
|
1568
|
-
}
|
|
1569
|
-
return helper;
|
|
1570
|
-
});
|
|
1571
|
-
built = {
|
|
1572
|
-
home,
|
|
1573
|
-
promise
|
|
1574
|
-
};
|
|
1575
|
-
return promise;
|
|
1576
|
-
}
|
|
1577
|
-
/** Parses the helper's `<pid> <bytes>` lines into footprint bytes by pid. */
|
|
1578
|
-
function parseFootprints(output) {
|
|
1579
|
-
const footprints = /* @__PURE__ */ new Map();
|
|
1580
|
-
for (const line of output.split("\n")) {
|
|
1581
|
-
const match = /^(\d+) (\d+)$/.exec(line.trim());
|
|
1582
|
-
if (match) footprints.set(Number(match[1]), Number(match[2]));
|
|
1583
|
-
}
|
|
1584
|
-
return footprints;
|
|
1585
|
-
}
|
|
1586
|
-
/** Every readable process's physical footprint in bytes, or null when the helper is unavailable or fails. */
|
|
1587
|
-
async function readFootprints() {
|
|
1588
|
-
const helper = await footprintHelper();
|
|
1589
|
-
const output = helper ? getExecutor().runFileQuiet(helper, [], { timeoutMs: READ_TIMEOUT_MS }) : null;
|
|
1590
|
-
return output === null ? null : parseFootprints(output);
|
|
1591
|
-
}
|
|
1592
|
-
//#endregion
|
|
1593
|
-
//#region src/machine-usage.ts
|
|
1594
|
-
const SHARED = [
|
|
1595
|
-
{
|
|
1596
|
-
name: "CoreSimulator services",
|
|
1597
|
-
pattern: /\/CoreSimulator\.framework\/|\/Simulator\.app\/Contents\/MacOS\//
|
|
1598
|
-
},
|
|
1599
|
-
{
|
|
1600
|
-
name: "adb server",
|
|
1601
|
-
pattern: /(^|\/)adb\s.*\bfork-server\b/
|
|
1602
|
-
},
|
|
1603
|
-
{
|
|
1604
|
-
name: "Android emulator services",
|
|
1605
|
-
pattern: /\/emulator\/netsimd(\s|$)/
|
|
1606
|
-
},
|
|
1607
|
-
{
|
|
1608
|
-
name: "Gradle daemon",
|
|
1609
|
-
pattern: /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\b/
|
|
1610
|
-
},
|
|
1611
|
-
{
|
|
1612
|
-
name: "Kotlin daemon",
|
|
1613
|
-
pattern: /\bKotlinCompileDaemon\b/
|
|
1614
|
-
},
|
|
1615
|
-
{
|
|
1616
|
-
name: "Watchman",
|
|
1617
|
-
pattern: /(^|\/)watchman(\s|$)/
|
|
1618
|
-
}
|
|
1619
|
-
];
|
|
1620
|
-
const STIM_SERVER_COMMAND = /(^|[\s/])stim-server(\.mjs)?(\s|$)/;
|
|
1621
|
-
const LAUNCHD_SIM = /^launchd_sim\s.*\/Devices\/([^/\s]+)\//;
|
|
1622
|
-
const MAX_DEPTH = 64;
|
|
1623
|
-
const EMULATOR = /(?:^|\/)(?:qemu-system-[^\s/]+|emulator)(?:\s.*)?\s(?:-avd\s+|@)(\S+)/;
|
|
1624
|
-
function emulatorAvd(command) {
|
|
1625
|
-
return EMULATOR.exec(command)?.[1] ?? null;
|
|
1626
|
-
}
|
|
1627
|
-
function workspaceOwner(kind, env, name, id) {
|
|
1628
|
-
return {
|
|
1629
|
-
kind,
|
|
1630
|
-
name,
|
|
1631
|
-
workspace: env.path,
|
|
1632
|
-
id,
|
|
1633
|
-
owned: true
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
function rank(owner) {
|
|
1637
|
-
return MACHINE_OWNER_KINDS.indexOf(owner.kind);
|
|
1638
|
-
}
|
|
1639
|
-
/**
|
|
1640
|
-
* Attributes every process in `processes` to at most one owner: the owner of its nearest ancestor (itself included)
|
|
1641
|
-
* that is a root. Roots are each booted simulator's `launchd_sim`, each emulator launcher and qemu process, and each
|
|
1642
|
-
* workspace's verified supervisor and Metro, running build and Chrome. Only a process under none of those falls back
|
|
1643
|
-
* to stim-server or a machine-wide service in `SHARED`, so a `simctl` a workspace's log collector runs stays with the
|
|
1644
|
-
* workspace. An emulator counts in the workspace that records its AVD whenever its process runs, including while adb
|
|
1645
|
-
* does not list it. A process with no root ancestor is left out. `footprints` holds physical footprint bytes by pid,
|
|
1646
|
-
* or is null when they could not be read, and the owners' `memoryMb` then sums resident sizes. A process missing from
|
|
1647
|
-
* `footprints`, another user's or one that exited between the two reads, counts its resident size.
|
|
1648
|
-
*/
|
|
1649
|
-
function attributeMachineUsage({ processes, footprints, environments, roots, simNames }) {
|
|
1650
|
-
const owners = /* @__PURE__ */ new Map();
|
|
1651
|
-
const rootOf = /* @__PURE__ */ new Map();
|
|
1652
|
-
const fallbackOf = /* @__PURE__ */ new Map();
|
|
1653
|
-
const claim = (pid, key, owner, into = rootOf) => {
|
|
1654
|
-
if (!owners.has(key)) owners.set(key, owner);
|
|
1655
|
-
into.set(pid, key);
|
|
1656
|
-
};
|
|
1657
|
-
const sims = /* @__PURE__ */ new Map();
|
|
1658
|
-
const emulators = /* @__PURE__ */ new Map();
|
|
1659
|
-
for (const env of environments) for (const device of [{
|
|
1660
|
-
slot: "default",
|
|
1661
|
-
ios: env.ios,
|
|
1662
|
-
android: env.android
|
|
1663
|
-
}, ...env.slots ?? []]) {
|
|
1664
|
-
const slot = device.slot === "default" ? {} : { slot: device.slot };
|
|
1665
|
-
if (device.ios?.state === "Booted") sims.set(device.ios.udid.toUpperCase(), {
|
|
1666
|
-
kind: "simulator",
|
|
1667
|
-
name: device.ios.name ?? device.ios.udid,
|
|
1668
|
-
workspace: env.path,
|
|
1669
|
-
...slot,
|
|
1670
|
-
id: device.ios.udid,
|
|
1671
|
-
owned: device.ios.owned
|
|
1672
|
-
});
|
|
1673
|
-
if (device.android?.name && !device.android.physical) emulators.set(device.android.name, {
|
|
1674
|
-
kind: "emulator",
|
|
1675
|
-
name: device.android.name,
|
|
1676
|
-
workspace: env.path,
|
|
1677
|
-
...slot,
|
|
1678
|
-
id: device.android.name,
|
|
1679
|
-
owned: device.android.owned
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
for (const p of processes) {
|
|
1683
|
-
const shared = SHARED.find(({ pattern }) => pattern.test(p.command));
|
|
1684
|
-
if (shared) {
|
|
1685
|
-
const owner = {
|
|
1686
|
-
kind: "shared",
|
|
1687
|
-
name: shared.name,
|
|
1688
|
-
workspace: null,
|
|
1689
|
-
id: null,
|
|
1690
|
-
owned: false
|
|
1691
|
-
};
|
|
1692
|
-
claim(p.pid, `shared:${shared.name}`, owner, fallbackOf);
|
|
1693
|
-
continue;
|
|
1694
|
-
}
|
|
1695
|
-
if (STIM_SERVER_COMMAND.test(p.command)) {
|
|
1696
|
-
claim(p.pid, "server", {
|
|
1697
|
-
kind: "server",
|
|
1698
|
-
name: "stim-server",
|
|
1699
|
-
workspace: null,
|
|
1700
|
-
id: null,
|
|
1701
|
-
owned: false
|
|
1702
|
-
}, fallbackOf);
|
|
1703
|
-
continue;
|
|
1704
|
-
}
|
|
1705
|
-
const udid = LAUNCHD_SIM.exec(p.command)?.[1]?.toUpperCase();
|
|
1706
|
-
if (udid) {
|
|
1707
|
-
const owner = sims.get(udid) ?? {
|
|
1708
|
-
kind: "simulator",
|
|
1709
|
-
name: simNames[udid] ?? udid,
|
|
1710
|
-
workspace: null,
|
|
1711
|
-
id: udid,
|
|
1712
|
-
owned: false
|
|
1713
|
-
};
|
|
1714
|
-
claim(p.pid, `simulator:${udid}`, owner);
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const avd = emulatorAvd(p.command);
|
|
1718
|
-
if (avd) {
|
|
1719
|
-
const owner = emulators.get(avd) ?? {
|
|
1720
|
-
kind: "emulator",
|
|
1721
|
-
name: avd,
|
|
1722
|
-
workspace: null,
|
|
1723
|
-
id: avd,
|
|
1724
|
-
owned: false
|
|
1725
|
-
};
|
|
1726
|
-
claim(p.pid, `emulator:${avd}`, owner);
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
const present = new Set(processes.map((p) => p.pid));
|
|
1730
|
-
const byPath = new Map(environments.map((env) => [env.path, env]));
|
|
1731
|
-
for (const root of roots) {
|
|
1732
|
-
const env = byPath.get(root.path);
|
|
1733
|
-
if (!env) continue;
|
|
1734
|
-
const metroPids = [root.supervisorPid, env.metro?.running ? env.metro.pid : null];
|
|
1735
|
-
for (const pid of metroPids) if (pid && present.has(pid)) {
|
|
1736
|
-
const port = env.metro?.port ?? null;
|
|
1737
|
-
claim(pid, `metro:${env.path}`, workspaceOwner("metro", env, port ? `Metro :${port}` : "Metro", port ? String(port) : null));
|
|
1738
|
-
}
|
|
1739
|
-
if (root.build && present.has(root.build.pid)) {
|
|
1740
|
-
const label = {
|
|
1741
|
-
ios: "iOS build",
|
|
1742
|
-
android: "Android build",
|
|
1743
|
-
macos: "macOS build"
|
|
1744
|
-
}[root.build.platform];
|
|
1745
|
-
claim(root.build.pid, `build:${env.path}`, workspaceOwner("build", env, label, root.build.platform));
|
|
1746
|
-
}
|
|
1747
|
-
for (const pid of root.macosPids ?? []) if (present.has(pid)) claim(pid, `macos:${env.path}`, workspaceOwner("macos", env, env.macos?.product ?? "macOS app", null));
|
|
1748
|
-
for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
|
|
1749
|
-
}
|
|
1750
|
-
const parentOf = new Map(processes.map((p) => [p.pid, p.ppid]));
|
|
1751
|
-
const totals = /* @__PURE__ */ new Map();
|
|
1752
|
-
for (const p of processes) {
|
|
1753
|
-
let pid = p.pid;
|
|
1754
|
-
let key;
|
|
1755
|
-
let fallback;
|
|
1756
|
-
for (let depth = 0; pid !== void 0 && pid > 1 && depth < MAX_DEPTH; depth++) {
|
|
1757
|
-
key = rootOf.get(pid);
|
|
1758
|
-
if (key) break;
|
|
1759
|
-
fallback ??= fallbackOf.get(pid);
|
|
1760
|
-
pid = parentOf.get(pid);
|
|
1761
|
-
}
|
|
1762
|
-
key ??= fallback;
|
|
1763
|
-
if (!key) continue;
|
|
1764
|
-
const total = totals.get(key) ?? {
|
|
1765
|
-
rssKb: 0,
|
|
1766
|
-
memoryKb: 0,
|
|
1767
|
-
cpu: 0,
|
|
1768
|
-
processes: 0
|
|
1769
|
-
};
|
|
1770
|
-
total.rssKb += p.rssKb;
|
|
1771
|
-
const footprint = footprints?.get(p.pid);
|
|
1772
|
-
total.memoryKb += footprint === void 0 ? p.rssKb : footprint / 1024;
|
|
1773
|
-
total.cpu += p.cpuPercent;
|
|
1774
|
-
total.processes += 1;
|
|
1775
|
-
totals.set(key, total);
|
|
1776
|
-
}
|
|
1777
|
-
const list = [];
|
|
1778
|
-
for (const [key, owner] of owners) {
|
|
1779
|
-
const total = totals.get(key);
|
|
1780
|
-
if (total) list.push({
|
|
1781
|
-
...owner,
|
|
1782
|
-
cpuPercent: Math.round(total.cpu),
|
|
1783
|
-
residentMb: Math.round(total.rssKb / 1024),
|
|
1784
|
-
memoryMb: Math.round(total.memoryKb / 1024),
|
|
1785
|
-
processes: total.processes
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
|
|
1789
|
-
return {
|
|
1790
|
-
memorySource: footprints ? "footprint" : "rss",
|
|
1791
|
-
owners: list
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
//#endregion
|
|
1795
|
-
//#region src/commands/gc/progress.ts
|
|
1796
|
-
function phase(label, text) {
|
|
1797
|
-
console.error(chalk.dim(phaseLine(label, text)));
|
|
1798
|
-
}
|
|
1799
|
-
//#endregion
|
|
1800
|
-
//#region src/commands/gc/memory.ts
|
|
1801
|
-
const WATCHMAN_KIND = "watchman";
|
|
1802
|
-
const GRADLE_DAEMONS_KIND = "gradle-daemons";
|
|
1803
|
-
/** The memory kind `--cache` names, or null for any other name. */
|
|
1804
|
-
function memoryCacheKind(cache) {
|
|
1805
|
-
const wanted = cache?.trim().toLowerCase();
|
|
1806
|
-
return wanted === "watchman" || wanted === "gradle-daemons" ? wanted : null;
|
|
1807
|
-
}
|
|
1808
|
-
/** Which helpers a gc run inspects: both without --cache, one for its own kind, none for any other --cache. */
|
|
1809
|
-
function memoryScope(cache) {
|
|
1810
|
-
if (!cache?.trim()) return {
|
|
1811
|
-
watchman: true,
|
|
1812
|
-
gradle: true
|
|
1813
|
-
};
|
|
1814
|
-
const kind = memoryCacheKind(cache);
|
|
1815
|
-
return kind ? {
|
|
1816
|
-
watchman: kind === WATCHMAN_KIND,
|
|
1817
|
-
gradle: kind === GRADLE_DAEMONS_KIND
|
|
1818
|
-
} : null;
|
|
1819
|
-
}
|
|
1820
|
-
const WATCHMAN_TIMEOUT_MS = 5e3;
|
|
1821
|
-
const WATCHMAN_DOCTOR_BYTES = 2 * 1024 ** 3;
|
|
1822
|
-
const GRADLE_STATUS_TIMEOUT_MS = 2e4;
|
|
1823
|
-
const GRADLE_STOP_TIMEOUT_MS = 6e4;
|
|
1824
|
-
const LSOF_TIMEOUT_MS = 1e4;
|
|
1825
|
-
const ROOT_PROBES_AT_ONCE = 4;
|
|
1826
|
-
const GRADLE_DAEMON_CLASS = /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\s+(\S+)/;
|
|
1827
|
-
const KOTLIN_DAEMON_CLASS = /^\S.*?\/bin\/java\s.*\sorg\.jetbrains\.kotlin\.daemon\.KotlinCompileDaemon(\s|$)/;
|
|
1828
|
-
/**
|
|
1829
|
-
* The Gradle version, Java home and distribution of a `GradleDaemon` process from its command line. The
|
|
1830
|
-
* distribution is the directory whose `lib/` holds the daemon's main jar (`gradle-daemon-main-<v>.jar`, or
|
|
1831
|
-
* `gradle-launcher-<v>.jar` before Gradle 8.13), so `<distribution>/bin/gradle` is the daemon's own launcher.
|
|
1832
|
-
*/
|
|
1833
|
-
function parseGradleDaemonCommand(command) {
|
|
1834
|
-
const version = GRADLE_DAEMON_CLASS.exec(command)?.[1];
|
|
1835
|
-
const java = /^(\/.*?)\/bin\/java\s/.exec(command)?.[1];
|
|
1836
|
-
if (!version || !java) return null;
|
|
1837
|
-
const jar = /(?:^|\s)-(?:cp|classpath)\s+(?:[^\s]*:)?(\/.*?\/lib\/gradle-(?:daemon-main|launcher)-[^/\s:]+\.jar)(?=[\s:])/.exec(command)?.[1];
|
|
1838
|
-
return {
|
|
1839
|
-
version,
|
|
1840
|
-
javaHome: java,
|
|
1841
|
-
distribution: jar ? dirname(dirname(jar)) : null
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
function isKotlinDaemonCommand(command) {
|
|
1845
|
-
return KOTLIN_DAEMON_CLASS.test(command) && !GRADLE_DAEMON_CLASS.test(command);
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* The offload client whose Gradle home under `workerRoot` a daemon uses: named by its classpath, as stim-server
|
|
1849
|
-
* matches it, or by the Gradle home it runs in.
|
|
1850
|
-
*/
|
|
1851
|
-
function offloadClientOf(command, workerRoot, gradleHome) {
|
|
1852
|
-
const root = resolve(workerRoot).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1853
|
-
const home = new RegExp(`${root}/([^/\\s]+)/cache/gradle(/|$)`);
|
|
1854
|
-
return home.exec(command)?.[1] ?? (gradleHome ? home.exec(gradleHome)?.[1] ?? null : null);
|
|
1855
|
-
}
|
|
1856
|
-
/** `gradle --status` rows: the state of each daemon pid it lists, lowercased (idle, busy, stopped, ...). */
|
|
1857
|
-
function parseGradleStatus(output) {
|
|
1858
|
-
const states = /* @__PURE__ */ new Map();
|
|
1859
|
-
for (const line of output.split("\n")) {
|
|
1860
|
-
const match = /^\s*(\d+)\s+([A-Z][A-Z_]*)\s+\S/.exec(line);
|
|
1861
|
-
if (match) states.set(Number(match[1]), match[2].toLowerCase());
|
|
1862
|
-
}
|
|
1863
|
-
return states;
|
|
1864
|
-
}
|
|
1865
|
-
/** `lsof -F pnT` output: each pid's open file names and its ESTABLISHED TCP connections. */
|
|
1866
|
-
function parseLsof(output) {
|
|
1867
|
-
const processes = /* @__PURE__ */ new Map();
|
|
1868
|
-
let current = null;
|
|
1869
|
-
let name = null;
|
|
1870
|
-
for (const line of output.split("\n")) if (line.startsWith("p")) {
|
|
1871
|
-
const pid = Number(line.slice(1));
|
|
1872
|
-
current = processes.get(pid) ?? {
|
|
1873
|
-
files: [],
|
|
1874
|
-
connections: []
|
|
1875
|
-
};
|
|
1876
|
-
processes.set(pid, current);
|
|
1877
|
-
} else if (current && line.startsWith("n")) {
|
|
1878
|
-
name = line.slice(1);
|
|
1879
|
-
current.files.push(name);
|
|
1880
|
-
} else if (current && name?.includes("->") && line === "TST=ESTABLISHED") current.connections.push(name);
|
|
1881
|
-
return processes;
|
|
1882
|
-
}
|
|
1883
|
-
/**
|
|
1884
|
-
* The process at the other end of each of `pid`'s connections, found among the other processes lsof listed by the
|
|
1885
|
-
* reversed `remote->local` name; null for a peer lsof did not list.
|
|
1886
|
-
*/
|
|
1887
|
-
function connectionPeers(pid, lsof) {
|
|
1888
|
-
return (lsof.get(pid)?.connections ?? []).map((connection) => {
|
|
1889
|
-
const [local, remote] = connection.split("->");
|
|
1890
|
-
const reversed = `${remote}->${local}`;
|
|
1891
|
-
for (const [other, entry] of lsof) if (other !== pid && entry.connections.includes(reversed)) return other;
|
|
1892
|
-
return null;
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
/** The Gradle user home a daemon runs in: the one whose `daemon/<version>/daemon-<pid>.out.log` it holds open. */
|
|
1896
|
-
function gradleHomeFromFiles(pid, version, files) {
|
|
1897
|
-
const suffix = `/daemon/${version}/daemon-${pid}.out.log`;
|
|
1898
|
-
const log = files.find((file) => file.endsWith(suffix));
|
|
1899
|
-
return log ? log.slice(0, -suffix.length) : null;
|
|
1900
|
-
}
|
|
1901
|
-
const fsProbe = {
|
|
1902
|
-
exists: existsSync,
|
|
1903
|
-
isFile(path) {
|
|
1904
|
-
try {
|
|
1905
|
-
return statSync(path).isFile();
|
|
1906
|
-
} catch {
|
|
1907
|
-
return false;
|
|
1908
|
-
}
|
|
1909
|
-
},
|
|
1910
|
-
read(path) {
|
|
1911
|
-
try {
|
|
1912
|
-
return readFileSync(path, "utf8");
|
|
1913
|
-
} catch {
|
|
1914
|
-
return null;
|
|
1915
|
-
}
|
|
1916
|
-
},
|
|
1917
|
-
mounted: (path) => isOnMountedVolume(path)
|
|
1918
|
-
};
|
|
1919
|
-
/**
|
|
1920
|
-
* Why a watch root is stale: its directory is gone from a mounted volume, or the nearest `.git` above it is a
|
|
1921
|
-
* linked worktree's `.git` file whose gitdir no longer exists (git pruned the worktree and left the directory).
|
|
1922
|
-
* Null when it is live or when that cannot be proven, such as a path on a volume that is not mounted.
|
|
1923
|
-
*/
|
|
1924
|
-
function watchmanRootStaleness(path, probe = fsProbe) {
|
|
1925
|
-
if (!probe.exists(path)) return probe.mounted(path) ? "missing" : null;
|
|
1926
|
-
for (let dir = path;; dir = dirname(dir)) {
|
|
1927
|
-
const git = join(dir, ".git");
|
|
1928
|
-
if (probe.exists(git)) {
|
|
1929
|
-
if (!probe.isFile(git)) return null;
|
|
1930
|
-
const target = /^gitdir:\s*(.+?)\s*$/m.exec(probe.read(git) ?? "")?.[1];
|
|
1931
|
-
if (!target) return null;
|
|
1932
|
-
const gitdir = isAbsolute(target) ? target : resolve(dir, target);
|
|
1933
|
-
return probe.exists(gitdir) || !probe.mounted(gitdir) ? null : "pruned-worktree";
|
|
1934
|
-
}
|
|
1935
|
-
if (dirname(dir) === dir) return null;
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
function staleText(stale) {
|
|
1939
|
-
return stale === "missing" ? "its directory is gone" : "its linked worktree was pruned";
|
|
1940
|
-
}
|
|
1941
|
-
function planWatchmanRoot(root) {
|
|
1942
|
-
const subscriptions = root.subscribers?.length ?? null;
|
|
1943
|
-
const base = {
|
|
1944
|
-
path: root.path,
|
|
1945
|
-
stale: root.stale,
|
|
1946
|
-
subscriptions,
|
|
1947
|
-
triggers: root.triggers
|
|
1948
|
-
};
|
|
1949
|
-
if (root.stale === null) return {
|
|
1950
|
-
...base,
|
|
1951
|
-
removable: false,
|
|
1952
|
-
detail: null
|
|
1953
|
-
};
|
|
1954
|
-
const kept = subscriptions === null ? "could not list its subscriptions" : subscriptions > 0 ? `${subscriptions} subscription(s) still use it` : root.triggers === null ? "could not list its triggers" : root.triggers > 0 ? `it has ${root.triggers} trigger(s)` : null;
|
|
1955
|
-
return {
|
|
1956
|
-
...base,
|
|
1957
|
-
removable: kept === null,
|
|
1958
|
-
detail: kept ? `${staleText(root.stale)}, but ${kept}` : staleText(root.stale)
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
/**
|
|
1962
|
-
* Decides the watchman roots gc removes and whether it may shut the daemon down. A stale root goes only while no
|
|
1963
|
-
* subscription or trigger uses it. The daemon goes only when `debug-status` lists no client besides gc's own call and
|
|
1964
|
-
* every root's subscriptions and triggers could be read, no kept root has a trigger, and no root has a subscription;
|
|
1965
|
-
* `describeClient` names each client that keeps it.
|
|
1966
|
-
*/
|
|
1967
|
-
function planWatchman(facts, describeClient) {
|
|
1968
|
-
const roots = (facts.roots ?? []).map(planWatchmanRoot);
|
|
1969
|
-
const unreadRoots = facts.roots === null || roots.some((root) => root.subscriptions === null || root.triggers === null);
|
|
1970
|
-
const triggered = roots.filter((root) => !root.removable && (root.triggers ?? 0) > 0);
|
|
1971
|
-
let reason = null;
|
|
1972
|
-
let detail = null;
|
|
1973
|
-
if (facts.clients === null || unreadRoots) {
|
|
1974
|
-
reason = "unknown";
|
|
1975
|
-
detail = facts.clients === null ? "watchman debug-status did not list its clients" : "could not list the subscriptions and triggers of every root";
|
|
1976
|
-
} else if (facts.clients.length > 0) {
|
|
1977
|
-
reason = "in-use";
|
|
1978
|
-
detail = `used by ${[...new Set(facts.clients.map(describeClient))].join(", ")}; shutting it down would break their file watching`;
|
|
1979
|
-
} else if (roots.some((root) => !root.removable && (root.subscriptions ?? 0) > 0)) {
|
|
1980
|
-
reason = "in-use";
|
|
1981
|
-
detail = "a root still has a subscription";
|
|
1982
|
-
} else if (triggered.length > 0) {
|
|
1983
|
-
reason = "in-use";
|
|
1984
|
-
detail = `${triggered.map((root) => root.path).join(", ")} ha${triggered.length === 1 ? "s" : "ve"} triggers that stop firing while watchman is down`;
|
|
1985
|
-
}
|
|
1986
|
-
return {
|
|
1987
|
-
process: {
|
|
1988
|
-
kind: "watchman",
|
|
1989
|
-
cacheKind: WATCHMAN_KIND,
|
|
1990
|
-
pid: facts.pid,
|
|
1991
|
-
startedAt: facts.startedAt,
|
|
1992
|
-
bytes: facts.bytes,
|
|
1993
|
-
measure: facts.measure,
|
|
1994
|
-
version: null,
|
|
1995
|
-
gradleHome: null,
|
|
1996
|
-
offloadClient: null,
|
|
1997
|
-
state: reason === "in-use" ? "busy" : reason === "unknown" ? "unknown" : "idle",
|
|
1998
|
-
reclaimable: reason === null,
|
|
1999
|
-
reason,
|
|
2000
|
-
detail
|
|
2001
|
-
},
|
|
2002
|
-
roots
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* Decides which Gradle and Kotlin daemons gc may stop. A running Stim build (`buildRunning`) keeps every one. An
|
|
2007
|
-
* offload worker's daemon is left to stim-server while it runs. A Gradle daemon goes only when its own `gradle
|
|
2008
|
-
* --status` says IDLE. A Kotlin daemon goes only when every Gradle daemon is idle and each of its connections comes from
|
|
2009
|
-
* a Gradle daemon gc stops in the same run.
|
|
2010
|
-
*/
|
|
2011
|
-
function planDaemons({ gradle, kotlin, buildRunning, stimServerRunning }) {
|
|
2012
|
-
const plans = [];
|
|
2013
|
-
const gradleBusy = gradle.some((daemon) => !("state" in daemon.status) || daemon.status.state !== "idle");
|
|
2014
|
-
for (const daemon of gradle) {
|
|
2015
|
-
const state = "state" in daemon.status ? daemon.status.state : null;
|
|
2016
|
-
const idle = state === "idle";
|
|
2017
|
-
let reason = null;
|
|
2018
|
-
let detail = null;
|
|
2019
|
-
if (daemon.offloadClient !== null && stimServerRunning) {
|
|
2020
|
-
reason = "stim-server";
|
|
2021
|
-
detail = `stim-server stops the daemons of offload client ${daemon.offloadClient} when that client loses build or memory runs low`;
|
|
2022
|
-
} else if (buildRunning) {
|
|
2023
|
-
reason = "build-running";
|
|
2024
|
-
detail = buildRunning;
|
|
2025
|
-
} else if (state === "busy") {
|
|
2026
|
-
reason = "busy";
|
|
2027
|
-
detail = "a build is using it";
|
|
2028
|
-
} else if (!idle) {
|
|
2029
|
-
reason = "unknown";
|
|
2030
|
-
detail = "unknown" in daemon.status ? daemon.status.unknown : `gradle --status reports it ${state}`;
|
|
2031
|
-
}
|
|
2032
|
-
plans.push({
|
|
2033
|
-
kind: "gradleDaemon",
|
|
2034
|
-
cacheKind: GRADLE_DAEMONS_KIND,
|
|
2035
|
-
pid: daemon.pid,
|
|
2036
|
-
startedAt: daemon.startedAt,
|
|
2037
|
-
bytes: daemon.bytes,
|
|
2038
|
-
measure: daemon.measure,
|
|
2039
|
-
version: daemon.command.version,
|
|
2040
|
-
gradleHome: daemon.gradleHome,
|
|
2041
|
-
offloadClient: daemon.offloadClient,
|
|
2042
|
-
state: idle ? "idle" : state === "busy" ? "busy" : "unknown",
|
|
2043
|
-
reclaimable: reason === null,
|
|
2044
|
-
reason,
|
|
2045
|
-
detail
|
|
2046
|
-
});
|
|
2047
|
-
}
|
|
2048
|
-
const stopping = new Set(plans.filter((plan) => plan.reclaimable).map((plan) => plan.pid));
|
|
2049
|
-
for (const daemon of kotlin) {
|
|
2050
|
-
const clients = daemon.peers?.filter((peer) => peer === null || !stopping.has(peer)) ?? [];
|
|
2051
|
-
let reason = null;
|
|
2052
|
-
let detail = null;
|
|
2053
|
-
if (buildRunning) {
|
|
2054
|
-
reason = "build-running";
|
|
2055
|
-
detail = buildRunning;
|
|
2056
|
-
} else if (daemon.peers === null) {
|
|
2057
|
-
reason = "unknown";
|
|
2058
|
-
detail = "lsof could not list its connections";
|
|
2059
|
-
} else if (gradleBusy) {
|
|
2060
|
-
reason = "busy";
|
|
2061
|
-
detail = "a Gradle daemon is busy or its state is unknown, and it may be compiling through this one";
|
|
2062
|
-
} else if (clients.length > 0) {
|
|
2063
|
-
reason = "busy";
|
|
2064
|
-
detail = `${clients.length} connection(s) from a process gc does not stop are open`;
|
|
2065
|
-
}
|
|
2066
|
-
plans.push({
|
|
2067
|
-
kind: "kotlinDaemon",
|
|
2068
|
-
cacheKind: GRADLE_DAEMONS_KIND,
|
|
2069
|
-
pid: daemon.pid,
|
|
2070
|
-
startedAt: daemon.startedAt,
|
|
2071
|
-
bytes: daemon.bytes,
|
|
2072
|
-
measure: daemon.measure,
|
|
2073
|
-
version: null,
|
|
2074
|
-
gradleHome: null,
|
|
2075
|
-
offloadClient: null,
|
|
2076
|
-
state: reason === null ? "idle" : reason === "unknown" ? "unknown" : "busy",
|
|
2077
|
-
reclaimable: reason === null,
|
|
2078
|
-
reason,
|
|
2079
|
-
detail
|
|
2080
|
-
});
|
|
2081
|
-
}
|
|
2082
|
-
return plans;
|
|
2083
|
-
}
|
|
2084
|
-
/**
|
|
2085
|
-
* Why no Gradle or Kotlin daemon may be stopped now, or null. An Android build lock, or a build slot that no live iOS
|
|
2086
|
-
* build lock of the same process holds, counts as a running build; one whose holder is unresolved counts too.
|
|
2087
|
-
*/
|
|
2088
|
-
function runningBuild(locks, slots) {
|
|
2089
|
-
const android = locks.find((lock) => lock.platform === "android" && (lock.alive || lock.unresolved));
|
|
2090
|
-
if (android) return `${android.unresolved ? "an unresolved" : "a running"} Android build holds ${android.path} (pid ${android.pid ?? "?"}, ${android.projectRoot || "unrecorded workspace"})`;
|
|
2091
|
-
const ios = new Set(locks.filter((lock) => lock.platform === "ios" && lock.alive).map((lock) => lock.pid));
|
|
2092
|
-
const slot = slots.find((s) => (s.alive || s.unresolved) && !(s.alive && s.pid !== null && ios.has(s.pid)));
|
|
2093
|
-
if (slot) return `${slot.unresolved ? "an unresolved" : "a running"} build holds build slot ${slot.index ?? "?"} (pid ${slot.pid ?? "?"}, ${slot.projectRoot || "unrecorded workspace"})`;
|
|
2094
|
-
return null;
|
|
2095
|
-
}
|
|
2096
|
-
/** A watchman client's pid resolved to the Stim workspace whose supervisor it is or descends from. */
|
|
2097
|
-
function stimWorkspaceOf(pid, parentOf, supervisors) {
|
|
2098
|
-
let current = pid;
|
|
2099
|
-
for (let depth = 0; current !== void 0 && current > 1 && depth < 64; depth++) {
|
|
2100
|
-
const workspace = supervisors.get(current);
|
|
2101
|
-
if (workspace) return workspace;
|
|
2102
|
-
current = parentOf.get(current);
|
|
2103
|
-
}
|
|
2104
|
-
return null;
|
|
2105
|
-
}
|
|
2106
|
-
function reclaimableBytes(report, kind) {
|
|
2107
|
-
return report.processes.filter((entry) => entry.reclaimable && (kind === void 0 || entry.cacheKind === kind)).reduce((sum, entry) => sum + entry.bytes, 0);
|
|
2108
|
-
}
|
|
2109
|
-
const boundWatchman = async (args) => {
|
|
2110
|
-
const answer = JSON.parse(await getExecutor().runFileAsync("watchman", ["--no-spawn", ...args], { timeoutMs: WATCHMAN_TIMEOUT_MS }));
|
|
2111
|
-
const error = answer?.error;
|
|
2112
|
-
if (error !== void 0) throw new Error(String(error));
|
|
2113
|
-
return answer;
|
|
2114
|
-
};
|
|
2115
|
-
async function quietly(fn) {
|
|
2116
|
-
try {
|
|
2117
|
-
return await fn();
|
|
2118
|
-
} catch {
|
|
2119
|
-
return null;
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
/**
|
|
2123
|
-
* `watchman debug-status` with gc's own call left out of its clients. The call is spawned directly so its pid is
|
|
2124
|
-
* known: debug-status lists the asking CLI as a client too.
|
|
2125
|
-
*/
|
|
2126
|
-
function watchmanClients() {
|
|
2127
|
-
return new Promise((done) => {
|
|
2128
|
-
let child;
|
|
2129
|
-
try {
|
|
2130
|
-
child = getExecutor().spawn("watchman", ["--no-spawn", "debug-status"], { stdio: [
|
|
2131
|
-
"ignore",
|
|
2132
|
-
"pipe",
|
|
2133
|
-
"ignore"
|
|
2134
|
-
] });
|
|
2135
|
-
} catch {
|
|
2136
|
-
done(null);
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
const self = child.pid;
|
|
2140
|
-
const chunks = [];
|
|
2141
|
-
const timer = setTimeout(() => child.kill("SIGTERM"), WATCHMAN_TIMEOUT_MS);
|
|
2142
|
-
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
2143
|
-
child.on("error", () => {
|
|
2144
|
-
clearTimeout(timer);
|
|
2145
|
-
done(null);
|
|
2146
|
-
});
|
|
2147
|
-
child.on("close", (status) => {
|
|
2148
|
-
clearTimeout(timer);
|
|
2149
|
-
let clients = null;
|
|
2150
|
-
try {
|
|
2151
|
-
if (status === 0 && self !== void 0) clients = parseWatchmanClients(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
2152
|
-
} catch {}
|
|
2153
|
-
done(clients ? clients.filter((client) => client.pid !== self) : null);
|
|
2154
|
-
});
|
|
2155
|
-
});
|
|
2156
|
-
}
|
|
2157
|
-
async function collectWatchmanRoots(pid, progress) {
|
|
2158
|
-
const watchman = boundWatchman;
|
|
2159
|
-
const paths = parseWatchRoots(await quietly(() => watchman(["watch-list"])));
|
|
2160
|
-
if (!paths) return null;
|
|
2161
|
-
if (progress) phase("daemons", `watchman pid ${pid}: checking ${plural(paths.length, "root")}`);
|
|
2162
|
-
const roots = [];
|
|
2163
|
-
let next = 0;
|
|
2164
|
-
const probe = async () => {
|
|
2165
|
-
while (next < paths.length) {
|
|
2166
|
-
const index = next++;
|
|
2167
|
-
const path = paths[index];
|
|
2168
|
-
const [subscriptions, triggers] = await Promise.all([quietly(() => watchman(["debug-get-subscriptions", path])), quietly(() => watchman(["trigger-list", path]))]);
|
|
2169
|
-
roots[index] = {
|
|
2170
|
-
path,
|
|
2171
|
-
stale: watchmanRootStaleness(path),
|
|
2172
|
-
subscribers: parseSubscriberNames(subscriptions),
|
|
2173
|
-
triggers: parseTriggerCount(triggers)
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
};
|
|
2177
|
-
await Promise.all(Array.from({ length: Math.min(ROOT_PROBES_AT_ONCE, paths.length) }, probe));
|
|
2178
|
-
return roots;
|
|
2179
|
-
}
|
|
2180
|
-
function measureOf(pid, rows, footprints) {
|
|
2181
|
-
const footprint = footprints?.get(pid);
|
|
2182
|
-
if (footprint !== void 0) return {
|
|
2183
|
-
bytes: footprint,
|
|
2184
|
-
measure: "footprint"
|
|
2185
|
-
};
|
|
2186
|
-
return {
|
|
2187
|
-
bytes: (rows.get(pid)?.rssKb ?? 0) * 1024,
|
|
2188
|
-
measure: "rss"
|
|
2189
|
-
};
|
|
2190
|
-
}
|
|
2191
|
-
function supervisorPids() {
|
|
2192
|
-
const supervisors = /* @__PURE__ */ new Map();
|
|
2193
|
-
for (const root of Object.keys(loadConfig()?.projects ?? {})) {
|
|
2194
|
-
const pid = readPidFile(root);
|
|
2195
|
-
if (pid) supervisors.set(pid, root);
|
|
2196
|
-
}
|
|
2197
|
-
return supervisors;
|
|
2198
|
-
}
|
|
2199
|
-
async function readLsof(pids) {
|
|
2200
|
-
if (pids.length === 0) return /* @__PURE__ */ new Map();
|
|
2201
|
-
try {
|
|
2202
|
-
return parseLsof(await getExecutor().runFileAsync("lsof", [
|
|
2203
|
-
"-n",
|
|
2204
|
-
"-P",
|
|
2205
|
-
"-F",
|
|
2206
|
-
"pnT",
|
|
2207
|
-
"-p",
|
|
2208
|
-
pids.join(",")
|
|
2209
|
-
], { timeoutMs: LSOF_TIMEOUT_MS }));
|
|
2210
|
-
} catch (error) {
|
|
2211
|
-
const { status, signal: killed, stdout } = error;
|
|
2212
|
-
return status === 1 && !killed && typeof stdout === "string" && stdout ? parseLsof(stdout) : null;
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
async function gradleStatus(command, home) {
|
|
2216
|
-
if (!command.distribution) return { unknown: "could not find its Gradle distribution on its classpath" };
|
|
2217
|
-
const launcher = join(command.distribution, "bin", "gradle");
|
|
2218
|
-
if (!existsSync(launcher)) return { unknown: `its distribution has no ${launcher}` };
|
|
2219
|
-
try {
|
|
2220
|
-
return parseGradleStatus(await getExecutor().runFileAsync(launcher, [
|
|
2221
|
-
"--status",
|
|
2222
|
-
"-q",
|
|
2223
|
-
"-g",
|
|
2224
|
-
home
|
|
2225
|
-
], {
|
|
2226
|
-
cwd: home,
|
|
2227
|
-
env: { JAVA_HOME: command.javaHome },
|
|
2228
|
-
omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
|
|
2229
|
-
timeoutMs: GRADLE_STATUS_TIMEOUT_MS
|
|
2230
|
-
}));
|
|
2231
|
-
} catch (error) {
|
|
2232
|
-
return { unknown: `gradle --status failed: ${error.message.split("\n")[0]}` };
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
function groupKey(command, home) {
|
|
2236
|
-
return JSON.stringify([
|
|
2237
|
-
home,
|
|
2238
|
-
command.version,
|
|
2239
|
-
command.distribution,
|
|
2240
|
-
command.javaHome
|
|
2241
|
-
]);
|
|
2242
|
-
}
|
|
2243
|
-
async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress) {
|
|
2244
|
-
const gradleRows = rows.flatMap((row) => {
|
|
2245
|
-
const command = parseGradleDaemonCommand(row.command);
|
|
2246
|
-
return command ? [{
|
|
2247
|
-
row,
|
|
2248
|
-
command
|
|
2249
|
-
}] : [];
|
|
2250
|
-
});
|
|
2251
|
-
const kotlinRows = rows.filter((row) => isKotlinDaemonCommand(row.command));
|
|
2252
|
-
if (gradleRows.length === 0 && kotlinRows.length === 0) return {
|
|
2253
|
-
gradle: [],
|
|
2254
|
-
kotlin: []
|
|
2255
|
-
};
|
|
2256
|
-
if (progress) phase("daemons", `checking ${plural(gradleRows.length, "Gradle daemon")} and ${plural(kotlinRows.length, "Kotlin daemon")}`);
|
|
2257
|
-
const lsof = await readLsof([...gradleRows.map(({ row }) => row.pid), ...kotlinRows.map((row) => row.pid)]);
|
|
2258
|
-
const workerRoot = buildWorkerRoot();
|
|
2259
|
-
const homes = gradleRows.map(({ row, command }) => {
|
|
2260
|
-
const files = lsof?.get(row.pid)?.files;
|
|
2261
|
-
return files ? gradleHomeFromFiles(row.pid, command.version, files) : null;
|
|
2262
|
-
});
|
|
2263
|
-
const offload = gradleRows.map(({ row }, index) => offloadClientOf(row.command, workerRoot, homes[index] ?? null));
|
|
2264
|
-
const statuses = /* @__PURE__ */ new Map();
|
|
2265
|
-
gradleRows.forEach(({ command }, index) => {
|
|
2266
|
-
const home = homes[index];
|
|
2267
|
-
if (offload[index] !== null && stimServerRunning) return;
|
|
2268
|
-
if (home && !statuses.has(groupKey(command, home))) {
|
|
2269
|
-
if (progress) phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
|
|
2270
|
-
statuses.set(groupKey(command, home), gradleStatus(command, home));
|
|
2271
|
-
}
|
|
2272
|
-
});
|
|
2273
|
-
const gradle = [];
|
|
2274
|
-
for (const [index, { row, command }] of gradleRows.entries()) {
|
|
2275
|
-
const home = homes[index] ?? null;
|
|
2276
|
-
let status;
|
|
2277
|
-
if (offload[index] !== null && stimServerRunning) status = { unknown: "stim-server manages it" };
|
|
2278
|
-
else if (!home) status = { unknown: lsof ? "could not find its Gradle user home among its open files" : "lsof failed" };
|
|
2279
|
-
else {
|
|
2280
|
-
const states = await statuses.get(groupKey(command, home));
|
|
2281
|
-
if (states instanceof Map) {
|
|
2282
|
-
const state = states.get(row.pid);
|
|
2283
|
-
status = state ? { state } : { unknown: "gradle --status did not list it" };
|
|
2284
|
-
} else status = states;
|
|
2285
|
-
}
|
|
2286
|
-
gradle.push({
|
|
2287
|
-
pid: row.pid,
|
|
2288
|
-
startedAt: row.startedAt,
|
|
2289
|
-
...measureOf(row.pid, byPid, footprints),
|
|
2290
|
-
command,
|
|
2291
|
-
gradleHome: home,
|
|
2292
|
-
offloadClient: offload[index] ?? null,
|
|
2293
|
-
status
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
return {
|
|
2297
|
-
gradle,
|
|
2298
|
-
kotlin: kotlinRows.map((row) => Object.assign(measureOf(row.pid, byPid, footprints), {
|
|
2299
|
-
pid: row.pid,
|
|
2300
|
-
startedAt: row.startedAt,
|
|
2301
|
-
peers: lsof?.get(row.pid) ? connectionPeers(row.pid, lsof) : null
|
|
2302
|
-
}))
|
|
2303
|
-
};
|
|
2304
|
-
}
|
|
2305
|
-
async function collectWatchmanFacts(byPid, footprints, progress) {
|
|
2306
|
-
const pid = parseWatchmanPid(await quietly(() => boundWatchman(["get-pid"])));
|
|
2307
|
-
if (pid === null) return null;
|
|
2308
|
-
const roots = await collectWatchmanRoots(pid, progress);
|
|
2309
|
-
const clients = (await watchmanClients())?.filter((client) => client.pid !== pid) ?? null;
|
|
2310
|
-
return {
|
|
2311
|
-
pid,
|
|
2312
|
-
startedAt: byPid.get(pid)?.startedAt ?? null,
|
|
2313
|
-
...measureOf(pid, byPid, footprints),
|
|
2314
|
-
roots,
|
|
2315
|
-
clients
|
|
2316
|
-
};
|
|
2317
|
-
}
|
|
2318
|
-
function watchmanClientDescriber(rows) {
|
|
2319
|
-
const parentOf = new Map(rows.map((row) => [row.pid, row.ppid]));
|
|
2320
|
-
const supervisors = supervisorPids();
|
|
2321
|
-
return (client) => {
|
|
2322
|
-
const workspace = stimWorkspaceOf(client.pid, parentOf, supervisors);
|
|
2323
|
-
return workspace ? `the Stim workspace ${workspace}` : `${client.name ?? "a client"} (pid ${client.pid})`;
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
function memorySweepIsScoped() {
|
|
2327
|
-
return Boolean(process.env.STIM_HOME);
|
|
2328
|
-
}
|
|
2329
|
-
/** The report of a run whose STIM_HOME is set: the helpers are machine-global, so none is inspected or stopped. */
|
|
2330
|
-
function scopedMemoryReport() {
|
|
2331
|
-
return {
|
|
2332
|
-
processes: [],
|
|
2333
|
-
watchmanRoots: [],
|
|
2334
|
-
notices: ["memory sweep skipped: STIM_HOME scopes this config, but watchman and the Gradle and Kotlin daemons are machine-global"]
|
|
2335
|
-
};
|
|
2336
|
-
}
|
|
2337
|
-
async function collectMemoryReport(scope, progress = false) {
|
|
2338
|
-
const report = {
|
|
2339
|
-
processes: [],
|
|
2340
|
-
watchmanRoots: [],
|
|
2341
|
-
notices: []
|
|
2342
|
-
};
|
|
2343
|
-
if (process.platform === "win32") return report;
|
|
2344
|
-
const rows = createDeviceProcessTables().host();
|
|
2345
|
-
if (rows === null) {
|
|
2346
|
-
report.notices.push("memory sweep skipped: ps did not answer");
|
|
2347
|
-
return report;
|
|
2348
|
-
}
|
|
2349
|
-
const byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
2350
|
-
const footprints = await readFootprints();
|
|
2351
|
-
if (scope.watchman) {
|
|
2352
|
-
const facts = await collectWatchmanFacts(byPid, footprints, progress);
|
|
2353
|
-
if (facts) {
|
|
2354
|
-
const plan = planWatchman(facts, watchmanClientDescriber(rows));
|
|
2355
|
-
report.processes.push(plan.process);
|
|
2356
|
-
report.watchmanRoots = plan.roots;
|
|
2357
|
-
} else if (!scope.gradle) report.notices.push("no watchman daemon answered");
|
|
2358
|
-
}
|
|
2359
|
-
if (scope.gradle) {
|
|
2360
|
-
const stimServerRunning = rows.some((row) => STIM_SERVER_COMMAND.test(row.command));
|
|
2361
|
-
const facts = await collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress);
|
|
2362
|
-
report.processes.push(...planDaemons({
|
|
2363
|
-
...facts,
|
|
2364
|
-
buildRunning: facts.gradle.length || facts.kotlin.length ? runningBuild(listBuildLocks(), listBuildSlots()) : null,
|
|
2365
|
-
stimServerRunning
|
|
2366
|
-
}));
|
|
2367
|
-
if (!scope.watchman && report.processes.length === 0) report.notices.push("no Gradle or Kotlin daemon is running");
|
|
2368
|
-
}
|
|
2369
|
-
return report;
|
|
2370
|
-
}
|
|
2371
|
-
function processLabel(entry) {
|
|
2372
|
-
if (entry.kind === "watchman") return `watchman pid ${entry.pid}`;
|
|
2373
|
-
if (entry.kind === "kotlinDaemon") return `Kotlin compile daemon pid ${entry.pid}`;
|
|
2374
|
-
const where = entry.offloadClient ? `offload client ${entry.offloadClient}` : entry.gradleHome ?? "unknown Gradle home";
|
|
2375
|
-
return `Gradle ${entry.version ?? "?"} daemon pid ${entry.pid} (${where})`;
|
|
2376
|
-
}
|
|
2377
|
-
function uptime(startedAt, now) {
|
|
2378
|
-
const at = startedAt ? Date.parse(startedAt) : NaN;
|
|
2379
|
-
return Number.isFinite(at) ? `up ${formatLongDuration(Math.max(0, now - at))}` : "uptime unknown";
|
|
2380
|
-
}
|
|
2381
|
-
function memoryLines(report, scoped, now = Date.now()) {
|
|
2382
|
-
if (!report || report.processes.length === 0 && report.notices.length === 0) return [];
|
|
2383
|
-
const lines = [`Memory (${report.processes.some((entry) => entry.measure === "rss") ? "footprint, resident size where unreadable" : "physical footprint"}) - shared helpers that grow while they run; only --cache watchman or gradle-daemons stops them:`];
|
|
2384
|
-
for (const entry of report.processes) {
|
|
2385
|
-
const state = entry.reclaimable ? "idle" : entry.kind === "watchman" && entry.state === "busy" ? "in use" : entry.state;
|
|
2386
|
-
lines.push(` ${processLabel(entry)} ${formatBytes(entry.bytes)}, ${uptime(entry.startedAt, now)}, ${state}`);
|
|
2387
|
-
if (entry.kind === "watchman") {
|
|
2388
|
-
const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
|
|
2389
|
-
lines.push(` ${report.watchmanRoots.length} root(s), ${stale} stale`);
|
|
2390
|
-
for (const root of report.watchmanRoots.filter((r) => r.stale !== null)) lines.push(` stale root ${root.path}: ${root.detail}${root.removable ? "" : "; kept"}`);
|
|
2391
|
-
}
|
|
2392
|
-
if (entry.detail) lines.push(` ${entry.reclaimable ? "" : "kept: "}${entry.detail}`);
|
|
2393
|
-
}
|
|
2394
|
-
for (const notice of report.notices) lines.push(` ${notice}`);
|
|
2395
|
-
const kinds = scoped ? [scoped] : [WATCHMAN_KIND, GRADLE_DAEMONS_KIND];
|
|
2396
|
-
const parts = [];
|
|
2397
|
-
for (const kind of kinds) {
|
|
2398
|
-
if (!report.processes.some((entry) => entry.cacheKind === kind)) continue;
|
|
2399
|
-
const roots = kind === "watchman" ? report.watchmanRoots.filter((root) => root.removable).length : 0;
|
|
2400
|
-
const note = roots ? ` (and ${roots} stale root(s) removed)` : "";
|
|
2401
|
-
parts.push(`${formatBytes(reclaimableBytes(report, kind))} with stim gc --delete --cache ${kind}${note}`);
|
|
2402
|
-
}
|
|
2403
|
-
if (parts.length) lines.push(` reclaimable ${formatBytes(reclaimableBytes(report))}: ${parts.join("; ")}`);
|
|
2404
|
-
return lines;
|
|
2405
|
-
}
|
|
2406
|
-
function sameProcess(entry, rows) {
|
|
2407
|
-
const row = rows?.get(entry.pid);
|
|
2408
|
-
return Boolean(row && entry.startedAt !== null && row.startedAt === entry.startedAt);
|
|
2409
|
-
}
|
|
2410
|
-
function signal(pid) {
|
|
2411
|
-
try {
|
|
2412
|
-
process.kill(pid, "SIGTERM");
|
|
2413
|
-
return null;
|
|
2414
|
-
} catch (error) {
|
|
2415
|
-
return error.code === "ESRCH" ? null : error.message;
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
function keep(entry, kind, detail) {
|
|
2419
|
-
console.log(chalk.yellow(`Kept ${processLabel(entry)}: ${detail}`));
|
|
2420
|
-
recordGcResult(kind, "kept", processLabel(entry), {
|
|
2421
|
-
id: String(entry.pid),
|
|
2422
|
-
bytes: entry.bytes,
|
|
2423
|
-
detail
|
|
2424
|
-
});
|
|
2425
|
-
}
|
|
2426
|
-
async function reclaimWatchman(before) {
|
|
2427
|
-
let failures = 0;
|
|
2428
|
-
const fresh = await collectMemoryReport({
|
|
2429
|
-
watchman: true,
|
|
2430
|
-
gradle: false
|
|
2431
|
-
}, true);
|
|
2432
|
-
const daemon = fresh.processes.find((entry) => entry.kind === "watchman");
|
|
2433
|
-
const planned = before.processes.find((entry) => entry.kind === "watchman");
|
|
2434
|
-
if (!daemon || !planned || daemon.pid !== planned.pid) {
|
|
2435
|
-
if (planned) keep(planned, "watchman", "the daemon changed or stopped since the report");
|
|
2436
|
-
return 0;
|
|
2437
|
-
}
|
|
2438
|
-
for (const root of fresh.watchmanRoots.filter((r) => r.stale !== null)) {
|
|
2439
|
-
if (!root.removable) {
|
|
2440
|
-
console.log(chalk.yellow(`Kept the watchman root ${root.path}: ${root.detail}`));
|
|
2441
|
-
recordGcResult("watchmanRoot", "kept", root.path, { detail: root.detail });
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
try {
|
|
2445
|
-
phase("daemons", `removing stale watchman root ${root.path}`);
|
|
2446
|
-
await boundWatchman(["watch-del", root.path]);
|
|
2447
|
-
console.log(chalk.green(`Removed the stale watchman root ${root.path} (${root.detail})`));
|
|
2448
|
-
recordGcResult("watchmanRoot", "done", root.path, { detail: root.detail });
|
|
2449
|
-
} catch (error) {
|
|
2450
|
-
failures++;
|
|
2451
|
-
console.log(chalk.red(`Could not remove the watchman root ${root.path}: ${error.message}`));
|
|
2452
|
-
recordGcResult("watchmanRoot", "failed", root.path, { detail: error.message });
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
const current = (await collectMemoryReport({
|
|
2456
|
-
watchman: true,
|
|
2457
|
-
gradle: false
|
|
2458
|
-
}, true)).processes.find((entry) => entry.kind === "watchman");
|
|
2459
|
-
if (!current || current.pid !== daemon.pid) return failures;
|
|
2460
|
-
if (!current.reclaimable) {
|
|
2461
|
-
keep(current, "watchman", current.detail ?? "it could not be proven unused");
|
|
2462
|
-
return failures;
|
|
2463
|
-
}
|
|
2464
|
-
try {
|
|
2465
|
-
phase("daemons", `shutting down watchman pid ${current.pid}: no client uses it`);
|
|
2466
|
-
await boundWatchman(["shutdown-server"]);
|
|
2467
|
-
console.log(chalk.green(`Shut down watchman pid ${current.pid}, freeing ${formatBytes(current.bytes)}; the next client that needs it starts it again`));
|
|
2468
|
-
recordGcResult("watchman", "done", processLabel(current), {
|
|
2469
|
-
id: String(current.pid),
|
|
2470
|
-
bytes: current.bytes
|
|
2471
|
-
});
|
|
2472
|
-
} catch (error) {
|
|
2473
|
-
failures++;
|
|
2474
|
-
console.log(chalk.red(`Could not shut down watchman pid ${current.pid}: ${error.message}`));
|
|
2475
|
-
recordGcResult("watchman", "failed", processLabel(current), {
|
|
2476
|
-
id: String(current.pid),
|
|
2477
|
-
bytes: current.bytes,
|
|
2478
|
-
detail: error.message
|
|
2479
|
-
});
|
|
2480
|
-
}
|
|
2481
|
-
return failures;
|
|
2482
|
-
}
|
|
2483
|
-
function stopKotlin(entry, rows) {
|
|
2484
|
-
if (!sameProcess(entry, rows)) {
|
|
2485
|
-
keep(entry, "kotlinDaemon", "the process changed or exited since the check");
|
|
2486
|
-
return 0;
|
|
2487
|
-
}
|
|
2488
|
-
const error = signal(entry.pid);
|
|
2489
|
-
if (error) {
|
|
2490
|
-
console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
|
|
2491
|
-
recordGcResult("kotlinDaemon", "failed", processLabel(entry), {
|
|
2492
|
-
id: String(entry.pid),
|
|
2493
|
-
bytes: entry.bytes,
|
|
2494
|
-
detail: error
|
|
2495
|
-
});
|
|
2496
|
-
return 1;
|
|
2497
|
-
}
|
|
2498
|
-
console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
|
|
2499
|
-
recordGcResult("kotlinDaemon", "done", processLabel(entry), {
|
|
2500
|
-
id: String(entry.pid),
|
|
2501
|
-
bytes: entry.bytes
|
|
2502
|
-
});
|
|
2503
|
-
return 0;
|
|
2504
|
-
}
|
|
2505
|
-
/**
|
|
2506
|
-
* Stops the Gradle daemons a fresh check still finds idle. When every daemon `gradle --status` lists for a home and
|
|
2507
|
-
* version is one of them, the daemon's own `gradle --stop` runs for that home; otherwise each idle one gets SIGTERM,
|
|
2508
|
-
* because `--stop` also stops the busy daemons of that home and version.
|
|
2509
|
-
*/
|
|
2510
|
-
async function stopGradle(idle, rows) {
|
|
2511
|
-
let failures = 0;
|
|
2512
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2513
|
-
for (const entry of idle) {
|
|
2514
|
-
const command = parseGradleDaemonCommand(rows?.get(entry.pid)?.command ?? "");
|
|
2515
|
-
if (!command || !entry.gradleHome || !sameProcess(entry, rows)) {
|
|
2516
|
-
keep(entry, "gradleDaemon", "the process changed or exited since the check");
|
|
2517
|
-
continue;
|
|
2518
|
-
}
|
|
2519
|
-
const key = groupKey(command, entry.gradleHome);
|
|
2520
|
-
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
2521
|
-
}
|
|
2522
|
-
for (const [key, entries] of groups) {
|
|
2523
|
-
const [home, , distribution, javaHome] = JSON.parse(key);
|
|
2524
|
-
const command = parseGradleDaemonCommand(rows.get(entries[0].pid).command);
|
|
2525
|
-
const building = runningBuild(listBuildLocks(), listBuildSlots());
|
|
2526
|
-
if (building) {
|
|
2527
|
-
for (const entry of entries) keep(entry, "gradleDaemon", building);
|
|
2528
|
-
continue;
|
|
2529
|
-
}
|
|
2530
|
-
phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
|
|
2531
|
-
let states = await gradleStatus(command, home);
|
|
2532
|
-
let current = hostRows();
|
|
2533
|
-
const listed = states instanceof Map ? [...states].filter(([, state]) => state !== "stopped") : [];
|
|
2534
|
-
if (distribution !== null && listed.length > 0 && listed.every(([pid, state]) => state === "idle" && entries.some((entry) => entry.pid === pid))) try {
|
|
2535
|
-
phase("daemons", `gradle --stop for Gradle ${command.version} in ${home}`);
|
|
2536
|
-
await getExecutor().runFileAsync(join(distribution, "bin", "gradle"), [
|
|
2537
|
-
"--stop",
|
|
2538
|
-
"-q",
|
|
2539
|
-
"-g",
|
|
2540
|
-
home
|
|
2541
|
-
], {
|
|
2542
|
-
cwd: home,
|
|
2543
|
-
env: { JAVA_HOME: javaHome },
|
|
2544
|
-
omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
|
|
2545
|
-
timeoutMs: GRADLE_STOP_TIMEOUT_MS
|
|
2546
|
-
});
|
|
2547
|
-
for (const entry of entries) {
|
|
2548
|
-
console.log(chalk.green(`Stopped ${processLabel(entry)} with gradle --stop, freeing ${formatBytes(entry.bytes)}`));
|
|
2549
|
-
recordGcResult("gradleDaemon", "done", processLabel(entry), {
|
|
2550
|
-
id: String(entry.pid),
|
|
2551
|
-
bytes: entry.bytes
|
|
2552
|
-
});
|
|
2553
|
-
}
|
|
2554
|
-
continue;
|
|
2555
|
-
} catch (error) {
|
|
2556
|
-
console.log(chalk.dim(`gradle --stop failed for ${home}; signalling each idle daemon: ${error.message.split("\n")[0]}`));
|
|
2557
|
-
states = await gradleStatus(command, home);
|
|
2558
|
-
current = hostRows();
|
|
2559
|
-
}
|
|
2560
|
-
for (const entry of entries) {
|
|
2561
|
-
const state = states instanceof Map ? states.get(entry.pid) : void 0;
|
|
2562
|
-
if (!sameProcess(entry, current)) {
|
|
2563
|
-
keep(entry, "gradleDaemon", "the process changed or exited since the check");
|
|
2564
|
-
continue;
|
|
2565
|
-
}
|
|
2566
|
-
if (state !== "idle") {
|
|
2567
|
-
keep(entry, "gradleDaemon", state ? `gradle --status now reports it ${state}` : "gradle --status no longer lists it idle");
|
|
2568
|
-
continue;
|
|
2569
|
-
}
|
|
2570
|
-
const error = signal(entry.pid);
|
|
2571
|
-
if (error) {
|
|
2572
|
-
failures++;
|
|
2573
|
-
console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
|
|
2574
|
-
recordGcResult("gradleDaemon", "failed", processLabel(entry), {
|
|
2575
|
-
id: String(entry.pid),
|
|
2576
|
-
bytes: entry.bytes,
|
|
2577
|
-
detail: error
|
|
2578
|
-
});
|
|
2579
|
-
continue;
|
|
2580
|
-
}
|
|
2581
|
-
console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
|
|
2582
|
-
recordGcResult("gradleDaemon", "done", processLabel(entry), {
|
|
2583
|
-
id: String(entry.pid),
|
|
2584
|
-
bytes: entry.bytes
|
|
2585
|
-
});
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
return failures;
|
|
2589
|
-
}
|
|
2590
|
-
function stillPlanned(fresh, before, kind) {
|
|
2591
|
-
const planned = new Map(before.processes.filter((entry) => entry.kind === kind).map((entry) => [entry.pid, entry]));
|
|
2592
|
-
return fresh.processes.filter((entry) => entry.kind === kind && planned.get(entry.pid)?.startedAt === entry.startedAt);
|
|
2593
|
-
}
|
|
2594
|
-
function hostRows() {
|
|
2595
|
-
const rows = createDeviceProcessTables().host();
|
|
2596
|
-
return rows ? new Map(rows.map((row) => [row.pid, row])) : null;
|
|
2597
|
-
}
|
|
2598
|
-
const EXIT_WAIT_MS = 1e4;
|
|
2599
|
-
function alive(pid) {
|
|
2600
|
-
try {
|
|
2601
|
-
process.kill(pid, 0);
|
|
2602
|
-
return true;
|
|
2603
|
-
} catch (error) {
|
|
2604
|
-
return error.code !== "ESRCH";
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
async function waitForExit(pids) {
|
|
2608
|
-
const running = pids.filter(alive);
|
|
2609
|
-
if (running.length === 0) return;
|
|
2610
|
-
phase("daemons", `waiting up to ${EXIT_WAIT_MS / 1e3}s for ${plural(running.length, "Gradle daemon")} to exit: pid ${running.join(", ")}`);
|
|
2611
|
-
const deadline = Date.now() + EXIT_WAIT_MS;
|
|
2612
|
-
while (running.some(alive) && Date.now() < deadline) await new Promise((done) => setTimeout(done, 200));
|
|
2613
|
-
}
|
|
2614
|
-
/** Stops the Gradle daemons first, then re-checks the Kotlin daemons, whose clients those Gradle daemons were. */
|
|
2615
|
-
async function reclaimDaemons(before) {
|
|
2616
|
-
const scope = {
|
|
2617
|
-
watchman: false,
|
|
2618
|
-
gradle: true
|
|
2619
|
-
};
|
|
2620
|
-
const gradle = stillPlanned(await collectMemoryReport(scope, true), before, "gradleDaemon");
|
|
2621
|
-
for (const entry of gradle.filter((e) => !e.reclaimable)) keep(entry, "gradleDaemon", entry.detail ?? "it could not be proven idle");
|
|
2622
|
-
const stopping = gradle.filter((entry) => entry.reclaimable);
|
|
2623
|
-
let failures = await stopGradle(stopping, hostRows());
|
|
2624
|
-
if (!before.processes.some((entry) => entry.kind === "kotlinDaemon")) return failures;
|
|
2625
|
-
await waitForExit(stopping.map((entry) => entry.pid));
|
|
2626
|
-
const kotlin = stillPlanned(await collectMemoryReport(scope, true), before, "kotlinDaemon");
|
|
2627
|
-
for (const entry of before.processes.filter((e) => e.kind === "kotlinDaemon" && e.reclaimable)) {
|
|
2628
|
-
if (kotlin.some((current) => current.pid === entry.pid) || alive(entry.pid)) continue;
|
|
2629
|
-
console.log(chalk.green(`${processLabel(entry)} exited with its Gradle daemon, freeing ${formatBytes(entry.bytes)}`));
|
|
2630
|
-
recordGcResult("kotlinDaemon", "done", processLabel(entry), {
|
|
2631
|
-
id: String(entry.pid),
|
|
2632
|
-
bytes: entry.bytes
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
|
-
const rows = hostRows();
|
|
2636
|
-
for (const entry of kotlin) if (entry.reclaimable) failures += stopKotlin(entry, rows);
|
|
2637
|
-
else keep(entry, "kotlinDaemon", entry.detail ?? "it could not be proven idle");
|
|
2638
|
-
return failures;
|
|
2639
|
-
}
|
|
2640
|
-
/** Whether `gc --delete --cache <cache>` would act on a memory kind: stop a process or remove a watchman root. */
|
|
2641
|
-
function memoryWorkPending(cache, report) {
|
|
2642
|
-
if (!memoryCacheKind(cache) || !report) return false;
|
|
2643
|
-
return reclaimableBytes(report) > 0 || report.watchmanRoots.some((root) => root.removable);
|
|
2644
|
-
}
|
|
2645
|
-
/** Acts on the one memory kind `--cache` names: never on an unscoped run or `--cache all`. */
|
|
2646
|
-
async function reclaimMemory(cache, report) {
|
|
2647
|
-
const kind = memoryCacheKind(cache);
|
|
2648
|
-
if (!kind || !report || report.processes.length === 0) return 0;
|
|
2649
|
-
return kind === "watchman" ? reclaimWatchman(report) : reclaimDaemons(report);
|
|
2650
|
-
}
|
|
2651
|
-
/** A doctor note when watchman's footprint is over 2 GiB: only a restart shrinks it, and gc says when one is safe. */
|
|
2652
|
-
function watchmanFinding(report) {
|
|
2653
|
-
const daemon = report.processes.find((entry) => entry.kind === "watchman");
|
|
2654
|
-
if (!daemon || daemon.bytes < WATCHMAN_DOCTOR_BYTES) return null;
|
|
2655
|
-
const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
|
|
2656
|
-
const state = daemon.reclaimable ? "no client uses it now" : `it is kept: ${daemon.detail}`;
|
|
2657
|
-
return {
|
|
2658
|
-
level: "note",
|
|
2659
|
-
title: `watchman uses ${formatBytes(daemon.bytes)}`,
|
|
2660
|
-
detail: `The shared watchman daemon (pid ${daemon.pid}) has a ${formatBytes(daemon.bytes)} ${daemon.measure} with ${report.watchmanRoots.length} watched root(s), ${stale} of them stale; ${state}. Removing roots stops their recrawls, but only a restart returns the memory.`,
|
|
2661
|
-
fix: "Run `stim gc --cache watchman` to see its roots and clients, then `stim gc --delete --cache watchman` once no workspace uses it."
|
|
2662
|
-
};
|
|
2663
|
-
}
|
|
2664
|
-
async function inspectWatchmanMemory() {
|
|
2665
|
-
if (process.platform === "win32" || memorySweepIsScoped()) return null;
|
|
2666
|
-
return watchmanFinding(await collectMemoryReport({
|
|
2667
|
-
watchman: true,
|
|
2668
|
-
gradle: false
|
|
2669
|
-
}));
|
|
2670
|
-
}
|
|
2671
|
-
//#endregion
|
|
2672
|
-
//#region src/commands/gc/archive-selector.ts
|
|
2673
|
-
const KIND_SELECTORS = {
|
|
2674
|
-
"archived-logs": "logs",
|
|
2675
|
-
"archived-recordings": "recordings",
|
|
2676
|
-
"archived-agent": "agentActions"
|
|
2677
|
-
};
|
|
2678
|
-
function parseArchiveSelector(scope) {
|
|
2679
|
-
const trimmed = scope.trim();
|
|
2680
|
-
const colon = trimmed.indexOf(":");
|
|
2681
|
-
const name = (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();
|
|
2682
|
-
const id = colon === -1 ? null : trimmed.slice(colon + 1);
|
|
2683
|
-
if (name === "archived") return {
|
|
2684
|
-
kind: null,
|
|
2685
|
-
id
|
|
2686
|
-
};
|
|
2687
|
-
const kind = KIND_SELECTORS[name];
|
|
2688
|
-
return kind ? {
|
|
2689
|
-
kind,
|
|
2690
|
-
id
|
|
2691
|
-
} : null;
|
|
2692
|
-
}
|
|
2693
|
-
//#endregion
|
|
2694
|
-
//#region src/commands/gc/caches.ts
|
|
2695
|
-
const DAY_MS$1 = 864e5;
|
|
2696
|
-
function cacheSweepIsScoped() {
|
|
2697
|
-
return Boolean(process.env.STIM_HOME);
|
|
2698
|
-
}
|
|
2699
|
-
function isInsideConfigDir(dir) {
|
|
2700
|
-
const root = canonicalPath(getConfigDir());
|
|
2701
|
-
const target = canonicalPath(dir);
|
|
2702
|
-
const rel = relative$1(root, target);
|
|
2703
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
2704
|
-
}
|
|
2705
|
-
function machineGlobalReason(cache) {
|
|
2706
|
-
if (!cacheSweepIsScoped()) return null;
|
|
2707
|
-
if (cache.source === "registered") return null;
|
|
2708
|
-
if (isInsideConfigDir(cache.dir)) return null;
|
|
2709
|
-
return `STIM_HOME scopes this config, but ${cache.dir} is outside it and therefore machine-global`;
|
|
2710
|
-
}
|
|
2711
|
-
const EVERY_CACHE = "all";
|
|
2712
|
-
const WORKSPACE_OUTPUTS = "workspaces";
|
|
2713
|
-
const RECORDINGS = "recordings";
|
|
2714
|
-
const PARKED_DEVICES = "parked";
|
|
2715
|
-
function includesArchives(name) {
|
|
2716
|
-
return name ? parseArchiveSelector(name) !== null : false;
|
|
2717
|
-
}
|
|
2718
|
-
function includesParkedDevices(name) {
|
|
2719
|
-
return name?.trim().toLowerCase() === PARKED_DEVICES;
|
|
2720
|
-
}
|
|
2721
|
-
function includesRecordings(name) {
|
|
2722
|
-
const wanted = name?.trim().toLowerCase();
|
|
2723
|
-
return !wanted || wanted === EVERY_CACHE || wanted === RECORDINGS;
|
|
2724
|
-
}
|
|
2725
|
-
function isWorkspacesScope(name) {
|
|
2726
|
-
return name?.trim().toLowerCase() === WORKSPACE_OUTPUTS;
|
|
2727
|
-
}
|
|
2728
|
-
function includesWorkspaceOutputs(name) {
|
|
2729
|
-
const wanted = name?.trim().toLowerCase();
|
|
2730
|
-
return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
|
|
2731
|
-
}
|
|
2732
|
-
/** Whether a --cache name selects something besides shared caches: recordings, parked devices or a memory kind. */
|
|
2733
|
-
function selectsBeyondCaches(name) {
|
|
2734
|
-
return includesArchives(name) || includesRecordings(name) || includesParkedDevices(name) || memoryCacheKind(name) !== null;
|
|
2735
|
-
}
|
|
2736
|
-
function selectCaches(caches, name) {
|
|
2737
|
-
if (!name) return caches;
|
|
2738
|
-
const wanted = name.trim().toLowerCase();
|
|
2739
|
-
if (wanted === EVERY_CACHE) return caches;
|
|
2740
|
-
if (includesArchives(wanted) || wanted === WORKSPACE_OUTPUTS || wanted === RECORDINGS || wanted === PARKED_DEVICES || memoryCacheKind(wanted)) return [];
|
|
2741
|
-
return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
|
|
2742
|
-
}
|
|
2743
|
-
function emptyVerdict(c) {
|
|
2744
|
-
if (c.prune === "report-only") return {
|
|
2745
|
-
willEmpty: false,
|
|
2746
|
-
emptySkipped: "report-only shared cache; Stim never deletes it"
|
|
2747
|
-
};
|
|
2748
|
-
if (c.machineGlobal) return {
|
|
2749
|
-
willEmpty: false,
|
|
2750
|
-
emptySkipped: c.machineGlobal
|
|
2751
|
-
};
|
|
2752
|
-
if (!ownsItsDirectory(c)) return {
|
|
2753
|
-
willEmpty: false,
|
|
2754
|
-
emptySkipped: `${c.dir} is not a directory this cache owns`
|
|
2755
|
-
};
|
|
2756
|
-
return {
|
|
2757
|
-
willEmpty: true,
|
|
2758
|
-
emptySkipped: null
|
|
2759
|
-
};
|
|
2760
|
-
}
|
|
2761
|
-
function planCacheEmptying(caches, all) {
|
|
2762
|
-
const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
|
|
2763
|
-
return all ? annotated.map((c) => Object.assign({}, c, emptyVerdict(c))) : annotated;
|
|
2764
|
-
}
|
|
2765
|
-
/** What `gc --delete --cache <name>` would do to each cache, for a report that does not run that scope. */
|
|
2766
|
-
function previewScopedEmptying(caches) {
|
|
2767
|
-
return planCacheEmptying(caches, false).map((c) => Object.assign({}, c, { scopedEmpty: emptyVerdict(c) }));
|
|
2768
|
-
}
|
|
2769
|
-
function ownsItsDirectory(cache) {
|
|
2770
|
-
return !Array.isArray(cache.files) && !cacheDirRefusal(cache.dir);
|
|
2771
|
-
}
|
|
2772
|
-
function emptyCache(cache) {
|
|
2773
|
-
if (cache.prune === "report-only") return {
|
|
2774
|
-
removed: 0,
|
|
2775
|
-
bytes: 0,
|
|
2776
|
-
skipped: "report-only shared cache; Stim never deletes it"
|
|
2777
|
-
};
|
|
2778
|
-
if (cache.prune !== "atomic") return pruneCache(cache, {
|
|
2779
|
-
olderThanDays: 0,
|
|
2780
|
-
now: Date.now() + DAY_MS$1
|
|
2781
|
-
});
|
|
2782
|
-
let names;
|
|
2783
|
-
try {
|
|
2784
|
-
names = readdirSync$1(cache.dir);
|
|
2785
|
-
} catch (e) {
|
|
2786
|
-
return {
|
|
2787
|
-
removed: 0,
|
|
2788
|
-
bytes: 0,
|
|
2789
|
-
skipped: `could not read ${cache.dir}: ${e.message}`
|
|
2790
|
-
};
|
|
2791
|
-
}
|
|
2792
|
-
let removed = 0;
|
|
2793
|
-
let failed = 0;
|
|
2794
|
-
for (const name of names) try {
|
|
2795
|
-
rmSync$1(join$1(cache.dir, name), {
|
|
2796
|
-
recursive: true,
|
|
2797
|
-
force: true
|
|
2798
|
-
});
|
|
2799
|
-
removed++;
|
|
2800
|
-
} catch {
|
|
2801
|
-
failed++;
|
|
2802
|
-
}
|
|
2803
|
-
return {
|
|
2804
|
-
removed,
|
|
2805
|
-
bytes: failed ? 0 : cache.bytes ?? 0,
|
|
2806
|
-
failed,
|
|
2807
|
-
skipped: null
|
|
2808
|
-
};
|
|
2809
|
-
}
|
|
2810
|
-
function reportFailures(cache, failed) {
|
|
2811
|
-
if (!failed) return;
|
|
2812
|
-
const message = `${failed} entr${failed === 1 ? "y" : "ies"} in ${cache.dir} could not be removed`;
|
|
2813
|
-
console.log(chalk.red(`${cache.name}: ${message}`));
|
|
2814
|
-
recordGcResult("cache", "failed", cache.name, {
|
|
2815
|
-
id: cache.dir,
|
|
2816
|
-
detail: message
|
|
2817
|
-
});
|
|
2818
|
-
process.exitCode = 1;
|
|
2819
|
-
}
|
|
2820
|
-
function trimCaches(caches, olderThan) {
|
|
2821
|
-
let cacheBytes = 0;
|
|
2822
|
-
const trimmed = [];
|
|
2823
|
-
for (const c of caches) {
|
|
2824
|
-
if (c.machineGlobal) {
|
|
2825
|
-
console.log(chalk.yellow(`Left ${c.name} alone: ${c.machineGlobal}`));
|
|
2826
|
-
recordGcResult("cache", "kept", c.name, {
|
|
2827
|
-
id: c.dir,
|
|
2828
|
-
detail: c.machineGlobal
|
|
2829
|
-
});
|
|
2830
|
-
continue;
|
|
2831
|
-
}
|
|
2832
|
-
const r = pruneCache(c, { olderThanDays: olderThan });
|
|
2833
|
-
if (r.skipped) {
|
|
2834
|
-
console.log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
|
|
2835
|
-
recordGcResult("cache", "kept", c.name, {
|
|
2836
|
-
id: c.dir,
|
|
2837
|
-
detail: r.skipped
|
|
2838
|
-
});
|
|
2839
|
-
} else if (r.removed) {
|
|
2840
|
-
cacheBytes += r.bytes;
|
|
2841
|
-
trimmed.push(c);
|
|
2842
|
-
recordGcResult("cache", "done", c.name, {
|
|
2843
|
-
id: c.dir,
|
|
2844
|
-
bytes: r.bytes
|
|
2845
|
-
});
|
|
2846
|
-
console.log(chalk.green(`Trimmed ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
|
|
2847
|
-
} else if (!r.failed) console.log(chalk.dim(`${c.name}: nothing older than ${olderThan}d`));
|
|
2848
|
-
reportFailures(c, r.failed);
|
|
2849
|
-
}
|
|
2850
|
-
if (cacheBytes) console.log(chalk.dim(`Trimmed ${formatBytes(cacheBytes)} of shared cache. The next build that wanted those entries pays to rebuild them.`));
|
|
2851
|
-
return trimmed;
|
|
2852
|
-
}
|
|
2853
|
-
function emptyCaches(caches) {
|
|
2854
|
-
let cacheBytes = 0;
|
|
2855
|
-
for (const c of caches) {
|
|
2856
|
-
if (!c.willEmpty) {
|
|
2857
|
-
console.log(chalk.yellow(`Left ${c.name} alone: ${c.emptySkipped}`));
|
|
2858
|
-
recordGcResult("cache", "kept", c.name, {
|
|
2859
|
-
id: c.dir,
|
|
2860
|
-
detail: c.emptySkipped ?? null
|
|
2861
|
-
});
|
|
2862
|
-
continue;
|
|
2863
|
-
}
|
|
2864
|
-
const r = emptyCache(c);
|
|
2865
|
-
if (r.skipped) {
|
|
2866
|
-
console.log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
|
|
2867
|
-
recordGcResult("cache", "kept", c.name, {
|
|
2868
|
-
id: c.dir,
|
|
2869
|
-
detail: r.skipped
|
|
2870
|
-
});
|
|
2871
|
-
} else if (r.removed) {
|
|
2872
|
-
cacheBytes += r.bytes;
|
|
2873
|
-
recordGcResult("cache", "done", c.name, {
|
|
2874
|
-
id: c.dir,
|
|
2875
|
-
bytes: r.bytes
|
|
2876
|
-
});
|
|
2877
|
-
console.log(chalk.green(`Emptied ${c.name}: ${r.removed} entr${r.removed === 1 ? "y" : "ies"} (${formatBytes(r.bytes)})`));
|
|
2878
|
-
} else if (!r.failed) console.log(chalk.dim(`${c.name}: already empty`));
|
|
2879
|
-
reportFailures(c, r.failed);
|
|
2880
|
-
}
|
|
2881
|
-
if (cacheBytes) console.log(chalk.dim(`Emptied ${formatBytes(cacheBytes)} of shared cache. Every build that wanted any of it now pays to rebuild it.`));
|
|
2882
|
-
}
|
|
2883
|
-
//#endregion
|
|
2884
|
-
//#region src/commands/gc/devices.ts
|
|
2885
|
-
const DEVICE_LIST_TIMEOUT_MS$1 = 3e4;
|
|
2886
|
-
const AVD_SIZE_TIMEOUT_MS = 5e3;
|
|
2887
|
-
const DAY_MS = 864e5;
|
|
2888
|
-
function describeKept(ref) {
|
|
2889
|
-
return ref.mounted ? `referenced by ${ref.path}` : `referenced by ${ref.path} (volume not mounted; kept just in case)`;
|
|
2890
|
-
}
|
|
2891
|
-
function findOrphanedDevices({ sims = [], avds = [], config, isMounted, deadProjects = [] }) {
|
|
2892
|
-
const dead = new Set(deadProjects);
|
|
2893
|
-
const referenced = /* @__PURE__ */ new Map();
|
|
2894
|
-
for (const [path, proj] of Object.entries(config?.projects || {})) {
|
|
2895
|
-
if (dead.has(path)) continue;
|
|
2896
|
-
const mounted = isMounted ? isMounted(path) : true;
|
|
2897
|
-
for (const { platforms } of projectDeviceSlots(proj)) {
|
|
2898
|
-
const ios = platforms.ios;
|
|
2899
|
-
if (ios?.deviceUdid) referenced.set(ios.deviceUdid, {
|
|
2900
|
-
path,
|
|
2901
|
-
mounted
|
|
2902
|
-
});
|
|
2903
|
-
const android = platforms.android;
|
|
2904
|
-
if (android?.avdName) referenced.set(android.avdName, {
|
|
2905
|
-
path,
|
|
2906
|
-
mounted
|
|
2907
|
-
});
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
for (const sim of readParked("ios", { config })) referenced.set(sim.udid, {
|
|
2911
|
-
path: "the simulator pool",
|
|
2912
|
-
mounted: true
|
|
2913
|
-
});
|
|
2914
|
-
for (const avd of readParked("android", { config })) referenced.set(avd.name, {
|
|
2915
|
-
path: "the emulator pool",
|
|
2916
|
-
mounted: true
|
|
2917
|
-
});
|
|
2918
|
-
const orphaned = [];
|
|
2919
|
-
const kept = [];
|
|
2920
|
-
const unverified = [];
|
|
2921
|
-
for (const sim of sims) {
|
|
2922
|
-
if (!sim?.name?.startsWith("stim-")) continue;
|
|
2923
|
-
const ref = referenced.get(sim.udid);
|
|
2924
|
-
const device = {
|
|
2925
|
-
kind: "ios",
|
|
2926
|
-
id: sim.udid,
|
|
2927
|
-
name: sim.name
|
|
2928
|
-
};
|
|
2929
|
-
if (!ref && !isStimCreated(device)) unverified.push(device);
|
|
2930
|
-
else if (!ref) orphaned.push({
|
|
2931
|
-
...device,
|
|
2932
|
-
...simBytes(sim)
|
|
2933
|
-
});
|
|
2934
|
-
else kept.push({
|
|
2935
|
-
kind: "ios",
|
|
2936
|
-
id: sim.udid,
|
|
2937
|
-
name: sim.name,
|
|
2938
|
-
reason: describeKept(ref)
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
for (const avdName of avds) {
|
|
2942
|
-
if (!avdName?.startsWith("stim-")) continue;
|
|
2943
|
-
const ref = referenced.get(avdName);
|
|
2944
|
-
const device = {
|
|
2945
|
-
kind: "android",
|
|
2946
|
-
id: avdName,
|
|
2947
|
-
name: avdName
|
|
2948
|
-
};
|
|
2949
|
-
if (!ref && !isStimCreated(device)) unverified.push(device);
|
|
2950
|
-
else if (!ref) orphaned.push(device);
|
|
2951
|
-
else kept.push({
|
|
2952
|
-
kind: "android",
|
|
2953
|
-
id: avdName,
|
|
2954
|
-
name: avdName,
|
|
2955
|
-
reason: describeKept(ref)
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
2958
|
-
return {
|
|
2959
|
-
orphaned,
|
|
2960
|
-
kept,
|
|
2961
|
-
unverified
|
|
2962
|
-
};
|
|
2963
|
-
}
|
|
2964
|
-
function simBytes(sim) {
|
|
2965
|
-
return typeof sim.dataPathSize === "number" ? { bytes: sim.dataPathSize } : {};
|
|
2966
|
-
}
|
|
2967
|
-
function isStimCreated(device) {
|
|
2968
|
-
return device.kind === "ios" ? isStimOwnedSim({
|
|
2969
|
-
udid: device.id,
|
|
2970
|
-
name: device.name
|
|
2971
|
-
}) : isStimOwnedAvd(device.id);
|
|
2972
|
-
}
|
|
2973
|
-
function unverifiedDeviceCommand(device) {
|
|
2974
|
-
if (device.kind === "ios") return `xcrun simctl delete ${device.id}`;
|
|
2975
|
-
return device.directory ? `rm -rf ${JSON.stringify(device.directory)}` : `avdmanager delete avd -n ${JSON.stringify(device.id)}`;
|
|
2976
|
-
}
|
|
2977
|
-
function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays, now = Date.now(), lastTouched, deadProjects = [] }) {
|
|
2978
|
-
if (!Number.isFinite(olderThanDays) || typeof lastTouched !== "function") return [];
|
|
2979
|
-
const cutoff = now - olderThanDays * DAY_MS;
|
|
2980
|
-
const dead = new Set(deadProjects);
|
|
2981
|
-
const liveSims = new Map(sims.filter((s) => s?.name?.startsWith("stim-")).map((s) => [s.udid, s]));
|
|
2982
|
-
const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
|
|
2983
|
-
const stale = [];
|
|
2984
|
-
for (const [path, proj] of Object.entries(config?.projects || {})) {
|
|
2985
|
-
if (dead.has(path) || !isAbsolute$1(path)) continue;
|
|
2986
|
-
const touched = lastTouched(path);
|
|
2987
|
-
if (!Number.isFinite(touched) || touched >= cutoff) continue;
|
|
2988
|
-
const idleDays = Math.floor((now - touched) / DAY_MS);
|
|
2989
|
-
for (const { slot, platforms } of projectDeviceSlots(proj)) {
|
|
2990
|
-
const ios = platforms.ios;
|
|
2991
|
-
const sim = ios?.owned && ios.deviceUdid ? liveSims.get(ios.deviceUdid) : void 0;
|
|
2992
|
-
if (sim) stale.push({
|
|
2993
|
-
kind: "ios",
|
|
2994
|
-
id: sim.udid,
|
|
2995
|
-
name: sim.name,
|
|
2996
|
-
project: path,
|
|
2997
|
-
...slot === "default" ? {} : { slot },
|
|
2998
|
-
idleDays,
|
|
2999
|
-
...simBytes(sim)
|
|
3000
|
-
});
|
|
3001
|
-
const android = platforms.android;
|
|
3002
|
-
if (android?.owned && android.avdName && liveAvds.has(android.avdName)) stale.push({
|
|
3003
|
-
kind: "android",
|
|
3004
|
-
id: android.avdName,
|
|
3005
|
-
name: android.avdName,
|
|
3006
|
-
project: path,
|
|
3007
|
-
idleDays,
|
|
3008
|
-
...slot === "default" ? {} : { slot }
|
|
3009
|
-
});
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
return stale;
|
|
3013
|
-
}
|
|
3014
|
-
function findStaleDeviceRecords({ config, sims = [], avds = [], deadProjects = [], simsChecked = true, avdsChecked = true }) {
|
|
3015
|
-
const dead = new Set(deadProjects);
|
|
3016
|
-
const liveSims = new Set(sims.map((s) => s?.udid).filter(Boolean));
|
|
3017
|
-
const liveAvds = new Set(avds.filter((a) => typeof a === "string"));
|
|
3018
|
-
const stale = [];
|
|
3019
|
-
for (const [path, proj] of Object.entries(config?.projects || {})) {
|
|
3020
|
-
if (dead.has(path)) continue;
|
|
3021
|
-
for (const { slot, platforms } of projectDeviceSlots(proj)) {
|
|
3022
|
-
const ios = platforms.ios;
|
|
3023
|
-
if (simsChecked && ios?.deviceUdid && !liveSims.has(ios.deviceUdid)) stale.push({
|
|
3024
|
-
kind: "ios",
|
|
3025
|
-
id: ios.deviceUdid,
|
|
3026
|
-
project: path,
|
|
3027
|
-
...slot === "default" ? {} : { slot },
|
|
3028
|
-
owned: Boolean(ios.owned)
|
|
3029
|
-
});
|
|
3030
|
-
const android = platforms.android;
|
|
3031
|
-
if (avdsChecked && android?.avdName && !liveAvds.has(android.avdName)) stale.push({
|
|
3032
|
-
kind: "android",
|
|
3033
|
-
id: android.avdName,
|
|
3034
|
-
project: path,
|
|
3035
|
-
...slot === "default" ? {} : { slot },
|
|
3036
|
-
owned: Boolean(android.owned)
|
|
3037
|
-
});
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
return stale;
|
|
3041
|
-
}
|
|
3042
|
-
function describeUnverifiableDevices(simNames = [], avdNames = [], { reason = "no Stim config found" } = {}) {
|
|
3043
|
-
const ours = [...simNames, ...avdNames].filter((n) => typeof n === "string" && n.startsWith("stim-"));
|
|
3044
|
-
if (ours.length === 0) return [`${reason}; device sweep skipped`];
|
|
3045
|
-
return [`${reason}, so ${ours.length} stim-created device(s) cannot be verified as orphaned: ${ours.join(", ")}`, "they were NOT touched. If they are stale, delete them with `xcrun simctl delete <udid>` or `avdmanager delete avd -n <name>`"];
|
|
3046
|
-
}
|
|
3047
|
-
function withAndroidAvdSizes(devices, { avdDirectory = ownedAvdDirectory, size = directorySize } = {}) {
|
|
3048
|
-
return devices.map((device) => {
|
|
3049
|
-
if (device.kind !== "android") return device;
|
|
3050
|
-
const dir = device.orphanedDirectory?.directory ?? avdDirectory(device.id);
|
|
3051
|
-
if (!dir) return device;
|
|
3052
|
-
try {
|
|
3053
|
-
const bytes = size(dir, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
|
|
3054
|
-
return bytes > 0 ? {
|
|
3055
|
-
...device,
|
|
3056
|
-
bytes
|
|
3057
|
-
} : device;
|
|
3058
|
-
} catch {
|
|
3059
|
-
return device;
|
|
3060
|
-
}
|
|
3061
|
-
});
|
|
3062
|
-
}
|
|
3063
|
-
function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } = {}) {
|
|
3064
|
-
const listed = new Map(sims.map((sim) => [sim.udid, sim]));
|
|
3065
|
-
return records.map((record) => {
|
|
3066
|
-
const sim = listed.get(record.udid);
|
|
3067
|
-
return {
|
|
3068
|
-
udid: record.udid,
|
|
3069
|
-
name: sim?.name ?? record.name,
|
|
3070
|
-
model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
|
|
3071
|
-
runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
|
|
3072
|
-
parkedAt: record.parkedAt,
|
|
3073
|
-
app: record.bundleId ?? null,
|
|
3074
|
-
bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
|
|
3075
|
-
listed: simsChecked ? Boolean(sim) : null
|
|
3076
|
-
};
|
|
3077
|
-
});
|
|
3078
|
-
}
|
|
3079
|
-
function parkedAtLeast(records, { olderThanDays = null, now = Date.now() }) {
|
|
3080
|
-
if (olderThanDays === null) return [...records];
|
|
3081
|
-
const cutoff = now - olderThanDays * DAY_MS;
|
|
3082
|
-
return records.filter((record) => Date.parse(record.parkedAt) <= cutoff);
|
|
3083
|
-
}
|
|
3084
|
-
function collectParkedSims(deps, age = {}) {
|
|
3085
|
-
const records = parkedAtLeast(readParked("ios"), age);
|
|
3086
|
-
if (records.length === 0) return [];
|
|
3087
|
-
let sims;
|
|
3088
|
-
let deviceTypes = [];
|
|
3089
|
-
try {
|
|
3090
|
-
sims = (deps.listAllIosSims ?? listAllIosSims)({
|
|
3091
|
-
timeoutMs: DEVICE_LIST_TIMEOUT_MS$1,
|
|
3092
|
-
includeUnavailable: true
|
|
3093
|
-
});
|
|
3094
|
-
} catch {
|
|
3095
|
-
return describeParkedSims(records, [], [], { simsChecked: false });
|
|
3096
|
-
}
|
|
3097
|
-
try {
|
|
3098
|
-
deviceTypes = (deps.listIosDeviceTypes ?? listIosDeviceTypes)();
|
|
3099
|
-
} catch {}
|
|
3100
|
-
return describeParkedSims(records, sims, deviceTypes);
|
|
3101
|
-
}
|
|
3102
|
-
function poolConfigurationDeviceProfile(configuration) {
|
|
3103
|
-
try {
|
|
3104
|
-
const entries = JSON.parse(configuration);
|
|
3105
|
-
const entry = Array.isArray(entries) ? entries.find((pair) => Array.isArray(pair) && pair[0] === "hw.device.name") : void 0;
|
|
3106
|
-
return typeof entry?.[1] === "string" ? entry[1] : null;
|
|
3107
|
-
} catch {
|
|
3108
|
-
return null;
|
|
3109
|
-
}
|
|
3110
|
-
}
|
|
3111
|
-
function collectParkedAvds(deps, age = {}) {
|
|
3112
|
-
const records = parkedAtLeast(readParked("android"), age);
|
|
3113
|
-
if (!records.length) return [];
|
|
3114
|
-
let avds = null;
|
|
3115
|
-
try {
|
|
3116
|
-
avds = (deps.listAvds ?? listAvds)();
|
|
3117
|
-
} catch {}
|
|
3118
|
-
return records.map((record) => {
|
|
3119
|
-
let bytes = null;
|
|
3120
|
-
try {
|
|
3121
|
-
const directory = (deps.avdDirectory ?? ownedAvdDirectory)(record.name);
|
|
3122
|
-
if (directory) bytes = (deps.directorySize ?? directorySize)(directory, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
|
|
3123
|
-
} catch {}
|
|
3124
|
-
return {
|
|
3125
|
-
name: record.name,
|
|
3126
|
-
systemImage: record.systemImage,
|
|
3127
|
-
deviceProfile: poolConfigurationDeviceProfile(record.configuration),
|
|
3128
|
-
parkedAt: record.parkedAt,
|
|
3129
|
-
app: record.packageName ?? null,
|
|
3130
|
-
bytes,
|
|
3131
|
-
listed: avds === null ? null : avds.includes(record.name)
|
|
3132
|
-
};
|
|
3133
|
-
});
|
|
3134
|
-
}
|
|
3135
|
-
function eraseParkedDevices(sims, avds) {
|
|
3136
|
-
let failures = 0;
|
|
3137
|
-
const devices = [...sims.map((sim) => ({
|
|
3138
|
-
platform: "ios",
|
|
3139
|
-
id: sim.udid,
|
|
3140
|
-
name: sim.name,
|
|
3141
|
-
listed: sim.listed,
|
|
3142
|
-
bytes: sim.bytes
|
|
3143
|
-
})), ...avds.map((avd) => ({
|
|
3144
|
-
platform: "android",
|
|
3145
|
-
id: avd.name,
|
|
3146
|
-
name: avd.name,
|
|
3147
|
-
listed: avd.listed,
|
|
3148
|
-
bytes: avd.bytes
|
|
3149
|
-
}))];
|
|
3150
|
-
for (const device of devices) {
|
|
3151
|
-
const what = `parked ${device.platform === "ios" ? "ios sim" : "android emulator"} ${device.name}`;
|
|
3152
|
-
if (device.listed !== true) {
|
|
3153
|
-
const unverified = device.listed === null;
|
|
3154
|
-
const detail = unverified ? "Stim could not list devices to verify it; it was kept. Retry stim gc --cache parked --delete." : "it is not on this machine; stim gc --delete drops its pool record.";
|
|
3155
|
-
if (unverified) failures++;
|
|
3156
|
-
console.log((unverified ? chalk.red : chalk.dim)(`Did not erase ${what}: ${detail}`));
|
|
3157
|
-
recordGcResult("parkedDevice", unverified ? "failed" : "kept", device.name, {
|
|
3158
|
-
id: device.id,
|
|
3159
|
-
detail
|
|
3160
|
-
});
|
|
3161
|
-
continue;
|
|
3162
|
-
}
|
|
3163
|
-
const result = device.platform === "ios" ? eraseParkedIosSim(device.id, { label: device.name }) : eraseParkedAvd(device.id);
|
|
3164
|
-
if (result.status === "torn-down") {
|
|
3165
|
-
console.log(chalk.green(`Erased ${what}; it stays parked`));
|
|
3166
|
-
recordGcResult("parkedDevice", "done", device.name, {
|
|
3167
|
-
id: device.id,
|
|
3168
|
-
bytes: device.bytes
|
|
3169
|
-
});
|
|
3170
|
-
} else if (result.status === "failed") {
|
|
3171
|
-
failures++;
|
|
3172
|
-
console.log(chalk.red(`Failed to erase ${what}: ${result.reason}`));
|
|
3173
|
-
recordGcResult("parkedDevice", "failed", device.name, {
|
|
3174
|
-
id: device.id,
|
|
3175
|
-
detail: result.reason
|
|
3176
|
-
});
|
|
3177
|
-
} else console.log(chalk.dim(`Skipped ${device.name}; it is no longer parked.`));
|
|
3178
|
-
}
|
|
3179
|
-
return failures;
|
|
3180
|
-
}
|
|
3181
|
-
function deleteParkedAvds(avds) {
|
|
3182
|
-
let failures = 0;
|
|
3183
|
-
for (const avd of avds) {
|
|
3184
|
-
if (avd.listed === null) {
|
|
3185
|
-
failures++;
|
|
3186
|
-
console.log(chalk.red(`Could not verify parked android emulator ${avd.name}; its pool record was kept.`));
|
|
3187
|
-
recordGcResult("parkedDevice", "failed", avd.name, {
|
|
3188
|
-
id: avd.name,
|
|
3189
|
-
detail: "Stim could not list emulators to verify it; its pool record was kept. Retry stim gc --delete."
|
|
3190
|
-
});
|
|
3191
|
-
continue;
|
|
3192
|
-
}
|
|
3193
|
-
const result = teardownParkedAvd(avd.name);
|
|
3194
|
-
if (result.status === "failed") {
|
|
3195
|
-
failures++;
|
|
3196
|
-
console.log(chalk.red(`Failed to delete parked android emulator ${avd.name}: ${result.reason}`));
|
|
3197
|
-
recordGcResult("parkedDevice", "failed", avd.name, {
|
|
3198
|
-
id: avd.name,
|
|
3199
|
-
detail: result.reason
|
|
3200
|
-
});
|
|
3201
|
-
} else if (result.status === "torn-down") {
|
|
3202
|
-
console.log(chalk.green(`Deleted parked android emulator ${avd.name}`));
|
|
3203
|
-
recordGcResult("parkedDevice", "done", avd.name, {
|
|
3204
|
-
id: avd.name,
|
|
3205
|
-
bytes: avd.bytes
|
|
3206
|
-
});
|
|
3207
|
-
} else console.log(chalk.dim(`Skipped ${avd.name}; it is no longer parked.`));
|
|
3208
|
-
}
|
|
3209
|
-
return failures;
|
|
3210
|
-
}
|
|
3211
|
-
function deleteParkedSims(parkedSims, deps = {}) {
|
|
3212
|
-
let failures = 0;
|
|
3213
|
-
let emptied = 0;
|
|
3214
|
-
for (const sim of parkedSims) {
|
|
3215
|
-
if (sim.listed === null) {
|
|
3216
|
-
failures++;
|
|
3217
|
-
console.log(chalk.red(`Could not verify parked ios sim ${sim.name} (${sim.udid}); its pool record was kept.`));
|
|
3218
|
-
recordGcResult("parkedDevice", "failed", sim.name, {
|
|
3219
|
-
id: sim.udid,
|
|
3220
|
-
detail: "Stim could not list simulators to verify it; its pool record was kept. Retry stim gc --delete."
|
|
3221
|
-
});
|
|
3222
|
-
continue;
|
|
3223
|
-
}
|
|
3224
|
-
try {
|
|
3225
|
-
const teardown = sim.listed ? teardownParkedIosSim(sim.udid, {
|
|
3226
|
-
label: sim.name,
|
|
3227
|
-
deleteSim: deps.deleteParkedIosSim
|
|
3228
|
-
}) : null;
|
|
3229
|
-
if (!(sim.listed ? teardown?.status === "torn-down" : dropParked("ios", sim.udid))) {
|
|
3230
|
-
if (teardown?.status === "failed") {
|
|
3231
|
-
failures++;
|
|
3232
|
-
console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${teardown.reason}`));
|
|
3233
|
-
recordGcResult("parkedDevice", "failed", sim.name, {
|
|
3234
|
-
id: sim.udid,
|
|
3235
|
-
detail: teardown.reason
|
|
3236
|
-
});
|
|
3237
|
-
} else console.log(chalk.dim(`Skipped ${sim.name} (${sim.udid}); it is no longer parked.`));
|
|
3238
|
-
continue;
|
|
3239
|
-
}
|
|
3240
|
-
emptied++;
|
|
3241
|
-
recordGcResult("parkedDevice", "done", sim.name, {
|
|
3242
|
-
id: sim.udid,
|
|
3243
|
-
bytes: sim.listed ? sim.bytes : null
|
|
3244
|
-
});
|
|
3245
|
-
console.log(chalk.green(sim.listed ? `Deleted parked ios sim ${sim.name} (${sim.udid})` : `Dropped the parked record for ${sim.name} (${sim.udid}); it is not on this machine.`));
|
|
3246
|
-
} catch (err) {
|
|
3247
|
-
failures++;
|
|
3248
|
-
console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${err?.message || err}`));
|
|
3249
|
-
recordGcResult("parkedDevice", "failed", sim.name, {
|
|
3250
|
-
id: sim.udid,
|
|
3251
|
-
detail: err?.message || String(err)
|
|
3252
|
-
});
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
if (emptied) console.log(chalk.dim(` emptied ${plural(emptied, "parked simulator")} from the pool`));
|
|
3256
|
-
return failures;
|
|
3257
|
-
}
|
|
3258
|
-
function deviceSweepIsScoped() {
|
|
3259
|
-
return Boolean(process.env.STIM_HOME);
|
|
3260
|
-
}
|
|
3261
|
-
function collectDeviceLeases(now) {
|
|
3262
|
-
const expired = [];
|
|
3263
|
-
const kept = [];
|
|
3264
|
-
for (const entry of listLeaseFiles()) {
|
|
3265
|
-
const lease = entry.lease;
|
|
3266
|
-
if (!lease) {
|
|
3267
|
-
kept.push({
|
|
3268
|
-
name: entry.name,
|
|
3269
|
-
path: entry.path,
|
|
3270
|
-
reason: "it does not parse as a lease, so no run may take that device"
|
|
3271
|
-
});
|
|
3272
|
-
continue;
|
|
3273
|
-
}
|
|
3274
|
-
if (leaseIsExpired(lease, now)) {
|
|
3275
|
-
expired.push(entry);
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
if (!existsSync$1(lease.holder)) kept.push({
|
|
3279
|
-
name: entry.name,
|
|
3280
|
-
path: entry.path,
|
|
3281
|
-
reason: `${lease.holder} is not on this machine, but the lease runs until ${lease.expiresAt}`
|
|
3282
|
-
});
|
|
3283
|
-
}
|
|
3284
|
-
return {
|
|
3285
|
-
expired,
|
|
3286
|
-
kept
|
|
3287
|
-
};
|
|
3288
|
-
}
|
|
3289
|
-
function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords) {
|
|
3290
|
-
let deleteFailures = 0;
|
|
3291
|
-
function reap(d, onlyIfMissing = false) {
|
|
3292
|
-
const r = d.kind === "ios" ? teardownOwnedIosSim(d.id, {
|
|
3293
|
-
del: true,
|
|
3294
|
-
label: d.name
|
|
3295
|
-
}) : teardownOwnedAvd(d.name, {
|
|
3296
|
-
del: true,
|
|
3297
|
-
onlyIfMissing,
|
|
3298
|
-
..."project" in d ? { owner: {
|
|
3299
|
-
projectPath: d.project,
|
|
3300
|
-
slot: d.slot
|
|
3301
|
-
} } : {},
|
|
3302
|
-
..."project" in d ? { onRemoved: () => {
|
|
3303
|
-
if (clearDevice(d.project, d.kind, d.slot, d.id)) console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
|
|
3304
|
-
} } : {},
|
|
3305
|
-
..."orphanedDirectory" in d ? { orphanedDirectory: d.orphanedDirectory } : {}
|
|
3306
|
-
});
|
|
3307
|
-
const what = d.kind === "ios" ? `ios sim ${d.name} (${d.id})` : `android avd ${d.name}`;
|
|
3308
|
-
const result = {
|
|
3309
|
-
id: d.id,
|
|
3310
|
-
bytes: d.bytes ?? null
|
|
3311
|
-
};
|
|
3312
|
-
if (r.status === "torn-down") {
|
|
3313
|
-
console.log(chalk.green(`Deleted ${what}`));
|
|
3314
|
-
recordGcResult("device", "done", d.name, result);
|
|
3315
|
-
} else if (r.status === "missing") console.log(chalk.dim(`${what} is already gone; nothing to delete.`));
|
|
3316
|
-
else if (r.status === "skipped") {
|
|
3317
|
-
console.log(chalk.yellow(`Skipped ${what}: ${r.reason} -- left for a later gc`));
|
|
3318
|
-
recordGcResult("device", "kept", d.name, {
|
|
3319
|
-
...result,
|
|
3320
|
-
detail: `${r.reason}; left for a later gc`
|
|
3321
|
-
});
|
|
3322
|
-
} else {
|
|
3323
|
-
deleteFailures++;
|
|
3324
|
-
console.log(chalk.red(`Failed to delete ${d.kind} device ${d.name}: ${r.reason}`));
|
|
3325
|
-
recordGcResult("device", "failed", d.name, {
|
|
3326
|
-
...result,
|
|
3327
|
-
detail: r.reason
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
return r.status;
|
|
3331
|
-
}
|
|
3332
|
-
for (const d of orphanedDevices) reap(d);
|
|
3333
|
-
for (const d of staleDevices) {
|
|
3334
|
-
const status = reap(d);
|
|
3335
|
-
if (d.kind === "ios" && (status === "torn-down" || status === "missing")) {
|
|
3336
|
-
clearDevice(d.project, d.kind, d.slot, d.id);
|
|
3337
|
-
console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
for (const r of staleDeviceRecords) if (r.kind === "android" && r.owned) reap({
|
|
3341
|
-
...r,
|
|
3342
|
-
name: r.id,
|
|
3343
|
-
idleDays: 0
|
|
3344
|
-
}, true);
|
|
3345
|
-
else if (withConfigLock$1(() => {
|
|
3346
|
-
if (r.kind === "android" && deviceSlotPlatforms(getProject(r.project), r.slot)?.android?.owned) return false;
|
|
3347
|
-
return clearDevice(r.project, r.kind, r.slot, r.id);
|
|
3348
|
-
})) {
|
|
3349
|
-
console.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
|
|
3350
|
-
recordGcResult("deviceRecord", "done", r.project, { id: r.id });
|
|
3351
|
-
}
|
|
3352
|
-
return deleteFailures;
|
|
3353
|
-
}
|
|
3354
|
-
//#endregion
|
|
3355
|
-
//#region src/commands/gc/idle.ts
|
|
3356
|
-
function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
|
|
3357
|
-
const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
|
|
3358
|
-
const dead = new Set(deadProjects);
|
|
3359
|
-
const found = [];
|
|
3360
|
-
for (const [project, record] of Object.entries(config?.projects ?? {})) {
|
|
3361
|
-
if (dead.has(project)) continue;
|
|
3362
|
-
for (const { slot, platforms } of projectDeviceSlots(record)) {
|
|
3363
|
-
const ios = platforms.ios;
|
|
3364
|
-
if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
|
|
3365
|
-
kind: "ios",
|
|
3366
|
-
id: ios.deviceUdid,
|
|
3367
|
-
name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
|
|
3368
|
-
project,
|
|
3369
|
-
slot,
|
|
3370
|
-
activity: readActivity({
|
|
3371
|
-
platform: "ios",
|
|
3372
|
-
id: ios.deviceUdid,
|
|
3373
|
-
slot,
|
|
3374
|
-
workspace: project
|
|
3375
|
-
})
|
|
3376
|
-
});
|
|
3377
|
-
const android = platforms.android;
|
|
3378
|
-
const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
|
|
3379
|
-
if (android?.avdName && serial) found.push({
|
|
3380
|
-
kind: "android",
|
|
3381
|
-
id: android.avdName,
|
|
3382
|
-
name: android.avdName,
|
|
3383
|
-
project,
|
|
3384
|
-
slot,
|
|
3385
|
-
activity: readActivity({
|
|
3386
|
-
platform: "android",
|
|
3387
|
-
id: serial,
|
|
3388
|
-
slot,
|
|
3389
|
-
workspace: project
|
|
3390
|
-
})
|
|
3391
|
-
});
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
return found;
|
|
3395
|
-
}
|
|
3396
|
-
function findIdleDevices({ buildInProgress, now, ...inputs }) {
|
|
3397
|
-
return findOwnedDeviceActivity(inputs).filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
|
|
3398
|
-
kind,
|
|
3399
|
-
id,
|
|
3400
|
-
name,
|
|
3401
|
-
project,
|
|
3402
|
-
slot,
|
|
3403
|
-
lastActivityAt: activity.lastActivityAt ?? null,
|
|
3404
|
-
idleForMs: idleForMs(activity, now),
|
|
3405
|
-
buildInProgress: buildInProgress(project)
|
|
3406
|
-
}));
|
|
3407
|
-
}
|
|
3408
|
-
function idleShutdownCandidates(devices, idleMs) {
|
|
3409
|
-
return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
|
|
3410
|
-
}
|
|
3411
|
-
function parseIdleDuration(value) {
|
|
3412
|
-
const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
|
|
3413
|
-
if (!match) return null;
|
|
3414
|
-
const unit = {
|
|
3415
|
-
m: 6e4,
|
|
3416
|
-
h: 36e5,
|
|
3417
|
-
d: 864e5
|
|
3418
|
-
}[match[2]];
|
|
3419
|
-
return Number(match[1]) * unit;
|
|
3420
|
-
}
|
|
3421
|
-
function workspaceBuildInProgress(project) {
|
|
3422
|
-
const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
|
|
3423
|
-
if (!record) return false;
|
|
3424
|
-
try {
|
|
3425
|
-
return activeBuildState(record.claim) !== "stale";
|
|
3426
|
-
} catch {
|
|
3427
|
-
return true;
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
function deviceActivityInputs(config, sims, now) {
|
|
3431
|
-
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
3432
|
-
return {
|
|
3433
|
-
config,
|
|
3434
|
-
sims,
|
|
3435
|
-
readActivity: createActivityReader({ now }),
|
|
3436
|
-
androidSerial: (avdName) => {
|
|
3437
|
-
try {
|
|
3438
|
-
return resolve(avdName).serial ?? null;
|
|
3439
|
-
} catch {
|
|
3440
|
-
return null;
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
};
|
|
3444
|
-
}
|
|
3445
|
-
function collectOwnedDeviceActivity(config, sims, now = Date.now()) {
|
|
3446
|
-
return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now));
|
|
3447
|
-
}
|
|
3448
|
-
function collectIdleDevices(config, sims, deadProjects, now = Date.now()) {
|
|
3449
|
-
return findIdleDevices({
|
|
3450
|
-
...deviceActivityInputs(config, sims, now),
|
|
3451
|
-
deadProjects,
|
|
3452
|
-
now,
|
|
3453
|
-
buildInProgress: workspaceBuildInProgress
|
|
3454
|
-
});
|
|
3455
|
-
}
|
|
3456
|
-
function idleDeviceLines(devices) {
|
|
3457
|
-
if (devices.length === 0) return [];
|
|
3458
|
-
return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
|
|
3459
|
-
const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
|
|
3460
|
-
const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
|
|
3461
|
-
const build = d.buildInProgress ? ", build in progress" : "";
|
|
3462
|
-
return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
|
|
3463
|
-
})];
|
|
3464
|
-
}
|
|
3465
|
-
function shutDownIdleDevices(devices, idleMs, recollect) {
|
|
3466
|
-
const candidates = idleShutdownCandidates(devices, idleMs);
|
|
3467
|
-
const shutDown = [];
|
|
3468
|
-
if (candidates.length === 0) {
|
|
3469
|
-
console.log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
|
|
3470
|
-
return {
|
|
3471
|
-
failures: 0,
|
|
3472
|
-
shutDown
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
const fresh = idleShutdownCandidates(recollect(), idleMs);
|
|
3476
|
-
let failures = 0;
|
|
3477
|
-
for (const device of candidates) {
|
|
3478
|
-
const what = `${device.kind} ${device.name}`;
|
|
3479
|
-
const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
|
|
3480
|
-
if (!current) {
|
|
3481
|
-
console.log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
|
|
3482
|
-
recordGcResult("idleDevice", "kept", device.name, {
|
|
3483
|
-
id: device.id,
|
|
3484
|
-
detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
|
|
3485
|
-
});
|
|
3486
|
-
continue;
|
|
3487
|
-
}
|
|
3488
|
-
const outcome = device.kind === "ios" ? teardownOwnedIosSim(device.id, { label: device.name }) : teardownOwnedAvd(device.id, { owner: {
|
|
3489
|
-
projectPath: device.project,
|
|
3490
|
-
slot: device.slot
|
|
3491
|
-
} });
|
|
3492
|
-
if (outcome.status === "torn-down") {
|
|
3493
|
-
shutDown.push(device);
|
|
3494
|
-
console.log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
|
|
3495
|
-
recordGcResult("idleDevice", "done", device.name, {
|
|
3496
|
-
id: device.id,
|
|
3497
|
-
detail: `idle ${formatLongDuration(current.idleForMs)}`
|
|
3498
|
-
});
|
|
3499
|
-
} else if (outcome.status === "missing") console.log(chalk.dim(`${what} is already gone; nothing to shut down.`));
|
|
3500
|
-
else if (outcome.status === "skipped") {
|
|
3501
|
-
console.log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
|
|
3502
|
-
recordGcResult("idleDevice", "kept", device.name, {
|
|
3503
|
-
id: device.id,
|
|
3504
|
-
detail: outcome.reason
|
|
3505
|
-
});
|
|
3506
|
-
} else {
|
|
3507
|
-
failures++;
|
|
3508
|
-
console.log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
|
|
3509
|
-
recordGcResult("idleDevice", "failed", device.name, {
|
|
3510
|
-
id: device.id,
|
|
3511
|
-
detail: outcome.reason
|
|
3512
|
-
});
|
|
3513
|
-
}
|
|
3514
|
-
}
|
|
3515
|
-
return {
|
|
3516
|
-
failures,
|
|
3517
|
-
shutDown
|
|
3518
|
-
};
|
|
3519
|
-
}
|
|
3520
|
-
//#endregion
|
|
3521
|
-
//#region src/budget.ts
|
|
3522
|
-
const MB = 1048576;
|
|
3523
|
-
const GB_IN_MB = 1024;
|
|
3524
|
-
const COMMITTED_MEMORY_SHARE = .6;
|
|
3525
|
-
const DEVICE_LIST_TIMEOUT_MS = 1e4;
|
|
3526
|
-
const USAGE_TIMEOUT_MS = 1e4;
|
|
3527
|
-
const BUDGET_SETTING_REMEDY = "Fix the value with `stim settings set <key> <value>`, or unset the environment variable. Run `stim guide settings` for the budget keys.";
|
|
3528
|
-
function valueAt(source, key) {
|
|
3529
|
-
let node = source;
|
|
3530
|
-
for (const part of key.split(".")) {
|
|
3531
|
-
if (node === null || typeof node !== "object" || Array.isArray(node)) return void 0;
|
|
3532
|
-
node = node[part];
|
|
3533
|
-
}
|
|
3534
|
-
return node;
|
|
3535
|
-
}
|
|
3536
|
-
function machineNumber(key, config, env) {
|
|
3537
|
-
const setting = settingDefinition(key);
|
|
3538
|
-
const fromEnv = setting.env ? env[setting.env] : void 0;
|
|
3539
|
-
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
3540
|
-
const value = fromEnv.trim() === "" ? NaN : Number(fromEnv);
|
|
3541
|
-
const problem = settingValueError(setting, value);
|
|
3542
|
-
return problem ? {
|
|
3543
|
-
value: void 0,
|
|
3544
|
-
error: `Invalid ${setting.env} value ${JSON.stringify(fromEnv)}. Expected ${problem}.`
|
|
3545
|
-
} : {
|
|
3546
|
-
value,
|
|
3547
|
-
error: null
|
|
3548
|
-
};
|
|
3549
|
-
}
|
|
3550
|
-
if (env.STIM_HOME && typeof setting.scopedHomeValue === "number") return {
|
|
3551
|
-
value: setting.scopedHomeValue,
|
|
3552
|
-
error: null
|
|
3553
|
-
};
|
|
3554
|
-
const raw = valueAt(config, key);
|
|
3555
|
-
if (raw === void 0) return {
|
|
3556
|
-
value: typeof setting.default === "number" ? setting.default : void 0,
|
|
3557
|
-
error: null
|
|
3558
|
-
};
|
|
3559
|
-
const problem = settingValueError(setting, raw);
|
|
3560
|
-
return problem ? {
|
|
3561
|
-
value: void 0,
|
|
3562
|
-
error: `Invalid ${key} value ${JSON.stringify(raw)}. Expected ${problem}.`
|
|
3563
|
-
} : {
|
|
3564
|
-
value: raw,
|
|
3565
|
-
error: null
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
|
-
function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemoryMb = totalmem$1() / MB } = {}) {
|
|
3569
|
-
const minFree = machineNumber("budget.minFreeDiskGb", config, env);
|
|
3570
|
-
const hardFloor = machineNumber("budget.hardFloorDiskGb", config, env);
|
|
3571
|
-
const memory = machineNumber("budget.maxCommittedMemoryGb", config, env);
|
|
3572
|
-
const live = machineNumber("budget.maxLiveWorkspaces", config, env);
|
|
3573
|
-
return {
|
|
3574
|
-
budget: {
|
|
3575
|
-
minFreeDiskMb: (minFree.value ?? 0) * GB_IN_MB,
|
|
3576
|
-
hardFloorDiskMb: (hardFloor.value ?? 0) * GB_IN_MB,
|
|
3577
|
-
maxCommittedMemoryMb: memory.value === void 0 ? Math.round(totalMemoryMb * COMMITTED_MEMORY_SHARE) : memory.value * GB_IN_MB,
|
|
3578
|
-
maxLiveWorkspaces: live.value ?? 0
|
|
3579
|
-
},
|
|
3580
|
-
error: minFree.error ?? hardFloor.error ?? memory.error ?? live.error
|
|
3581
|
-
};
|
|
3582
|
-
}
|
|
3583
|
-
function existingAncestor(path) {
|
|
3584
|
-
let current = path;
|
|
3585
|
-
while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
|
|
3586
|
-
return current;
|
|
3587
|
-
}
|
|
3588
|
-
function readVolumeSpace(paths) {
|
|
3589
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3590
|
-
const volumes = [];
|
|
3591
|
-
for (const path of paths) try {
|
|
3592
|
-
const existing = existingAncestor(path);
|
|
3593
|
-
const device = statSync(existing).dev;
|
|
3594
|
-
if (seen.has(device)) continue;
|
|
3595
|
-
seen.add(device);
|
|
3596
|
-
const fs = statfsSync(existing);
|
|
3597
|
-
volumes.push({
|
|
3598
|
-
volume: volumeRootFor(path),
|
|
3599
|
-
freeMb: fs.bavail * fs.bsize / MB
|
|
3600
|
-
});
|
|
3601
|
-
} catch {}
|
|
3602
|
-
return volumes;
|
|
3603
|
-
}
|
|
3604
|
-
function estimateMemoryUse({ config, sims, androidSerial, supervisorRunning }) {
|
|
3605
|
-
const simsByUdid = Object.fromEntries((sims ?? []).map((sim) => [sim.udid, sim]));
|
|
3606
|
-
const runtime = (avdName) => {
|
|
3607
|
-
if (!avdName) return null;
|
|
3608
|
-
const serial = androidSerial(avdName);
|
|
3609
|
-
return {
|
|
3610
|
-
serial,
|
|
3611
|
-
state: serial ? "detected" : "not-detected"
|
|
3612
|
-
};
|
|
3613
|
-
};
|
|
3614
|
-
const states = Object.entries(config?.projects ?? {}).map(([path, project]) => {
|
|
3615
|
-
const pid = supervisorRunning(path, project);
|
|
3616
|
-
const slots = projectDeviceSlots(project);
|
|
3617
|
-
return environmentState({
|
|
3618
|
-
...project,
|
|
3619
|
-
__path: path
|
|
3620
|
-
}, {
|
|
3621
|
-
simsByUdid,
|
|
3622
|
-
simsAvailable: sims !== null,
|
|
3623
|
-
metro: pid === null ? { missing: true } : { metro: { pid } },
|
|
3624
|
-
androidRuntime: project.platforms?.android?.owned ? runtime(project.platforms.android.avdName) : null,
|
|
3625
|
-
androidRuntimes: Object.fromEntries(slots.slice(1).map(({ slot, platforms }) => [slot, platforms.android?.owned ? runtime(platforms.android.avdName) : null]))
|
|
3626
|
-
});
|
|
3627
|
-
});
|
|
3628
|
-
const { committedMb, liveCount } = capacity(states, 0);
|
|
3629
|
-
return {
|
|
3630
|
-
committedMb,
|
|
3631
|
-
liveWorkspaces: liveCount
|
|
3632
|
-
};
|
|
3633
|
-
}
|
|
3634
|
-
function listSims() {
|
|
3635
|
-
try {
|
|
3636
|
-
return listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
|
|
3637
|
-
} catch {
|
|
3638
|
-
return null;
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
function snapshotSims(snapshot) {
|
|
3642
|
-
try {
|
|
3643
|
-
return snapshot.read().filter((sim) => sim.available);
|
|
3644
|
-
} catch {
|
|
3645
|
-
return null;
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
function runningSupervisorPid(path, project) {
|
|
3649
|
-
const target = resolveSupervisorTarget({
|
|
3650
|
-
state: readWorkspaceState$1(path)?.supervisor,
|
|
3651
|
-
record: project.supervisor,
|
|
3652
|
-
reservedPort: project.metroPort
|
|
3653
|
-
});
|
|
3654
|
-
return target.status === "ours" ? target.pid ?? null : null;
|
|
3655
|
-
}
|
|
3656
|
-
function readMemoryUse(simSnapshot) {
|
|
3657
|
-
const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
|
|
3658
|
-
return estimateMemoryUse({
|
|
3659
|
-
config: loadConfig$1(),
|
|
3660
|
-
sims: simSnapshot ? snapshotSims(simSnapshot) : listSims(),
|
|
3661
|
-
androidSerial: (avdName) => {
|
|
3662
|
-
try {
|
|
3663
|
-
return resolve(avdName).serial ?? null;
|
|
3664
|
-
} catch {
|
|
3665
|
-
return null;
|
|
3666
|
-
}
|
|
3667
|
-
},
|
|
3668
|
-
supervisorRunning: runningSupervisorPid
|
|
3669
|
-
});
|
|
3670
|
-
}
|
|
3671
|
-
function budgetShortfalls(budget, measure) {
|
|
3672
|
-
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
3673
|
-
return {
|
|
3674
|
-
disk: floor > 0 ? measure.volumes.filter((v) => v.freeMb < floor) : [],
|
|
3675
|
-
hardFloor: budget.hardFloorDiskMb > 0 ? measure.volumes.filter((v) => v.freeMb < budget.hardFloorDiskMb) : [],
|
|
3676
|
-
memory: Boolean(budget.maxCommittedMemoryMb > 0 && measure.memory && measure.memory.committedMb > budget.maxCommittedMemoryMb),
|
|
3677
|
-
workspaces: Boolean(budget.maxLiveWorkspaces > 0 && measure.memory && measure.memory.liveWorkspaces > budget.maxLiveWorkspaces)
|
|
3678
|
-
};
|
|
3679
|
-
}
|
|
3680
|
-
const DISK_ONLY = ["workspace-outputs", "stale-cache-entries"];
|
|
3681
|
-
function reclaimPlan(short) {
|
|
3682
|
-
const disk = short.disk.length > 0;
|
|
3683
|
-
return [...disk || short.memory || short.workspaces ? ["idle-devices", "idle-dev-servers"] : [], ...disk ? DISK_ONLY : []];
|
|
3684
|
-
}
|
|
3685
|
-
function overBudget(short) {
|
|
3686
|
-
return short.disk.length > 0 || short.memory || short.workspaces;
|
|
3687
|
-
}
|
|
3688
|
-
function isOtherWorkspace(root) {
|
|
3689
|
-
const self = canonicalPath(root);
|
|
3690
|
-
return (path) => canonicalPath(path) !== self;
|
|
3691
|
-
}
|
|
3692
|
-
function deviceLabel(device) {
|
|
3693
|
-
return `${device.kind} ${device.name} in ${device.project}`;
|
|
3694
|
-
}
|
|
3695
|
-
function reclaimIdleDevices({ root, dryRun, note, now }) {
|
|
3696
|
-
if (deviceSweepIsScoped()) {
|
|
3697
|
-
note(chalk.dim(phaseLine("budget", "idle devices left alone: STIM_HOME scopes this config, devices are global")));
|
|
3698
|
-
return {
|
|
3699
|
-
targets: [],
|
|
3700
|
-
failures: 0
|
|
3701
|
-
};
|
|
3702
|
-
}
|
|
3703
|
-
const other = isOtherWorkspace(root);
|
|
3704
|
-
const collect = (at) => idleShutdownCandidates(collectIdleDevices(loadConfig$1(), listSims() ?? [], [], at).filter((device) => other(device.project)), 0);
|
|
3705
|
-
const idle = collect(now);
|
|
3706
|
-
if (dryRun || idle.length === 0) return {
|
|
3707
|
-
targets: idle.map(deviceLabel),
|
|
3708
|
-
failures: 0
|
|
3709
|
-
};
|
|
3710
|
-
const { failures, shutDown } = shutDownIdleDevices(idle, 0, () => collect(Date.now()));
|
|
3711
|
-
return {
|
|
3712
|
-
targets: shutDown.map(deviceLabel),
|
|
3713
|
-
failures
|
|
3714
|
-
};
|
|
3715
|
-
}
|
|
3716
|
-
function idleSince(path, now) {
|
|
3717
|
-
const activity = workspaceActivity(path, now);
|
|
3718
|
-
const at = Date.parse(activity.lastActivityAt ?? "");
|
|
3719
|
-
return activity.state === "idle" && Number.isFinite(at) ? at : null;
|
|
3720
|
-
}
|
|
3721
|
-
async function reclaimIdleDevServers({ root, dryRun, note, now }) {
|
|
3722
|
-
const config = loadConfig$1();
|
|
3723
|
-
const other = isOtherWorkspace(root);
|
|
3724
|
-
const serving = Object.entries(config?.projects ?? {}).flatMap(([path, project]) => {
|
|
3725
|
-
const pid = other(path) ? runningSupervisorPid(path, project) : null;
|
|
3726
|
-
return pid !== null && !workspaceBuildInProgress(path) && idleSince(path, now) !== null ? [{
|
|
3727
|
-
path,
|
|
3728
|
-
pid
|
|
3729
|
-
}] : [];
|
|
3730
|
-
});
|
|
3731
|
-
if (serving.length === 0) return {
|
|
3732
|
-
targets: [],
|
|
3733
|
-
failures: 0
|
|
3734
|
-
};
|
|
3735
|
-
const sims = listSims();
|
|
3736
|
-
if (sims === null) {
|
|
3737
|
-
note(chalk.dim(phaseLine("budget", "dev servers left alone: simulators could not be listed to prove them idle")));
|
|
3738
|
-
return {
|
|
3739
|
-
targets: [],
|
|
3740
|
-
failures: 0
|
|
3741
|
-
};
|
|
3742
|
-
}
|
|
3743
|
-
const busy = new Set(collectOwnedDeviceActivity(config, sims, now).filter((device) => device.activity.state !== "idle").map((device) => device.project));
|
|
3744
|
-
const idle = serving.filter(({ path }) => !busy.has(path));
|
|
3745
|
-
if (dryRun) return {
|
|
3746
|
-
targets: idle.map(({ path }) => path),
|
|
3747
|
-
failures: 0
|
|
3748
|
-
};
|
|
3749
|
-
const targets = [];
|
|
3750
|
-
let failures = 0;
|
|
3751
|
-
const stillIdle = (path, pid) => {
|
|
3752
|
-
const project = loadConfig$1()?.projects?.[path];
|
|
3753
|
-
return Boolean(project && runningSupervisorPid(path, project) === pid && !workspaceBuildInProgress(path) && idleSince(path, Date.now()) !== null);
|
|
3754
|
-
};
|
|
3755
|
-
for (const { path, pid } of idle) {
|
|
3756
|
-
let run;
|
|
3757
|
-
try {
|
|
3758
|
-
run = await withIdleWorkspace(path, async () => {
|
|
3759
|
-
try {
|
|
3760
|
-
return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path, {
|
|
3761
|
-
by: "budget reclaim",
|
|
3762
|
-
workspace: root
|
|
3763
|
-
}) : "busy", {
|
|
3764
|
-
external: true,
|
|
3765
|
-
waitMs: 0,
|
|
3766
|
-
ownerPurpose: "budget reclaim"
|
|
3767
|
-
});
|
|
3768
|
-
} catch (error) {
|
|
3769
|
-
if (workspaceProcessLockError(error)) return "starting";
|
|
3770
|
-
throw error;
|
|
3771
|
-
}
|
|
3772
|
-
}, {
|
|
3773
|
-
purpose: "budget reclaim",
|
|
3774
|
-
supervisor: false
|
|
3775
|
-
});
|
|
3776
|
-
} catch (error) {
|
|
3777
|
-
failures++;
|
|
3778
|
-
note(chalk.red(`Could not stop the dev server of ${path}: ${error?.message || error}`));
|
|
3779
|
-
continue;
|
|
3780
|
-
}
|
|
3781
|
-
if (!run.ran) {
|
|
3782
|
-
note(chalk.yellow(`Kept the dev server of ${path}: ${run.reasons.join("; ")}`));
|
|
3783
|
-
continue;
|
|
3784
|
-
}
|
|
3785
|
-
const stopped = run.value;
|
|
3786
|
-
if (stopped === "busy" || stopped === "starting") {
|
|
3787
|
-
const why = stopped === "busy" ? "it is no longer idle" : "a stim start holds its metro-start lock";
|
|
3788
|
-
note(chalk.dim(`Kept the dev server of ${path}: ${why}`));
|
|
3789
|
-
} else if (stopped.status === "stopped") {
|
|
3790
|
-
targets.push(path);
|
|
3791
|
-
note(chalk.green(`Stopped the idle dev server of ${path}`));
|
|
3792
|
-
} else if ("reason" in stopped) {
|
|
3793
|
-
if (stopped.status === "stuck") failures++;
|
|
3794
|
-
note(chalk.yellow(`Kept the dev server of ${path}: ${stopped.reason}`));
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
return {
|
|
3798
|
-
targets,
|
|
3799
|
-
failures
|
|
3800
|
-
};
|
|
3801
|
-
}
|
|
3802
|
-
async function reclaimWorkspaceOutputs({ root, dryRun, now, diskRecovered }) {
|
|
3803
|
-
const other = isOtherWorkspace(root);
|
|
3804
|
-
const report = collectWorkspaceOutputs({
|
|
3805
|
-
olderThan: null,
|
|
3806
|
-
now,
|
|
3807
|
-
measure: false
|
|
3808
|
-
});
|
|
3809
|
-
const clearable = report.workspaces.flatMap((entry) => {
|
|
3810
|
-
const project = entry.projectRoot;
|
|
3811
|
-
const since = project !== null && entry.willClear && other(project) ? idleSince(project, now) : null;
|
|
3812
|
-
return since === null ? [] : [{
|
|
3813
|
-
entry,
|
|
3814
|
-
since
|
|
3815
|
-
}];
|
|
3816
|
-
}).toSorted((a, b) => a.since - b.since).map(({ entry }) => entry);
|
|
3817
|
-
if (dryRun) return {
|
|
3818
|
-
targets: clearable.map((entry) => entry.projectRoot),
|
|
3819
|
-
failures: 0
|
|
3820
|
-
};
|
|
3821
|
-
const targets = [];
|
|
3822
|
-
let failures = 0;
|
|
3823
|
-
for (const entry of clearable) {
|
|
3824
|
-
const result = await clearWorkspaceOutputs({
|
|
3825
|
-
...report,
|
|
3826
|
-
workspaces: [entry]
|
|
3827
|
-
}, {
|
|
3828
|
-
olderThan: null,
|
|
3829
|
-
now
|
|
3830
|
-
});
|
|
3831
|
-
failures += result.failures;
|
|
3832
|
-
targets.push(...result.cleared);
|
|
3833
|
-
if (diskRecovered()) break;
|
|
3834
|
-
}
|
|
3835
|
-
return {
|
|
3836
|
-
targets,
|
|
3837
|
-
failures
|
|
3838
|
-
};
|
|
3839
|
-
}
|
|
3840
|
-
function cacheLabel(cache) {
|
|
3841
|
-
return `${cache.name} ${cache.dir}`;
|
|
3842
|
-
}
|
|
3843
|
-
function trimmableCaches() {
|
|
3844
|
-
return planCacheEmptying(discoverCaches(), false).filter((cache) => cache.prune !== "report-only" && cache.prune !== "atomic" && !cache.machineGlobal);
|
|
3845
|
-
}
|
|
3846
|
-
function reclaimStaleCacheEntries({ dryRun }) {
|
|
3847
|
-
const caches = trimmableCaches();
|
|
3848
|
-
if (dryRun) return {
|
|
3849
|
-
targets: caches.map(cacheLabel),
|
|
3850
|
-
failures: 0
|
|
3851
|
-
};
|
|
3852
|
-
return {
|
|
3853
|
-
targets: trimCaches(caches, 14).map(cacheLabel),
|
|
3854
|
-
failures: 0
|
|
3855
|
-
};
|
|
3856
|
-
}
|
|
3857
|
-
const STEPS = {
|
|
3858
|
-
"idle-devices": reclaimIdleDevices,
|
|
3859
|
-
"idle-dev-servers": reclaimIdleDevServers,
|
|
3860
|
-
"workspace-outputs": reclaimWorkspaceOutputs,
|
|
3861
|
-
"stale-cache-entries": reclaimStaleCacheEntries
|
|
3862
|
-
};
|
|
3863
|
-
const STEP_ACTIONS = {
|
|
3864
|
-
"idle-devices": "shut down idle owned devices",
|
|
3865
|
-
"idle-dev-servers": "stop idle dev servers",
|
|
3866
|
-
"workspace-outputs": "clear build outputs of idle workspaces",
|
|
3867
|
-
"stale-cache-entries": `trim cache entries unused for 14 days`
|
|
3868
|
-
};
|
|
3869
|
-
async function runStep(step, context) {
|
|
3870
|
-
const log = console.log;
|
|
3871
|
-
const exitCode = process.exitCode;
|
|
3872
|
-
console.log = console.error;
|
|
3873
|
-
try {
|
|
3874
|
-
const result = await STEPS[step](context);
|
|
3875
|
-
return process.exitCode === exitCode ? result : {
|
|
3876
|
-
...result,
|
|
3877
|
-
failures: result.failures + 1
|
|
3878
|
-
};
|
|
3879
|
-
} catch (error) {
|
|
3880
|
-
context.note(chalk.red(phaseLine("budget", `${STEP_ACTIONS[step]} failed: ${error?.message || error}`)));
|
|
3881
|
-
return {
|
|
3882
|
-
targets: [],
|
|
3883
|
-
failures: 1
|
|
3884
|
-
};
|
|
3885
|
-
} finally {
|
|
3886
|
-
console.log = log;
|
|
3887
|
-
process.exitCode = exitCode;
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
function spaceUsers() {
|
|
3891
|
-
const home = homedir$1();
|
|
3892
|
-
const candidates = [
|
|
3893
|
-
["Stim state", getConfigDir()],
|
|
3894
|
-
["shared build cache", sharedBuildCache()],
|
|
3895
|
-
...platform() === "darwin" ? [["simulators", join(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
|
|
3896
|
-
["Android emulators", join(home, ".android", "avd")]
|
|
3897
|
-
];
|
|
3898
|
-
const measured = /* @__PURE__ */ new Set();
|
|
3899
|
-
const users = [];
|
|
3900
|
-
for (const [label, path] of candidates) {
|
|
3901
|
-
const canonical = canonicalPath(path);
|
|
3902
|
-
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
|
|
3903
|
-
measured.add(canonical);
|
|
3904
|
-
if (!existsSync(path)) continue;
|
|
3905
|
-
const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
|
|
3906
|
-
if (bytes) users.push({
|
|
3907
|
-
label: `${label} ${path}`,
|
|
3908
|
-
bytes
|
|
3909
|
-
});
|
|
3910
|
-
}
|
|
3911
|
-
return users.toSorted((a, b) => b.bytes - a.bytes).map(({ label, bytes }) => `${label}: ${formatBytes(bytes)}`);
|
|
3912
|
-
}
|
|
3913
|
-
const DEFAULT_DEPS = {
|
|
3914
|
-
volumes: readVolumeSpace,
|
|
3915
|
-
memory: readMemoryUse,
|
|
3916
|
-
step: runStep,
|
|
3917
|
-
usage: spaceUsers,
|
|
3918
|
-
now: Date.now
|
|
3919
|
-
};
|
|
3920
|
-
const LOW_DISK_REMEDY = "Run `stim gc` to see what else Stim can reclaim, then `stim gc --delete` (add `--cache all` to empty the shared caches, or `--worktrees` to remove finished worktrees). Free space outside Stim from the largest uses listed, or lower budget.hardFloorDiskGb.";
|
|
3921
|
-
function describeShortfall(budget, measure, short) {
|
|
3922
|
-
const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
|
|
3923
|
-
return [
|
|
3924
|
-
...short.disk.map((v) => `${formatSpace(v.freeMb)} free on ${v.volume}, below the ${formatSpace(floor)} floor`),
|
|
3925
|
-
...short.memory && measure.memory ? [`roughly ${formatSpace(measure.memory.committedMb)} of memory committed, over the ${formatSpace(budget.maxCommittedMemoryMb)} budget`] : [],
|
|
3926
|
-
...short.workspaces && measure.memory ? [`${measure.memory.liveWorkspaces} active workspaces, over the limit of ${budget.maxLiveWorkspaces}`] : []
|
|
3927
|
-
];
|
|
3928
|
-
}
|
|
3929
|
-
async function enforceBudget({ root, note, dryRun = false, budget: resolved, simSnapshot }, overrides = {}) {
|
|
3930
|
-
const deps = {
|
|
3931
|
-
...DEFAULT_DEPS,
|
|
3932
|
-
...overrides
|
|
3933
|
-
};
|
|
3934
|
-
let budget = resolved;
|
|
3935
|
-
if (!budget) {
|
|
3936
|
-
const read = resolveBudget();
|
|
3937
|
-
if (read.error) return {
|
|
3938
|
-
status: "invalid",
|
|
3939
|
-
refusal: {
|
|
3940
|
-
code: "STIM_BAD_ARG",
|
|
3941
|
-
message: read.error,
|
|
3942
|
-
remedy: BUDGET_SETTING_REMEDY
|
|
3943
|
-
}
|
|
3944
|
-
};
|
|
3945
|
-
budget = read.budget;
|
|
3946
|
-
}
|
|
3947
|
-
const paths = [root, getConfigDir()];
|
|
3948
|
-
const measureNow = () => ({
|
|
3949
|
-
volumes: budget.minFreeDiskMb > 0 || budget.hardFloorDiskMb > 0 ? deps.volumes(paths) : [],
|
|
3950
|
-
memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? deps.memory(simSnapshot) : null
|
|
3951
|
-
});
|
|
3952
|
-
let measure = measureNow();
|
|
3953
|
-
let short = budgetShortfalls(budget, measure);
|
|
3954
|
-
const reclaimed = [];
|
|
3955
|
-
const plan = reclaimPlan(short);
|
|
3956
|
-
if (plan.length) note(chalk.yellow(phaseLine("budget", `${describeShortfall(budget, measure, short).join("; ")}; ${dryRun ? "would reclaim" : "reclaiming"}`)));
|
|
3957
|
-
for (const step of plan) {
|
|
3958
|
-
if (!dryRun && (!overBudget(short) || DISK_ONLY.includes(step) && short.disk.length === 0)) break;
|
|
3959
|
-
const freeBefore = measure.volumes.reduce((sum, v) => sum + v.freeMb, 0);
|
|
3960
|
-
const result = await deps.step(step, {
|
|
3961
|
-
root,
|
|
3962
|
-
dryRun,
|
|
3963
|
-
note,
|
|
3964
|
-
now: deps.now(),
|
|
3965
|
-
diskRecovered: () => budgetShortfalls(budget, {
|
|
3966
|
-
volumes: deps.volumes(paths),
|
|
3967
|
-
memory: null
|
|
3968
|
-
}).disk.length === 0
|
|
3969
|
-
});
|
|
3970
|
-
if (!dryRun) {
|
|
3971
|
-
simSnapshot?.invalidate();
|
|
3972
|
-
measure = measureNow();
|
|
3973
|
-
short = budgetShortfalls(budget, measure);
|
|
3974
|
-
}
|
|
3975
|
-
if (result.targets.length === 0 && result.failures === 0) continue;
|
|
3976
|
-
const freedMb = Math.max(0, Math.round(measure.volumes.reduce((sum, v) => sum + v.freeMb, 0) - freeBefore));
|
|
3977
|
-
reclaimed.push({
|
|
3978
|
-
step,
|
|
3979
|
-
...result,
|
|
3980
|
-
...dryRun ? {} : { freedMb }
|
|
3981
|
-
});
|
|
3982
|
-
const outcome = dryRun ? result.targets.join(", ") : `${result.targets.length}${result.failures ? `, ${result.failures} failed` : ""} (${formatSpace(freedMb)} freed)`;
|
|
3983
|
-
note(chalk.dim(phaseLine("budget", `${STEP_ACTIONS[step]}: ${outcome}`)));
|
|
3984
|
-
}
|
|
3985
|
-
if (!dryRun && short.hardFloor.length) {
|
|
3986
|
-
const tight = short.hardFloor.map((v) => `${formatSpace(v.freeMb)} on ${v.volume}`).join(" and ");
|
|
3987
|
-
const users = deps.usage();
|
|
3988
|
-
return {
|
|
3989
|
-
status: "refused",
|
|
3990
|
-
reclaimed,
|
|
3991
|
-
refusal: {
|
|
3992
|
-
code: "STIM_LOW_DISK",
|
|
3993
|
-
message: `Only ${tight} is free, below the ${formatSpace(budget.hardFloorDiskMb)} hard floor (budget.hardFloorDiskGb)${reclaimed.length ? " after reclaiming what Stim safely could" : ""}, so this run could fill the disk.` + (users.length ? ` Largest uses: ${users.join("; ")}.` : ""),
|
|
3994
|
-
remedy: LOW_DISK_REMEDY
|
|
3995
|
-
}
|
|
3996
|
-
};
|
|
3997
|
-
}
|
|
3998
|
-
if (!dryRun && overBudget(short)) note(chalk.yellow(phaseLine("budget", `still over budget: ${describeShortfall(budget, measure, short).join("; ")}`)));
|
|
3999
|
-
return {
|
|
4000
|
-
status: "ok",
|
|
4001
|
-
reclaimed,
|
|
4002
|
-
budget,
|
|
4003
|
-
measure,
|
|
4004
|
-
short
|
|
4005
|
-
};
|
|
4006
|
-
}
|
|
4007
|
-
async function peekBudget(root, overrides = {}) {
|
|
4008
|
-
const outcome = await enforceBudget({
|
|
4009
|
-
root,
|
|
4010
|
-
note: () => {},
|
|
4011
|
-
dryRun: true
|
|
4012
|
-
}, {
|
|
4013
|
-
...overrides,
|
|
4014
|
-
step: async () => ({
|
|
4015
|
-
targets: [],
|
|
4016
|
-
failures: 0
|
|
4017
|
-
})
|
|
4018
|
-
});
|
|
4019
|
-
if (outcome.status !== "ok") return outcome.refusal.message;
|
|
4020
|
-
const shortfall = describeShortfall(outcome.budget, outcome.measure, outcome.short);
|
|
4021
|
-
return shortfall.length ? shortfall.join("; ") : null;
|
|
4022
|
-
}
|
|
4023
|
-
async function budgetGate(args, overrides = {}) {
|
|
4024
|
-
const outcome = await enforceBudget(args, overrides);
|
|
4025
|
-
return {
|
|
4026
|
-
reclaimed: "reclaimed" in outcome ? outcome.reclaimed : [],
|
|
4027
|
-
refusal: outcome.status === "ok" ? null : outcome.refusal
|
|
4028
|
-
};
|
|
4029
|
-
}
|
|
4030
|
-
const gb = (mb) => Math.round(mb / GB_IN_MB * 10) / 10;
|
|
4031
|
-
function budgetFindings(budget, measure, short, plan) {
|
|
4032
|
-
const findings = [];
|
|
4033
|
-
const steps = plan.length ? plan.map((entry) => `${STEP_ACTIONS[entry.step]} (${entry.targets.join(", ")})`).join("; then ") : "nothing Stim can reclaim on its own right now";
|
|
4034
|
-
if (short.disk.length) {
|
|
4035
|
-
const refuses = short.hardFloor.length ? ` Below ${formatSpace(budget.hardFloorDiskMb)} it refuses with STIM_LOW_DISK when reclaiming does not free enough.` : "";
|
|
4036
|
-
findings.push({
|
|
4037
|
-
level: "cost",
|
|
4038
|
-
title: "Free disk is below the Stim budget",
|
|
4039
|
-
detail: `${describeShortfall(budget, measure, {
|
|
4040
|
-
...short,
|
|
4041
|
-
memory: false,
|
|
4042
|
-
workspaces: false
|
|
4043
|
-
}).join("; ")}. The next ios, android or start will first ${steps}.${refuses}`,
|
|
4044
|
-
fix: "Run `stim gc` to see what Stim can reclaim, then `stim gc --delete`. Free space outside Stim, or lower budget.minFreeDiskGb."
|
|
4045
|
-
});
|
|
4046
|
-
}
|
|
4047
|
-
if (short.memory || short.workspaces) findings.push({
|
|
4048
|
-
level: "note",
|
|
4049
|
-
title: "Active environments are over the memory budget",
|
|
4050
|
-
detail: `${describeShortfall(budget, measure, {
|
|
4051
|
-
...short,
|
|
4052
|
-
disk: []
|
|
4053
|
-
}).join("; ")}. The next ios, android or start shuts down idle owned devices and stops idle dev servers in other workspaces first.`,
|
|
4054
|
-
fix: "Run `stim status` to see what is active, and `stim stop` in workspaces you own that you no longer need."
|
|
4055
|
-
});
|
|
4056
|
-
return findings;
|
|
4057
|
-
}
|
|
4058
|
-
async function inspectBudget(root, deps = {}) {
|
|
4059
|
-
const read = resolveBudget();
|
|
4060
|
-
if (read.error) return {
|
|
4061
|
-
report: null,
|
|
4062
|
-
findings: [{
|
|
4063
|
-
level: "cost",
|
|
4064
|
-
title: "A budget setting is invalid",
|
|
4065
|
-
detail: read.error,
|
|
4066
|
-
fix: BUDGET_SETTING_REMEDY
|
|
4067
|
-
}]
|
|
4068
|
-
};
|
|
4069
|
-
const outcome = await enforceBudget({
|
|
4070
|
-
root,
|
|
4071
|
-
note: () => {},
|
|
4072
|
-
dryRun: true,
|
|
4073
|
-
budget: read.budget
|
|
4074
|
-
}, deps);
|
|
4075
|
-
if (outcome.status !== "ok") return {
|
|
4076
|
-
report: null,
|
|
4077
|
-
findings: []
|
|
4078
|
-
};
|
|
4079
|
-
const { budget, measure, short, reclaimed } = outcome;
|
|
4080
|
-
return {
|
|
4081
|
-
report: {
|
|
4082
|
-
budget: {
|
|
4083
|
-
minFreeDiskGb: gb(budget.minFreeDiskMb),
|
|
4084
|
-
hardFloorDiskGb: gb(budget.hardFloorDiskMb),
|
|
4085
|
-
maxCommittedMemoryGb: gb(budget.maxCommittedMemoryMb),
|
|
4086
|
-
maxLiveWorkspaces: budget.maxLiveWorkspaces
|
|
4087
|
-
},
|
|
4088
|
-
volumes: measure.volumes.map((v) => ({
|
|
4089
|
-
volume: v.volume,
|
|
4090
|
-
freeGb: gb(v.freeMb)
|
|
4091
|
-
})),
|
|
4092
|
-
memory: measure.memory ? {
|
|
4093
|
-
committedGb: gb(measure.memory.committedMb),
|
|
4094
|
-
liveWorkspaces: measure.memory.liveWorkspaces
|
|
4095
|
-
} : null,
|
|
4096
|
-
plan: reclaimed
|
|
4097
|
-
},
|
|
4098
|
-
findings: budgetFindings(budget, measure, short, reclaimed)
|
|
4099
|
-
};
|
|
4100
|
-
}
|
|
4101
|
-
function budgetLine(report) {
|
|
4102
|
-
const { budget } = report;
|
|
4103
|
-
return `${report.volumes.length ? `${report.volumes.map((v) => `${v.freeGb} GB free on ${v.volume}`).join(", ")} (reclaims below ${Math.max(budget.minFreeDiskGb, budget.hardFloorDiskGb)} GB, ${budget.hardFloorDiskGb ? `refuses below ${budget.hardFloorDiskGb} GB` : "never refuses"})` : "disk budget off"}; ${report.memory ? `roughly ${report.memory.committedGb} GB of a ${budget.maxCommittedMemoryGb} GB memory budget committed` : "memory budget off"}`;
|
|
4104
|
-
}
|
|
4105
|
-
//#endregion
|
|
4106
|
-
export { memoryWorkPending as $, findStaleProjectDevices as A, createRunRecorder as At, previewScopedEmptying as B, deleteProjectDevices as C, bundlingActivity as Ct, eraseParkedDevices as D, recordFinishedBuild as Dt, deviceSweepIsScoped as E, recordBuildPhase as Et, includesParkedDevices as F, recordRunStats as Ft, GRADLE_DAEMONS_KIND as G, selectsBeyondCaches as H, includesRecordings as I, statsProjectKey$1 as It, inspectWatchmanMemory as J, WATCHMAN_KIND as K, includesWorkspaceOutputs as L, withAndroidAvdSizes as M, readStats$1 as Mt, emptyCaches as N, recordCapacityRefusal as Nt, findOrphanedDevices as O, startBuildProgress as Ot, includesArchives as P, recordCapacityWait as Pt, memorySweepIsScoped as Q, isWorkspacesScope as R, deleteParkedSims as S, buildStatusLine as St, describeUnverifiableDevices as T, parseActiveBuild as Tt, trimCaches as U, selectCaches as V, parseArchiveSelector as W, memoryLines as X, memoryCacheKind as Y, memoryScope as Z, DEVICE_LIST_TIMEOUT_MS$1 as _, withdrawDevServerStopRequest as _t, inspectBudget as a, discoverCaches as at, collectParkedSims as b, activeBuildState as bt, readVolumeSpace as c, stopOwnedMetroForReset as ct, collectIdleDevices as d, devServerStopLevel as dt, reclaimMemory as et, collectOwnedDeviceActivity as f, devServerStopRecord as ft, workspaceBuildInProgress as g, vanishedSupervisorMessage as gt, shutDownIdleDevices as h, requestDevServerStop as ht, enforceBudget as i, readFootprints as it, unverifiedDeviceCommand as j, readRunEstimates as jt, findStaleDeviceRecords as k, tapBuildLog as kt, resolveBudget as l, describeDevServerStop as lt, parseIdleDuration as m, metroLastStop as mt, budgetLine as n, phase as nt, machineNumber as o, pruneCache as ot, idleDeviceLines as p, logVanishedSupervisor as pt, collectMemoryReport as q, cacheLabel as r, attributeMachineUsage as rt, peekBudget as s, sizeCaches as st, budgetGate as t, scopedMemoryReport as tt, trimmableCaches as u, describeMetroLastStop as ut, collectDeviceLeases as v, ACTIVE_BUILD_KEY as vt, describeParkedSims as w, estimateBuild as wt, deleteParkedAvds as x, buildReport as xt, collectParkedAvds as y, NO_BUILD_PROGRESS as yt, planCacheEmptying as z };
|