disk-space-saviour 0.17.0 → 0.18.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/CHANGELOG.md +12 -0
- package/README.md +27 -13
- package/bin/dss.js +5 -1
- package/package.json +1 -1
- package/src/clean.js +26 -18
- package/src/cli.js +1 -1
- package/src/docker/scan.js +1 -0
- package/src/emergency.js +4 -0
- package/src/env/local.js +53 -29
- package/src/env/shell.js +0 -31
- package/src/git.js +101 -39
- package/src/index.d.ts +1 -1
- package/src/liveness.js +131 -9
- package/src/report.js +51 -0
- package/src/rules/ecosystems.js +5 -4
- package/src/rules/other.js +4 -1
- package/src/rules/versions.js +23 -0
- package/src/scan.js +24 -5
- package/src/scanners/global.js +32 -14
- package/src/scanners/projects.js +67 -111
- package/src/scanners/versions.js +102 -3
- package/src/timings.js +54 -0
package/src/liveness.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - a process holds a file, working directory, or executable inside the path
|
|
9
9
|
* (`/proc/<pid>/{fd,cwd,exe}` on Linux, `lsof` on macOS).
|
|
10
10
|
* - a command argument names a path inside the item (including scripts
|
|
11
|
-
* whose interpreter has already closed its input descriptor)
|
|
11
|
+
* whose interpreter has already closed its input descriptor);
|
|
12
|
+
* - a directory inside the item is on a process's `PATH`, so the process
|
|
13
|
+
* may start a program from it at any time.
|
|
12
14
|
*
|
|
13
15
|
* The cleaner refreshes the probe right before acting on each item.
|
|
14
16
|
*/
|
|
@@ -20,6 +22,44 @@ import { formatDuration } from './units.js';
|
|
|
20
22
|
|
|
21
23
|
const PROC_COMM_LIMIT = 15;
|
|
22
24
|
|
|
25
|
+
/** Bun subcommands that read or write the install cache. */
|
|
26
|
+
const BUN_PACKAGE_COMMANDS = new Set([
|
|
27
|
+
'install',
|
|
28
|
+
'i',
|
|
29
|
+
'add',
|
|
30
|
+
'a',
|
|
31
|
+
'remove',
|
|
32
|
+
'rm',
|
|
33
|
+
'update',
|
|
34
|
+
'upgrade',
|
|
35
|
+
'outdated',
|
|
36
|
+
'link',
|
|
37
|
+
'unlink',
|
|
38
|
+
'pm',
|
|
39
|
+
'x',
|
|
40
|
+
'create',
|
|
41
|
+
'c',
|
|
42
|
+
'init',
|
|
43
|
+
'patch',
|
|
44
|
+
'publish',
|
|
45
|
+
'audit',
|
|
46
|
+
'why',
|
|
47
|
+
'info',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function isBun(proc) {
|
|
51
|
+
return Boolean(matchingName(proc, ['bun', 'bunx']));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bunPackageCommand(proc) {
|
|
55
|
+
const args = processArgs(proc);
|
|
56
|
+
if (path.posix.basename(args[0] ?? '') === 'bunx' || proc.name === 'bunx') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const command = args.slice(1).find((arg) => !arg.startsWith('-'));
|
|
60
|
+
return BUN_PACKAGE_COMMANDS.has(command);
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
function uncertaintyRoots(proc) {
|
|
24
64
|
const roots = processArgs(proc).flatMap((arg) => {
|
|
25
65
|
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : arg;
|
|
@@ -182,6 +222,7 @@ export class LivenessProbe {
|
|
|
182
222
|
const processes = await this.env.processes().catch(() => null);
|
|
183
223
|
this.listed = processes !== null;
|
|
184
224
|
this.processes = processes ?? [];
|
|
225
|
+
this.bunCacheUsers = await this.#bunCacheUsers();
|
|
185
226
|
this.probeError =
|
|
186
227
|
(processes === null || this.openPaths === null) &&
|
|
187
228
|
(this.env.platform === 'linux' || this.env.platform === 'darwin');
|
|
@@ -191,6 +232,49 @@ export class LivenessProbe {
|
|
|
191
232
|
this.pathOwners.clear();
|
|
192
233
|
}
|
|
193
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Pids of Bun processes that may use the install cache: package commands,
|
|
237
|
+
* and anything else (scripts, tests, the REPL) without a `node_modules`
|
|
238
|
+
* directory above its working directory, because Bun then auto-installs
|
|
239
|
+
* from the cache. A long-running script of an installed project does not
|
|
240
|
+
* read the cache, so it does not keep the cache busy.
|
|
241
|
+
*/
|
|
242
|
+
async #bunCacheUsers() {
|
|
243
|
+
const users = new Set();
|
|
244
|
+
const installed = new Map();
|
|
245
|
+
const hasModules = async (dir) => {
|
|
246
|
+
if (!installed.has(dir)) {
|
|
247
|
+
installed.set(
|
|
248
|
+
dir,
|
|
249
|
+
Boolean(
|
|
250
|
+
await this.env.exists?.(this.env.path.join(dir, 'node_modules'))
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return installed.get(dir);
|
|
255
|
+
};
|
|
256
|
+
for (const proc of this.processes) {
|
|
257
|
+
if (!isBun(proc)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (bunPackageCommand(proc) || !proc.cwd || !this.env.exists) {
|
|
261
|
+
users.add(proc.pid);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
let found = false;
|
|
265
|
+
for (const dir of [proc.cwd, ...ancestors(proc.cwd, this.env.path)]) {
|
|
266
|
+
if (await hasModules(dir)) {
|
|
267
|
+
found = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!found) {
|
|
272
|
+
users.add(proc.pid);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return users;
|
|
276
|
+
}
|
|
277
|
+
|
|
194
278
|
/** Processes of the last refresh, or null when they could not be listed. */
|
|
195
279
|
processList() {
|
|
196
280
|
return this.listed ? this.processes : null;
|
|
@@ -269,6 +353,13 @@ export class LivenessProbe {
|
|
|
269
353
|
) {
|
|
270
354
|
continue;
|
|
271
355
|
}
|
|
356
|
+
if (
|
|
357
|
+
item.rule === 'bun-cache' &&
|
|
358
|
+
isBun(proc) &&
|
|
359
|
+
!this.bunCacheUsers?.has(proc.pid)
|
|
360
|
+
) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
272
363
|
const name = matchingName(proc, names);
|
|
273
364
|
if (!name) {
|
|
274
365
|
continue;
|
|
@@ -291,10 +382,7 @@ export class LivenessProbe {
|
|
|
291
382
|
return null;
|
|
292
383
|
}
|
|
293
384
|
const pathApi = this.env.path;
|
|
294
|
-
const targets =
|
|
295
|
-
const real = this.realPaths.get(target);
|
|
296
|
-
return real && real !== target ? [target, real] : [target];
|
|
297
|
-
});
|
|
385
|
+
const targets = this.withRealPaths(paths);
|
|
298
386
|
for (const open of this.openPaths) {
|
|
299
387
|
for (const target of targets) {
|
|
300
388
|
if (isWithin(open, target, pathApi)) {
|
|
@@ -340,6 +428,26 @@ export class LivenessProbe {
|
|
|
340
428
|
return null;
|
|
341
429
|
}
|
|
342
430
|
|
|
431
|
+
/** `paths` plus their resolved forms, as processes report paths. */
|
|
432
|
+
withRealPaths(paths) {
|
|
433
|
+
return paths.flatMap((target) => {
|
|
434
|
+
const real = this.realPaths.get(target);
|
|
435
|
+
return real && real !== target ? [target, real] : [target];
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
searchPathUsage(paths) {
|
|
440
|
+
const targets = this.withRealPaths(paths);
|
|
441
|
+
for (const proc of this.processes) {
|
|
442
|
+
for (const dir of proc.searchPath ?? []) {
|
|
443
|
+
if (targets.some((target) => isWithin(dir, target, this.env.path))) {
|
|
444
|
+
return `on the PATH of ${proc.name || 'a process'} (pid ${proc.pid}): ${dir}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
343
451
|
uncertainUsage(paths) {
|
|
344
452
|
for (const proc of this.env.unreadableProcesses ?? []) {
|
|
345
453
|
const args = processArgs(proc);
|
|
@@ -399,14 +507,28 @@ export class LivenessProbe {
|
|
|
399
507
|
const paths = checks.perPath ? [] : (item.paths ?? []);
|
|
400
508
|
return (
|
|
401
509
|
this.installationBlocker(item.paths ?? []) ??
|
|
402
|
-
|
|
403
|
-
? `cannot verify process and open-file activity${this.env.probeDiagnostic ? `: ${this.env.probeDiagnostic}` : ''}`
|
|
404
|
-
: null) ??
|
|
510
|
+
this.probeBlocker() ??
|
|
405
511
|
(checks.mtime === false ? null : this.recentWrite(item.newestMtimeMs)) ??
|
|
406
512
|
this.runningTool(checks.busy, checks.cwd, item) ??
|
|
513
|
+
this.processUsage(paths) ??
|
|
514
|
+
this.uncertainUsage(item.paths ?? [])
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
probeBlocker() {
|
|
519
|
+
if (!this.probeError) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const diagnostic = this.env.probeDiagnostic;
|
|
523
|
+
return `cannot verify process and open-file activity${diagnostic ? `: ${diagnostic}` : ''}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Open files, command arguments and PATH entries inside the paths. */
|
|
527
|
+
processUsage(paths) {
|
|
528
|
+
return (
|
|
407
529
|
this.openInside(paths) ??
|
|
408
530
|
this.commandUsage(paths) ??
|
|
409
|
-
this.
|
|
531
|
+
this.searchPathUsage(paths)
|
|
410
532
|
);
|
|
411
533
|
}
|
|
412
534
|
|
package/src/report.js
CHANGED
|
@@ -305,6 +305,52 @@ function errorsSection(errors) {
|
|
|
305
305
|
];
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
const TIMING_LIMIT = 5;
|
|
309
|
+
|
|
310
|
+
function phaseList(phases) {
|
|
311
|
+
return phases
|
|
312
|
+
.map(
|
|
313
|
+
(phase) =>
|
|
314
|
+
`${phase.phase} ${(phase.ms / 1000).toFixed(1)}s${phase.count > 1 ? ` ×${phase.count}` : ''}`
|
|
315
|
+
)
|
|
316
|
+
.join(', ');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Slowest cleanup phases and items. `audit.timings` is a live PhaseTimer
|
|
321
|
+
* while cleaning and a plain array once read back from JSON.
|
|
322
|
+
*/
|
|
323
|
+
function auditTimingLines(audit, { verbose }) {
|
|
324
|
+
const phases = audit.timings?.toJSON?.() ?? audit.timings ?? [];
|
|
325
|
+
const items = audit.entries
|
|
326
|
+
.filter((entry) => entry.durationMs > 0)
|
|
327
|
+
.sort((a, b) => b.durationMs - a.durationMs);
|
|
328
|
+
const limit = verbose ? Infinity : TIMING_LIMIT;
|
|
329
|
+
const lines = [];
|
|
330
|
+
if (phases.length > 0) {
|
|
331
|
+
lines.push(` slowest phases: ${phaseList(phases.slice(0, limit))}`);
|
|
332
|
+
}
|
|
333
|
+
for (const entry of items.slice(0, limit)) {
|
|
334
|
+
lines.push(
|
|
335
|
+
` ${(entry.durationMs / 1000).toFixed(1)}s ${entry.rule} ${entry.path ?? entry.description}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return lines.length > 0 ? ['', 'TIMINGS', ...lines] : [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The slowest phases of each environment, so bottlenecks are visible
|
|
343
|
+
* without debug flags.
|
|
344
|
+
*/
|
|
345
|
+
function timingLines(report, { verbose }) {
|
|
346
|
+
return report.environments
|
|
347
|
+
.filter((env) => env.timings?.length > 0)
|
|
348
|
+
.map((env) => {
|
|
349
|
+
const phases = verbose ? env.timings : env.timings.slice(0, TIMING_LIMIT);
|
|
350
|
+
return ` ${env.label} slowest phases: ${phaseList(phases)}`;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
308
354
|
/**
|
|
309
355
|
* Renders a scan report.
|
|
310
356
|
* @param {object} report
|
|
@@ -352,6 +398,10 @@ export function formatReport(report, options = {}) {
|
|
|
352
398
|
lines.push(...dockerSection(report));
|
|
353
399
|
lines.push(...errorsSection(report.errors));
|
|
354
400
|
lines.push(...totalsSection(report));
|
|
401
|
+
const timings = timingLines(report, options);
|
|
402
|
+
if (timings.length > 0) {
|
|
403
|
+
lines.push('', 'TIMINGS', ...timings);
|
|
404
|
+
}
|
|
355
405
|
return lines.join('\n');
|
|
356
406
|
}
|
|
357
407
|
|
|
@@ -445,6 +495,7 @@ export function formatAudit(audit, options = {}) {
|
|
|
445
495
|
}
|
|
446
496
|
lines.push('', 'Per environment:', ...environmentLines(audit.environments));
|
|
447
497
|
lines.push(...healthLines(audit.health));
|
|
498
|
+
lines.push(...auditTimingLines(audit, options));
|
|
448
499
|
lines.push(
|
|
449
500
|
'',
|
|
450
501
|
audit.dryRun
|
package/src/rules/ecosystems.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* `projects` are directories inside project trees, recognised by name plus a
|
|
10
10
|
* marker file next to them (`markers`), inside them (`selfMarkers`), or next
|
|
11
|
-
* to their parent (`parentMarkers`, with `parentName`).
|
|
12
|
-
* `
|
|
13
|
-
* inactive for the configured number of days and `aggressive` otherwise.
|
|
11
|
+
* to their parent (`parentMarkers`, with `parentName`). All kinds (`cache`,
|
|
12
|
+
* `build`, `dependencies`) are `safe` whenever nothing is using them.
|
|
14
13
|
*
|
|
15
14
|
* `busy` lists process names that, when running (inside the project for
|
|
16
15
|
* project directories), mean the tool may be using the directory right now.
|
|
@@ -71,7 +70,9 @@ export const ECOSYSTEMS = [
|
|
|
71
70
|
description: 'Bun install cache',
|
|
72
71
|
paths: ['~/.bun/install/cache'],
|
|
73
72
|
native: { tool: 'bun', argv: ['bun', 'pm', 'cache', 'rm'] },
|
|
74
|
-
|
|
73
|
+
// Only package commands and auto-installing scripts read the cache
|
|
74
|
+
// (see LivenessProbe.refresh).
|
|
75
|
+
busy: ['bun', 'bunx'],
|
|
75
76
|
},
|
|
76
77
|
{
|
|
77
78
|
id: 'deno-cache',
|
package/src/rules/other.js
CHANGED
|
@@ -349,8 +349,11 @@ export const OTHER_RULES = [
|
|
|
349
349
|
ecosystem: 'agents',
|
|
350
350
|
description: 'abandoned Codex runtime install staging directories',
|
|
351
351
|
paths: ['~/.cache/codex-runtimes/codex-runtime-install-*'],
|
|
352
|
+
// The Codex daemon runs for days, so a running `codex` says nothing
|
|
353
|
+
// about a staging directory: an install in progress writes to it and
|
|
354
|
+
// holds its files open, which the activity window and open-file checks
|
|
355
|
+
// catch.
|
|
352
356
|
minAge: 'stale',
|
|
353
|
-
busy: ['codex'],
|
|
354
357
|
},
|
|
355
358
|
{
|
|
356
359
|
id: 'claude-cli-mcp-logs',
|
package/src/rules/versions.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* - the newest installed version,
|
|
7
7
|
* - in use by a running process (checked by the liveness layer).
|
|
8
8
|
*
|
|
9
|
+
* `refsPickNewest` makes a partial ref keep only the newest install it
|
|
10
|
+
* matches (nvm runs the newest 20.x for `20`); otherwise it keeps them all.
|
|
11
|
+
* `globalPackages` blocks versions holding packages the user installed
|
|
12
|
+
* globally, which reinstalling the version would not bring back.
|
|
13
|
+
*
|
|
9
14
|
* `versionOf(dirPath)` maps an install directory to the version string that
|
|
10
15
|
* defaults and pin files are compared with (see `versionMatches`).
|
|
11
16
|
*/
|
|
@@ -112,6 +117,16 @@ export const VERSION_RULES = [
|
|
|
112
117
|
busy: ['claude'],
|
|
113
118
|
recheck: true,
|
|
114
119
|
},
|
|
120
|
+
{
|
|
121
|
+
id: 'codex-daemon-releases',
|
|
122
|
+
ecosystem: 'agents',
|
|
123
|
+
manager: 'Codex',
|
|
124
|
+
description: 'Codex app-server daemon release',
|
|
125
|
+
dirs: ['~/.codex/packages/app-server-daemon/releases/*'],
|
|
126
|
+
defaultLinks: ['~/.codex/packages/app-server-daemon/current'],
|
|
127
|
+
busy: ['codex'],
|
|
128
|
+
recheck: true,
|
|
129
|
+
},
|
|
115
130
|
{
|
|
116
131
|
id: 'copilot-cli-versions',
|
|
117
132
|
ecosystem: 'agents',
|
|
@@ -133,6 +148,14 @@ export const VERSION_RULES = [
|
|
|
133
148
|
aliasDir: '~/.nvm/alias',
|
|
134
149
|
defaults: [{ path: '~/.nvm/alias/default', parse: firstLines }],
|
|
135
150
|
projectFiles: { '.nvmrc': firstLines, '.node-version': firstLines },
|
|
151
|
+
refsPickNewest: true,
|
|
152
|
+
globalPackages: {
|
|
153
|
+
dir: 'lib/node_modules',
|
|
154
|
+
// Bundled with Node.js, or a use-m cache (`<name>-v-<version>`) that
|
|
155
|
+
// use-m installs again on first use.
|
|
156
|
+
regenerable: (name) =>
|
|
157
|
+
name === 'npm' || name === 'corepack' || /^[^@][^/]*-v-./.test(name),
|
|
158
|
+
},
|
|
136
159
|
busy: ['node'],
|
|
137
160
|
},
|
|
138
161
|
{
|
package/src/scan.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
ScanIndex,
|
|
33
33
|
scopeKey,
|
|
34
34
|
} from './scan-state.js';
|
|
35
|
+
import { PhaseTimer, timed } from './timings.js';
|
|
35
36
|
|
|
36
37
|
export const SCANNERS = {
|
|
37
38
|
projects: scanProjects,
|
|
@@ -266,6 +267,12 @@ export async function scanEnvironment(env, options, extra = {}) {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/** Phase timings of an environment, started on its first scan. */
|
|
271
|
+
function environmentTimings(env) {
|
|
272
|
+
env.timings ??= new PhaseTimer();
|
|
273
|
+
return env.timings;
|
|
274
|
+
}
|
|
275
|
+
|
|
269
276
|
async function scanEnvironmentScoped(
|
|
270
277
|
env,
|
|
271
278
|
options,
|
|
@@ -275,11 +282,14 @@ async function scanEnvironmentScoped(
|
|
|
275
282
|
stage
|
|
276
283
|
) {
|
|
277
284
|
const errors = extra.errors ?? [];
|
|
278
|
-
const context = await
|
|
285
|
+
const context = await environmentTimings(env).measure('context', () =>
|
|
286
|
+
environmentContext(env, options, extra)
|
|
287
|
+
);
|
|
288
|
+
context.timings = env.timings;
|
|
279
289
|
context.emit = (item) => found.set(item.id, item);
|
|
280
290
|
let stopped = false;
|
|
281
291
|
const ready = async (items) => {
|
|
282
|
-
await attributeStorage(env, items);
|
|
292
|
+
await env.timings.measure('storage', () => attributeStorage(env, items));
|
|
283
293
|
for (const item of items) {
|
|
284
294
|
for (const target of item.paths) {
|
|
285
295
|
block(item, env.scanPolicy.removalReason(target));
|
|
@@ -324,7 +334,9 @@ async function scanEnvironmentScoped(
|
|
|
324
334
|
const started = Date.now();
|
|
325
335
|
const previous = new Set(found.keys());
|
|
326
336
|
try {
|
|
327
|
-
const entries = await scanner(
|
|
337
|
+
const entries = await env.timings.measure(`scanner:${name}`, () =>
|
|
338
|
+
scanner(context)
|
|
339
|
+
);
|
|
328
340
|
for (const item of entries) {
|
|
329
341
|
context.emit(item);
|
|
330
342
|
}
|
|
@@ -480,6 +492,7 @@ function envDescriptor(env, depth, chain) {
|
|
|
480
492
|
hint: env.probeHint ?? null,
|
|
481
493
|
scanStatus: env.scanStatus ?? null,
|
|
482
494
|
layerSource: env.writableLayer?.source ?? null,
|
|
495
|
+
timings: env.timings?.toJSON() ?? [],
|
|
483
496
|
};
|
|
484
497
|
}
|
|
485
498
|
|
|
@@ -590,6 +603,7 @@ async function scanScoped(input) {
|
|
|
590
603
|
resetWritableLayers(env.executor);
|
|
591
604
|
env.writableLayer = null;
|
|
592
605
|
env.scanPolicy = null;
|
|
606
|
+
env.timings = new PhaseTimer();
|
|
593
607
|
await configureScanPolicy(env, options);
|
|
594
608
|
await configureContainerHost(
|
|
595
609
|
env,
|
|
@@ -603,7 +617,9 @@ async function scanScoped(input) {
|
|
|
603
617
|
const docker =
|
|
604
618
|
env.scanStatus === 'goal-reached'
|
|
605
619
|
? null
|
|
606
|
-
: await
|
|
620
|
+
: await timed(env.timings, 'docker', () =>
|
|
621
|
+
dockerSection(env, options, errors)
|
|
622
|
+
);
|
|
607
623
|
blockMountedHostItems(hostItems, docker?.bindMounts ?? [], env.path);
|
|
608
624
|
const items = filterItems(
|
|
609
625
|
[...hostItems, ...(docker?.items ?? [])],
|
|
@@ -615,7 +631,10 @@ async function scanScoped(input) {
|
|
|
615
631
|
for (const inner of docker?.environments ?? []) {
|
|
616
632
|
environments.push(inner);
|
|
617
633
|
}
|
|
618
|
-
environments[0] = await
|
|
634
|
+
environments[0] = await timed(env.timings, 'disk', () =>
|
|
635
|
+
withDisk(env, environments[0])
|
|
636
|
+
);
|
|
637
|
+
environments[0].timings = env.timings.toJSON();
|
|
619
638
|
const report = buildReport({
|
|
620
639
|
options,
|
|
621
640
|
env,
|
package/src/scanners/global.js
CHANGED
|
@@ -14,6 +14,7 @@ import { expandGlobPaths, expandRulePath, isWithin } from '../paths.js';
|
|
|
14
14
|
import { olderThan, scanTimeBusy, resolvePaths } from './common.js';
|
|
15
15
|
import { configuredPattern } from '../env/cache-roots.js';
|
|
16
16
|
import { TOOLCHAIN_CACHE_RULES } from '../rules/toolchains.js';
|
|
17
|
+
import { timed } from '../timings.js';
|
|
17
18
|
|
|
18
19
|
export const GLOBAL_RULES = [
|
|
19
20
|
...CACHE_RULES,
|
|
@@ -242,9 +243,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
242
243
|
patterns: rulePatterns(env, rule, base),
|
|
243
244
|
}))
|
|
244
245
|
);
|
|
245
|
-
const matches = await
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
const matches = await timed(context.timings, 'global:expand', () =>
|
|
247
|
+
expandGlobPaths(
|
|
248
|
+
env,
|
|
249
|
+
planned.flatMap((entry) => entry.patterns)
|
|
250
|
+
)
|
|
248
251
|
);
|
|
249
252
|
const typedPaths = planned
|
|
250
253
|
.filter(
|
|
@@ -253,7 +256,9 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
253
256
|
.flatMap(({ patterns }) =>
|
|
254
257
|
patterns.flatMap((pattern) => matches.get(pattern) ?? [])
|
|
255
258
|
);
|
|
256
|
-
const stats = await
|
|
259
|
+
const stats = await timed(context.timings, 'global:stat', () =>
|
|
260
|
+
env.statMany([...new Set(typedPaths)])
|
|
261
|
+
);
|
|
257
262
|
const found = [];
|
|
258
263
|
const seen = new Set();
|
|
259
264
|
for (const { rule, base, patterns } of planned) {
|
|
@@ -263,7 +268,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
263
268
|
|
|
264
269
|
const accepted = [];
|
|
265
270
|
for (const target of candidates) {
|
|
266
|
-
if (
|
|
271
|
+
if (
|
|
272
|
+
!(await timed(context.timings, 'global:accept', () =>
|
|
273
|
+
acceptedPath(env, rule, target, stats)
|
|
274
|
+
))
|
|
275
|
+
) {
|
|
267
276
|
continue;
|
|
268
277
|
}
|
|
269
278
|
accepted.push(target);
|
|
@@ -274,9 +283,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
274
283
|
found.push({ rule, base, paths });
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
|
-
await
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
await timed(context.timings, 'global:resolve', () =>
|
|
287
|
+
resolvePaths(
|
|
288
|
+
context,
|
|
289
|
+
found.flatMap((entry) => entry.paths)
|
|
290
|
+
)
|
|
280
291
|
);
|
|
281
292
|
const items = scanItems(context);
|
|
282
293
|
// Deliver cheap native measurements before rules needing recursive activity
|
|
@@ -284,20 +295,27 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
284
295
|
found.sort(
|
|
285
296
|
(a, b) => Number(requiresMtime(a.rule)) - Number(requiresMtime(b.rule))
|
|
286
297
|
);
|
|
298
|
+
// The report drops items below `minSize`; skipping them here saves their
|
|
299
|
+
// liveness checks (Telegram media alone can hold 100k small files). In a
|
|
300
|
+
// container the writable layer re-measures sizes later, so all are kept.
|
|
301
|
+
const large = (entry) =>
|
|
302
|
+
Boolean(env.writableLayer) ||
|
|
303
|
+
entry.usage.bytes >= (options.minSizeBytes ?? 0);
|
|
287
304
|
for (const { rule, base, paths } of found) {
|
|
288
|
-
const usages = await
|
|
289
|
-
mtime: requiresMtime(rule),
|
|
290
|
-
|
|
291
|
-
});
|
|
305
|
+
const usages = await timed(context.timings, 'global:measure', () =>
|
|
306
|
+
env.usageMany(paths, { mtime: requiresMtime(rule), cache: true })
|
|
307
|
+
);
|
|
292
308
|
const accept = ageFilter(context, rule);
|
|
293
309
|
const measured = paths
|
|
294
310
|
.map((target) => ({ path: target, usage: usages.get(target) }))
|
|
295
311
|
.filter((entry) => entry.usage && accept(entry.usage));
|
|
296
312
|
const groups = rule.aggregate
|
|
297
313
|
? [measured]
|
|
298
|
-
: measured.map((entry) => [entry]);
|
|
314
|
+
: measured.filter(large).map((entry) => [entry]);
|
|
299
315
|
for (const group of groups.filter((g) => g.length > 0)) {
|
|
300
|
-
const item = await
|
|
316
|
+
const item = await timed(context.timings, 'global:item', () =>
|
|
317
|
+
buildItem(context, rule, base, group)
|
|
318
|
+
);
|
|
301
319
|
items.push(item);
|
|
302
320
|
if ((await context.ready?.([item])) === false) {
|
|
303
321
|
return items;
|