All four collections were fetched at DOMContentLoaded and refetched on every invalidation, whichever view was showing. On a 26 138-track library the track list was 20.5 MB of that, and encoding it cost the backend ~170 MB of transient allocation — paid by someone looking at Home, which draws none of it. - The store warms only albums, artists and genres, on idle after first paint: 1.6 MB together, and what made those views instant. - A nav item prefetches on hover and on keyboard focus, which is the ~100 ms before the click that a cold open would otherwise wait. - An invalidation refetches what something had loaded, and nothing else — a scan no longer loads the track list of a library whose Tracks view nobody has opened. - `index.html`'s first-paint `<track-list>` is `view-hidden`, and `index.ts` no longer activates it: it is markup, not a decision about which view the launch lands on, and activating it was what fetched the whole list for a landing on Home. The track list itself loads on view activation, which the shell drives. Measured on 50 000 tracks: backend RSS at rest 543 → 296 MB, peak 571 → 296 MB, Go heap held 361 → 125 MB, JS heap 31.8 → 18 MB, binding bytes at rest 35.9 → 12.1 MB, heap after a browse 36.5 → 22.7 MB. Tracks first open 26 ms; slowest view open 57 ms. Closes #280
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
/**
|
|
* #280: the app loads the library when a view needs it, not at startup.
|
|
*
|
|
* Every collection used to be fetched together at `DOMContentLoaded`
|
|
* and refetched on every invalidation, whether or not anything was
|
|
* showing it. On a 26 138-track library the track list was 20.5 MB of
|
|
* that and cost the backend ~170 MB of transient allocation — for
|
|
* someone looking at Home, which draws none of it.
|
|
*
|
|
* **This file must not load the track list before the first test.** It
|
|
* asserts the state the app is actually left in by its own startup, so
|
|
* a test added above that asks for tracks would invalidate the
|
|
* precondition rather than silently pass.
|
|
*/
|
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
|
|
import { libraryStore } from '@store/library-store';
|
|
import { Events } from '../../src/events';
|
|
import {
|
|
calls,
|
|
emit,
|
|
flush,
|
|
resetHarness,
|
|
stub,
|
|
} from '@test/support/harness';
|
|
|
|
const TRACKS = 'library.Library.GetTrackTable';
|
|
const SMALL = [
|
|
'library.Library.GetAlbums',
|
|
'library.Library.GetArtists',
|
|
'library.Library.GetGenres',
|
|
];
|
|
|
|
/**
|
|
* Wait for the store's own idle warm-up to land.
|
|
*
|
|
* It asks for the three small collections on `requestIdleCallback`, so
|
|
* when it has happened is the browser's decision — waiting for the
|
|
* effect rather than for a duration is the only way this is not a race.
|
|
*/
|
|
function stubReads(): void {
|
|
stub(TRACKS, { strings: [''] });
|
|
for (const path of SMALL) stub(path, []);
|
|
}
|
|
|
|
async function waitForWarm(): Promise<void> {
|
|
for (let i = 0; i < 200 && libraryStore.getCachedAlbums() === null; i++) {
|
|
await flush();
|
|
}
|
|
|
|
expect(libraryStore.getCachedAlbums(), 'the warm-up ran').not.toBeNull();
|
|
}
|
|
|
|
describe('what the app loads on its own (#280)', () => {
|
|
beforeEach(async () => {
|
|
stubReads();
|
|
await waitForWarm();
|
|
// resetHarness clears the stubs as well as the recorded calls.
|
|
resetHarness();
|
|
stubReads();
|
|
});
|
|
|
|
it('leaves the track list alone', () => {
|
|
expect(libraryStore.getCachedTracks()).toBeNull();
|
|
expect(calls(TRACKS)).toEqual([]);
|
|
});
|
|
|
|
it('does not load it to answer a scan', async () => {
|
|
emit(Events.LibraryScanComplete);
|
|
await flush();
|
|
|
|
// The small collections are in use, so they are refreshed; the
|
|
// track list nobody has opened is not.
|
|
expect(calls().map((c) => c.path).sort()).toEqual([...SMALL].sort());
|
|
});
|
|
|
|
it('loads it for the view that draws it, and refreshes it from then on', async () => {
|
|
libraryStore.prefetch('tracks');
|
|
await flush();
|
|
|
|
expect(calls(TRACKS)).toHaveLength(1);
|
|
expect(libraryStore.getCachedTracks()).not.toBeNull();
|
|
|
|
resetHarness();
|
|
emit(Events.LibraryScanComplete);
|
|
await flush();
|
|
|
|
expect(calls(TRACKS), 'in use now, so a scan refreshes it').toHaveLength(1);
|
|
});
|
|
|
|
it('is asked for nothing by a prefetch for a view it has no data for', async () => {
|
|
libraryStore.prefetch('home');
|
|
libraryStore.prefetch('settings');
|
|
libraryStore.prefetch('explore');
|
|
await flush();
|
|
|
|
expect(calls()).toEqual([]);
|
|
});
|
|
});
|