perf(library): send the track list as a dictionary-encoded column table
GetTracks answered with one JSON object per track: 20.5 MB at 26k tracks, ~350 bytes a row of repeated key names, four cover URLs that are identical across an album, and the album, artist and genre strings repeated on every track. Encoding it was ~170 MB of transient Go allocation, and parsing it the WebView's memory peak. GetTrackTable replaces it: one array per column, every repeated string stored once and sent as an index, genre lists interned, and only the fields the Tracks view reads. LastPlayed and the three larger cover tiers are left to the details dialog, which now fetches whole tracks by path. Each row still goes through trackFromRow, so this is an encoding of the one projection, not a second one. 167 bytes a track against 929 in the test library; a Go test holds it under a quarter of the object encoding, and a Vitest test reads the Go columns and the generated Track interface and fails if they drift. An empty library is now an empty table rather than the "no tracks in library" error the old binding returned. Closes #281
This commit is contained in:
1 parent
4a9fe1d207
commit
7a1412383f
42 files changed
+1011
-275
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '../support/fixtures.js';
|
||||
import { test, expect, libraryTracks } from '../support/fixtures.js';
|
||||
|
||||
/**
|
||||
* Now Playing on a short screen (#51).
|
||||
@@ -57,19 +57,15 @@ const REFLOW_AT = 500;
|
||||
|
||||
/** Put a track in the player, so the view has art and names to lay out. */
|
||||
async function stageATrack(page: Page): Promise<void> {
|
||||
await page.evaluate(async () => {
|
||||
const tracks = (await window.__yjEvents.call(
|
||||
'library.Library.GetTracks',
|
||||
[0],
|
||||
10_000,
|
||||
)) as { FilePath: string }[];
|
||||
const paths = (await libraryTracks(page)).slice(0, 4).map((t) => t.FilePath);
|
||||
|
||||
await page.evaluate(async (paths) => {
|
||||
await window.__yjEvents.call(
|
||||
'queue.Queue.SetQueue',
|
||||
[tracks.slice(0, 4).map((t) => t.FilePath), 0, false, { type: '', id: 0, label: '' }],
|
||||
[paths, 0, false, { type: '', id: 0, label: '' }],
|
||||
10_000,
|
||||
);
|
||||
});
|
||||
}, paths);
|
||||
}
|
||||
|
||||
/** Open the full-screen view and wait for the shell to say so. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
libraryTracks,
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
@@ -49,11 +50,7 @@ async function rectOf(app: Page, selector: string): Promise<Rect | null> {
|
||||
* three rectangles.
|
||||
*/
|
||||
async function play(app: Page): Promise<void> {
|
||||
const tracks = await callBinding<{ FilePath: string; TrackName: string }[]>(
|
||||
app,
|
||||
'library.Library.GetTracks',
|
||||
[0],
|
||||
);
|
||||
const tracks = await libraryTracks(app);
|
||||
|
||||
// `TrackName`, not `Title`: that is what the library model calls it.
|
||||
const long = tracks.find((t) => t.TrackName === LONG_TRACK);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, LONG_TRACK } from '../support/fixtures.js';
|
||||
import { test, expect, LONG_TRACK, libraryTracks } from '../support/fixtures.js';
|
||||
|
||||
/**
|
||||
* The phone shell (plan 016 B2, phase 1).
|
||||
@@ -235,15 +235,8 @@ test.describe('the shell on a phone', () => {
|
||||
// nothing in particular and picked a 2-second track, which had
|
||||
// finished before the assertions ran. The placeholder check below
|
||||
// is what actually holds the property this test needs.
|
||||
const started = await app.evaluate(async (longTitle) => {
|
||||
const tracks = (await window.__yjEvents.call(
|
||||
'library.Library.GetTracks',
|
||||
[0],
|
||||
10_000,
|
||||
)) as { FilePath: string; TrackName: string }[];
|
||||
|
||||
const bare = tracks.find((t) => t.TrackName === longTitle);
|
||||
|
||||
const bare = (await libraryTracks(app)).find((t) => t.TrackName === LONG_TRACK);
|
||||
const started = await app.evaluate(async (bare) => {
|
||||
if (!bare) return null;
|
||||
|
||||
await window.__yjEvents.call(
|
||||
@@ -254,7 +247,7 @@ test.describe('the shell on a phone', () => {
|
||||
await window.__yjEvents.call('queue.Queue.Play', [], 5_000);
|
||||
|
||||
return bare.TrackName;
|
||||
}, LONG_TRACK);
|
||||
}, bare);
|
||||
|
||||
expect(started).toBe(LONG_TRACK);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
libraryTracks,
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
@@ -30,18 +31,7 @@ async function order(app: Page): Promise<string[]> {
|
||||
}
|
||||
|
||||
async function queueFourAndOpen(app: Page): Promise<string[]> {
|
||||
const paths: string[] = await app.evaluate(async () => {
|
||||
// One argument, and 0 means every library: the scoped and
|
||||
// unscoped list queries collapsed into one when the schema did
|
||||
// (plan 013 R3), so `GetTracks()` no longer exists to call.
|
||||
const tracks = await window.__yjEvents.call(
|
||||
'library.Library.GetTracks',
|
||||
[0],
|
||||
10_000,
|
||||
);
|
||||
|
||||
return (tracks as { FilePath: string }[]).slice(0, 4).map((t) => t.FilePath);
|
||||
});
|
||||
const paths = (await libraryTracks(app)).slice(0, 4).map((t) => t.FilePath);
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [paths, 0, false, NO_QUEUE_SOURCE]);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
libraryTracks,
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
@@ -86,15 +87,11 @@ const selected = (app: Page) =>
|
||||
* is read back.
|
||||
*/
|
||||
async function queueSixAndOpen(app: Page): Promise<void> {
|
||||
const paths = await app.evaluate(async (longTitle) => {
|
||||
// `TrackName`, not `Title`: the library model names it after the
|
||||
// tag, and the *queue* is what calls it `title`.
|
||||
const tracks = (await window.__yjEvents.call(
|
||||
'library.Library.GetTracks',
|
||||
[0],
|
||||
10_000,
|
||||
)) as { FilePath: string; TrackName: string; Album: string }[];
|
||||
|
||||
// `TrackName`, not `Title`: the library model names it after the
|
||||
// tag, and the *queue* is what calls it `title`.
|
||||
const tracks = await libraryTracks(app);
|
||||
const longTitle = LONG_TRACK;
|
||||
const paths = (() => {
|
||||
const long = tracks.find((t) => t.TrackName === longTitle);
|
||||
|
||||
/**
|
||||
@@ -131,7 +128,7 @@ async function queueSixAndOpen(app: Page): Promise<void> {
|
||||
long!.FilePath,
|
||||
...rest.slice(3).map((t) => t.FilePath),
|
||||
];
|
||||
}, LONG_TRACK);
|
||||
})();
|
||||
|
||||
await callBinding(app, 'queue.Queue.SetQueue', [
|
||||
paths,
|
||||
|
||||
Reference in new issue
Block a user