stim 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
- package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
- package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
- package/dist/{android-qx_31wJo.mjs → android-Db2eEMaf.mjs} +152 -65
- package/dist/android-cas-PnKfAdB2.mjs +167 -0
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{app-install-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
- package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
- package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
- package/dist/build-progress-C2Jv1AtA.mjs +872 -0
- package/dist/build-slots-Df1ApSuH.mjs +127 -0
- package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
- package/dist/cli-0cX5s4NM.mjs +45 -0
- package/dist/cli.mjs +6 -42
- package/dist/client-C2IS36Qf.mjs +1333 -0
- package/dist/collector-run.mjs +8 -7
- package/dist/{command-output-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
- package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
- package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
- package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
- package/dist/{deps-2Wo81np9.mjs → deps-BFlKbOIp.mjs} +3 -3
- package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
- package/dist/dev-client-CjzLoNs0.mjs +80 -0
- package/dist/{device-Cf7vl-so.mjs → device-BZ1IyS5A.mjs} +7 -7
- package/dist/device-capacity-DMuKC7nx.mjs +136 -0
- package/dist/device-host-worker.d.mts +1 -0
- package/dist/device-host-worker.mjs +769 -0
- package/dist/device-ios-DMQBUPus.mjs +381 -0
- package/dist/{device-lease-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
- package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
- package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
- package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
- package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
- package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
- package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
- package/dist/{guide-status-BaMYu55M.mjs → guide-status-CIoWK6mG.mjs} +3 -3
- package/dist/hosted-macos-DwowNU7W.mjs +447 -0
- package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
- package/dist/in-use-CndPMilN.mjs +110 -0
- package/dist/{ios-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
- package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
- package/dist/{ios-device-BW-t_Lkv.mjs → ios-device-DERmMmk-.mjs} +12 -7
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{launch-verify-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
- package/dist/{logs-DcSRbC2t.mjs → logs-h9h6MUvE.mjs} +95 -13
- package/dist/machine-usage-D_YYMzfR.mjs +283 -0
- package/dist/machines-CYtOCqcj.mjs +259 -0
- package/dist/macos-BwtbUtTN.mjs +468 -0
- package/dist/macos-run.d.mts +5 -0
- package/dist/macos-run.mjs +2 -0
- package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
- package/dist/{metro-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
- package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
- package/dist/native-run-DFDoIRbY.mjs +147 -0
- package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
- package/dist/offload-worker.d.mts +121 -0
- package/dist/offload-worker.mjs +601 -0
- package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
- package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-C5UutqpJ.mjs → ports-DddwttKM.mjs} +3 -3
- package/dist/prebuild-BvxDlD6C.mjs +213 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-DGeclhvO.mjs → project-CV3tDFHl.mjs} +9 -18
- package/dist/pull-request-pxnDCQzn.mjs +322 -0
- package/dist/pull-requests.d.mts +120 -0
- package/dist/pull-requests.mjs +2 -0
- package/dist/recordings-GEXp4iBT.mjs +37 -0
- package/dist/{reload-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
- package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
- package/dist/run-B-xqRrsg.mjs +119 -0
- package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
- package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
- package/dist/server-expo-KJeD109I.mjs +2 -0
- package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
- package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
- package/dist/settings.schema.json +357 -2
- package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
- package/dist/slot-launch-BY0BrOLh.mjs +65 -0
- package/dist/{spawn-claims-PT8aQpCa.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
- package/dist/spawn-entry-DncpJvWL.mjs +16 -0
- package/dist/stage-CzaiwFsI.mjs +143 -0
- package/dist/start-CPO4wRg7.mjs +2 -0
- package/dist/{start-BZhItkiL.mjs → start-CUD9nasA.mjs} +108 -61
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/state-BxCgeTQu.mjs +41 -0
- package/dist/{state-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
- package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
- package/dist/stats-DBLLFmKd.mjs +66 -0
- package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
- package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -20
- package/dist/stdout-reader-C8yjre1x.mjs +23 -0
- package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
- package/dist/stim-footprint.swift +32 -0
- package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
- package/dist/{stop-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
- package/dist/stop-Bvf1MflS.mjs +48 -0
- package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
- package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
- package/dist/supervisor-run.d.mts +44 -12
- package/dist/supervisor-run.mjs +276 -33
- package/dist/support-CDQbDkL5.mjs +315 -0
- package/dist/support-WLOzVKUv.mjs +176 -0
- package/dist/teardown-BYuQafiA.mjs +718 -0
- package/dist/{xcode-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
- package/dist/warm-progress-DVHLN4uC.mjs +463 -0
- package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
- package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
- package/dist/web-run.mjs +326 -13
- package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
- package/dist/workspace-state-CL2V2Ons.mjs +371 -0
- package/dist/worktree-BT_uULFx.mjs +2 -0
- package/dist/{worktree-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
- package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
- package/package.json +10 -5
- package/shim/bundle-response.cjs +55 -10
- package/shim/bundle-response.d.cts +1 -0
- package/shim/expo-metro-config.cjs +10 -2
- package/dist/build-progress-l7d-X-ZP.mjs +0 -247
- package/dist/in-use-wYEaKQM9.mjs +0 -249
- package/dist/ownership-PUP4Dwai.mjs +0 -1138
- package/dist/server-expo-BeYwP_Xy.mjs +0 -2
- package/dist/slot-launch-BIWEW-wI.mjs +0 -19
- package/dist/start-Rw2GD3nV.mjs +0 -2
- package/dist/stats-CaSwmsSZ.mjs +0 -62
- package/dist/stats-D8rUTPHd.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/worktree-3cBa-bQt.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -1,29 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { t as getExecutor } from "./exec-CaErVv7t.mjs";
|
|
2
|
+
import { A as directorySize, D as withConfigLock$1, F as volumeRootFor, H as projectDeviceSlots, M as isOnMountedVolume, N as listMountedVolumes, P as measuredDirectorySize, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as formatBytes, o as clearSupervisor, p as getProject, z as deviceSlotPlatforms } from "./config-D0DbpZtM.mjs";
|
|
3
|
+
import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
|
|
4
|
+
import { i as registeredCaches } from "./cache-manifest-DnUHBJaU.mjs";
|
|
5
|
+
import { b as workspaceDir, m as sharedBuildCache, r as readWorkspaceState$1, s as workspaceLastUsed, w as workspaceName$1 } from "./workspace-state-CL2V2Ons.mjs";
|
|
4
6
|
import { n as supervisorError } from "./errors-B6W7FkxQ.mjs";
|
|
5
|
-
import { s as clearWorkspaceSupervisor } from "./state-
|
|
6
|
-
import {
|
|
7
|
-
import { l as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { i as
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
7
|
+
import { l as readPidFile, s as clearWorkspaceSupervisor } from "./state-CaeaHbDA.mjs";
|
|
8
|
+
import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
|
|
9
|
+
import { l as workspaceActivity, r as createDeviceProcessTables } from "./activity-B2-kb7k0.mjs";
|
|
10
|
+
import { l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-CliHHn7a.mjs";
|
|
11
|
+
import { h as listBuildLocks, i as listLeaseFiles, r as leaseIsExpired } from "./device-lease-W5Pi1q38.mjs";
|
|
12
|
+
import { A as listAvds, I as ownedAvdDirectory, ft as readParked, it as isStimOwnedSim, rt as isStimOwnedAvd, st as dropParked, z as ownedAvdSerialResolver } from "./android-Db2eEMaf.mjs";
|
|
13
|
+
import { l as resolveProjectMetro, r as killMetroTree } from "./metro-Di8EN2Tb.mjs";
|
|
14
|
+
import { a as teardownOwnedBrowser, c as teardownParkedIosSim, i as teardownOwnedAvd, n as eraseParkedIosSim, o as teardownOwnedIosSim, s as teardownParkedAvd, t as eraseParkedAvd } from "./teardown-BYuQafiA.mjs";
|
|
15
|
+
import { _ as parseRuntimeVersion, f as listIosDeviceTypes, u as listAllIosSims } from "./ios-3CtCRhwr.mjs";
|
|
16
|
+
import { a as parseWatchmanPid, c as collectOwnedDeviceActivity, f as shutDownIdleDevices, i as parseWatchmanClients, m as recordGcResult, n as parseTriggerCount, p as workspaceBuildInProgress, r as parseWatchRoots, s as collectIdleDevices, t as parseSubscriberNames, u as idleShutdownCandidates } from "./watchman-Bx0wceoF.mjs";
|
|
17
|
+
import { i as canonicalPath, n as withIdleWorkspace, r as workspaceInUse, t as emptyWorkspaceDir } from "./in-use-CndPMilN.mjs";
|
|
18
|
+
import { n as listBuildSlots } from "./build-slots-Df1ApSuH.mjs";
|
|
19
|
+
import { l as withdrawDevServerStopRequest, s as requestDevServerStop, u as resolveSupervisorTarget } from "./stop-cause-CZ_Mr-v9.mjs";
|
|
20
|
+
import { a as capacity, l as environmentState, u as formatSpace } from "./status-l84Gbvp2.mjs";
|
|
21
|
+
import { r as readFootprints, t as STIM_SERVER_COMMAND } from "./machine-usage-D_YYMzfR.mjs";
|
|
22
|
+
import { existsSync, readFileSync, statSync, statfsSync } from "node:fs";
|
|
23
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
17
24
|
import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
|
|
18
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
19
|
-
import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
|
|
20
|
-
import { isJsonObject, readJsonFile, settingDefinition, settingValueError } from "@stim-cli/core/state";
|
|
21
|
-
import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
|
|
22
25
|
import { homedir, tmpdir } from "os";
|
|
26
|
+
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";
|
|
27
|
+
import { METRO_NAMED_CACHE_LAYOUT } from "@stim-cli/core";
|
|
28
|
+
import { buildWorkerRoot, isJsonObject, loadConfig, readJsonFile, settingDefinition, settingValueError } from "@stim-cli/core/state";
|
|
23
29
|
import { homedir as homedir$1, platform, totalmem as totalmem$1 } from "node:os";
|
|
24
30
|
import chalk from "chalk";
|
|
25
31
|
//#region src/supervisor/cache-reset.ts
|
|
26
|
-
async function stopOwnedMetro(root) {
|
|
32
|
+
async function stopOwnedMetro(root, requester) {
|
|
27
33
|
const project = getProject(root);
|
|
28
34
|
const port = project?.metroPort;
|
|
29
35
|
const target = resolveSupervisorTarget({
|
|
@@ -44,7 +50,10 @@ async function stopOwnedMetro(root) {
|
|
|
44
50
|
pid: target.pid,
|
|
45
51
|
processToken: target.processToken
|
|
46
52
|
};
|
|
47
|
-
|
|
53
|
+
requestDevServerStop(root, target.processToken, requester);
|
|
54
|
+
const signalled = killMetroTree(target.pid, target.processToken);
|
|
55
|
+
if (!signalled) withdrawDevServerStopRequest(root, target.processToken);
|
|
56
|
+
if (!signalled || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
|
|
48
57
|
status: "stuck",
|
|
49
58
|
reason: `supervisor pid ${target.pid} did not exit`
|
|
50
59
|
};
|
|
@@ -57,7 +66,7 @@ async function stopOwnedMetro(root) {
|
|
|
57
66
|
return { status: "stopped" };
|
|
58
67
|
}
|
|
59
68
|
async function stopOwnedMetroForReset(root) {
|
|
60
|
-
const stopped = await stopOwnedMetro(root);
|
|
69
|
+
const stopped = await stopOwnedMetro(root, { by: "stim start --reset-cache" });
|
|
61
70
|
switch (stopped.status) {
|
|
62
71
|
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.");
|
|
63
72
|
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`.");
|
|
@@ -68,10 +77,10 @@ async function stopOwnedMetroForReset(root) {
|
|
|
68
77
|
//#endregion
|
|
69
78
|
//#region src/cache/caches.ts
|
|
70
79
|
function xcodeDerivedDataRoot() {
|
|
71
|
-
return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
80
|
+
return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
72
81
|
}
|
|
73
82
|
function compilationCache() {
|
|
74
|
-
const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
83
|
+
const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
75
84
|
if (!existsSync$1(dir)) return null;
|
|
76
85
|
return {
|
|
77
86
|
name: "Xcode compilation cache",
|
|
@@ -81,8 +90,8 @@ function compilationCache() {
|
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
function gradleBuildCache() {
|
|
84
|
-
const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
|
|
85
|
-
const dir = resolve(gradleHome, "caches", "build-cache-1");
|
|
93
|
+
const gradleHome = process.env.GRADLE_USER_HOME || join$1(homedir(), ".gradle");
|
|
94
|
+
const dir = resolve$1(gradleHome, "caches", "build-cache-1");
|
|
86
95
|
if (!existsSync$1(dir)) return null;
|
|
87
96
|
return {
|
|
88
97
|
name: "Gradle build cache",
|
|
@@ -104,9 +113,9 @@ function metroFileMaps() {
|
|
|
104
113
|
const files = [];
|
|
105
114
|
let bytes = 0;
|
|
106
115
|
for (const n of names) try {
|
|
107
|
-
const st = statSync$1(join(root, n));
|
|
116
|
+
const st = statSync$1(join$1(root, n));
|
|
108
117
|
if (uid !== void 0 && st.uid !== uid) continue;
|
|
109
|
-
files.push(join(root, n));
|
|
118
|
+
files.push(join$1(root, n));
|
|
110
119
|
bytes += st.size;
|
|
111
120
|
} catch {}
|
|
112
121
|
if (!files.length) return null;
|
|
@@ -133,7 +142,15 @@ function discoverCaches() {
|
|
|
133
142
|
gradle,
|
|
134
143
|
metroFileMaps()
|
|
135
144
|
].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
|
|
136
|
-
return mergeCacheDescriptors([...registered, ...detected], gradleDir);
|
|
145
|
+
return distinguishSharedNames(mergeCacheDescriptors([...registered, ...detected], gradleDir));
|
|
146
|
+
}
|
|
147
|
+
function distinguishSharedNames(caches) {
|
|
148
|
+
const counts = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const cache of caches) counts.set(cache.name, (counts.get(cache.name) ?? 0) + 1);
|
|
150
|
+
const labelled = caches.map((cache) => (counts.get(cache.name) ?? 0) < 2 ? cache : Object.assign({}, cache, { name: `${cache.name}: ${basename$1(cache.dir) || cache.dir}` }));
|
|
151
|
+
const labels = /* @__PURE__ */ new Map();
|
|
152
|
+
for (const cache of labelled) labels.set(cache.name, (labels.get(cache.name) ?? 0) + 1);
|
|
153
|
+
return labelled.map((cache, i) => (labels.get(cache.name) ?? 0) < 2 || cache === caches[i] ? cache : Object.assign({}, cache, { name: `${caches[i].name}: ${cache.dir}` }));
|
|
137
154
|
}
|
|
138
155
|
function suppressLegacyMetroAncestors(caches) {
|
|
139
156
|
const current = caches.filter(isCurrentMetroStore);
|
|
@@ -165,7 +182,7 @@ function isMetroStore(cache) {
|
|
|
165
182
|
function isNamedMetroChild(parent, candidate) {
|
|
166
183
|
const dir = canonicalCacheDir(parent);
|
|
167
184
|
const child = canonicalCacheDir(candidate);
|
|
168
|
-
return dirname(child) === dir || canonicalCacheDir(dirname(candidate)) === dir;
|
|
185
|
+
return dirname$1(child) === dir || canonicalCacheDir(dirname$1(candidate)) === dir;
|
|
169
186
|
}
|
|
170
187
|
function mergeCacheDescriptors(caches, gradleDir) {
|
|
171
188
|
const merged = [];
|
|
@@ -216,14 +233,14 @@ function cachePathsOverlap(left, right) {
|
|
|
216
233
|
return cachePathContains(left, right) || cachePathContains(right, left);
|
|
217
234
|
}
|
|
218
235
|
function cachePathContains(parent, child) {
|
|
219
|
-
const rel = relative(parent, child);
|
|
220
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
236
|
+
const rel = relative$1(parent, child);
|
|
237
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
221
238
|
}
|
|
222
239
|
function canonicalCacheDir(dir) {
|
|
223
240
|
try {
|
|
224
241
|
return realpathSync$1(dir);
|
|
225
242
|
} catch {
|
|
226
|
-
return resolve(dir);
|
|
243
|
+
return resolve$1(dir);
|
|
227
244
|
}
|
|
228
245
|
}
|
|
229
246
|
function sizeCaches(caches) {
|
|
@@ -285,13 +302,890 @@ function entriesAtDepth(dir, depth) {
|
|
|
285
302
|
} catch {
|
|
286
303
|
continue;
|
|
287
304
|
}
|
|
288
|
-
for (const name of names) next.push(join(parent, name));
|
|
305
|
+
for (const name of names) next.push(join$1(parent, name));
|
|
289
306
|
}
|
|
290
307
|
level = next;
|
|
291
308
|
}
|
|
292
309
|
return level;
|
|
293
310
|
}
|
|
294
311
|
//#endregion
|
|
312
|
+
//#region src/commands/gc/progress.ts
|
|
313
|
+
function phase(label, text) {
|
|
314
|
+
console.error(chalk.dim(phaseLine(label, text)));
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/commands/gc/memory.ts
|
|
318
|
+
const WATCHMAN_KIND = "watchman";
|
|
319
|
+
const GRADLE_DAEMONS_KIND = "gradle-daemons";
|
|
320
|
+
/** The memory kind `--cache` names, or null for any other name. */
|
|
321
|
+
function memoryCacheKind(cache) {
|
|
322
|
+
const wanted = cache?.trim().toLowerCase();
|
|
323
|
+
return wanted === "watchman" || wanted === "gradle-daemons" ? wanted : null;
|
|
324
|
+
}
|
|
325
|
+
/** Which helpers a gc run inspects: both without --cache, one for its own kind, none for any other --cache. */
|
|
326
|
+
function memoryScope(cache) {
|
|
327
|
+
if (!cache?.trim()) return {
|
|
328
|
+
watchman: true,
|
|
329
|
+
gradle: true
|
|
330
|
+
};
|
|
331
|
+
const kind = memoryCacheKind(cache);
|
|
332
|
+
return kind ? {
|
|
333
|
+
watchman: kind === WATCHMAN_KIND,
|
|
334
|
+
gradle: kind === GRADLE_DAEMONS_KIND
|
|
335
|
+
} : null;
|
|
336
|
+
}
|
|
337
|
+
const WATCHMAN_TIMEOUT_MS = 5e3;
|
|
338
|
+
const WATCHMAN_DOCTOR_BYTES = 2 * 1024 ** 3;
|
|
339
|
+
const GRADLE_STATUS_TIMEOUT_MS = 2e4;
|
|
340
|
+
const GRADLE_STOP_TIMEOUT_MS = 6e4;
|
|
341
|
+
const LSOF_TIMEOUT_MS = 1e4;
|
|
342
|
+
const ROOT_PROBES_AT_ONCE = 4;
|
|
343
|
+
const GRADLE_DAEMON_CLASS = /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\s+(\S+)/;
|
|
344
|
+
const KOTLIN_DAEMON_CLASS = /^\S.*?\/bin\/java\s.*\sorg\.jetbrains\.kotlin\.daemon\.KotlinCompileDaemon(\s|$)/;
|
|
345
|
+
/**
|
|
346
|
+
* The Gradle version, Java home and distribution of a `GradleDaemon` process from its command line. The
|
|
347
|
+
* distribution is the directory whose `lib/` holds the daemon's main jar (`gradle-daemon-main-<v>.jar`, or
|
|
348
|
+
* `gradle-launcher-<v>.jar` before Gradle 8.13), so `<distribution>/bin/gradle` is the daemon's own launcher.
|
|
349
|
+
*/
|
|
350
|
+
function parseGradleDaemonCommand(command) {
|
|
351
|
+
const version = GRADLE_DAEMON_CLASS.exec(command)?.[1];
|
|
352
|
+
const java = /^(\/.*?)\/bin\/java\s/.exec(command)?.[1];
|
|
353
|
+
if (!version || !java) return null;
|
|
354
|
+
const jar = /(?:^|\s)-(?:cp|classpath)\s+(?:[^\s]*:)?(\/.*?\/lib\/gradle-(?:daemon-main|launcher)-[^/\s:]+\.jar)(?=[\s:])/.exec(command)?.[1];
|
|
355
|
+
return {
|
|
356
|
+
version,
|
|
357
|
+
javaHome: java,
|
|
358
|
+
distribution: jar ? dirname(dirname(jar)) : null
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function isKotlinDaemonCommand(command) {
|
|
362
|
+
return KOTLIN_DAEMON_CLASS.test(command) && !GRADLE_DAEMON_CLASS.test(command);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The offload client whose Gradle home under `workerRoot` a daemon uses: named by its classpath, as stim-server
|
|
366
|
+
* matches it, or by the Gradle home it runs in.
|
|
367
|
+
*/
|
|
368
|
+
function offloadClientOf(command, workerRoot, gradleHome) {
|
|
369
|
+
const root = resolve(workerRoot).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
370
|
+
const home = new RegExp(`${root}/([^/\\s]+)/cache/gradle(/|$)`);
|
|
371
|
+
return home.exec(command)?.[1] ?? (gradleHome ? home.exec(gradleHome)?.[1] ?? null : null);
|
|
372
|
+
}
|
|
373
|
+
/** `gradle --status` rows: the state of each daemon pid it lists, lowercased (idle, busy, stopped, ...). */
|
|
374
|
+
function parseGradleStatus(output) {
|
|
375
|
+
const states = /* @__PURE__ */ new Map();
|
|
376
|
+
for (const line of output.split("\n")) {
|
|
377
|
+
const match = /^\s*(\d+)\s+([A-Z][A-Z_]*)\s+\S/.exec(line);
|
|
378
|
+
if (match) states.set(Number(match[1]), match[2].toLowerCase());
|
|
379
|
+
}
|
|
380
|
+
return states;
|
|
381
|
+
}
|
|
382
|
+
/** `lsof -F pnT` output: each pid's open file names and its ESTABLISHED TCP connections. */
|
|
383
|
+
function parseLsof(output) {
|
|
384
|
+
const processes = /* @__PURE__ */ new Map();
|
|
385
|
+
let current = null;
|
|
386
|
+
let name = null;
|
|
387
|
+
for (const line of output.split("\n")) if (line.startsWith("p")) {
|
|
388
|
+
const pid = Number(line.slice(1));
|
|
389
|
+
current = processes.get(pid) ?? {
|
|
390
|
+
files: [],
|
|
391
|
+
connections: []
|
|
392
|
+
};
|
|
393
|
+
processes.set(pid, current);
|
|
394
|
+
} else if (current && line.startsWith("n")) {
|
|
395
|
+
name = line.slice(1);
|
|
396
|
+
current.files.push(name);
|
|
397
|
+
} else if (current && name?.includes("->") && line === "TST=ESTABLISHED") current.connections.push(name);
|
|
398
|
+
return processes;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The process at the other end of each of `pid`'s connections, found among the other processes lsof listed by the
|
|
402
|
+
* reversed `remote->local` name; null for a peer lsof did not list.
|
|
403
|
+
*/
|
|
404
|
+
function connectionPeers(pid, lsof) {
|
|
405
|
+
return (lsof.get(pid)?.connections ?? []).map((connection) => {
|
|
406
|
+
const [local, remote] = connection.split("->");
|
|
407
|
+
const reversed = `${remote}->${local}`;
|
|
408
|
+
for (const [other, entry] of lsof) if (other !== pid && entry.connections.includes(reversed)) return other;
|
|
409
|
+
return null;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** The Gradle user home a daemon runs in: the one whose `daemon/<version>/daemon-<pid>.out.log` it holds open. */
|
|
413
|
+
function gradleHomeFromFiles(pid, version, files) {
|
|
414
|
+
const suffix = `/daemon/${version}/daemon-${pid}.out.log`;
|
|
415
|
+
const log = files.find((file) => file.endsWith(suffix));
|
|
416
|
+
return log ? log.slice(0, -suffix.length) : null;
|
|
417
|
+
}
|
|
418
|
+
const fsProbe = {
|
|
419
|
+
exists: existsSync,
|
|
420
|
+
isFile(path) {
|
|
421
|
+
try {
|
|
422
|
+
return statSync(path).isFile();
|
|
423
|
+
} catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
read(path) {
|
|
428
|
+
try {
|
|
429
|
+
return readFileSync(path, "utf8");
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
mounted: (path) => isOnMountedVolume(path)
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Why a watch root is stale: its directory is gone from a mounted volume, or the nearest `.git` above it is a
|
|
438
|
+
* linked worktree's `.git` file whose gitdir no longer exists (git pruned the worktree and left the directory).
|
|
439
|
+
* Null when it is live or when that cannot be proven, such as a path on a volume that is not mounted.
|
|
440
|
+
*/
|
|
441
|
+
function watchmanRootStaleness(path, probe = fsProbe) {
|
|
442
|
+
if (!probe.exists(path)) return probe.mounted(path) ? "missing" : null;
|
|
443
|
+
for (let dir = path;; dir = dirname(dir)) {
|
|
444
|
+
const git = join(dir, ".git");
|
|
445
|
+
if (probe.exists(git)) {
|
|
446
|
+
if (!probe.isFile(git)) return null;
|
|
447
|
+
const target = /^gitdir:\s*(.+?)\s*$/m.exec(probe.read(git) ?? "")?.[1];
|
|
448
|
+
if (!target) return null;
|
|
449
|
+
const gitdir = isAbsolute(target) ? target : resolve(dir, target);
|
|
450
|
+
return probe.exists(gitdir) || !probe.mounted(gitdir) ? null : "pruned-worktree";
|
|
451
|
+
}
|
|
452
|
+
if (dirname(dir) === dir) return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function staleText(stale) {
|
|
456
|
+
return stale === "missing" ? "its directory is gone" : "its linked worktree was pruned";
|
|
457
|
+
}
|
|
458
|
+
function planWatchmanRoot(root) {
|
|
459
|
+
const subscriptions = root.subscribers?.length ?? null;
|
|
460
|
+
const base = {
|
|
461
|
+
path: root.path,
|
|
462
|
+
stale: root.stale,
|
|
463
|
+
subscriptions,
|
|
464
|
+
triggers: root.triggers
|
|
465
|
+
};
|
|
466
|
+
if (root.stale === null) return {
|
|
467
|
+
...base,
|
|
468
|
+
removable: false,
|
|
469
|
+
detail: null
|
|
470
|
+
};
|
|
471
|
+
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;
|
|
472
|
+
return {
|
|
473
|
+
...base,
|
|
474
|
+
removable: kept === null,
|
|
475
|
+
detail: kept ? `${staleText(root.stale)}, but ${kept}` : staleText(root.stale)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Decides the watchman roots gc removes and whether it may shut the daemon down. A stale root goes only while no
|
|
480
|
+
* subscription or trigger uses it. The daemon goes only when `debug-status` lists no client besides gc's own call and
|
|
481
|
+
* every root's subscriptions and triggers could be read, no kept root has a trigger, and no root has a subscription;
|
|
482
|
+
* `describeClient` names each client that keeps it.
|
|
483
|
+
*/
|
|
484
|
+
function planWatchman(facts, describeClient) {
|
|
485
|
+
const roots = (facts.roots ?? []).map(planWatchmanRoot);
|
|
486
|
+
const unreadRoots = facts.roots === null || roots.some((root) => root.subscriptions === null || root.triggers === null);
|
|
487
|
+
const triggered = roots.filter((root) => !root.removable && (root.triggers ?? 0) > 0);
|
|
488
|
+
let reason = null;
|
|
489
|
+
let detail = null;
|
|
490
|
+
if (facts.clients === null || unreadRoots) {
|
|
491
|
+
reason = "unknown";
|
|
492
|
+
detail = facts.clients === null ? "watchman debug-status did not list its clients" : "could not list the subscriptions and triggers of every root";
|
|
493
|
+
} else if (facts.clients.length > 0) {
|
|
494
|
+
reason = "in-use";
|
|
495
|
+
detail = `used by ${[...new Set(facts.clients.map(describeClient))].join(", ")}; shutting it down would break their file watching`;
|
|
496
|
+
} else if (roots.some((root) => !root.removable && (root.subscriptions ?? 0) > 0)) {
|
|
497
|
+
reason = "in-use";
|
|
498
|
+
detail = "a root still has a subscription";
|
|
499
|
+
} else if (triggered.length > 0) {
|
|
500
|
+
reason = "in-use";
|
|
501
|
+
detail = `${triggered.map((root) => root.path).join(", ")} ha${triggered.length === 1 ? "s" : "ve"} triggers that stop firing while watchman is down`;
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
process: {
|
|
505
|
+
kind: "watchman",
|
|
506
|
+
cacheKind: WATCHMAN_KIND,
|
|
507
|
+
pid: facts.pid,
|
|
508
|
+
startedAt: facts.startedAt,
|
|
509
|
+
bytes: facts.bytes,
|
|
510
|
+
measure: facts.measure,
|
|
511
|
+
version: null,
|
|
512
|
+
gradleHome: null,
|
|
513
|
+
offloadClient: null,
|
|
514
|
+
state: reason === "in-use" ? "busy" : reason === "unknown" ? "unknown" : "idle",
|
|
515
|
+
reclaimable: reason === null,
|
|
516
|
+
reason,
|
|
517
|
+
detail
|
|
518
|
+
},
|
|
519
|
+
roots
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Decides which Gradle and Kotlin daemons gc may stop. A running Stim build (`buildRunning`) keeps every one. An
|
|
524
|
+
* offload worker's daemon is left to stim-server while it runs. A Gradle daemon goes only when its own `gradle
|
|
525
|
+
* --status` says IDLE. A Kotlin daemon goes only when every Gradle daemon is idle and each of its connections comes from
|
|
526
|
+
* a Gradle daemon gc stops in the same run.
|
|
527
|
+
*/
|
|
528
|
+
function planDaemons({ gradle, kotlin, buildRunning, stimServerRunning }) {
|
|
529
|
+
const plans = [];
|
|
530
|
+
const gradleBusy = gradle.some((daemon) => !("state" in daemon.status) || daemon.status.state !== "idle");
|
|
531
|
+
for (const daemon of gradle) {
|
|
532
|
+
const state = "state" in daemon.status ? daemon.status.state : null;
|
|
533
|
+
const idle = state === "idle";
|
|
534
|
+
let reason = null;
|
|
535
|
+
let detail = null;
|
|
536
|
+
if (daemon.offloadClient !== null && stimServerRunning) {
|
|
537
|
+
reason = "stim-server";
|
|
538
|
+
detail = `stim-server stops the daemons of offload client ${daemon.offloadClient} when that client loses build or memory runs low`;
|
|
539
|
+
} else if (buildRunning) {
|
|
540
|
+
reason = "build-running";
|
|
541
|
+
detail = buildRunning;
|
|
542
|
+
} else if (state === "busy") {
|
|
543
|
+
reason = "busy";
|
|
544
|
+
detail = "a build is using it";
|
|
545
|
+
} else if (!idle) {
|
|
546
|
+
reason = "unknown";
|
|
547
|
+
detail = "unknown" in daemon.status ? daemon.status.unknown : `gradle --status reports it ${state}`;
|
|
548
|
+
}
|
|
549
|
+
plans.push({
|
|
550
|
+
kind: "gradleDaemon",
|
|
551
|
+
cacheKind: GRADLE_DAEMONS_KIND,
|
|
552
|
+
pid: daemon.pid,
|
|
553
|
+
startedAt: daemon.startedAt,
|
|
554
|
+
bytes: daemon.bytes,
|
|
555
|
+
measure: daemon.measure,
|
|
556
|
+
version: daemon.command.version,
|
|
557
|
+
gradleHome: daemon.gradleHome,
|
|
558
|
+
offloadClient: daemon.offloadClient,
|
|
559
|
+
state: idle ? "idle" : state === "busy" ? "busy" : "unknown",
|
|
560
|
+
reclaimable: reason === null,
|
|
561
|
+
reason,
|
|
562
|
+
detail
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const stopping = new Set(plans.filter((plan) => plan.reclaimable).map((plan) => plan.pid));
|
|
566
|
+
for (const daemon of kotlin) {
|
|
567
|
+
const clients = daemon.peers?.filter((peer) => peer === null || !stopping.has(peer)) ?? [];
|
|
568
|
+
let reason = null;
|
|
569
|
+
let detail = null;
|
|
570
|
+
if (buildRunning) {
|
|
571
|
+
reason = "build-running";
|
|
572
|
+
detail = buildRunning;
|
|
573
|
+
} else if (daemon.peers === null) {
|
|
574
|
+
reason = "unknown";
|
|
575
|
+
detail = "lsof could not list its connections";
|
|
576
|
+
} else if (gradleBusy) {
|
|
577
|
+
reason = "busy";
|
|
578
|
+
detail = "a Gradle daemon is busy or its state is unknown, and it may be compiling through this one";
|
|
579
|
+
} else if (clients.length > 0) {
|
|
580
|
+
reason = "busy";
|
|
581
|
+
detail = `${clients.length} connection(s) from a process gc does not stop are open`;
|
|
582
|
+
}
|
|
583
|
+
plans.push({
|
|
584
|
+
kind: "kotlinDaemon",
|
|
585
|
+
cacheKind: GRADLE_DAEMONS_KIND,
|
|
586
|
+
pid: daemon.pid,
|
|
587
|
+
startedAt: daemon.startedAt,
|
|
588
|
+
bytes: daemon.bytes,
|
|
589
|
+
measure: daemon.measure,
|
|
590
|
+
version: null,
|
|
591
|
+
gradleHome: null,
|
|
592
|
+
offloadClient: null,
|
|
593
|
+
state: reason === null ? "idle" : reason === "unknown" ? "unknown" : "busy",
|
|
594
|
+
reclaimable: reason === null,
|
|
595
|
+
reason,
|
|
596
|
+
detail
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return plans;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Why no Gradle or Kotlin daemon may be stopped now, or null. An Android build lock, or a build slot that no live iOS
|
|
603
|
+
* build lock of the same process holds, counts as a running build; one whose holder is unresolved counts too.
|
|
604
|
+
*/
|
|
605
|
+
function runningBuild(locks, slots) {
|
|
606
|
+
const android = locks.find((lock) => lock.platform === "android" && (lock.alive || lock.unresolved));
|
|
607
|
+
if (android) return `${android.unresolved ? "an unresolved" : "a running"} Android build holds ${android.path} (pid ${android.pid ?? "?"}, ${android.projectRoot || "unrecorded workspace"})`;
|
|
608
|
+
const ios = new Set(locks.filter((lock) => lock.platform === "ios" && lock.alive).map((lock) => lock.pid));
|
|
609
|
+
const slot = slots.find((s) => (s.alive || s.unresolved) && !(s.alive && s.pid !== null && ios.has(s.pid)));
|
|
610
|
+
if (slot) return `${slot.unresolved ? "an unresolved" : "a running"} build holds build slot ${slot.index ?? "?"} (pid ${slot.pid ?? "?"}, ${slot.projectRoot || "unrecorded workspace"})`;
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
/** A watchman client's pid resolved to the Stim workspace whose supervisor it is or descends from. */
|
|
614
|
+
function stimWorkspaceOf(pid, parentOf, supervisors) {
|
|
615
|
+
let current = pid;
|
|
616
|
+
for (let depth = 0; current !== void 0 && current > 1 && depth < 64; depth++) {
|
|
617
|
+
const workspace = supervisors.get(current);
|
|
618
|
+
if (workspace) return workspace;
|
|
619
|
+
current = parentOf.get(current);
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function reclaimableBytes(report, kind) {
|
|
624
|
+
return report.processes.filter((entry) => entry.reclaimable && (kind === void 0 || entry.cacheKind === kind)).reduce((sum, entry) => sum + entry.bytes, 0);
|
|
625
|
+
}
|
|
626
|
+
const boundWatchman = async (args) => {
|
|
627
|
+
const answer = JSON.parse(await getExecutor().runFileAsync("watchman", ["--no-spawn", ...args], { timeoutMs: WATCHMAN_TIMEOUT_MS }));
|
|
628
|
+
const error = answer?.error;
|
|
629
|
+
if (error !== void 0) throw new Error(String(error));
|
|
630
|
+
return answer;
|
|
631
|
+
};
|
|
632
|
+
async function quietly(fn) {
|
|
633
|
+
try {
|
|
634
|
+
return await fn();
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* `watchman debug-status` with gc's own call left out of its clients. The call is spawned directly so its pid is
|
|
641
|
+
* known: debug-status lists the asking CLI as a client too.
|
|
642
|
+
*/
|
|
643
|
+
function watchmanClients() {
|
|
644
|
+
return new Promise((done) => {
|
|
645
|
+
let child;
|
|
646
|
+
try {
|
|
647
|
+
child = getExecutor().spawn("watchman", ["--no-spawn", "debug-status"], { stdio: [
|
|
648
|
+
"ignore",
|
|
649
|
+
"pipe",
|
|
650
|
+
"ignore"
|
|
651
|
+
] });
|
|
652
|
+
} catch {
|
|
653
|
+
done(null);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const self = child.pid;
|
|
657
|
+
const chunks = [];
|
|
658
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), WATCHMAN_TIMEOUT_MS);
|
|
659
|
+
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
660
|
+
child.on("error", () => {
|
|
661
|
+
clearTimeout(timer);
|
|
662
|
+
done(null);
|
|
663
|
+
});
|
|
664
|
+
child.on("close", (status) => {
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
let clients = null;
|
|
667
|
+
try {
|
|
668
|
+
if (status === 0 && self !== void 0) clients = parseWatchmanClients(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
669
|
+
} catch {}
|
|
670
|
+
done(clients ? clients.filter((client) => client.pid !== self) : null);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
async function collectWatchmanRoots(pid, progress) {
|
|
675
|
+
const watchman = boundWatchman;
|
|
676
|
+
const paths = parseWatchRoots(await quietly(() => watchman(["watch-list"])));
|
|
677
|
+
if (!paths) return null;
|
|
678
|
+
if (progress) phase("daemons", `watchman pid ${pid}: checking ${plural(paths.length, "root")}`);
|
|
679
|
+
const roots = [];
|
|
680
|
+
let next = 0;
|
|
681
|
+
const probe = async () => {
|
|
682
|
+
while (next < paths.length) {
|
|
683
|
+
const index = next++;
|
|
684
|
+
const path = paths[index];
|
|
685
|
+
const [subscriptions, triggers] = await Promise.all([quietly(() => watchman(["debug-get-subscriptions", path])), quietly(() => watchman(["trigger-list", path]))]);
|
|
686
|
+
roots[index] = {
|
|
687
|
+
path,
|
|
688
|
+
stale: watchmanRootStaleness(path),
|
|
689
|
+
subscribers: parseSubscriberNames(subscriptions),
|
|
690
|
+
triggers: parseTriggerCount(triggers)
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
await Promise.all(Array.from({ length: Math.min(ROOT_PROBES_AT_ONCE, paths.length) }, probe));
|
|
695
|
+
return roots;
|
|
696
|
+
}
|
|
697
|
+
function measureOf(pid, rows, footprints) {
|
|
698
|
+
const footprint = footprints?.get(pid);
|
|
699
|
+
if (footprint !== void 0) return {
|
|
700
|
+
bytes: footprint,
|
|
701
|
+
measure: "footprint"
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
bytes: (rows.get(pid)?.rssKb ?? 0) * 1024,
|
|
705
|
+
measure: "rss"
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function supervisorPids() {
|
|
709
|
+
const supervisors = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const root of Object.keys(loadConfig()?.projects ?? {})) {
|
|
711
|
+
const pid = readPidFile(root);
|
|
712
|
+
if (pid) supervisors.set(pid, root);
|
|
713
|
+
}
|
|
714
|
+
return supervisors;
|
|
715
|
+
}
|
|
716
|
+
async function readLsof(pids) {
|
|
717
|
+
if (pids.length === 0) return /* @__PURE__ */ new Map();
|
|
718
|
+
try {
|
|
719
|
+
return parseLsof(await getExecutor().runFileAsync("lsof", [
|
|
720
|
+
"-n",
|
|
721
|
+
"-P",
|
|
722
|
+
"-F",
|
|
723
|
+
"pnT",
|
|
724
|
+
"-p",
|
|
725
|
+
pids.join(",")
|
|
726
|
+
], { timeoutMs: LSOF_TIMEOUT_MS }));
|
|
727
|
+
} catch (error) {
|
|
728
|
+
const { status, signal: killed, stdout } = error;
|
|
729
|
+
return status === 1 && !killed && typeof stdout === "string" && stdout ? parseLsof(stdout) : null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async function gradleStatus(command, home) {
|
|
733
|
+
if (!command.distribution) return { unknown: "could not find its Gradle distribution on its classpath" };
|
|
734
|
+
const launcher = join(command.distribution, "bin", "gradle");
|
|
735
|
+
if (!existsSync(launcher)) return { unknown: `its distribution has no ${launcher}` };
|
|
736
|
+
try {
|
|
737
|
+
return parseGradleStatus(await getExecutor().runFileAsync(launcher, [
|
|
738
|
+
"--status",
|
|
739
|
+
"-q",
|
|
740
|
+
"-g",
|
|
741
|
+
home
|
|
742
|
+
], {
|
|
743
|
+
cwd: home,
|
|
744
|
+
env: { JAVA_HOME: command.javaHome },
|
|
745
|
+
omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
|
|
746
|
+
timeoutMs: GRADLE_STATUS_TIMEOUT_MS
|
|
747
|
+
}));
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return { unknown: `gradle --status failed: ${error.message.split("\n")[0]}` };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function groupKey(command, home) {
|
|
753
|
+
return JSON.stringify([
|
|
754
|
+
home,
|
|
755
|
+
command.version,
|
|
756
|
+
command.distribution,
|
|
757
|
+
command.javaHome
|
|
758
|
+
]);
|
|
759
|
+
}
|
|
760
|
+
async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress) {
|
|
761
|
+
const gradleRows = rows.flatMap((row) => {
|
|
762
|
+
const command = parseGradleDaemonCommand(row.command);
|
|
763
|
+
return command ? [{
|
|
764
|
+
row,
|
|
765
|
+
command
|
|
766
|
+
}] : [];
|
|
767
|
+
});
|
|
768
|
+
const kotlinRows = rows.filter((row) => isKotlinDaemonCommand(row.command));
|
|
769
|
+
if (gradleRows.length === 0 && kotlinRows.length === 0) return {
|
|
770
|
+
gradle: [],
|
|
771
|
+
kotlin: []
|
|
772
|
+
};
|
|
773
|
+
if (progress) phase("daemons", `checking ${plural(gradleRows.length, "Gradle daemon")} and ${plural(kotlinRows.length, "Kotlin daemon")}`);
|
|
774
|
+
const lsof = await readLsof([...gradleRows.map(({ row }) => row.pid), ...kotlinRows.map((row) => row.pid)]);
|
|
775
|
+
const workerRoot = buildWorkerRoot();
|
|
776
|
+
const homes = gradleRows.map(({ row, command }) => {
|
|
777
|
+
const files = lsof?.get(row.pid)?.files;
|
|
778
|
+
return files ? gradleHomeFromFiles(row.pid, command.version, files) : null;
|
|
779
|
+
});
|
|
780
|
+
const offload = gradleRows.map(({ row }, index) => offloadClientOf(row.command, workerRoot, homes[index] ?? null));
|
|
781
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
782
|
+
gradleRows.forEach(({ command }, index) => {
|
|
783
|
+
const home = homes[index];
|
|
784
|
+
if (offload[index] !== null && stimServerRunning) return;
|
|
785
|
+
if (home && !statuses.has(groupKey(command, home))) {
|
|
786
|
+
if (progress) phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
|
|
787
|
+
statuses.set(groupKey(command, home), gradleStatus(command, home));
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
const gradle = [];
|
|
791
|
+
for (const [index, { row, command }] of gradleRows.entries()) {
|
|
792
|
+
const home = homes[index] ?? null;
|
|
793
|
+
let status;
|
|
794
|
+
if (offload[index] !== null && stimServerRunning) status = { unknown: "stim-server manages it" };
|
|
795
|
+
else if (!home) status = { unknown: lsof ? "could not find its Gradle user home among its open files" : "lsof failed" };
|
|
796
|
+
else {
|
|
797
|
+
const states = await statuses.get(groupKey(command, home));
|
|
798
|
+
if (states instanceof Map) {
|
|
799
|
+
const state = states.get(row.pid);
|
|
800
|
+
status = state ? { state } : { unknown: "gradle --status did not list it" };
|
|
801
|
+
} else status = states;
|
|
802
|
+
}
|
|
803
|
+
gradle.push({
|
|
804
|
+
pid: row.pid,
|
|
805
|
+
startedAt: row.startedAt,
|
|
806
|
+
...measureOf(row.pid, byPid, footprints),
|
|
807
|
+
command,
|
|
808
|
+
gradleHome: home,
|
|
809
|
+
offloadClient: offload[index] ?? null,
|
|
810
|
+
status
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
gradle,
|
|
815
|
+
kotlin: kotlinRows.map((row) => Object.assign(measureOf(row.pid, byPid, footprints), {
|
|
816
|
+
pid: row.pid,
|
|
817
|
+
startedAt: row.startedAt,
|
|
818
|
+
peers: lsof?.get(row.pid) ? connectionPeers(row.pid, lsof) : null
|
|
819
|
+
}))
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
async function collectWatchmanFacts(byPid, footprints, progress) {
|
|
823
|
+
const pid = parseWatchmanPid(await quietly(() => boundWatchman(["get-pid"])));
|
|
824
|
+
if (pid === null) return null;
|
|
825
|
+
const roots = await collectWatchmanRoots(pid, progress);
|
|
826
|
+
const clients = (await watchmanClients())?.filter((client) => client.pid !== pid) ?? null;
|
|
827
|
+
return {
|
|
828
|
+
pid,
|
|
829
|
+
startedAt: byPid.get(pid)?.startedAt ?? null,
|
|
830
|
+
...measureOf(pid, byPid, footprints),
|
|
831
|
+
roots,
|
|
832
|
+
clients
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function watchmanClientDescriber(rows) {
|
|
836
|
+
const parentOf = new Map(rows.map((row) => [row.pid, row.ppid]));
|
|
837
|
+
const supervisors = supervisorPids();
|
|
838
|
+
return (client) => {
|
|
839
|
+
const workspace = stimWorkspaceOf(client.pid, parentOf, supervisors);
|
|
840
|
+
return workspace ? `the Stim workspace ${workspace}` : `${client.name ?? "a client"} (pid ${client.pid})`;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function memorySweepIsScoped() {
|
|
844
|
+
return Boolean(process.env.STIM_HOME);
|
|
845
|
+
}
|
|
846
|
+
/** The report of a run whose STIM_HOME is set: the helpers are machine-global, so none is inspected or stopped. */
|
|
847
|
+
function scopedMemoryReport() {
|
|
848
|
+
return {
|
|
849
|
+
processes: [],
|
|
850
|
+
watchmanRoots: [],
|
|
851
|
+
notices: ["memory sweep skipped: STIM_HOME scopes this config, but watchman and the Gradle and Kotlin daemons are machine-global"]
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
async function collectMemoryReport(scope, progress = false) {
|
|
855
|
+
const report = {
|
|
856
|
+
processes: [],
|
|
857
|
+
watchmanRoots: [],
|
|
858
|
+
notices: []
|
|
859
|
+
};
|
|
860
|
+
if (process.platform === "win32") return report;
|
|
861
|
+
const rows = createDeviceProcessTables().host();
|
|
862
|
+
if (rows === null) {
|
|
863
|
+
report.notices.push("memory sweep skipped: ps did not answer");
|
|
864
|
+
return report;
|
|
865
|
+
}
|
|
866
|
+
const byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
867
|
+
const footprints = await readFootprints();
|
|
868
|
+
if (scope.watchman) {
|
|
869
|
+
const facts = await collectWatchmanFacts(byPid, footprints, progress);
|
|
870
|
+
if (facts) {
|
|
871
|
+
const plan = planWatchman(facts, watchmanClientDescriber(rows));
|
|
872
|
+
report.processes.push(plan.process);
|
|
873
|
+
report.watchmanRoots = plan.roots;
|
|
874
|
+
} else if (!scope.gradle) report.notices.push("no watchman daemon answered");
|
|
875
|
+
}
|
|
876
|
+
if (scope.gradle) {
|
|
877
|
+
const stimServerRunning = rows.some((row) => STIM_SERVER_COMMAND.test(row.command));
|
|
878
|
+
const facts = await collectGradleFacts(rows, byPid, footprints, stimServerRunning, progress);
|
|
879
|
+
report.processes.push(...planDaemons({
|
|
880
|
+
...facts,
|
|
881
|
+
buildRunning: facts.gradle.length || facts.kotlin.length ? runningBuild(listBuildLocks(), listBuildSlots()) : null,
|
|
882
|
+
stimServerRunning
|
|
883
|
+
}));
|
|
884
|
+
if (!scope.watchman && report.processes.length === 0) report.notices.push("no Gradle or Kotlin daemon is running");
|
|
885
|
+
}
|
|
886
|
+
return report;
|
|
887
|
+
}
|
|
888
|
+
function processLabel(entry) {
|
|
889
|
+
if (entry.kind === "watchman") return `watchman pid ${entry.pid}`;
|
|
890
|
+
if (entry.kind === "kotlinDaemon") return `Kotlin compile daemon pid ${entry.pid}`;
|
|
891
|
+
const where = entry.offloadClient ? `offload client ${entry.offloadClient}` : entry.gradleHome ?? "unknown Gradle home";
|
|
892
|
+
return `Gradle ${entry.version ?? "?"} daemon pid ${entry.pid} (${where})`;
|
|
893
|
+
}
|
|
894
|
+
function uptime(startedAt, now) {
|
|
895
|
+
const at = startedAt ? Date.parse(startedAt) : NaN;
|
|
896
|
+
return Number.isFinite(at) ? `up ${formatLongDuration(Math.max(0, now - at))}` : "uptime unknown";
|
|
897
|
+
}
|
|
898
|
+
function memoryLines(report, scoped, now = Date.now()) {
|
|
899
|
+
if (!report || report.processes.length === 0 && report.notices.length === 0) return [];
|
|
900
|
+
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:`];
|
|
901
|
+
for (const entry of report.processes) {
|
|
902
|
+
const state = entry.reclaimable ? "idle" : entry.kind === "watchman" && entry.state === "busy" ? "in use" : entry.state;
|
|
903
|
+
lines.push(` ${processLabel(entry)} ${formatBytes(entry.bytes)}, ${uptime(entry.startedAt, now)}, ${state}`);
|
|
904
|
+
if (entry.kind === "watchman") {
|
|
905
|
+
const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
|
|
906
|
+
lines.push(` ${report.watchmanRoots.length} root(s), ${stale} stale`);
|
|
907
|
+
for (const root of report.watchmanRoots.filter((r) => r.stale !== null)) lines.push(` stale root ${root.path}: ${root.detail}${root.removable ? "" : "; kept"}`);
|
|
908
|
+
}
|
|
909
|
+
if (entry.detail) lines.push(` ${entry.reclaimable ? "" : "kept: "}${entry.detail}`);
|
|
910
|
+
}
|
|
911
|
+
for (const notice of report.notices) lines.push(` ${notice}`);
|
|
912
|
+
const kinds = scoped ? [scoped] : [WATCHMAN_KIND, GRADLE_DAEMONS_KIND];
|
|
913
|
+
const parts = [];
|
|
914
|
+
for (const kind of kinds) {
|
|
915
|
+
if (!report.processes.some((entry) => entry.cacheKind === kind)) continue;
|
|
916
|
+
const roots = kind === "watchman" ? report.watchmanRoots.filter((root) => root.removable).length : 0;
|
|
917
|
+
const note = roots ? ` (and ${roots} stale root(s) removed)` : "";
|
|
918
|
+
parts.push(`${formatBytes(reclaimableBytes(report, kind))} with stim gc --delete --cache ${kind}${note}`);
|
|
919
|
+
}
|
|
920
|
+
if (parts.length) lines.push(` reclaimable ${formatBytes(reclaimableBytes(report))}: ${parts.join("; ")}`);
|
|
921
|
+
return lines;
|
|
922
|
+
}
|
|
923
|
+
function sameProcess(entry, rows) {
|
|
924
|
+
const row = rows?.get(entry.pid);
|
|
925
|
+
return Boolean(row && entry.startedAt !== null && row.startedAt === entry.startedAt);
|
|
926
|
+
}
|
|
927
|
+
function signal(pid) {
|
|
928
|
+
try {
|
|
929
|
+
process.kill(pid, "SIGTERM");
|
|
930
|
+
return null;
|
|
931
|
+
} catch (error) {
|
|
932
|
+
return error.code === "ESRCH" ? null : error.message;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function keep(entry, kind, detail) {
|
|
936
|
+
console.log(chalk.yellow(`Kept ${processLabel(entry)}: ${detail}`));
|
|
937
|
+
recordGcResult(kind, "kept", processLabel(entry), {
|
|
938
|
+
id: String(entry.pid),
|
|
939
|
+
bytes: entry.bytes,
|
|
940
|
+
detail
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
async function reclaimWatchman(before) {
|
|
944
|
+
let failures = 0;
|
|
945
|
+
const fresh = await collectMemoryReport({
|
|
946
|
+
watchman: true,
|
|
947
|
+
gradle: false
|
|
948
|
+
}, true);
|
|
949
|
+
const daemon = fresh.processes.find((entry) => entry.kind === "watchman");
|
|
950
|
+
const planned = before.processes.find((entry) => entry.kind === "watchman");
|
|
951
|
+
if (!daemon || !planned || daemon.pid !== planned.pid) {
|
|
952
|
+
if (planned) keep(planned, "watchman", "the daemon changed or stopped since the report");
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
for (const root of fresh.watchmanRoots.filter((r) => r.stale !== null)) {
|
|
956
|
+
if (!root.removable) {
|
|
957
|
+
console.log(chalk.yellow(`Kept the watchman root ${root.path}: ${root.detail}`));
|
|
958
|
+
recordGcResult("watchmanRoot", "kept", root.path, { detail: root.detail });
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
phase("daemons", `removing stale watchman root ${root.path}`);
|
|
963
|
+
await boundWatchman(["watch-del", root.path]);
|
|
964
|
+
console.log(chalk.green(`Removed the stale watchman root ${root.path} (${root.detail})`));
|
|
965
|
+
recordGcResult("watchmanRoot", "done", root.path, { detail: root.detail });
|
|
966
|
+
} catch (error) {
|
|
967
|
+
failures++;
|
|
968
|
+
console.log(chalk.red(`Could not remove the watchman root ${root.path}: ${error.message}`));
|
|
969
|
+
recordGcResult("watchmanRoot", "failed", root.path, { detail: error.message });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const current = (await collectMemoryReport({
|
|
973
|
+
watchman: true,
|
|
974
|
+
gradle: false
|
|
975
|
+
}, true)).processes.find((entry) => entry.kind === "watchman");
|
|
976
|
+
if (!current || current.pid !== daemon.pid) return failures;
|
|
977
|
+
if (!current.reclaimable) {
|
|
978
|
+
keep(current, "watchman", current.detail ?? "it could not be proven unused");
|
|
979
|
+
return failures;
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
phase("daemons", `shutting down watchman pid ${current.pid}: no client uses it`);
|
|
983
|
+
await boundWatchman(["shutdown-server"]);
|
|
984
|
+
console.log(chalk.green(`Shut down watchman pid ${current.pid}, freeing ${formatBytes(current.bytes)}; the next client that needs it starts it again`));
|
|
985
|
+
recordGcResult("watchman", "done", processLabel(current), {
|
|
986
|
+
id: String(current.pid),
|
|
987
|
+
bytes: current.bytes
|
|
988
|
+
});
|
|
989
|
+
} catch (error) {
|
|
990
|
+
failures++;
|
|
991
|
+
console.log(chalk.red(`Could not shut down watchman pid ${current.pid}: ${error.message}`));
|
|
992
|
+
recordGcResult("watchman", "failed", processLabel(current), {
|
|
993
|
+
id: String(current.pid),
|
|
994
|
+
bytes: current.bytes,
|
|
995
|
+
detail: error.message
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return failures;
|
|
999
|
+
}
|
|
1000
|
+
function stopKotlin(entry, rows) {
|
|
1001
|
+
if (!sameProcess(entry, rows)) {
|
|
1002
|
+
keep(entry, "kotlinDaemon", "the process changed or exited since the check");
|
|
1003
|
+
return 0;
|
|
1004
|
+
}
|
|
1005
|
+
const error = signal(entry.pid);
|
|
1006
|
+
if (error) {
|
|
1007
|
+
console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
|
|
1008
|
+
recordGcResult("kotlinDaemon", "failed", processLabel(entry), {
|
|
1009
|
+
id: String(entry.pid),
|
|
1010
|
+
bytes: entry.bytes,
|
|
1011
|
+
detail: error
|
|
1012
|
+
});
|
|
1013
|
+
return 1;
|
|
1014
|
+
}
|
|
1015
|
+
console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
|
|
1016
|
+
recordGcResult("kotlinDaemon", "done", processLabel(entry), {
|
|
1017
|
+
id: String(entry.pid),
|
|
1018
|
+
bytes: entry.bytes
|
|
1019
|
+
});
|
|
1020
|
+
return 0;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Stops the Gradle daemons a fresh check still finds idle. When every daemon `gradle --status` lists for a home and
|
|
1024
|
+
* version is one of them, the daemon's own `gradle --stop` runs for that home; otherwise each idle one gets SIGTERM,
|
|
1025
|
+
* because `--stop` also stops the busy daemons of that home and version.
|
|
1026
|
+
*/
|
|
1027
|
+
async function stopGradle(idle, rows) {
|
|
1028
|
+
let failures = 0;
|
|
1029
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const entry of idle) {
|
|
1031
|
+
const command = parseGradleDaemonCommand(rows?.get(entry.pid)?.command ?? "");
|
|
1032
|
+
if (!command || !entry.gradleHome || !sameProcess(entry, rows)) {
|
|
1033
|
+
keep(entry, "gradleDaemon", "the process changed or exited since the check");
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const key = groupKey(command, entry.gradleHome);
|
|
1037
|
+
groups.set(key, [...groups.get(key) ?? [], entry]);
|
|
1038
|
+
}
|
|
1039
|
+
for (const [key, entries] of groups) {
|
|
1040
|
+
const [home, , distribution, javaHome] = JSON.parse(key);
|
|
1041
|
+
const command = parseGradleDaemonCommand(rows.get(entries[0].pid).command);
|
|
1042
|
+
const building = runningBuild(listBuildLocks(), listBuildSlots());
|
|
1043
|
+
if (building) {
|
|
1044
|
+
for (const entry of entries) keep(entry, "gradleDaemon", building);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
|
|
1048
|
+
let states = await gradleStatus(command, home);
|
|
1049
|
+
let current = hostRows();
|
|
1050
|
+
const listed = states instanceof Map ? [...states].filter(([, state]) => state !== "stopped") : [];
|
|
1051
|
+
if (distribution !== null && listed.length > 0 && listed.every(([pid, state]) => state === "idle" && entries.some((entry) => entry.pid === pid))) try {
|
|
1052
|
+
phase("daemons", `gradle --stop for Gradle ${command.version} in ${home}`);
|
|
1053
|
+
await getExecutor().runFileAsync(join(distribution, "bin", "gradle"), [
|
|
1054
|
+
"--stop",
|
|
1055
|
+
"-q",
|
|
1056
|
+
"-g",
|
|
1057
|
+
home
|
|
1058
|
+
], {
|
|
1059
|
+
cwd: home,
|
|
1060
|
+
env: { JAVA_HOME: javaHome },
|
|
1061
|
+
omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
|
|
1062
|
+
timeoutMs: GRADLE_STOP_TIMEOUT_MS
|
|
1063
|
+
});
|
|
1064
|
+
for (const entry of entries) {
|
|
1065
|
+
console.log(chalk.green(`Stopped ${processLabel(entry)} with gradle --stop, freeing ${formatBytes(entry.bytes)}`));
|
|
1066
|
+
recordGcResult("gradleDaemon", "done", processLabel(entry), {
|
|
1067
|
+
id: String(entry.pid),
|
|
1068
|
+
bytes: entry.bytes
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
continue;
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
console.log(chalk.dim(`gradle --stop failed for ${home}; signalling each idle daemon: ${error.message.split("\n")[0]}`));
|
|
1074
|
+
states = await gradleStatus(command, home);
|
|
1075
|
+
current = hostRows();
|
|
1076
|
+
}
|
|
1077
|
+
for (const entry of entries) {
|
|
1078
|
+
const state = states instanceof Map ? states.get(entry.pid) : void 0;
|
|
1079
|
+
if (!sameProcess(entry, current)) {
|
|
1080
|
+
keep(entry, "gradleDaemon", "the process changed or exited since the check");
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
if (state !== "idle") {
|
|
1084
|
+
keep(entry, "gradleDaemon", state ? `gradle --status now reports it ${state}` : "gradle --status no longer lists it idle");
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
const error = signal(entry.pid);
|
|
1088
|
+
if (error) {
|
|
1089
|
+
failures++;
|
|
1090
|
+
console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
|
|
1091
|
+
recordGcResult("gradleDaemon", "failed", processLabel(entry), {
|
|
1092
|
+
id: String(entry.pid),
|
|
1093
|
+
bytes: entry.bytes,
|
|
1094
|
+
detail: error
|
|
1095
|
+
});
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
|
|
1099
|
+
recordGcResult("gradleDaemon", "done", processLabel(entry), {
|
|
1100
|
+
id: String(entry.pid),
|
|
1101
|
+
bytes: entry.bytes
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return failures;
|
|
1106
|
+
}
|
|
1107
|
+
function stillPlanned(fresh, before, kind) {
|
|
1108
|
+
const planned = new Map(before.processes.filter((entry) => entry.kind === kind).map((entry) => [entry.pid, entry]));
|
|
1109
|
+
return fresh.processes.filter((entry) => entry.kind === kind && planned.get(entry.pid)?.startedAt === entry.startedAt);
|
|
1110
|
+
}
|
|
1111
|
+
function hostRows() {
|
|
1112
|
+
const rows = createDeviceProcessTables().host();
|
|
1113
|
+
return rows ? new Map(rows.map((row) => [row.pid, row])) : null;
|
|
1114
|
+
}
|
|
1115
|
+
const EXIT_WAIT_MS = 1e4;
|
|
1116
|
+
function alive(pid) {
|
|
1117
|
+
try {
|
|
1118
|
+
process.kill(pid, 0);
|
|
1119
|
+
return true;
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
return error.code !== "ESRCH";
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function waitForExit(pids) {
|
|
1125
|
+
const running = pids.filter(alive);
|
|
1126
|
+
if (running.length === 0) return;
|
|
1127
|
+
phase("daemons", `waiting up to ${EXIT_WAIT_MS / 1e3}s for ${plural(running.length, "Gradle daemon")} to exit: pid ${running.join(", ")}`);
|
|
1128
|
+
const deadline = Date.now() + EXIT_WAIT_MS;
|
|
1129
|
+
while (running.some(alive) && Date.now() < deadline) await new Promise((done) => setTimeout(done, 200));
|
|
1130
|
+
}
|
|
1131
|
+
/** Stops the Gradle daemons first, then re-checks the Kotlin daemons, whose clients those Gradle daemons were. */
|
|
1132
|
+
async function reclaimDaemons(before) {
|
|
1133
|
+
const scope = {
|
|
1134
|
+
watchman: false,
|
|
1135
|
+
gradle: true
|
|
1136
|
+
};
|
|
1137
|
+
const gradle = stillPlanned(await collectMemoryReport(scope, true), before, "gradleDaemon");
|
|
1138
|
+
for (const entry of gradle.filter((e) => !e.reclaimable)) keep(entry, "gradleDaemon", entry.detail ?? "it could not be proven idle");
|
|
1139
|
+
const stopping = gradle.filter((entry) => entry.reclaimable);
|
|
1140
|
+
let failures = await stopGradle(stopping, hostRows());
|
|
1141
|
+
if (!before.processes.some((entry) => entry.kind === "kotlinDaemon")) return failures;
|
|
1142
|
+
await waitForExit(stopping.map((entry) => entry.pid));
|
|
1143
|
+
const kotlin = stillPlanned(await collectMemoryReport(scope, true), before, "kotlinDaemon");
|
|
1144
|
+
for (const entry of before.processes.filter((e) => e.kind === "kotlinDaemon" && e.reclaimable)) {
|
|
1145
|
+
if (kotlin.some((current) => current.pid === entry.pid) || alive(entry.pid)) continue;
|
|
1146
|
+
console.log(chalk.green(`${processLabel(entry)} exited with its Gradle daemon, freeing ${formatBytes(entry.bytes)}`));
|
|
1147
|
+
recordGcResult("kotlinDaemon", "done", processLabel(entry), {
|
|
1148
|
+
id: String(entry.pid),
|
|
1149
|
+
bytes: entry.bytes
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
const rows = hostRows();
|
|
1153
|
+
for (const entry of kotlin) if (entry.reclaimable) failures += stopKotlin(entry, rows);
|
|
1154
|
+
else keep(entry, "kotlinDaemon", entry.detail ?? "it could not be proven idle");
|
|
1155
|
+
return failures;
|
|
1156
|
+
}
|
|
1157
|
+
/** Whether `gc --delete --cache <cache>` would act on a memory kind: stop a process or remove a watchman root. */
|
|
1158
|
+
function memoryWorkPending(cache, report) {
|
|
1159
|
+
if (!memoryCacheKind(cache) || !report) return false;
|
|
1160
|
+
return reclaimableBytes(report) > 0 || report.watchmanRoots.some((root) => root.removable);
|
|
1161
|
+
}
|
|
1162
|
+
/** Acts on the one memory kind `--cache` names: never on an unscoped run or `--cache all`. */
|
|
1163
|
+
async function reclaimMemory(cache, report) {
|
|
1164
|
+
const kind = memoryCacheKind(cache);
|
|
1165
|
+
if (!kind || !report || report.processes.length === 0) return 0;
|
|
1166
|
+
return kind === "watchman" ? reclaimWatchman(report) : reclaimDaemons(report);
|
|
1167
|
+
}
|
|
1168
|
+
/** A doctor note when watchman's footprint is over 2 GiB: only a restart shrinks it, and gc says when one is safe. */
|
|
1169
|
+
function watchmanFinding(report) {
|
|
1170
|
+
const daemon = report.processes.find((entry) => entry.kind === "watchman");
|
|
1171
|
+
if (!daemon || daemon.bytes < WATCHMAN_DOCTOR_BYTES) return null;
|
|
1172
|
+
const stale = report.watchmanRoots.filter((root) => root.stale !== null).length;
|
|
1173
|
+
const state = daemon.reclaimable ? "no client uses it now" : `it is kept: ${daemon.detail}`;
|
|
1174
|
+
return {
|
|
1175
|
+
level: "note",
|
|
1176
|
+
title: `watchman uses ${formatBytes(daemon.bytes)}`,
|
|
1177
|
+
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.`,
|
|
1178
|
+
fix: "Run `stim gc --cache watchman` to see its roots and clients, then `stim gc --delete --cache watchman` once no workspace uses it."
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async function inspectWatchmanMemory() {
|
|
1182
|
+
if (process.platform === "win32" || memorySweepIsScoped()) return null;
|
|
1183
|
+
return watchmanFinding(await collectMemoryReport({
|
|
1184
|
+
watchman: true,
|
|
1185
|
+
gradle: false
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
295
1189
|
//#region src/commands/gc/caches.ts
|
|
296
1190
|
const DAY_MS$2 = 864e5;
|
|
297
1191
|
function cacheSweepIsScoped() {
|
|
@@ -300,8 +1194,8 @@ function cacheSweepIsScoped() {
|
|
|
300
1194
|
function isInsideConfigDir(dir) {
|
|
301
1195
|
const root = canonicalPath(getConfigDir());
|
|
302
1196
|
const target = canonicalPath(dir);
|
|
303
|
-
const rel = relative(root, target);
|
|
304
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1197
|
+
const rel = relative$1(root, target);
|
|
1198
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
305
1199
|
}
|
|
306
1200
|
function machineGlobalReason(cache) {
|
|
307
1201
|
if (!cacheSweepIsScoped()) return null;
|
|
@@ -311,43 +1205,63 @@ function machineGlobalReason(cache) {
|
|
|
311
1205
|
}
|
|
312
1206
|
const EVERY_CACHE = "all";
|
|
313
1207
|
const WORKSPACE_OUTPUTS = "workspaces";
|
|
1208
|
+
const RECORDINGS = "recordings";
|
|
1209
|
+
const PARKED_DEVICES = "parked";
|
|
1210
|
+
function includesParkedDevices(name) {
|
|
1211
|
+
return name?.trim().toLowerCase() === PARKED_DEVICES;
|
|
1212
|
+
}
|
|
1213
|
+
function includesRecordings(name) {
|
|
1214
|
+
const wanted = name?.trim().toLowerCase();
|
|
1215
|
+
return !wanted || wanted === EVERY_CACHE || wanted === RECORDINGS;
|
|
1216
|
+
}
|
|
1217
|
+
function isWorkspacesScope(name) {
|
|
1218
|
+
return name?.trim().toLowerCase() === WORKSPACE_OUTPUTS;
|
|
1219
|
+
}
|
|
314
1220
|
function includesWorkspaceOutputs(name) {
|
|
315
1221
|
const wanted = name?.trim().toLowerCase();
|
|
316
1222
|
return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
|
|
317
1223
|
}
|
|
1224
|
+
/** Whether a --cache name selects something besides shared caches: recordings, parked devices or a memory kind. */
|
|
1225
|
+
function selectsBeyondCaches(name) {
|
|
1226
|
+
return includesRecordings(name) || includesParkedDevices(name) || memoryCacheKind(name) !== null;
|
|
1227
|
+
}
|
|
318
1228
|
function selectCaches(caches, name) {
|
|
319
1229
|
if (!name) return caches;
|
|
320
1230
|
const wanted = name.trim().toLowerCase();
|
|
321
1231
|
if (wanted === EVERY_CACHE) return caches;
|
|
322
|
-
if (wanted === WORKSPACE_OUTPUTS) return [];
|
|
1232
|
+
if (wanted === WORKSPACE_OUTPUTS || wanted === RECORDINGS || wanted === PARKED_DEVICES || memoryCacheKind(wanted)) return [];
|
|
323
1233
|
return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
|
|
324
1234
|
}
|
|
1235
|
+
function emptyVerdict(c) {
|
|
1236
|
+
if (c.prune === "report-only") return {
|
|
1237
|
+
willEmpty: false,
|
|
1238
|
+
emptySkipped: "report-only shared cache; Stim never deletes it"
|
|
1239
|
+
};
|
|
1240
|
+
if (c.machineGlobal) return {
|
|
1241
|
+
willEmpty: false,
|
|
1242
|
+
emptySkipped: c.machineGlobal
|
|
1243
|
+
};
|
|
1244
|
+
if (!ownsItsDirectory(c)) return {
|
|
1245
|
+
willEmpty: false,
|
|
1246
|
+
emptySkipped: `${c.dir} is not a directory this cache owns`
|
|
1247
|
+
};
|
|
1248
|
+
return {
|
|
1249
|
+
willEmpty: true,
|
|
1250
|
+
emptySkipped: null
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
325
1253
|
function planCacheEmptying(caches, all) {
|
|
326
1254
|
const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
});
|
|
333
|
-
if (c.machineGlobal) return Object.assign({}, c, {
|
|
334
|
-
willEmpty: false,
|
|
335
|
-
emptySkipped: c.machineGlobal
|
|
336
|
-
});
|
|
337
|
-
if (!ownsItsDirectory(c)) return Object.assign({}, c, {
|
|
338
|
-
willEmpty: false,
|
|
339
|
-
emptySkipped: `${c.dir} is not a directory this cache owns`
|
|
340
|
-
});
|
|
341
|
-
return Object.assign({}, c, {
|
|
342
|
-
willEmpty: true,
|
|
343
|
-
emptySkipped: null
|
|
344
|
-
});
|
|
345
|
-
});
|
|
1255
|
+
return all ? annotated.map((c) => Object.assign({}, c, emptyVerdict(c))) : annotated;
|
|
1256
|
+
}
|
|
1257
|
+
/** What `gc --delete --cache <name>` would do to each cache, for a report that does not run that scope. */
|
|
1258
|
+
function previewScopedEmptying(caches) {
|
|
1259
|
+
return planCacheEmptying(caches, false).map((c) => Object.assign({}, c, { scopedEmpty: emptyVerdict(c) }));
|
|
346
1260
|
}
|
|
347
1261
|
function ownsItsDirectory(cache) {
|
|
348
1262
|
if (Array.isArray(cache.files)) return false;
|
|
349
1263
|
const dir = canonicalPath(cache.dir);
|
|
350
|
-
if (dirname(dir) === dir) return false;
|
|
1264
|
+
if (dirname$1(dir) === dir) return false;
|
|
351
1265
|
return ![
|
|
352
1266
|
homedir(),
|
|
353
1267
|
tmpdir(),
|
|
@@ -377,7 +1291,7 @@ function emptyCache(cache) {
|
|
|
377
1291
|
let removed = 0;
|
|
378
1292
|
let failed = 0;
|
|
379
1293
|
for (const name of names) try {
|
|
380
|
-
rmSync$1(join(cache.dir, name), {
|
|
1294
|
+
rmSync$1(join$1(cache.dir, name), {
|
|
381
1295
|
recursive: true,
|
|
382
1296
|
force: true
|
|
383
1297
|
});
|
|
@@ -567,7 +1481,7 @@ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays,
|
|
|
567
1481
|
const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
|
|
568
1482
|
const stale = [];
|
|
569
1483
|
for (const [path, proj] of Object.entries(config?.projects || {})) {
|
|
570
|
-
if (dead.has(path) || !isAbsolute(path)) continue;
|
|
1484
|
+
if (dead.has(path) || !isAbsolute$1(path)) continue;
|
|
571
1485
|
const touched = lastTouched(path);
|
|
572
1486
|
if (!Number.isFinite(touched) || touched >= cutoff) continue;
|
|
573
1487
|
const idleDays = Math.floor((now - touched) / DAY_MS$1);
|
|
@@ -655,6 +1569,7 @@ function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } =
|
|
|
655
1569
|
model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
|
|
656
1570
|
runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
|
|
657
1571
|
parkedAt: record.parkedAt,
|
|
1572
|
+
app: record.bundleId ?? null,
|
|
658
1573
|
bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
|
|
659
1574
|
listed: simsChecked ? Boolean(sim) : null
|
|
660
1575
|
};
|
|
@@ -710,11 +1625,58 @@ function collectParkedAvds(deps, age = {}) {
|
|
|
710
1625
|
systemImage: record.systemImage,
|
|
711
1626
|
deviceProfile: poolConfigurationDeviceProfile(record.configuration),
|
|
712
1627
|
parkedAt: record.parkedAt,
|
|
1628
|
+
app: record.packageName ?? null,
|
|
713
1629
|
bytes,
|
|
714
1630
|
listed: avds === null ? null : avds.includes(record.name)
|
|
715
1631
|
};
|
|
716
1632
|
});
|
|
717
1633
|
}
|
|
1634
|
+
function eraseParkedDevices(sims, avds) {
|
|
1635
|
+
let failures = 0;
|
|
1636
|
+
const devices = [...sims.map((sim) => ({
|
|
1637
|
+
platform: "ios",
|
|
1638
|
+
id: sim.udid,
|
|
1639
|
+
name: sim.name,
|
|
1640
|
+
listed: sim.listed,
|
|
1641
|
+
bytes: sim.bytes
|
|
1642
|
+
})), ...avds.map((avd) => ({
|
|
1643
|
+
platform: "android",
|
|
1644
|
+
id: avd.name,
|
|
1645
|
+
name: avd.name,
|
|
1646
|
+
listed: avd.listed,
|
|
1647
|
+
bytes: avd.bytes
|
|
1648
|
+
}))];
|
|
1649
|
+
for (const device of devices) {
|
|
1650
|
+
const what = `parked ${device.platform === "ios" ? "ios sim" : "android emulator"} ${device.name}`;
|
|
1651
|
+
if (device.listed !== true) {
|
|
1652
|
+
const unverified = device.listed === null;
|
|
1653
|
+
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.";
|
|
1654
|
+
if (unverified) failures++;
|
|
1655
|
+
console.log((unverified ? chalk.red : chalk.dim)(`Did not erase ${what}: ${detail}`));
|
|
1656
|
+
recordGcResult("parkedDevice", unverified ? "failed" : "kept", device.name, {
|
|
1657
|
+
id: device.id,
|
|
1658
|
+
detail
|
|
1659
|
+
});
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
const result = device.platform === "ios" ? eraseParkedIosSim(device.id, { label: device.name }) : eraseParkedAvd(device.id);
|
|
1663
|
+
if (result.status === "torn-down") {
|
|
1664
|
+
console.log(chalk.green(`Erased ${what}; it stays parked`));
|
|
1665
|
+
recordGcResult("parkedDevice", "done", device.name, {
|
|
1666
|
+
id: device.id,
|
|
1667
|
+
bytes: device.bytes
|
|
1668
|
+
});
|
|
1669
|
+
} else if (result.status === "failed") {
|
|
1670
|
+
failures++;
|
|
1671
|
+
console.log(chalk.red(`Failed to erase ${what}: ${result.reason}`));
|
|
1672
|
+
recordGcResult("parkedDevice", "failed", device.name, {
|
|
1673
|
+
id: device.id,
|
|
1674
|
+
detail: result.reason
|
|
1675
|
+
});
|
|
1676
|
+
} else console.log(chalk.dim(`Skipped ${device.name}; it is no longer parked.`));
|
|
1677
|
+
}
|
|
1678
|
+
return failures;
|
|
1679
|
+
}
|
|
718
1680
|
function deleteParkedAvds(avds) {
|
|
719
1681
|
let failures = 0;
|
|
720
1682
|
for (const avd of avds) {
|
|
@@ -891,16 +1853,16 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
|
|
|
891
1853
|
//#endregion
|
|
892
1854
|
//#region src/commands/gc/workspaces.ts
|
|
893
1855
|
function workspacesRoot() {
|
|
894
|
-
return join(getConfigDir(), "workspaces");
|
|
1856
|
+
return join$1(getConfigDir(), "workspaces");
|
|
895
1857
|
}
|
|
896
1858
|
function isInsideWorkspaces(dir) {
|
|
897
|
-
const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
|
|
898
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1859
|
+
const rel = relative$1(canonicalPath(workspacesRoot()), canonicalPath(dir));
|
|
1860
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
899
1861
|
}
|
|
900
1862
|
function readRecordedRoot(dir, name) {
|
|
901
1863
|
let parsed;
|
|
902
1864
|
try {
|
|
903
|
-
parsed = readJsonFile(join(dir, "workspace.json"));
|
|
1865
|
+
parsed = readJsonFile(join$1(dir, "workspace.json"));
|
|
904
1866
|
} catch (error) {
|
|
905
1867
|
return {
|
|
906
1868
|
projectRoot: null,
|
|
@@ -908,7 +1870,7 @@ function readRecordedRoot(dir, name) {
|
|
|
908
1870
|
};
|
|
909
1871
|
}
|
|
910
1872
|
const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
|
|
911
|
-
if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
|
|
1873
|
+
if (typeof projectRoot !== "string" || !isAbsolute$1(projectRoot)) return {
|
|
912
1874
|
projectRoot: null,
|
|
913
1875
|
problem: "its workspace.json records no absolute project root"
|
|
914
1876
|
};
|
|
@@ -931,7 +1893,7 @@ function listWorkspaceDirs() {
|
|
|
931
1893
|
}
|
|
932
1894
|
const entries = [];
|
|
933
1895
|
for (const name of names) {
|
|
934
|
-
const dir = join(root, name);
|
|
1896
|
+
const dir = join$1(root, name);
|
|
935
1897
|
try {
|
|
936
1898
|
if (!statSync$1(dir).isDirectory()) continue;
|
|
937
1899
|
} catch {
|
|
@@ -999,7 +1961,7 @@ function rootPresence(path) {
|
|
|
999
1961
|
return code === "ENOENT" || code === "ENOTDIR" ? false : null;
|
|
1000
1962
|
}
|
|
1001
1963
|
}
|
|
1002
|
-
function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
|
|
1964
|
+
function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
|
|
1003
1965
|
const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
|
|
1004
1966
|
registryKeys,
|
|
1005
1967
|
exists: rootPresence,
|
|
@@ -1007,11 +1969,11 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
|
|
|
1007
1969
|
inUse: (root) => workspaceInUse(root)
|
|
1008
1970
|
});
|
|
1009
1971
|
return {
|
|
1010
|
-
orphaned: classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })),
|
|
1972
|
+
orphaned: measure ? classified.orphaned.map((entry) => Object.assign({}, entry, { bytes: sizeOf(entry.dir) })) : classified.orphaned,
|
|
1011
1973
|
skipped: classified.skipped
|
|
1012
1974
|
};
|
|
1013
1975
|
}
|
|
1014
|
-
const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift.";
|
|
1976
|
+
const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
|
|
1015
1977
|
const WORKSPACE_OUTPUT_DIRS = [
|
|
1016
1978
|
"derived-data",
|
|
1017
1979
|
"gradle-build",
|
|
@@ -1039,7 +2001,13 @@ function sizeOf(dir) {
|
|
|
1039
2001
|
return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
|
|
1040
2002
|
}
|
|
1041
2003
|
function outputPaths(dir) {
|
|
1042
|
-
|
|
2004
|
+
const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
|
|
2005
|
+
const macos = join$1(dir, "macos");
|
|
2006
|
+
try {
|
|
2007
|
+
if (!lstatSync$1(macos).isDirectory()) return paths;
|
|
2008
|
+
for (const entry of readdirSync$1(macos, { withFileTypes: true })) if (entry.name === "build" || entry.name.endsWith(".app") || entry.name.startsWith("staging-") && entry.isDirectory()) paths.push(join$1(macos, entry.name));
|
|
2009
|
+
} catch {}
|
|
2010
|
+
return paths;
|
|
1043
2011
|
}
|
|
1044
2012
|
function outputBytes(paths) {
|
|
1045
2013
|
let total = 0;
|
|
@@ -1093,7 +2061,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1093
2061
|
now
|
|
1094
2062
|
});
|
|
1095
2063
|
if (again?.keptReason) return again.keptReason;
|
|
1096
|
-
for (const
|
|
2064
|
+
for (const path of outputPaths(entry.dir)) rmSync$1(path, {
|
|
1097
2065
|
recursive: true,
|
|
1098
2066
|
force: true
|
|
1099
2067
|
});
|
|
@@ -1129,7 +2097,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1129
2097
|
};
|
|
1130
2098
|
}
|
|
1131
2099
|
function stillOrphaned({ dir, projectRoot }) {
|
|
1132
|
-
if (readRecordedRoot(dir, basename(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
|
|
2100
|
+
if (readRecordedRoot(dir, basename$1(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
|
|
1133
2101
|
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
|
|
1134
2102
|
}
|
|
1135
2103
|
async function deleteOrphanedWorkspaces(orphaned) {
|
|
@@ -1245,7 +2213,7 @@ function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemory
|
|
|
1245
2213
|
}
|
|
1246
2214
|
function existingAncestor(path) {
|
|
1247
2215
|
let current = path;
|
|
1248
|
-
while (!existsSync(current) && dirname
|
|
2216
|
+
while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
|
|
1249
2217
|
return current;
|
|
1250
2218
|
}
|
|
1251
2219
|
function readVolumeSpace(paths) {
|
|
@@ -1413,7 +2381,10 @@ async function reclaimIdleDevServers({ root, dryRun, note, now }) {
|
|
|
1413
2381
|
try {
|
|
1414
2382
|
run = await withIdleWorkspace(path, async () => {
|
|
1415
2383
|
try {
|
|
1416
|
-
return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path
|
|
2384
|
+
return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path, {
|
|
2385
|
+
by: "budget reclaim",
|
|
2386
|
+
workspace: root
|
|
2387
|
+
}) : "busy", {
|
|
1417
2388
|
external: true,
|
|
1418
2389
|
waitMs: 0,
|
|
1419
2390
|
ownerPurpose: "budget reclaim"
|
|
@@ -1542,14 +2513,14 @@ function spaceUsers() {
|
|
|
1542
2513
|
const candidates = [
|
|
1543
2514
|
["Stim state", getConfigDir()],
|
|
1544
2515
|
["shared build cache", sharedBuildCache()],
|
|
1545
|
-
...platform() === "darwin" ? [["simulators", join
|
|
1546
|
-
["Android emulators", join
|
|
2516
|
+
...platform() === "darwin" ? [["simulators", join(home, "Library", "Developer", "CoreSimulator", "Devices")], ["Xcode DerivedData", join(home, "Library", "Developer", "Xcode", "DerivedData")]] : [],
|
|
2517
|
+
["Android emulators", join(home, ".android", "avd")]
|
|
1547
2518
|
];
|
|
1548
2519
|
const measured = /* @__PURE__ */ new Set();
|
|
1549
2520
|
const users = [];
|
|
1550
2521
|
for (const [label, path] of candidates) {
|
|
1551
2522
|
const canonical = canonicalPath(path);
|
|
1552
|
-
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep
|
|
2523
|
+
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
|
|
1553
2524
|
measured.add(canonical);
|
|
1554
2525
|
if (!existsSync(path)) continue;
|
|
1555
2526
|
const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
|
|
@@ -1736,4 +2707,4 @@ function budgetLine(report) {
|
|
|
1736
2707
|
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"}`;
|
|
1737
2708
|
}
|
|
1738
2709
|
//#endregion
|
|
1739
|
-
export {
|
|
2710
|
+
export { discoverCaches as $, withAndroidAvdSizes as A, trimCaches as B, describeUnverifiableDevices as C, findStaleDeviceRecords as D, findOrphanedDevices as E, isWorkspacesScope as F, memoryCacheKind as G, WATCHMAN_KIND as H, planCacheEmptying as I, memorySweepIsScoped as J, memoryLines as K, previewScopedEmptying as L, includesParkedDevices as M, includesRecordings as N, findStaleProjectDevices as O, includesWorkspaceOutputs as P, phase as Q, selectCaches as R, describeParkedSims as S, eraseParkedDevices as T, collectMemoryReport as U, GRADLE_DAEMONS_KIND as V, inspectWatchmanMemory as W, reclaimMemory as X, memoryWorkPending as Y, scopedMemoryReport as Z, collectParkedAvds as _, REBUILD_COST as a, deleteParkedSims as b, collectOrphanedWorkspaces as c, isInsideWorkspaces as d, sizeCaches as et, listWorkspaceDirs as f, collectDeviceLeases as g, DEVICE_LIST_TIMEOUT_MS$1 as h, machineNumber as i, emptyCaches as j, unverifiedDeviceCommand as k, collectWorkspaceOutputs as l, sizeText as m, budgetLine as n, WORKSPACE_OUTPUT_DIRS as o, rootPresence as p, memoryScope as q, inspectBudget as r, clearWorkspaceOutputs as s, budgetGate as t, stopOwnedMetroForReset as tt, deleteOrphanedWorkspaces as u, collectParkedSims as v, deviceSweepIsScoped as w, deleteProjectDevices as x, deleteParkedAvds as y, selectsBeyondCaches as z };
|