Repository navigation
refactor(locallibrary): split the player's ui.rs and app.rs by view - #462
Queued
botandbang wants to merge 5 commits into
Queued
botandbang wants to merge 5 commits into
botandbang wants to merge 5 commits into
Conversation
The player is about to be redrawn view by view, so what a frame costs needs a number that can't drift. A test draws frames of the Library view, the full-screen deck and the Drives view at 160x45, with the 3,886-track fixture open, a demo track playing with its real analysis into a test mixer, and the playhead moved a fixed step a frame. The `unit` binary gets a counting allocator, so the `budgets` job holds what a frame allocates to benchmarks/budgets/tui.json on every pull request. The release-budgets action takes a frame's instructions under cachegrind, from a release-profile test binary, as a run of 600 frames less a run of none. That's a second release build, so the two jobs that run the action get an hour. `TrackInfo`'s fields open up to the tui module for the test, and the guard against opening a browser from a test is now only compiled with debug assertions, where the function it calls exists, so the test binary builds in the release profile.
This is a pure move, with no change in behaviour. ui.rs (3,100 lines) and app.rs (4,000) held every view, so each design change would have touched them both. ui.rs becomes chrome, library, player, drives and overlays, and app.rs becomes background, chrome, drives, input, library, loaded, mouse, player, playing and saved, with one `impl App` per file and the `App` struct left in the root. The code moved as it was. What changed with it: `pub(super)` became `pub(in crate::tui)` and `super::x` became `crate::tui::x` where a file moved a level down, items another file now needs got `pub(super)`, imports went to the files that use them, and the section banners went, as each file is one section now. The test modules are untouched, so the roots import what they find through `use super::*`.
docs/player.md's key table was kept by hand. It's now generated from KEYMAP, a table per view and group, and a test fails if the file differs and says how to update it. The same test module writes each view's screenshot at 80x24 and 160x45 as SVG, from the buffer the tests draw. player.md links to them rather than embedding them, as the website's Markdown doesn't carry images over, and its markers are Markdown comments because it shows HTML comments as text. GLOSSARY.md gets the words the player's design work uses: Drive, Snapshot, History, Prepare, Crate, Deck, Pads, Recording, Panel and Suggestion, and a note that sign in and sign out are what the player and docs say, with `login` and `logout` kept as the DJ account's CLI subcommands.
The split touched every line of the player's ui.rs and app.rs, and some of those lines had no test of their own: the player starting on a saved view or a drive named on the command line, the Drives view reading status, a drive plugged in reaching plugins, fields that can't be read, clicks in the details panel and the sidebar, matches from a newer read of the drive, an output that panics as it opens, and parts of the details panel, the plays column and an export's confirmation. A few only ran when a property test happened to draw them. These tests run them from the outside, with properties where a rule holds for every input. They start the real `App::new` with `LOCALLIBRARY_AUDIO=silent` and `LOCALLIBRARY_VOLUMES` pointing at a temporary folder, so they touch neither the machine's drives nor its audio devices. Whether the terminal has 24-bit colour is read once a process, so that test runs a process per terminal. No production code changes. Fixes LOC-161
A pull request only counted what a frame allocates, so a frame that got slower without allocating more had to wait for the weekly release-profile run. The `budgets` job now also runs the frame test under cachegrind, from the debug `unit` binary in its archive, and holds that to benchmarks/budgets/tui-debug-instructions.json. The binary is unoptimised and built for coverage, so these numbers catch a regression and don't say what a user's frame costs. The shell moves out of the release-budgets action into frame-instructions.sh beside it, and both use it. It takes the views from the budget file and runs them side by side, because reading the fixture takes most of a debug run: six runs in a row would use up about 3 minutes on a runner before counting a frame. Setup polls a clock, so the numbers move by under 0.02% between runs, well inside the file's 1%. The limits stay null until CI's first run measures them on its own architecture.
SizeCompared with main. KiB are 1,024 bytes.
|
bnb
approved these changes
Nov 5, 2026
bnb
approved these changes
Nov 5, 2026
Rust coverageFrom run 382. Both are held to the thresholds in .cargo/config.toml.
|
botandbang
added this pull request to the merge queue
Nov 5, 2026
Any commits made after this event will not be merged.
bnb
approved these changes
Nov 5, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Fixes LOC-161.
apps/locallibrary/src/tui/ui.rs(about 3,100 lines) andapp.rs(about 4,000) hold every view, so every design change would touch both. This splits them by view with no change in what's drawn or how it behaves. It also adds a budget for what a frame costs, so the work that follows has a number to hold to, and bringsdocs/player.mdandGLOSSARY.mdup to date.Added
apps/locallibrary/src/tui/frames/tests.rs: draws frames of the Library view, the full-screen deck and the Drives view at 160x45, with the 3,886-track fixture open, a demo track playing with its real analysis into a test mixer, and the playhead moved a fixed step a frame. No wall clock and no worker decides what's drawn.src/unit.rs, the binary's unit tests, so nothing is added to the shipped binary.benchmarks/budgets/tui.json(allocations and bytes per frame, on every pull request that runs the Linux jobs) andbenchmarks/budgets/tui-instructions.json(instructions per frame under cachegrind, in.github/actions/release-budgets). The second starts asnulluntil the first run on the real runner.benchmarks/budgets/tui-debug-instructions.json(instructions per frame under cachegrind in the debug profile, on every pull request that runs the Linux jobs), measured by thebudgetsjob over 200 frames from its archive'sunitbinary. These catch a regression and aren't what a user's frame costs: the binary is unoptimised and built for coverage. Its limits start asnulltoo..github/actions/release-budgets/frame-instructions.sh: the shell that measures a frame, used by that action and thebudgetsjob. It takes the views from the budget file and runs them side by side.apps/locallibrary/src/tui/docs/tests.rs(tui/docs.rsandtui/frames.rshold the two test modules): generates the key tables indocs/player.mdfromKEYMAPand writesdocs/player/*.svg, each of Library, Drives, Snapshots, Profile and Mixes at 80x24 and 160x45, dark and truecolor, from the test backend's buffer. Each test fails when a file differs and prints the command that updates it (LOCALLIBRARY_BLESS=1, as the repository's other generated files do).tui/app/tests.rs,tui/ui/tests.rsandtui/theme/tests.rs, 17 of them Hegel properties, for what the moved files had no test of.tui/audio/tests.rsgainsseek_fails, which leaves a track as a seek it couldn't make does, for what the player makes of that.Changed
ui.rsis nowui/{chrome,library,player,drives,overlays}.rsandapp.rsisapp/{background,chrome,drives,input,library,loaded,mouse,player,playing,saved}.rs, with oneimpl Appper file. The roots keepdraw,Appand the view enums. The second commit is a pure move: outside imports,usepaths, visibility and the section banners it dropped, a line-by-line comparison finds only two signatures rustfmt rewrapped because their visibility got longer. No test file changed.docs/player.md's hand-kept key table is replaced by generated tables, one per view and group.release-budgetsand the weeklybudgetsjob get 60 minutes in place of 30, for the second release build.budgetsjob's filter also runstui::frames::tests, it extracts its archive to a folder the next step reads, and it installs valgrind.never_openexists, so theunitbinary builds in the release profile. Every test but the frame test still runs with debug assertions.TrackInfo's fields are visible to the tui module, for the frame test.Removed
The hand-kept key table.
How it was tested
Rendering. A temporary hook, not in this PR, wrote every cell (symbol, colours, modifiers) of every frame the
tui::tests draw to a file per test, as drawn and again in dark, light and plain, four runs on each side with Hegel seeded. It normalised temporary folder names, clock times, drive and snapshot ids and the rows showing the machine's real free space, and held the animation clock at zero on both sides.Frame cost. Allocations and bytes per frame are identical before and after. On macOS, 5 runs each gave the same numbers:
The limits in
tui.jsoncome from 5 identical runs in a Linux container (rust:1.96.1-bookworm, aarch64) on this branch: the same allocations, with 147,577, 109,882 and 67,249 bytes, a temporary folder's path being shorter there. Loweringlibrary.allocationsto 1,200 failstools/budgets/cli.ts checkwith1251 allocations is over its limit of 1200.Instructions per frame, from the release-profile test binary under
/usr/bin/time -lon macOS, medians of 5 runs of 600 frames less 5 of none:The 600-frame runs spread by about 0.2%. Valgrind isn't available on macOS, so those aren't cachegrind's numbers, and the release-profile step in
.github/actions/release-budgetsfirst runs on this PR. It uses the shell below, and its build line was run as written.Debug frame instructions. The
budgetsjob's two steps ran as written in a Linux container (rust:1.96.1-bookworm, aarch64, valgrind 3.19.0) limited to 4 CPUs, with notarget/in the checkout, as on CI. The archive came fromcargo llvm-cov nextest-archive -p locallibrary-cli --test unit, the build job's command for that one package, and the filter left outbinary(counters), which that archive doesn't hold. Five runs of 200 frames:The runs don't match exactly, because the test's wait for the fixture's workers polls a clock. A run of 20 frames was 0.5% off on the deck, which is why the step uses 200. Lowering
libraryto 63,900,000 failstools/budgets/cli.ts checkwith65206801 instructions is over its limit of 63900000,ratchetsets thenulllimits, andtui.jsonhas the same hash before and after the step. A binary that fails stops the step with exit 123.The step took 44 s with the machine otherwise quiet, and 65 to 91 s beside other builds; a fifth run's time is left out, as the Mac slept through it. A
release-budgetsjob ran 2,632 million instructions under cachegrind in 15.45 s, about 3.2 times slower than here, which puts the step at about 2.5 minutes on a runner. That's an estimate. CI is x86_64 with a newer valgrind, so its instruction counts will differ from these, and its first run on this PR gives the real time and sets the limits. Neither x86_64 nor the release-profile binary went through this shell here.Startup. Medians of 15 runs at 120x40 with the two release binaries alternating, before then after:
The release binary is 53,388,272 bytes on both sides.
Coverage of the moved files. The lcov of
cargo coverageleaves 14 lines uncovered in them, none of which a test can reach:app/input.rs:200:Act::Noneis only built by keymap entries with no keys and no name, so no key or palette item performs it.app/input.rs:528andapp/mouse.rs:219:activate's last arm andclick_row'sView::Profile | View::Mixesarm. Both are those two views, and every caller stops at them first.app/player.rs:337: onlyopened_nextqueues a track on the audio side and it marks the upcoming track queued, and every path that drops that track drops the queued audio too.app/player.rs:406-407and534-536:start_currentis false only with no track at the queue's position, whichplaying_queueandnext_poshave just ruled out.ui/overlays.rs:73:Menu::sub_itemsonly holdsRateandColour.tui/mod.rs:81, 141, 142, 146: these need a real terminal, forrun's exit message and forevent_loop's mouse and other events.Checks. These ran before this branch's last merge of main, so CI is the gate on the final tree.
cargo fmt --check, clippy for the workspace and for--no-default-features --features tui,account,cargo coverage(4,342 tests passed; 97.77% lines, 96.69% functions, 96.01% regions),tools/hegel-coverage.sh(1,535 tests passed; two runs read 95.60 / 94.13 / 93.66 and 95.64 / 94.19 / 93.73 for lines, functions and regions, against 95.56 / 94.24 / 93.60 before the new tests. Hegel's draws aren't seeded: those two runs of the same code differ on 409 functions in the lcov, mostly outside the player),tools/policy.sh,ast-grep scan,ast-grep test --skip-snapshot-tests,pnpm test:tools(442 tests passed),actionlint,zizmorandshellcheck. No existing test changed. The new tests were each broken once: an edited row inplayer.mdand an edited colour in a screenshot fail with the update command, and a wrong expected string fails the frame test. The 34 coverage tests were each seen failing at their own assertion, with the line each one covers changed. The exception is the arm of the replay check for a drive with no export: the plan fails there either way, so that test was broken by forcing an offer instead. The screenshots come out the same on macOS and Linux and under another timezone, and two rendered through QuickLook look like the player.Checklist
docs/and--helpstill say what the code does