disk-space-saviour 0.17.0 → 0.19.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 +24 -0
- package/README.md +34 -14
- 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/apps.js +7 -3
- package/src/rules/downloads.js +175 -0
- package/src/rules/ecosystems.js +5 -4
- package/src/rules/other.js +7 -1
- package/src/rules/versions.js +23 -0
- package/src/scan.js +24 -5
- package/src/scanners/global.js +36 -15
- package/src/scanners/projects.js +67 -111
- package/src/scanners/versions.js +102 -3
- package/src/timings.js +54 -0
package/src/scanners/global.js
CHANGED
|
@@ -2,7 +2,8 @@ import { scanItems } from '../env/scope.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Global per-user caches (ecosystem download and compilation caches, IDE and
|
|
4
4
|
* browser caches), system package caches, and leftovers in temporary
|
|
5
|
-
* directories. See ../rules/ecosystems.js
|
|
5
|
+
* directories. See ../rules/ecosystems.js, ../rules/other.js and
|
|
6
|
+
* ../rules/downloads.js.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { CACHE_RULES } from '../rules/ecosystems.js';
|
|
@@ -14,12 +15,15 @@ import { expandGlobPaths, expandRulePath, isWithin } from '../paths.js';
|
|
|
14
15
|
import { olderThan, scanTimeBusy, resolvePaths } from './common.js';
|
|
15
16
|
import { configuredPattern } from '../env/cache-roots.js';
|
|
16
17
|
import { TOOLCHAIN_CACHE_RULES } from '../rules/toolchains.js';
|
|
18
|
+
import { DOWNLOAD_RULES } from '../rules/downloads.js';
|
|
19
|
+
import { timed } from '../timings.js';
|
|
17
20
|
|
|
18
21
|
export const GLOBAL_RULES = [
|
|
19
22
|
...CACHE_RULES,
|
|
20
23
|
...expandChromiumCaches(OTHER_RULES),
|
|
21
24
|
...APP_RULES,
|
|
22
25
|
...TOOLCHAIN_CACHE_RULES,
|
|
26
|
+
...DOWNLOAD_RULES,
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
function scopeBases(rule, homes, tmpDirs) {
|
|
@@ -242,9 +246,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
242
246
|
patterns: rulePatterns(env, rule, base),
|
|
243
247
|
}))
|
|
244
248
|
);
|
|
245
|
-
const matches = await
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
const matches = await timed(context.timings, 'global:expand', () =>
|
|
250
|
+
expandGlobPaths(
|
|
251
|
+
env,
|
|
252
|
+
planned.flatMap((entry) => entry.patterns)
|
|
253
|
+
)
|
|
248
254
|
);
|
|
249
255
|
const typedPaths = planned
|
|
250
256
|
.filter(
|
|
@@ -253,7 +259,9 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
253
259
|
.flatMap(({ patterns }) =>
|
|
254
260
|
patterns.flatMap((pattern) => matches.get(pattern) ?? [])
|
|
255
261
|
);
|
|
256
|
-
const stats = await
|
|
262
|
+
const stats = await timed(context.timings, 'global:stat', () =>
|
|
263
|
+
env.statMany([...new Set(typedPaths)])
|
|
264
|
+
);
|
|
257
265
|
const found = [];
|
|
258
266
|
const seen = new Set();
|
|
259
267
|
for (const { rule, base, patterns } of planned) {
|
|
@@ -263,7 +271,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
263
271
|
|
|
264
272
|
const accepted = [];
|
|
265
273
|
for (const target of candidates) {
|
|
266
|
-
if (
|
|
274
|
+
if (
|
|
275
|
+
!(await timed(context.timings, 'global:accept', () =>
|
|
276
|
+
acceptedPath(env, rule, target, stats)
|
|
277
|
+
))
|
|
278
|
+
) {
|
|
267
279
|
continue;
|
|
268
280
|
}
|
|
269
281
|
accepted.push(target);
|
|
@@ -274,9 +286,11 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
274
286
|
found.push({ rule, base, paths });
|
|
275
287
|
}
|
|
276
288
|
}
|
|
277
|
-
await
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
await timed(context.timings, 'global:resolve', () =>
|
|
290
|
+
resolvePaths(
|
|
291
|
+
context,
|
|
292
|
+
found.flatMap((entry) => entry.paths)
|
|
293
|
+
)
|
|
280
294
|
);
|
|
281
295
|
const items = scanItems(context);
|
|
282
296
|
// Deliver cheap native measurements before rules needing recursive activity
|
|
@@ -284,20 +298,27 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
284
298
|
found.sort(
|
|
285
299
|
(a, b) => Number(requiresMtime(a.rule)) - Number(requiresMtime(b.rule))
|
|
286
300
|
);
|
|
301
|
+
// The report drops items below `minSize`; skipping them here saves their
|
|
302
|
+
// liveness checks (Telegram media alone can hold 100k small files). In a
|
|
303
|
+
// container the writable layer re-measures sizes later, so all are kept.
|
|
304
|
+
const large = (entry) =>
|
|
305
|
+
Boolean(env.writableLayer) ||
|
|
306
|
+
entry.usage.bytes >= (options.minSizeBytes ?? 0);
|
|
287
307
|
for (const { rule, base, paths } of found) {
|
|
288
|
-
const usages = await
|
|
289
|
-
mtime: requiresMtime(rule),
|
|
290
|
-
|
|
291
|
-
});
|
|
308
|
+
const usages = await timed(context.timings, 'global:measure', () =>
|
|
309
|
+
env.usageMany(paths, { mtime: requiresMtime(rule), cache: true })
|
|
310
|
+
);
|
|
292
311
|
const accept = ageFilter(context, rule);
|
|
293
312
|
const measured = paths
|
|
294
313
|
.map((target) => ({ path: target, usage: usages.get(target) }))
|
|
295
314
|
.filter((entry) => entry.usage && accept(entry.usage));
|
|
296
315
|
const groups = rule.aggregate
|
|
297
316
|
? [measured]
|
|
298
|
-
: measured.map((entry) => [entry]);
|
|
317
|
+
: measured.filter(large).map((entry) => [entry]);
|
|
299
318
|
for (const group of groups.filter((g) => g.length > 0)) {
|
|
300
|
-
const item = await
|
|
319
|
+
const item = await timed(context.timings, 'global:item', () =>
|
|
320
|
+
buildItem(context, rule, base, group)
|
|
321
|
+
);
|
|
301
322
|
items.push(item);
|
|
302
323
|
if ((await context.ready?.([item])) === false) {
|
|
303
324
|
return items;
|
package/src/scanners/projects.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
1
3
|
import { scanItems } from '../env/scope.js';
|
|
2
4
|
/**
|
|
3
5
|
* Per-project dependency, build and cache directories (node_modules, target,
|
|
@@ -16,8 +18,9 @@ import { scanItems } from '../env/scope.js';
|
|
|
16
18
|
import { ECOSYSTEMS, PROJECT_RULES } from '../rules/ecosystems.js';
|
|
17
19
|
import { block, makeItem } from '../items.js';
|
|
18
20
|
import { isWithin, matchesGlob } from '../paths.js';
|
|
19
|
-
import { listMany,
|
|
21
|
+
import { listMany, scanTimeBusy } from './common.js';
|
|
20
22
|
import { analyzeRustProfile, findRustProfiles } from './rust.js';
|
|
23
|
+
import { timed } from '../timings.js';
|
|
21
24
|
|
|
22
25
|
const ECOSYSTEM_NAMES = new Map(ECOSYSTEMS.map((e) => [e.id, e.name]));
|
|
23
26
|
|
|
@@ -183,106 +186,38 @@ export async function findProjectDirs(env, roots, options = {}) {
|
|
|
183
186
|
return matches;
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function projectTier(context, rule, lastActivityMs) {
|
|
197
|
-
if (rule.kind === 'cache') {
|
|
198
|
-
return { tier: 'safe', reason: 'cache, re-created on demand' };
|
|
199
|
-
}
|
|
200
|
-
const { inactiveMs } = context.options;
|
|
201
|
-
if (olderThan(context, lastActivityMs, inactiveMs)) {
|
|
202
|
-
return {
|
|
203
|
-
tier: rule.id === 'node-modules' ? 'safe' : 'moderate',
|
|
204
|
-
reason: 'project inactive, re-created by the build or install step',
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
tier: 'aggressive',
|
|
209
|
-
reason: 'project recently active, re-created by the build or install step',
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Re-evaluate activity before cleaning a reused project plan. */
|
|
214
|
-
export async function currentProjectTier(context, item) {
|
|
215
|
-
const rule = PROJECT_RULES.find((entry) => entry.id === item.rule);
|
|
216
|
-
if (!rule || rule.kind === 'cache') {
|
|
217
|
-
return item.tier;
|
|
218
|
-
}
|
|
219
|
-
const globs = [...new Set(PROJECT_RULES.flatMap((entry) => entry.names))];
|
|
220
|
-
const parent = context.env.path.dirname(item.path);
|
|
221
|
-
const candidate = {
|
|
222
|
-
name: context.env.path.basename(item.path),
|
|
223
|
-
siblings: await context.env.list(parent),
|
|
224
|
-
};
|
|
225
|
-
context.git.invalidate();
|
|
226
|
-
const activity = await projectActivity(
|
|
227
|
-
context,
|
|
228
|
-
item.project,
|
|
229
|
-
candidate,
|
|
230
|
-
{ newestMtimeMs: item.newestMtimeMs },
|
|
231
|
-
globs
|
|
232
|
-
);
|
|
233
|
-
return projectTier(context, rule, activity).tier;
|
|
189
|
+
/**
|
|
190
|
+
* Project caches, build outputs and dependencies are all regenerable, so how
|
|
191
|
+
* recently the project was used does not matter: only liveness (a tool
|
|
192
|
+
* running in the project, open files, recent writes) and Git safety block
|
|
193
|
+
* them.
|
|
194
|
+
*/
|
|
195
|
+
function projectTier(rule) {
|
|
196
|
+
return rule.kind === 'cache'
|
|
197
|
+
? { tier: 'safe', reason: 'cache, re-created on demand' }
|
|
198
|
+
: { tier: 'safe', reason: 're-created by the build or install step' };
|
|
234
199
|
}
|
|
235
200
|
|
|
236
201
|
async function addBlockers(context, item, requireClean) {
|
|
237
|
-
|
|
238
|
-
requireClean
|
|
239
|
-
|
|
202
|
+
const reasons = await timed(context.timings, 'projects:git', () =>
|
|
203
|
+
context.git.pathBlockers(item.path, { requireClean })
|
|
204
|
+
);
|
|
205
|
+
for (const reason of reasons) {
|
|
240
206
|
block(item, reason);
|
|
241
207
|
}
|
|
242
|
-
const busy = await
|
|
208
|
+
const busy = await timed(context.timings, 'projects:busy', () =>
|
|
209
|
+
scanTimeBusy(context, item)
|
|
210
|
+
);
|
|
243
211
|
if (busy) {
|
|
244
212
|
block(item, `busy: ${busy}`);
|
|
245
213
|
}
|
|
246
214
|
}
|
|
247
215
|
|
|
248
|
-
async function
|
|
249
|
-
const { env, git } = context;
|
|
250
|
-
const repo = await git.findRepoRoot(project);
|
|
251
|
-
const lastCommitMs = repo ? (await git.repoState(repo)).lastCommitMs : 0;
|
|
252
|
-
context.projectUsage ??= new Map();
|
|
253
|
-
if (!context.projectUsage.has(project)) {
|
|
254
|
-
context.projectUsage.set(
|
|
255
|
-
project,
|
|
256
|
-
env.projectActivity
|
|
257
|
-
? await env.projectActivity(
|
|
258
|
-
project,
|
|
259
|
-
globs,
|
|
260
|
-
context.now - context.options.inactiveMs
|
|
261
|
-
)
|
|
262
|
-
: (await env.usage(project))?.newestMtimeMs
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
const projectUsage = context.projectUsage.get(project);
|
|
266
|
-
return Math.max(
|
|
267
|
-
usage?.newestMtimeMs ?? 0,
|
|
268
|
-
projectUsage ?? 0,
|
|
269
|
-
siblingActivity(candidate, globs),
|
|
270
|
-
lastCommitMs
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function projectItem(context, { candidate, rule }, usage, globs) {
|
|
216
|
+
async function projectItem(context, { candidate, rule }, usage) {
|
|
275
217
|
const { env } = context;
|
|
276
218
|
const project = rule.parentName
|
|
277
219
|
? env.path.dirname(candidate.parent)
|
|
278
220
|
: candidate.parent;
|
|
279
|
-
const lastActivityMs = await projectActivity(
|
|
280
|
-
context,
|
|
281
|
-
project,
|
|
282
|
-
candidate,
|
|
283
|
-
usage,
|
|
284
|
-
globs
|
|
285
|
-
);
|
|
286
221
|
const item = makeItem(env, {
|
|
287
222
|
rule: rule.id,
|
|
288
223
|
kind: rule.kind,
|
|
@@ -292,10 +227,9 @@ async function projectItem(context, { candidate, rule }, usage, globs) {
|
|
|
292
227
|
bytes: usage?.bytes ?? 0,
|
|
293
228
|
sizeEstimated: Boolean(usage?.estimated),
|
|
294
229
|
newestMtimeMs: usage?.newestMtimeMs ?? 0,
|
|
295
|
-
lastActivityMs,
|
|
296
230
|
project,
|
|
297
231
|
lockfiles: rule.lockfiles ?? null,
|
|
298
|
-
...projectTier(
|
|
232
|
+
...projectTier(rule),
|
|
299
233
|
checks: { busy: rule.busy ?? [], cwd: project, mtime: true },
|
|
300
234
|
});
|
|
301
235
|
await addBlockers(context, item, false);
|
|
@@ -397,29 +331,51 @@ async function rustItems(context, targetItem, rule) {
|
|
|
397
331
|
*/
|
|
398
332
|
export async function scanProjects(context, { rules = PROJECT_RULES } = {}) {
|
|
399
333
|
const { env, options } = context;
|
|
400
|
-
const globs = [...new Set(rules.flatMap((rule) => rule.names))];
|
|
401
334
|
const items = scanItems(context);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
335
|
+
const started = performance.now();
|
|
336
|
+
// Matches are measured while the walk continues; `projects:walk` excludes
|
|
337
|
+
// that time, which `projects:measure`, `projects:git` and `projects:busy`
|
|
338
|
+
// record.
|
|
339
|
+
let streamedMs = 0;
|
|
340
|
+
try {
|
|
341
|
+
await findProjectDirs(env, options.roots, {
|
|
342
|
+
rules,
|
|
343
|
+
maxDepth: options.maxDepth,
|
|
344
|
+
includeRoots: options.includeRoots,
|
|
345
|
+
onMatches: async (matches) => {
|
|
346
|
+
const streamStarted = performance.now();
|
|
347
|
+
try {
|
|
348
|
+
await projectMatches(context, matches, items);
|
|
349
|
+
} finally {
|
|
350
|
+
streamedMs += performance.now() - streamStarted;
|
|
415
351
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
} finally {
|
|
355
|
+
context.timings?.record(
|
|
356
|
+
'projects:walk',
|
|
357
|
+
performance.now() - started - streamedMs
|
|
358
|
+
);
|
|
359
|
+
}
|
|
424
360
|
return items;
|
|
425
361
|
}
|
|
362
|
+
|
|
363
|
+
async function projectMatches(context, matches, items) {
|
|
364
|
+
const usages = await timed(context.timings, 'projects:measure', () =>
|
|
365
|
+
context.env.usageMany(
|
|
366
|
+
matches.map((match) => match.candidate.path),
|
|
367
|
+
{ cache: true }
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
for (const match of matches) {
|
|
371
|
+
const usage = usages.get(match.candidate.path);
|
|
372
|
+
if (!usage) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const item = await projectItem(context, match, usage);
|
|
376
|
+
items.push(item);
|
|
377
|
+
if (match.rule.id === 'cargo-target') {
|
|
378
|
+
items.push(...(await rustItems(context, item, match.rule)));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
package/src/scanners/versions.js
CHANGED
|
@@ -6,7 +6,9 @@ import { scanItems } from '../env/scope.js';
|
|
|
6
6
|
* Kept: the manager's default and alias targets, versions pinned by any
|
|
7
7
|
* project below the scan roots, the newest install of each group, and
|
|
8
8
|
* anything a running process executes from (checked by the liveness probe).
|
|
9
|
-
* Everything else is reported as `moderate
|
|
9
|
+
* Everything else is reported as `moderate`, and blocked while it holds
|
|
10
|
+
* data that reinstalling the version would not bring back (pyenv virtual
|
|
11
|
+
* environments, globally installed npm packages).
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import {
|
|
@@ -171,6 +173,98 @@ function newestInstalls(env, rule, installs, usages) {
|
|
|
171
173
|
return newest;
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Follows a chain of manager aliases (`default` -> `lts/iron` -> `v20.20.2`).
|
|
178
|
+
* An LTS alias also keeps its release line: nvm records the newest remote
|
|
179
|
+
* release there, which may not be the one installed.
|
|
180
|
+
* @returns {Promise<string[]>}
|
|
181
|
+
*/
|
|
182
|
+
async function resolveAlias(env, aliasDir, ref) {
|
|
183
|
+
const refs = [ref];
|
|
184
|
+
let current = ref;
|
|
185
|
+
for (let hop = 0; hop < 8 && !/^v?\d/.test(current); hop++) {
|
|
186
|
+
if (current.split('/').some((part) => part === '..' || part === '')) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const text = await env.readText(env.path.join(aliasDir, current), 4096);
|
|
190
|
+
const next = (text ?? '').split(/\s+/).find(Boolean);
|
|
191
|
+
if (!next || refs.includes(next)) {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
refs.push(next);
|
|
195
|
+
current = next;
|
|
196
|
+
}
|
|
197
|
+
if (ref.startsWith('lts/') && /^v?\d/.test(current)) {
|
|
198
|
+
refs.push(current.replace(/^v?(\d+).*$/, 'v$1'));
|
|
199
|
+
}
|
|
200
|
+
return refs;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function resolveAliases(env, rule, home, refs) {
|
|
204
|
+
if (!rule.aliasDir) {
|
|
205
|
+
return refs;
|
|
206
|
+
}
|
|
207
|
+
const aliasDir = expandRulePath(configuredPattern(env, home, rule.aliasDir), {
|
|
208
|
+
home,
|
|
209
|
+
pathApi: env.path,
|
|
210
|
+
});
|
|
211
|
+
const resolved = [];
|
|
212
|
+
for (const ref of refs) {
|
|
213
|
+
resolved.push(...(await resolveAlias(env, aliasDir, ref)));
|
|
214
|
+
}
|
|
215
|
+
return [...new Set(resolved)];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Installs that the refs keep. With `refsPickNewest` a ref keeps only the
|
|
220
|
+
* newest install it matches, as nvm resolves `20` to the newest 20.x.
|
|
221
|
+
* @returns {Set<string>}
|
|
222
|
+
*/
|
|
223
|
+
function referencedInstalls(rule, installs, refs, versionOf) {
|
|
224
|
+
const kept = new Set();
|
|
225
|
+
for (const ref of refs) {
|
|
226
|
+
const matching = installs
|
|
227
|
+
.filter((dir) => versionMatches(versionOf(dir), ref))
|
|
228
|
+
.sort((a, b) => compareVersions(versionOf(b), versionOf(a)));
|
|
229
|
+
for (const dir of rule.refsPickNewest ? matching.slice(0, 1) : matching) {
|
|
230
|
+
kept.add(dir);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return kept;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Global packages a user installed into a version (`npm install -g`) are
|
|
238
|
+
* lost with it. Packages bundled with the runtime and caches that are
|
|
239
|
+
* installed again on demand (`rule.globalPackages.regenerable`) are not.
|
|
240
|
+
*/
|
|
241
|
+
async function blockGlobalPackages(env, rule, dir, item) {
|
|
242
|
+
if (!rule.globalPackages) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const names = [];
|
|
246
|
+
const root = env.path.join(dir, rule.globalPackages.dir);
|
|
247
|
+
for (const entry of await env.list(root)) {
|
|
248
|
+
if (entry.name.startsWith('.')) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!entry.name.startsWith('@')) {
|
|
252
|
+
names.push(entry.name);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const scoped = await env.list(entry.path);
|
|
256
|
+
names.push(...scoped.map((child) => `${entry.name}/${child.name}`));
|
|
257
|
+
}
|
|
258
|
+
const own = names
|
|
259
|
+
.filter((name) => !rule.globalPackages.regenerable(name))
|
|
260
|
+
.sort();
|
|
261
|
+
if (own.length > 0) {
|
|
262
|
+
const shown = own.slice(0, 5).join(', ');
|
|
263
|
+
const more = own.length > 5 ? ` and ${own.length - 5} more` : '';
|
|
264
|
+
block(item, `has globally installed packages: ${shown}${more}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
174
268
|
async function blockPyenvEnvironments(env, rule, dir, item) {
|
|
175
269
|
if (rule.id !== 'pyenv-python') {
|
|
176
270
|
return;
|
|
@@ -189,17 +283,21 @@ async function ruleItems(context, rule, home, pins) {
|
|
|
189
283
|
if (rule.protectAll) {
|
|
190
284
|
return [];
|
|
191
285
|
}
|
|
192
|
-
const refs =
|
|
286
|
+
const refs = await resolveAliases(env, rule, home, [
|
|
287
|
+
...(await managerRefs(env, rule, home, installs)),
|
|
288
|
+
...pins,
|
|
289
|
+
]);
|
|
193
290
|
const usages = await env.usageMany(installs, {
|
|
194
291
|
cache: !context.options.freshSizes,
|
|
195
292
|
});
|
|
196
293
|
await resolvePaths(context, installs);
|
|
197
294
|
const newest = newestInstalls(env, rule, installs, usages);
|
|
198
295
|
const versionOf = rule.versionOf ?? ((dir) => env.path.basename(dir));
|
|
296
|
+
const referenced = referencedInstalls(rule, installs, refs, versionOf);
|
|
199
297
|
const items = scanItems(context);
|
|
200
298
|
for (const dir of installs) {
|
|
201
299
|
const version = versionOf(dir);
|
|
202
|
-
if (newest.has(dir) ||
|
|
300
|
+
if (newest.has(dir) || referenced.has(dir)) {
|
|
203
301
|
continue;
|
|
204
302
|
}
|
|
205
303
|
const usage = usages.get(dir) ?? {};
|
|
@@ -222,6 +320,7 @@ async function ruleItems(context, rule, home, pins) {
|
|
|
222
320
|
: null,
|
|
223
321
|
});
|
|
224
322
|
await blockPyenvEnvironments(env, rule, dir, item);
|
|
323
|
+
await blockGlobalPackages(env, rule, dir, item);
|
|
225
324
|
const busy = await scanTimeBusy(context, item);
|
|
226
325
|
if (busy) {
|
|
227
326
|
block(item, `busy: ${busy}`);
|
package/src/timings.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wall-clock phase timings.
|
|
5
|
+
*
|
|
6
|
+
* Collected on every run, not only with `--verbose`, so reports and audit
|
|
7
|
+
* logs show which phase was slow. A phase that throws or is aborted (for
|
|
8
|
+
* example by the scan budget) still records the time it consumed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class PhaseTimer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.phases = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs `fn` and adds its duration to `name`.
|
|
18
|
+
* @template T
|
|
19
|
+
* @param {string} name
|
|
20
|
+
* @param {() => Promise<T>|T} fn
|
|
21
|
+
* @returns {Promise<T>}
|
|
22
|
+
*/
|
|
23
|
+
async measure(name, fn) {
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
try {
|
|
26
|
+
return await fn();
|
|
27
|
+
} finally {
|
|
28
|
+
this.record(name, performance.now() - started);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
record(name, ms) {
|
|
33
|
+
const phase = this.phases.get(name) ?? { phase: name, ms: 0, count: 0 };
|
|
34
|
+
phase.ms += ms;
|
|
35
|
+
phase.count++;
|
|
36
|
+
this.phases.set(name, phase);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @returns {Array<{phase: string, ms: number, count: number}>} slowest first
|
|
41
|
+
*/
|
|
42
|
+
toJSON() {
|
|
43
|
+
return [...this.phases.values()]
|
|
44
|
+
.map((phase) => ({ ...phase, ms: Math.round(phase.ms) }))
|
|
45
|
+
.sort((a, b) => b.ms - a.ms);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Measures `fn` when `timer` exists, otherwise just runs it.
|
|
51
|
+
*/
|
|
52
|
+
export function timed(timer, name, fn) {
|
|
53
|
+
return timer ? timer.measure(name, fn) : fn();
|
|
54
|
+
}
|