pi-cache-optimizer 2.8.25 → 2.8.27

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.
@@ -345,9 +345,75 @@ type CachedShardRead = {
345
345
  ino: number | bigint;
346
346
  size: number;
347
347
  mtimeMs: number;
348
+ /** Valid shard whose embedded instanceId matches its filename. */
348
349
  shard: PersistedStatsShardV7 | undefined;
350
+ /** Schema-valid parse regardless of filename match (used by retention). */
351
+ parsed: PersistedStatsShardV7 | undefined;
349
352
  };
350
353
 
354
+ /**
355
+ * Upper bound on concurrent filesystem operations during shard scans. Startup
356
+ * on Windows (antivirus-scanned AppData) was dominated by thousands of
357
+ * serialized lstat/readFile awaits; a small pool hides per-call latency
358
+ * without opening an unbounded number of descriptors.
359
+ */
360
+ export const SHARD_IO_CONCURRENCY = 16;
361
+
362
+ /** Run `worker` over `items` with at most `limit` in flight; results keep input order. */
363
+ export async function mapWithConcurrency<T, R>(
364
+ items: readonly T[],
365
+ limit: number,
366
+ worker: (item: T, index: number) => Promise<R>,
367
+ ): Promise<R[]> {
368
+ const results = new Array<R>(items.length);
369
+ let next = 0;
370
+ const run = async (): Promise<void> => {
371
+ while (next < items.length) {
372
+ const index = next++;
373
+ results[index] = await worker(items[index] as T, index);
374
+ }
375
+ };
376
+ const pool = Math.max(1, Math.min(limit, items.length));
377
+ await Promise.all(Array.from({ length: pool }, run));
378
+ return results;
379
+ }
380
+
381
+ type ShardFileRead = {
382
+ info: import("node:fs").Stats;
383
+ /** Valid shard with matching filename instanceId. */
384
+ shard: PersistedStatsShardV7 | undefined;
385
+ /** Schema-valid parse regardless of filename match. */
386
+ parsed: PersistedStatsShardV7 | undefined;
387
+ };
388
+
389
+ /**
390
+ * Stat a shard file and return its parsed contents, re-reading only when the
391
+ * inode/size/mtime changed. Returns undefined for non-regular files. Throws on
392
+ * read/parse failure (after dropping any cache entry) so callers can apply
393
+ * their own policy.
394
+ */
395
+ async function readShardFileCached(path: string, name: string): Promise<ShardFileRead | undefined> {
396
+ try {
397
+ const info = await lstat(path);
398
+ if (!info.isFile() || info.isSymbolicLink()) {
399
+ shardReadCache.delete(path);
400
+ return undefined;
401
+ }
402
+ const cached = shardReadCache.get(path);
403
+ if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
404
+ return { info, shard: cached.shard, parsed: cached.parsed };
405
+ }
406
+ const filenameInstanceId = name.slice(0, -".json".length);
407
+ const parsed = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
408
+ const shard = parsed && parsed.instanceId === filenameInstanceId ? parsed : undefined;
409
+ shardReadCache.set(path, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, shard, parsed });
410
+ return { info, shard, parsed };
411
+ } catch (error) {
412
+ shardReadCache.delete(path);
413
+ throw error;
414
+ }
415
+ }
416
+
351
417
  /**
352
418
  * Parsed-shard cache keyed by path and validated by inode, size, and mtime.
353
419
  * Writers always publish through temp-file + rename, so an updated shard gets
@@ -371,37 +437,22 @@ export async function readValidStatsShardsV7(directory: string = SHARD_FILES_DIR
371
437
  if (getErrorCode(error) === "ENOENT") return [];
372
438
  throw error;
373
439
  }
374
- const shards: PersistedStatsShardV7[] = [];
375
440
  const seenPaths = new Set<string>();
376
- for (const name of names) {
377
- if (!/^[0-9a-f-]+\.json$/i.test(name)) continue;
378
- const path = join(directory, name);
379
- seenPaths.add(path);
441
+ const candidates = names.filter((name) => /^[0-9a-f-]+\.json$/i.test(name));
442
+ for (const name of candidates) seenPaths.add(join(directory, name));
443
+ const reads = await mapWithConcurrency(candidates, SHARD_IO_CONCURRENCY, async (name) => {
380
444
  try {
381
- const info = await lstat(path);
382
- if (!info.isFile() || info.isSymbolicLink()) {
383
- shardReadCache.delete(path);
384
- continue;
385
- }
386
- const cached = shardReadCache.get(path);
387
- let parsed: PersistedStatsShardV7 | undefined;
388
- if (cached && cached.ino === info.ino && cached.size === info.size && cached.mtimeMs === info.mtimeMs) {
389
- parsed = cached.shard;
390
- } else {
391
- const filenameInstanceId = name.slice(0, -".json".length);
392
- const candidate = parsePersistedStatsShardV7(JSON.parse(await readFile(path, "utf8")));
393
- parsed = candidate && candidate.instanceId === filenameInstanceId ? candidate : undefined;
394
- shardReadCache.set(path, { ino: info.ino, size: info.size, mtimeMs: info.mtimeMs, shard: parsed });
395
- }
396
- // Callers may mutate the aggregate they build; hand out copies so the
397
- // cache stays authoritative.
398
- if (parsed) shards.push(cloneShard(parsed));
445
+ return (await readShardFileCached(join(directory, name), name))?.shard;
399
446
  } catch {
400
447
  // Ignore malformed or transiently unavailable shards. Atomic writers will
401
448
  // publish a complete replacement on the next successful update.
402
- shardReadCache.delete(path);
449
+ return undefined;
403
450
  }
404
- }
451
+ });
452
+ // Callers may mutate the aggregate they build; hand out copies so the cache
453
+ // stays authoritative. Preserve directory order for deterministic ties.
454
+ const shards: PersistedStatsShardV7[] = [];
455
+ for (const shard of reads) if (shard) shards.push(cloneShard(shard));
405
456
  for (const path of Array.from(shardReadCache.keys())) {
406
457
  if (!seenPaths.has(path) && path.startsWith(directory)) shardReadCache.delete(path);
407
458
  }
@@ -491,67 +542,76 @@ export async function cleanupStatsShardsV7(now = Date.now(), directory: string =
491
542
  return getErrorCode(error) === "ENOENT" ? 0 : Promise.reject(error);
492
543
  }
493
544
  const today = currentLocalDay();
494
- let removed = 0;
495
- for (const name of names) {
496
- const isShard = /^[0-9a-f-]+\.json$/i.test(name);
497
- const isTemp = name.endsWith(".tmp");
498
- if (!isShard && !isTemp) continue;
545
+ const candidates = names.filter((name) => /^[0-9a-f-]+\.json$/i.test(name) || name.endsWith(".tmp"));
546
+ const outcomes = await mapWithConcurrency(candidates, SHARD_IO_CONCURRENCY, async (name): Promise<number> => {
547
+ const isTemp = !/^[0-9a-f-]+\.json$/i.test(name);
499
548
  const path = join(directory, name);
500
549
  try {
501
- const info = await lstat(path);
502
- if (!info.isFile() || info.isSymbolicLink()) continue;
503
550
  if (isTemp) {
504
- if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) continue;
551
+ const info = await lstat(path);
552
+ if (!info.isFile() || info.isSymbolicLink()) return 0;
553
+ if (now - info.mtimeMs < SHARD_TEMP_RETENTION_MS) return 0;
505
554
  } else {
506
- const text = await readFile(path, "utf8");
507
- let parsed: PersistedStatsShardV7 | undefined;
555
+ let read: ShardFileRead | undefined;
556
+ let mtimeMs: number;
508
557
  try {
509
- parsed = parsePersistedStatsShardV7(JSON.parse(text));
510
- } catch {
558
+ read = await readShardFileCached(path, name);
559
+ if (!read) return 0;
560
+ mtimeMs = read.info.mtimeMs;
561
+ } catch (error) {
511
562
  // A corrupt JSON shard has no usable lifecycle metadata. Apply the
512
563
  // same mtime retention as schema-invalid shards instead of keeping
513
564
  // it forever; read failures still leave the file untouched.
565
+ if (!(error instanceof SyntaxError)) return 0;
566
+ const info = await lstat(path);
567
+ if (!info.isFile() || info.isSymbolicLink()) return 0;
568
+ mtimeMs = info.mtimeMs;
514
569
  }
515
- if (parsed?.day === today) continue;
516
- const updatedAt = parsed?.lifecycle.updatedAt ?? info.mtimeMs;
517
- if (now - updatedAt < SHARD_RETENTION_MS) continue;
518
- if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) continue;
570
+ const parsed = read?.parsed;
571
+ if (parsed?.day === today) return 0;
572
+ const updatedAt = parsed?.lifecycle.updatedAt ?? mtimeMs;
573
+ if (now - updatedAt < SHARD_RETENTION_MS) return 0;
574
+ if (parsed?.lifecycle.state === "active" && isProcessAlive(parsed.process.pid)) return 0;
519
575
  }
520
576
  await unlink(path);
521
- removed += 1;
577
+ shardReadCache.delete(path);
578
+ return 1;
522
579
  } catch {
523
580
  // Best-effort maintenance must never block the extension.
581
+ return 0;
524
582
  }
525
- }
526
- return removed;
583
+ });
584
+ return outcomes.reduce((sum, value) => sum + value, 0);
527
585
  }
528
586
 
529
- export async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<void> {
587
+ /** Run due retention maintenance; resolves to the number of files removed (0 when skipped). */
588
+ export async function maybeCleanupStatsShardsV7(now = Date.now()): Promise<number> {
530
589
  await mkdir(SHARD_MAINTENANCE_DIR, { recursive: true });
531
590
  try {
532
591
  const marker = await stat(SHARD_CLEANUP_MARKER_PATH);
533
- if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return;
592
+ if (now - marker.mtimeMs < SHARD_CLEANUP_INTERVAL_MS) return 0;
534
593
  } catch {}
535
594
 
536
595
  try {
537
596
  await mkdir(SHARD_CLEANUP_LOCK_PATH);
538
597
  } catch (error) {
539
- if (getErrorCode(error) !== "EEXIST") return;
598
+ if (getErrorCode(error) !== "EEXIST") return 0;
540
599
  try {
541
600
  const lock = await stat(SHARD_CLEANUP_LOCK_PATH);
542
- if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return;
601
+ if (now - lock.mtimeMs <= SHARD_CLEANUP_LOCK_STALE_MS) return 0;
543
602
  await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
544
603
  await mkdir(SHARD_CLEANUP_LOCK_PATH);
545
604
  } catch {
546
- return;
605
+ return 0;
547
606
  }
548
607
  }
549
608
 
550
609
  try {
551
- await cleanupStatsShardsV7(now);
610
+ const removed = await cleanupStatsShardsV7(now);
552
611
  const tempPath = `${SHARD_CLEANUP_MARKER_PATH}.${process.pid}.${randomUUID()}.tmp`;
553
612
  await writeFile(tempPath, `${now}\n`, "utf8");
554
613
  await rename(tempPath, SHARD_CLEANUP_MARKER_PATH);
614
+ return removed;
555
615
  } finally {
556
616
  await rm(SHARD_CLEANUP_LOCK_PATH, { recursive: true, force: true });
557
617
  }