stim 1.14.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-CYVDcrt3.mjs → activity-B2-kb7k0.mjs} +281 -41
- package/dist/{android-XpvCrV7q.mjs → android-CYmq0jNi.mjs} +2 -2
- package/dist/{android-LbPFRWVL.mjs → android-Cu_L4RpL.mjs} +502 -407
- package/dist/{android-d6rGq1pA.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-BpgSgkJj.mjs → app-install-CAAplXAt.mjs} +95 -161
- package/dist/{budget-CGDkhvv_.mjs → budget-CMiqghig.mjs} +1038 -73
- package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-aeGMFX7e.mjs} +193 -653
- package/dist/build-progress-C2Jv1AtA.mjs +872 -0
- package/dist/build-slots-Df1ApSuH.mjs +127 -0
- package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
- 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-DVcpw6eE.mjs → command-output-CliHHn7a.mjs} +1 -1
- package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
- package/dist/{created-devices-CllH_8eb.mjs → created-devices-BwVb3liH.mjs} +2 -2
- package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
- package/dist/{deps-D9IcKqXS.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-CjuuAJjQ.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-5c3WrVRm.mjs → device-lease-W5Pi1q38.mjs} +43 -36
- package/dist/{device-pool-BiyDyPP-.mjs → device-pool-B2r38YjS.mjs} +4 -4
- package/dist/{device-remote-D87zEzaq.mjs → device-remote-9ulPsdhY.mjs} +68 -162
- package/dist/{doctor-x9pnU9fq.mjs → doctor-BcE5xsBn.mjs} +191 -99
- package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
- package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
- package/dist/{gc-ko6mUAYD.mjs → gc-BuL1Genr.mjs} +429 -101
- package/dist/{android-cas-iZK3DOCr.mjs → gradle-CmdOBq1d.mjs} +8 -294
- package/dist/{guide-6R0VNFqI.mjs → guide-cCSGRFd3.mjs} +1959 -222
- package/dist/{guide-status-BZrFPIA_.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-BwDPrnYQ.mjs → ios-3CtCRhwr.mjs} +201 -10
- package/dist/{ios-P1f5nm2n.mjs → ios-Chob8ny7.mjs} +512 -263
- package/dist/{ios-device-DGglU3E_.mjs → ios-device-DERmMmk-.mjs} +12 -7
- package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
- package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-CLlofxRt.mjs} +4 -4
- package/dist/{logs-Civ4gbBH.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-CcEo8Fl8.mjs → metro-Di8EN2Tb.mjs} +20 -120
- package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Dj_XBNaL.mjs} +6 -59
- package/dist/native-run-DFDoIRbY.mjs +147 -0
- package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-i6OkVg-w.mjs} +9 -5
- package/dist/offload-worker.d.mts +121 -0
- package/dist/offload-worker.mjs +601 -0
- package/dist/{ownership-DyYDF5ww.mjs → ownership-BkYxUtuj.mjs} +3 -2
- package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
- package/dist/{ports-BguS83ex.mjs → ports-DddwttKM.mjs} +3 -3
- package/dist/prebuild-BvxDlD6C.mjs +213 -0
- package/dist/process-identity-BmRm1qV0.mjs +12 -0
- package/dist/{project-CWlvEaeN.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-BiniwsSZ.mjs → reload-ChPiwvot.mjs} +70 -22
- package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-CO2elm_b.mjs} +4 -4
- package/dist/run-B-xqRrsg.mjs +119 -0
- package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-X7PUgizL.mjs} +33 -15
- package/dist/{server-expo-BVuwQ65v.mjs → server-expo-DhtX6bom.mjs} +6 -10
- package/dist/server-expo-KJeD109I.mjs +2 -0
- package/dist/{settings-D61iNOsB.mjs → settings-CduVcoxj.mjs} +77 -26
- package/dist/{settings-B-fsC-Om.mjs → settings-DbL6uhH8.mjs} +16 -10
- package/dist/settings.schema.json +357 -2
- package/dist/{simslim-DrDN4WuV.mjs → simslim-Ciluxlq4.mjs} +8 -132
- package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-BY0BrOLh.mjs} +6 -6
- package/dist/{spawn-claims-Drw_o-Fb.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-s4RXp1wy.mjs → start-CUD9nasA.mjs} +108 -61
- package/dist/state-B4PQFXMW.d.mts +12 -0
- package/dist/state-BxCgeTQu.mjs +41 -0
- package/dist/{state-DHqvsbqC.mjs → state-CaeaHbDA.mjs} +7 -4
- package/dist/{state-Dk9RVXI7.mjs → state-DFm77wRn.mjs} +67 -5
- package/dist/stats-DBLLFmKd.mjs +66 -0
- package/dist/{status-BsPAnfOI.mjs → status-DYXxNOT0.mjs} +423 -268
- package/dist/{status-BgAuxESg.mjs → status-l84Gbvp2.mjs} +116 -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-BzreOWJp.mjs → stop-Bad61z_2.mjs} +38 -12
- package/dist/stop-Bvf1MflS.mjs +48 -0
- package/dist/{stop-BWIF_6BK.mjs → stop-CceYh1yG.mjs} +2 -2
- package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
- package/dist/supervisor-run.d.mts +43 -11
- package/dist/supervisor-run.mjs +227 -19
- package/dist/support-CDQbDkL5.mjs +315 -0
- package/dist/support-WLOzVKUv.mjs +176 -0
- package/dist/teardown-BYuQafiA.mjs +718 -0
- package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DmeYjXD9.mjs} +363 -13
- package/dist/warm-progress-DVHLN4uC.mjs +463 -0
- package/dist/{idle-DXtx3RbX.mjs → watchman-Bx0wceoF.mjs} +55 -8
- package/dist/{web-BsQs1WlO.mjs → web-k6w8-HnE.mjs} +37 -20
- package/dist/web-run.mjs +309 -12
- 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-BVrHsYZG.mjs → worktree-C0c5Udwa.mjs} +55 -54
- package/dist/{worktree-UrQWXSeb.mjs → worktree-qXKzADSC.mjs} +154 -252
- package/package.json +10 -5
- package/shim/bundle-response.cjs +28 -12
- package/dist/build-progress-DxKpwnaS.mjs +0 -247
- package/dist/in-use-DlqBDKqY.mjs +0 -249
- package/dist/server-expo-DIHDhvGg.mjs +0 -2
- package/dist/start-Cetfvy8l.mjs +0 -2
- package/dist/stats-CFpFGlJV.mjs +0 -62
- package/dist/stats-CX1Z8Tca.mjs +0 -368
- package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
- package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
- package/dist/worktree-CHWWvm5v.mjs +0 -2
- package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
|
@@ -1,25 +1,31 @@
|
|
|
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
|
|
@@ -71,10 +77,10 @@ async function stopOwnedMetroForReset(root) {
|
|
|
71
77
|
//#endregion
|
|
72
78
|
//#region src/cache/caches.ts
|
|
73
79
|
function xcodeDerivedDataRoot() {
|
|
74
|
-
return join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
80
|
+
return join$1(homedir(), "Library", "Developer", "Xcode", "DerivedData");
|
|
75
81
|
}
|
|
76
82
|
function compilationCache() {
|
|
77
|
-
const dir = join(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
83
|
+
const dir = join$1(xcodeDerivedDataRoot(), "CompilationCache.noindex");
|
|
78
84
|
if (!existsSync$1(dir)) return null;
|
|
79
85
|
return {
|
|
80
86
|
name: "Xcode compilation cache",
|
|
@@ -84,8 +90,8 @@ function compilationCache() {
|
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
function gradleBuildCache() {
|
|
87
|
-
const gradleHome = process.env.GRADLE_USER_HOME || join(homedir(), ".gradle");
|
|
88
|
-
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");
|
|
89
95
|
if (!existsSync$1(dir)) return null;
|
|
90
96
|
return {
|
|
91
97
|
name: "Gradle build cache",
|
|
@@ -107,9 +113,9 @@ function metroFileMaps() {
|
|
|
107
113
|
const files = [];
|
|
108
114
|
let bytes = 0;
|
|
109
115
|
for (const n of names) try {
|
|
110
|
-
const st = statSync$1(join(root, n));
|
|
116
|
+
const st = statSync$1(join$1(root, n));
|
|
111
117
|
if (uid !== void 0 && st.uid !== uid) continue;
|
|
112
|
-
files.push(join(root, n));
|
|
118
|
+
files.push(join$1(root, n));
|
|
113
119
|
bytes += st.size;
|
|
114
120
|
} catch {}
|
|
115
121
|
if (!files.length) return null;
|
|
@@ -136,7 +142,15 @@ function discoverCaches() {
|
|
|
136
142
|
gradle,
|
|
137
143
|
metroFileMaps()
|
|
138
144
|
].filter((c) => Boolean(c)).map((c) => Object.assign({}, c, { source: "detected" }));
|
|
139
|
-
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}` }));
|
|
140
154
|
}
|
|
141
155
|
function suppressLegacyMetroAncestors(caches) {
|
|
142
156
|
const current = caches.filter(isCurrentMetroStore);
|
|
@@ -168,7 +182,7 @@ function isMetroStore(cache) {
|
|
|
168
182
|
function isNamedMetroChild(parent, candidate) {
|
|
169
183
|
const dir = canonicalCacheDir(parent);
|
|
170
184
|
const child = canonicalCacheDir(candidate);
|
|
171
|
-
return dirname(child) === dir || canonicalCacheDir(dirname(candidate)) === dir;
|
|
185
|
+
return dirname$1(child) === dir || canonicalCacheDir(dirname$1(candidate)) === dir;
|
|
172
186
|
}
|
|
173
187
|
function mergeCacheDescriptors(caches, gradleDir) {
|
|
174
188
|
const merged = [];
|
|
@@ -219,14 +233,14 @@ function cachePathsOverlap(left, right) {
|
|
|
219
233
|
return cachePathContains(left, right) || cachePathContains(right, left);
|
|
220
234
|
}
|
|
221
235
|
function cachePathContains(parent, child) {
|
|
222
|
-
const rel = relative(parent, child);
|
|
223
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
236
|
+
const rel = relative$1(parent, child);
|
|
237
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
224
238
|
}
|
|
225
239
|
function canonicalCacheDir(dir) {
|
|
226
240
|
try {
|
|
227
241
|
return realpathSync$1(dir);
|
|
228
242
|
} catch {
|
|
229
|
-
return resolve(dir);
|
|
243
|
+
return resolve$1(dir);
|
|
230
244
|
}
|
|
231
245
|
}
|
|
232
246
|
function sizeCaches(caches) {
|
|
@@ -288,13 +302,890 @@ function entriesAtDepth(dir, depth) {
|
|
|
288
302
|
} catch {
|
|
289
303
|
continue;
|
|
290
304
|
}
|
|
291
|
-
for (const name of names) next.push(join(parent, name));
|
|
305
|
+
for (const name of names) next.push(join$1(parent, name));
|
|
292
306
|
}
|
|
293
307
|
level = next;
|
|
294
308
|
}
|
|
295
309
|
return level;
|
|
296
310
|
}
|
|
297
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
|
|
298
1189
|
//#region src/commands/gc/caches.ts
|
|
299
1190
|
const DAY_MS$2 = 864e5;
|
|
300
1191
|
function cacheSweepIsScoped() {
|
|
@@ -303,8 +1194,8 @@ function cacheSweepIsScoped() {
|
|
|
303
1194
|
function isInsideConfigDir(dir) {
|
|
304
1195
|
const root = canonicalPath(getConfigDir());
|
|
305
1196
|
const target = canonicalPath(dir);
|
|
306
|
-
const rel = relative(root, target);
|
|
307
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1197
|
+
const rel = relative$1(root, target);
|
|
1198
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
308
1199
|
}
|
|
309
1200
|
function machineGlobalReason(cache) {
|
|
310
1201
|
if (!cacheSweepIsScoped()) return null;
|
|
@@ -314,43 +1205,63 @@ function machineGlobalReason(cache) {
|
|
|
314
1205
|
}
|
|
315
1206
|
const EVERY_CACHE = "all";
|
|
316
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
|
+
}
|
|
317
1220
|
function includesWorkspaceOutputs(name) {
|
|
318
1221
|
const wanted = name?.trim().toLowerCase();
|
|
319
1222
|
return !wanted || wanted === EVERY_CACHE || wanted === WORKSPACE_OUTPUTS;
|
|
320
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
|
+
}
|
|
321
1228
|
function selectCaches(caches, name) {
|
|
322
1229
|
if (!name) return caches;
|
|
323
1230
|
const wanted = name.trim().toLowerCase();
|
|
324
1231
|
if (wanted === EVERY_CACHE) return caches;
|
|
325
|
-
if (wanted === WORKSPACE_OUTPUTS) return [];
|
|
1232
|
+
if (wanted === WORKSPACE_OUTPUTS || wanted === RECORDINGS || wanted === PARKED_DEVICES || memoryCacheKind(wanted)) return [];
|
|
326
1233
|
return caches.filter((c) => c.name.toLowerCase().includes(wanted) || c.dir.toLowerCase().includes(wanted));
|
|
327
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
|
+
}
|
|
328
1253
|
function planCacheEmptying(caches, all) {
|
|
329
1254
|
const annotated = caches.map((c) => Object.assign({}, c, { machineGlobal: machineGlobalReason(c) }));
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
if (c.machineGlobal) return Object.assign({}, c, {
|
|
337
|
-
willEmpty: false,
|
|
338
|
-
emptySkipped: c.machineGlobal
|
|
339
|
-
});
|
|
340
|
-
if (!ownsItsDirectory(c)) return Object.assign({}, c, {
|
|
341
|
-
willEmpty: false,
|
|
342
|
-
emptySkipped: `${c.dir} is not a directory this cache owns`
|
|
343
|
-
});
|
|
344
|
-
return Object.assign({}, c, {
|
|
345
|
-
willEmpty: true,
|
|
346
|
-
emptySkipped: null
|
|
347
|
-
});
|
|
348
|
-
});
|
|
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) }));
|
|
349
1260
|
}
|
|
350
1261
|
function ownsItsDirectory(cache) {
|
|
351
1262
|
if (Array.isArray(cache.files)) return false;
|
|
352
1263
|
const dir = canonicalPath(cache.dir);
|
|
353
|
-
if (dirname(dir) === dir) return false;
|
|
1264
|
+
if (dirname$1(dir) === dir) return false;
|
|
354
1265
|
return ![
|
|
355
1266
|
homedir(),
|
|
356
1267
|
tmpdir(),
|
|
@@ -380,7 +1291,7 @@ function emptyCache(cache) {
|
|
|
380
1291
|
let removed = 0;
|
|
381
1292
|
let failed = 0;
|
|
382
1293
|
for (const name of names) try {
|
|
383
|
-
rmSync$1(join(cache.dir, name), {
|
|
1294
|
+
rmSync$1(join$1(cache.dir, name), {
|
|
384
1295
|
recursive: true,
|
|
385
1296
|
force: true
|
|
386
1297
|
});
|
|
@@ -570,7 +1481,7 @@ function findStaleProjectDevices({ config, sims = [], avds = [], olderThanDays,
|
|
|
570
1481
|
const liveAvds = new Set(avds.filter((a) => typeof a === "string" && a.startsWith("stim-")));
|
|
571
1482
|
const stale = [];
|
|
572
1483
|
for (const [path, proj] of Object.entries(config?.projects || {})) {
|
|
573
|
-
if (dead.has(path) || !isAbsolute(path)) continue;
|
|
1484
|
+
if (dead.has(path) || !isAbsolute$1(path)) continue;
|
|
574
1485
|
const touched = lastTouched(path);
|
|
575
1486
|
if (!Number.isFinite(touched) || touched >= cutoff) continue;
|
|
576
1487
|
const idleDays = Math.floor((now - touched) / DAY_MS$1);
|
|
@@ -658,6 +1569,7 @@ function describeParkedSims(records, sims, deviceTypes, { simsChecked = true } =
|
|
|
658
1569
|
model: deviceTypes.find((d) => d.identifier === record.deviceTypeIdentifier)?.name ?? null,
|
|
659
1570
|
runtime: sim ? parseRuntimeVersion(sim.runtime) : parseRuntimeVersion(record.runtimeIdentifier),
|
|
660
1571
|
parkedAt: record.parkedAt,
|
|
1572
|
+
app: record.bundleId ?? null,
|
|
661
1573
|
bytes: typeof sim?.dataPathSize === "number" ? sim.dataPathSize : null,
|
|
662
1574
|
listed: simsChecked ? Boolean(sim) : null
|
|
663
1575
|
};
|
|
@@ -713,11 +1625,58 @@ function collectParkedAvds(deps, age = {}) {
|
|
|
713
1625
|
systemImage: record.systemImage,
|
|
714
1626
|
deviceProfile: poolConfigurationDeviceProfile(record.configuration),
|
|
715
1627
|
parkedAt: record.parkedAt,
|
|
1628
|
+
app: record.packageName ?? null,
|
|
716
1629
|
bytes,
|
|
717
1630
|
listed: avds === null ? null : avds.includes(record.name)
|
|
718
1631
|
};
|
|
719
1632
|
});
|
|
720
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
|
+
}
|
|
721
1680
|
function deleteParkedAvds(avds) {
|
|
722
1681
|
let failures = 0;
|
|
723
1682
|
for (const avd of avds) {
|
|
@@ -894,16 +1853,16 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
|
|
|
894
1853
|
//#endregion
|
|
895
1854
|
//#region src/commands/gc/workspaces.ts
|
|
896
1855
|
function workspacesRoot() {
|
|
897
|
-
return join(getConfigDir(), "workspaces");
|
|
1856
|
+
return join$1(getConfigDir(), "workspaces");
|
|
898
1857
|
}
|
|
899
1858
|
function isInsideWorkspaces(dir) {
|
|
900
|
-
const rel = relative(canonicalPath(workspacesRoot()), canonicalPath(dir));
|
|
901
|
-
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1859
|
+
const rel = relative$1(canonicalPath(workspacesRoot()), canonicalPath(dir));
|
|
1860
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute$1(rel);
|
|
902
1861
|
}
|
|
903
1862
|
function readRecordedRoot(dir, name) {
|
|
904
1863
|
let parsed;
|
|
905
1864
|
try {
|
|
906
|
-
parsed = readJsonFile(join(dir, "workspace.json"));
|
|
1865
|
+
parsed = readJsonFile(join$1(dir, "workspace.json"));
|
|
907
1866
|
} catch (error) {
|
|
908
1867
|
return {
|
|
909
1868
|
projectRoot: null,
|
|
@@ -911,7 +1870,7 @@ function readRecordedRoot(dir, name) {
|
|
|
911
1870
|
};
|
|
912
1871
|
}
|
|
913
1872
|
const projectRoot = isJsonObject(parsed) ? parsed.projectRoot : void 0;
|
|
914
|
-
if (typeof projectRoot !== "string" || !isAbsolute(projectRoot)) return {
|
|
1873
|
+
if (typeof projectRoot !== "string" || !isAbsolute$1(projectRoot)) return {
|
|
915
1874
|
projectRoot: null,
|
|
916
1875
|
problem: "its workspace.json records no absolute project root"
|
|
917
1876
|
};
|
|
@@ -934,7 +1893,7 @@ function listWorkspaceDirs() {
|
|
|
934
1893
|
}
|
|
935
1894
|
const entries = [];
|
|
936
1895
|
for (const name of names) {
|
|
937
|
-
const dir = join(root, name);
|
|
1896
|
+
const dir = join$1(root, name);
|
|
938
1897
|
try {
|
|
939
1898
|
if (!statSync$1(dir).isDirectory()) continue;
|
|
940
1899
|
} catch {
|
|
@@ -1002,7 +1961,7 @@ function rootPresence(path) {
|
|
|
1002
1961
|
return code === "ENOENT" || code === "ENOTDIR" ? false : null;
|
|
1003
1962
|
}
|
|
1004
1963
|
}
|
|
1005
|
-
function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
|
|
1964
|
+
function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure = true } = {}) {
|
|
1006
1965
|
const classified = classifyWorkspaceDirs(listWorkspaceDirs(), {
|
|
1007
1966
|
registryKeys,
|
|
1008
1967
|
exists: rootPresence,
|
|
@@ -1010,11 +1969,11 @@ function collectOrphanedWorkspaces(registryKeys, mountedVolumes) {
|
|
|
1010
1969
|
inUse: (root) => workspaceInUse(root)
|
|
1011
1970
|
});
|
|
1012
1971
|
return {
|
|
1013
|
-
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,
|
|
1014
1973
|
skipped: classified.skipped
|
|
1015
1974
|
};
|
|
1016
1975
|
}
|
|
1017
|
-
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.";
|
|
1018
1977
|
const WORKSPACE_OUTPUT_DIRS = [
|
|
1019
1978
|
"derived-data",
|
|
1020
1979
|
"gradle-build",
|
|
@@ -1042,7 +2001,13 @@ function sizeOf(dir) {
|
|
|
1042
2001
|
return measuredDirectorySize(dir, { timeoutMs: SIZE_TIMEOUT_MS });
|
|
1043
2002
|
}
|
|
1044
2003
|
function outputPaths(dir) {
|
|
1045
|
-
|
|
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;
|
|
1046
2011
|
}
|
|
1047
2012
|
function outputBytes(paths) {
|
|
1048
2013
|
let total = 0;
|
|
@@ -1096,7 +2061,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1096
2061
|
now
|
|
1097
2062
|
});
|
|
1098
2063
|
if (again?.keptReason) return again.keptReason;
|
|
1099
|
-
for (const
|
|
2064
|
+
for (const path of outputPaths(entry.dir)) rmSync$1(path, {
|
|
1100
2065
|
recursive: true,
|
|
1101
2066
|
force: true
|
|
1102
2067
|
});
|
|
@@ -1132,7 +2097,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now() }) {
|
|
|
1132
2097
|
};
|
|
1133
2098
|
}
|
|
1134
2099
|
function stillOrphaned({ dir, projectRoot }) {
|
|
1135
|
-
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;
|
|
1136
2101
|
return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
|
|
1137
2102
|
}
|
|
1138
2103
|
async function deleteOrphanedWorkspaces(orphaned) {
|
|
@@ -1248,7 +2213,7 @@ function resolveBudget({ config = loadConfig$1(), env = process.env, totalMemory
|
|
|
1248
2213
|
}
|
|
1249
2214
|
function existingAncestor(path) {
|
|
1250
2215
|
let current = path;
|
|
1251
|
-
while (!existsSync(current) && dirname
|
|
2216
|
+
while (!existsSync(current) && dirname(current) !== current) current = dirname(current);
|
|
1252
2217
|
return current;
|
|
1253
2218
|
}
|
|
1254
2219
|
function readVolumeSpace(paths) {
|
|
@@ -1548,14 +2513,14 @@ function spaceUsers() {
|
|
|
1548
2513
|
const candidates = [
|
|
1549
2514
|
["Stim state", getConfigDir()],
|
|
1550
2515
|
["shared build cache", sharedBuildCache()],
|
|
1551
|
-
...platform() === "darwin" ? [["simulators", join
|
|
1552
|
-
["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")]
|
|
1553
2518
|
];
|
|
1554
2519
|
const measured = /* @__PURE__ */ new Set();
|
|
1555
2520
|
const users = [];
|
|
1556
2521
|
for (const [label, path] of candidates) {
|
|
1557
2522
|
const canonical = canonicalPath(path);
|
|
1558
|
-
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep
|
|
2523
|
+
if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
|
|
1559
2524
|
measured.add(canonical);
|
|
1560
2525
|
if (!existsSync(path)) continue;
|
|
1561
2526
|
const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
|
|
@@ -1742,4 +2707,4 @@ function budgetLine(report) {
|
|
|
1742
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"}`;
|
|
1743
2708
|
}
|
|
1744
2709
|
//#endregion
|
|
1745
|
-
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 };
|