disk-space-saviour 0.22.0 → 0.23.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 +28 -0
- package/README.md +87 -5
- package/package.json +1 -1
- package/src/audit.js +32 -1
- package/src/clean.js +38 -12
- package/src/cli.js +11 -0
- package/src/conditions.js +7 -0
- package/src/docker/scan.js +1 -0
- package/src/docker/writable.js +7 -1
- package/src/emergency.js +4 -4
- package/src/env/hardlinks.js +139 -0
- package/src/env/local.js +53 -11
- package/src/env/roots.js +138 -0
- package/src/env/shell.js +14 -2
- package/src/index.d.ts +46 -0
- package/src/liveness.js +116 -8
- package/src/mac-apps.js +286 -0
- package/src/options.js +3 -0
- package/src/report.js +18 -2
- package/src/rules/downloads.js +27 -0
- package/src/rules/other.js +9 -0
- package/src/scan-state.js +1 -0
- package/src/scan.js +33 -23
- package/src/scanners/agents.js +2 -1
- package/src/scanners/global.js +2 -1
- package/src/scanners/node-modules.js +89 -0
- package/src/scanners/projects.js +11 -1
- package/src/swap.js +9 -2
- package/src/usage.js +86 -0
package/src/scanners/projects.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
|
|
3
|
+
import { itemSize } from '../env/hardlinks.js';
|
|
3
4
|
import { scanItems } from '../env/scope.js';
|
|
4
5
|
/**
|
|
5
6
|
* Per-project dependency, build and cache directories (node_modules, target,
|
|
@@ -23,6 +24,7 @@ import { block, makeItem } from '../items.js';
|
|
|
23
24
|
import { isWithin, matchesGlob } from '../paths.js';
|
|
24
25
|
import { listMany, scanTimeBusy } from './common.js';
|
|
25
26
|
import { analyzeRustProfile, findRustProfiles } from './rust.js';
|
|
27
|
+
import { foreignDataBlocker } from './node-modules.js';
|
|
26
28
|
import { timed } from '../timings.js';
|
|
27
29
|
|
|
28
30
|
const ECOSYSTEM_NAMES = new Map(ECOSYSTEMS.map((e) => [e.id, e.name]));
|
|
@@ -240,7 +242,7 @@ async function projectItem(context, { candidate, rule }, usage) {
|
|
|
240
242
|
ecosystem: rule.ecosystem,
|
|
241
243
|
description: `${ECOSYSTEM_NAMES.get(rule.ecosystem)} ${rule.description}`,
|
|
242
244
|
path: candidate.path,
|
|
243
|
-
|
|
245
|
+
...itemSize([[candidate.path, usage]]),
|
|
244
246
|
sizeEstimated: Boolean(usage?.estimated),
|
|
245
247
|
newestMtimeMs: usage?.newestMtimeMs ?? 0,
|
|
246
248
|
project,
|
|
@@ -248,6 +250,14 @@ async function projectItem(context, { candidate, rule }, usage) {
|
|
|
248
250
|
...projectTier(rule),
|
|
249
251
|
checks: { busy: rule.busy ?? [], cwd: project, mtime: true },
|
|
250
252
|
});
|
|
253
|
+
if (rule.id === 'node-modules') {
|
|
254
|
+
block(
|
|
255
|
+
item,
|
|
256
|
+
await timed(context.timings, 'projects:contents', () =>
|
|
257
|
+
foreignDataBlocker(env, candidate.path)
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
251
261
|
await addBlockers(context, item, false);
|
|
252
262
|
return item;
|
|
253
263
|
}
|
package/src/swap.js
CHANGED
|
@@ -260,8 +260,11 @@ export function swapWarnings(swap, disk) {
|
|
|
260
260
|
if (reasons.length === 0) {
|
|
261
261
|
return [];
|
|
262
262
|
}
|
|
263
|
-
// The kernel (PID 0) cannot be quit
|
|
264
|
-
|
|
263
|
+
// The kernel (PID 0) cannot be quit and this dss run ends by itself, so
|
|
264
|
+
// naming either helps nobody.
|
|
265
|
+
const candidates = swap.holders.filter(
|
|
266
|
+
(holder) => holder.pid > 0 && !holder.self
|
|
267
|
+
);
|
|
265
268
|
const growing = candidates.filter(
|
|
266
269
|
(holder) => holder.growth?.memoryBytes >= GROWTH_WARNING_BYTES
|
|
267
270
|
);
|
|
@@ -308,6 +311,10 @@ export async function readSwap(env, options = {}) {
|
|
|
308
311
|
if (!swap) {
|
|
309
312
|
return null;
|
|
310
313
|
}
|
|
314
|
+
// This dss run shows up among the holders while it scans; it ends on its own.
|
|
315
|
+
swap.holders = swap.holders.map((holder) =>
|
|
316
|
+
holder.pid === process.pid ? { ...holder, self: true } : holder
|
|
317
|
+
);
|
|
311
318
|
const history = await loadSwapHistory(stateDir, now);
|
|
312
319
|
const grown = withGrowth(swap, history);
|
|
313
320
|
await saveSwapHistory(stateDir, [
|
package/src/usage.js
CHANGED
|
@@ -26,6 +26,22 @@ import { formatBytes } from './units.js';
|
|
|
26
26
|
|
|
27
27
|
const MiB = 1024 * 1024;
|
|
28
28
|
const GiB = 1024 * MiB;
|
|
29
|
+
const GROUP_PATTERNS = 64;
|
|
30
|
+
const GROUP_MIN_ENTRIES = 10;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The name pattern an entry is grouped under: its leading word and the
|
|
34
|
+
* separator after it (`ms-tg-creds-a1b2` is `ms-*`), else its extension
|
|
35
|
+
* (`trace.zip` is `*.zip`), else null.
|
|
36
|
+
*/
|
|
37
|
+
export function namePattern(name) {
|
|
38
|
+
const word = /^[A-Za-z][A-Za-z0-9]*[-_.]/.exec(name);
|
|
39
|
+
if (word && word[0].length < name.length) {
|
|
40
|
+
return `${word[0]}*`;
|
|
41
|
+
}
|
|
42
|
+
const extension = /\.[A-Za-z0-9]{1,8}$/.exec(name);
|
|
43
|
+
return extension ? `*${extension[0]}` : null;
|
|
44
|
+
}
|
|
29
45
|
const DATA_VOLUME = '/System/Volumes/Data';
|
|
30
46
|
const DENIED = /^du: (.+): (Permission denied|Operation not permitted)$/;
|
|
31
47
|
|
|
@@ -52,6 +68,10 @@ export class UsageCollector {
|
|
|
52
68
|
this.entries = 0;
|
|
53
69
|
this.previous = null;
|
|
54
70
|
this.last = null;
|
|
71
|
+
// Small entries summed by name pattern, per directory still being
|
|
72
|
+
// listed, so a directory of many small entries can say what fills it.
|
|
73
|
+
this.open = new Map();
|
|
74
|
+
this.groups = new Map();
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
add(line) {
|
|
@@ -64,7 +84,11 @@ export class UsageCollector {
|
|
|
64
84
|
const isDir = this.previous?.startsWith(prefix) ?? false;
|
|
65
85
|
this.previous = entry.path;
|
|
66
86
|
this.last = entry;
|
|
87
|
+
if (isDir) {
|
|
88
|
+
this.#closeDir(entry);
|
|
89
|
+
}
|
|
67
90
|
if (entry.bytes < this.minBytes) {
|
|
91
|
+
this.#group(entry);
|
|
68
92
|
return;
|
|
69
93
|
}
|
|
70
94
|
if (isDir) {
|
|
@@ -77,6 +101,45 @@ export class UsageCollector {
|
|
|
77
101
|
}
|
|
78
102
|
}
|
|
79
103
|
|
|
104
|
+
#group(entry) {
|
|
105
|
+
const slash = entry.path.lastIndexOf('/');
|
|
106
|
+
const pattern = namePattern(entry.path.slice(slash + 1));
|
|
107
|
+
if (slash <= 0 || !pattern) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const parent = entry.path.slice(0, slash);
|
|
111
|
+
const groups = this.open.get(parent) ?? new Map();
|
|
112
|
+
this.open.set(parent, groups);
|
|
113
|
+
const group = groups.get(pattern);
|
|
114
|
+
if (group) {
|
|
115
|
+
group.bytes += entry.bytes;
|
|
116
|
+
group.count += 1;
|
|
117
|
+
} else if (groups.size < GROUP_PATTERNS) {
|
|
118
|
+
groups.set(pattern, { pattern, bytes: entry.bytes, count: 1 });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `du` lists a directory after everything inside it. */
|
|
123
|
+
#closeDir(entry) {
|
|
124
|
+
const groups = this.open.get(entry.path);
|
|
125
|
+
if (!groups) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.open.delete(entry.path);
|
|
129
|
+
const kept = [...groups.values()].filter(
|
|
130
|
+
(group) =>
|
|
131
|
+
group.bytes >= this.minBytes && group.count >= GROUP_MIN_ENTRIES
|
|
132
|
+
);
|
|
133
|
+
if (entry.bytes >= this.minBytes && kept.length > 0) {
|
|
134
|
+
this.groups.set(
|
|
135
|
+
entry.path,
|
|
136
|
+
kept
|
|
137
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
138
|
+
.map((group) => ({ ...group, below: this.minBytes }))
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
80
143
|
#trimFiles() {
|
|
81
144
|
this.files.sort((a, b) => b.bytes - a.bytes);
|
|
82
145
|
this.files.length = Math.min(this.files.length, this.topFiles);
|
|
@@ -93,6 +156,7 @@ export class UsageCollector {
|
|
|
93
156
|
entries: this.entries,
|
|
94
157
|
dirs: this.dirs,
|
|
95
158
|
files: this.files,
|
|
159
|
+
groups: this.groups,
|
|
96
160
|
};
|
|
97
161
|
}
|
|
98
162
|
}
|
|
@@ -417,9 +481,13 @@ function rootReport(result, items, options) {
|
|
|
417
481
|
[...result.dirs].map(([dir, bytes]) => [show(dir), bytes])
|
|
418
482
|
);
|
|
419
483
|
const rootPath = show(result.root);
|
|
484
|
+
const groups = new Map(
|
|
485
|
+
[...(result.groups ?? [])].map(([dir, list]) => [show(dir), list])
|
|
486
|
+
);
|
|
420
487
|
const annotate = (node) => ({
|
|
421
488
|
...node,
|
|
422
489
|
dss: summary(scanCoverage(node.path, items)),
|
|
490
|
+
groups: smallEntries(node, groups.get(node.path), minBytes),
|
|
423
491
|
children: node.children.map(annotate),
|
|
424
492
|
});
|
|
425
493
|
const inside = items.filter((item) =>
|
|
@@ -458,6 +526,18 @@ function rootReport(result, items, options) {
|
|
|
458
526
|
};
|
|
459
527
|
}
|
|
460
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Groups of small entries worth naming in `node`: those as large as a
|
|
531
|
+
* shown directory, when the shown subdirectories leave much of it unnamed.
|
|
532
|
+
*/
|
|
533
|
+
function smallEntries(node, groups, minBytes) {
|
|
534
|
+
const named = node.children.reduce((sum, child) => sum + child.bytes, 0);
|
|
535
|
+
if (!groups || named >= node.bytes * 0.5) {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
return groups.filter((group) => group.bytes >= minBytes).slice(0, 5);
|
|
539
|
+
}
|
|
540
|
+
|
|
461
541
|
function summary(coverage) {
|
|
462
542
|
return {
|
|
463
543
|
rule: coverage.item?.rule ?? null,
|
|
@@ -517,9 +597,15 @@ function treeLines(node, depth = 0) {
|
|
|
517
597
|
) {
|
|
518
598
|
return treeLines(only, depth);
|
|
519
599
|
}
|
|
600
|
+
const indent = ' '.repeat(depth + 1);
|
|
601
|
+
const separator = node.path.endsWith('/') ? '' : '/';
|
|
520
602
|
return [
|
|
521
603
|
`${formatBytes(node.bytes).padStart(10)} ${' '.repeat(depth)}${node.path}${dssNote(node.dss)}`,
|
|
522
604
|
...node.children.flatMap((child) => treeLines(child, depth + 1)),
|
|
605
|
+
...(node.groups ?? []).map(
|
|
606
|
+
(group) =>
|
|
607
|
+
`${formatBytes(group.bytes).padStart(10)} ${indent}${node.path}${separator}${group.pattern} (${group.count.toLocaleString('en-US')} entries, each under ${formatBytes(group.below)})`
|
|
608
|
+
),
|
|
523
609
|
];
|
|
524
610
|
}
|
|
525
611
|
|