Four small modules the views below adopt: - `lru-map.ts` — a Map re-inserted on read and trimmed from the front. `explore-view` never unmounts and its two art caches were plain Maps: twenty-four searches retained 20.58 MB and were still accelerating, a cover thumbnail being ~27 kB of base64 and an artist photo ~128 kB. - `cache-stats.ts` — a bound has to stay checkable, so caches register and `window.__yjCacheStats()` reports entries, retained chars and cap in one eval, rather than the next session having to rebuild the twenty-four-search reproduction first. - `track-index.ts` — a WeakMap from the tracks array's identity to a Map<FilePath, Track>. Five components turned selected file paths back into tracks with `filePaths.map(fp => tracks.find(...))`, so "Select all -> Edit tags" at 50 000 tracks blocked the main thread for 3.0 to 6.3 s. 68 ms after. Keying on the array's identity is safe for the same reason the memoized filter caches are, and it is collected for free when the store drops the array. - `lazy-track-details.ts` — one memoised dynamic import, because `track-details` (42 kB) was imported for side effect by all five components that open it and so was evaluated before first paint however the routes were split.
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
/**
|
|
* A registry of the app's in-memory caches, for measurement.
|
|
*
|
|
* `perf.M7`/`M8` are retention findings, and retention is only ever
|
|
* credible as a number: the two Explore caches did not show as heap
|
|
* growth in two separate sessions of a ten-view browse script, because
|
|
* that script *visits* Explore and never types in it, so the caches
|
|
* stayed empty. What made them real was a session that searched twelve
|
|
* times and watched the heap climb 9 MB monotonically.
|
|
*
|
|
* The lesson is that a bound needs a way to be *checked*, not just
|
|
* written — otherwise the next session has to rediscover the same
|
|
* reproduction before it can tell whether the LRU still holds. A cache
|
|
* registers itself here and `window.__yjCacheStats()` reports every one
|
|
* in a single eval, which is what `e2e/perf/measure.mjs` reads.
|
|
*
|
|
* This follows `src/icons/index.ts`'s `__yjIconMisses`: an unconditional
|
|
* measurement surface, costing one function and one Map, rather than a
|
|
* dev-only build branch that is therefore never exercised in the build
|
|
* that ships.
|
|
*/
|
|
|
|
/** What one cache reports about itself. */
|
|
export interface CacheStat {
|
|
/** Number of live entries. */
|
|
entries: number;
|
|
/** Total length of the strings retained, where the cache holds strings. */
|
|
chars: number;
|
|
/** The cap, so a reading can be read against its bound. */
|
|
limit: number;
|
|
}
|
|
|
|
const probes = new Map<string, () => CacheStat>();
|
|
|
|
declare global {
|
|
interface Window {
|
|
__yjCacheStats?: () => Record<string, CacheStat>;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a cache under a stable name. Re-registering the same name
|
|
* replaces the probe, which is what a cached view remounting in a test
|
|
* needs.
|
|
*/
|
|
export function registerCacheProbe(name: string, probe: () => CacheStat): void {
|
|
probes.set(name, probe);
|
|
}
|
|
|
|
/** Drop a probe — a per-instance cache going away with its host. */
|
|
export function unregisterCacheProbe(name: string): void {
|
|
probes.delete(name);
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.__yjCacheStats = () => {
|
|
const out: Record<string, CacheStat> = {};
|
|
|
|
for (const [name, probe] of probes) {
|
|
try {
|
|
out[name] = probe();
|
|
} catch {
|
|
// A probe must never be able to break a measurement run.
|
|
}
|
|
}
|
|
|
|
return out;
|
|
};
|
|
}
|