`scripts/session-stats.py` turns every `startup*.log` into one row per FramePump session: tier, scene, measured fps, drops, stalls, average/worst render, capture rate, and the build — with the sink marked, because that is what makes the numbers quotable: - **RECORD** — machine only, no socket. The cleanest minimum-config evidence. - **TEST** — machine + socket, no audience; recognised by `Test session dock open` / `Test chat simulation`, which the creator pointed out are recorded too. - **LIVE** — network and audience included. - **UNKNOWN** — no marker; not evidence. Verdicts use the average render against the frame budget, not the worst: the worst is dominated by the 100 ms first-frame bake (a session that "holds" can still show worst 35 ms). `holds` = no drops, no stalls, average under half the budget. First run over the existing logs already separates the two halves of TASK 33: static Starting scenes render at ~1.5 ms, the same pump with display capture running at 35–46 ms — the capture path, not the overlays, is where a budget streamer loses the frame budget.
11 KiB
Environment, build & tests
Lookup material: LAN hosts, the Windows build commands, where the DB/auth/log live, and the test suite map. Read when you are about to build, run, or add a test — not on every session.
Paths and secrets belong with the runbook in
../../HANDOFF.md→ "Secrets / paths" until the queue trim moves them here.
LAN infrastructure (192.168.50.x)
All LAN credentials live in CREDENTIALS.md (gitignored). Server IPs and roles:
| Host | IP | Role |
|---|---|---|
| corsair | .132 | Gaming/streaming primary — RTX 5070 Ti (this machine) |
| llamavault | .87 | ODROID-HC4, 12TB RAID5, media storage (NFS). SSH: ssh mshallop@192.168.50.87. DHCP-bound — it moved off .86 after the 2026-10 power outage, so confirm with a ping before trusting any path. SMB is reached by the llamavault name, pinned in the Windows hosts file, never by literal IP: \\llamavault\llamaVault\... |
| pivault | .158 | RPi5 8GB, 10.83TB RAID5 NAS |
| jarvis | .210 | AMD Ryzen 5 5600X, RX 6600 XT — AI inference, SearXNG, Docker registry, Immich |
| ultron | .108 | AMD Ryzen 7 7840HS — AI orchestrator, llama-server, Qdrant |
| gordito | .144 | Home Assistant, Alarmo, Zigbee |
| officerfriendly | .26 | Pi-hole DNS |
| octopi | .197 | OctoPrint (Prusa MK4) |
| BigBlinkyRouter | .1 | ASUS ROG GT-BE98 Pro, WiFi 7 |
Web infra (DO droplet): llamacasty.com + llamachile.tube at 143.244.176.131.
Pushing to the Gitea instance (llgit.llamachile.tube). The repo is gramps/LlamaCasty
(renamed from gramps/ytLlive; the old path 301-redirects). Push over HTTPS, not SSH:
origin = https://llgit.llamachile.tube/gramps/LlamaCasty.git, with the credential helper
/home/gramps/bin/gitea-credential (registered as
credential.https://llgit.llamachile.tube.helper) reading the account password from the
gitignored CREDENTIALS.md on demand — nothing stored in plaintext, nothing in a command line.
The Gitea account password IS the portal password in CREDENTIALS.md, and the same password
authenticates the REST API (curl --config …/api/v1/user) — that API is the fastest way to ask
the server questions (the repo's real name and clone URL, the account's SSH keys, a token).
⛔ The SSH endpoint on llgit:2214 is not a usable push path: the account's SSH keys ARE
registered (key id 6 "Corsair VSC" = this machine's ~/.ssh/id_ed25519) yet the server refuses
the publickey, and the port later refused connections outright. Do not spend a session on key
archaeology — ask the API, then push over HTTPS.
Run
First time on a fresh clone (or if ffmpeg/ was deleted) — this is REQUIRED, not optional:
./scripts/fetch-ffmpeg.sh # ~65 MB once; verifies SHA-256, extracts 9 files into ./ffmpeg
FFmpeg is bundled, never downloaded at runtime (Windows Store policy 10.2.2 — the old runtime
download 404'd on 2026-09-01 and would fail certification). The binaries are gitignored because
135 MB of DLLs in git would tax every commit on this 9p tree; the pinned URL + SHA-256 in the script
are the audit trail. ytLive.csproj's GuardBundledFfmpeg target fails the build with that
exact instruction if the folder is missing. FFMPEG_ZIP=<path> reuses a pre-downloaded archive.
# From WSL, ALWAYS use the Windows dotnet host — never Linux `dotnet` for this project:
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
"/mnt/c/Program Files/dotnet/dotnet.exe" run -c Release # the launch command (creator, 2026-10-16): Release, not Debug — perf takes must read Release numbers
Run-mode convention (creator directive 2026-10-16): the creator launches with -c Release
by DEFAULT. Debug runs happen ONLY when the AI explicitly names the Debug command for a
specific reason — never implicitly, and never for reading performance numbers (Debug pixel
paths are several × slower; every take in the 141ms saga read Debug numbers where Release is
the real reading).
Pre-commit gate (write it down — I forgot once): ALWAYS run
./scripts/verify.sh "<file1>" "<file2>" ... (declared scope) before committing. It forces a
clean build (0 warnings), the full test suite (passes only the 2 known pre-existing [FAIL]s), and
the scope check in one shot. An incremental build can report 0 Warning(s) while skipping
recompiles — that's how CS8601 slipped past on 2026-08-29. Never claim 0 warnings from anything but
verify.sh's clean build.
EnableWindowsTargeting=true in ytLive.csproj lets a cold restore work from WSL, but a Linux
dotnet run/build re-downloads 100M+ of windowsdesktop.app.* packs into the Linux NuGet cache
(which lacks them) over the slow 9p /mnt/c bridge — twice, because the WPF _wpftmp generated
project triggers a second restore (203s observed). The Windows cache has the SQLite packages and the
packs resolve from C:\Program Files\dotnet\packs, so the Windows host never re-downloads.
Never use --no-restore right after an interrupted restore — the stale project.assets.json
produces misleading NETSDK1064 "package not found" errors. Running requires Windows anyway.
Tests
xUnit in ytLive.Tests (net8.0-windows10.0.19041.0, matches the app TFM). Run on Windows —
WSL can't run net8.0-windows tests:
dotnet.exe vstest "C:\...\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
Good Dog Rule: ONE integration test per change (no feature branches pre-1.0 — work lands on
main, see AGENTS.md). Current: TokenStore DPAPI
roundtrip/corrupt/missing/clear, OAuth exchange/refresh/ClearSession, CameraManager refcount +
frame pump + failure handling (fakes for the WinRT seams), real-MainWindow round-clip
interaction test, LayoutStore delete roundtrip, LayoutStore pre-round-rect-dims roundtrip,
LayoutStore background roundtrip, LayoutStore HasBackground roundtrip + v5→v6 non-Live backfill,
ScreenCaptureManager refcount + shared-bitmap + coalescing
(fake IScreenCaptureSource + a real background-STA Dispatcher), BackgroundTests, SceneCatalogTests,
the TASK 25 dirty-layout heal (BackgroundHealIntegrationTests), WebcamSafeguardTests,
SceneCompositorTests (full-scene composite + vertical tier), StretchMathTests, FfmpegLocatorTests,
FfmpegEncoderTests, FramePumpTests, the TASK 8/11/12 audio chain (AudioPipelineTests + MasterLimiter),
the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegrationTests (TASK 24),
GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests
(TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests
(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests +
WebcamOutputKeyTests + GameMeterHonestyTests (2026-09-01 recording-verification fixes) — ZERO
known failures as of 2026-09-01. AudioPipelineTests 25/25 green in 26ms —
the "known failing" Mix_HonorsProviderGains… and the class's notorious STANDALONE HANG shared
one root cause: TASK 22's _delayedMix (nullable, never initialized) was dereferenced
(delayed.Length on null) every live-mix tick — a swallowed NRE starved the pipe the tests
read and flooded startup.log. Test was right, code drifted from the map's own contract
(loopbackGain = GameAudioVolume — now honored; the "unity because loopback scales with the
endpoint" assumption was disproven by the creator's 20%-volume meter observation). The former
RoundClip "known failure" (stale-test layers: namescoped FindName + VisualTreeHelper.HitTest
where UIElement.InputHitTest models input — see MyMistakes) was fixed the same day.
Per-class runs through the Windows dotnet.exe host execute the RealApp/MainWindow suites fine;
only the FULL-suite run still hangs (WASAPI teardown, pre-existing) — and the whole-suite
"247 total" era count is stale; trust per-class results.
Reward-event capture (monetization awareness, see the Monetization section) will add its integration
tests here when it ships: one real chat-poll payload containing all seven reward event types →
assert the persisted canonical RewardEvent rows (type + amount/currency/tier/memberLevel/participant
ids) round-trip into SQLite.
Real-MainWindow tests MUST be hermetic (DB pollution bug)
The integration test boots a real MainWindow → MainViewModel → real LayoutStore
(%APPDATA%\ytLlive\ytLlive.db). Shutdown() on close saves the layout (full rewrite:
DELETE all scenes/sources, re-insert), so any source a test adds would be persisted over the
user's real ones — this happened and wiped the real webcam source (DeviceId replaced by the
test's fake test-camera). Rule: a test that constructs MainWindow MUST first set
MainViewModel.LayoutPathOverride to a temp DB path and reset it (plus
SqliteConnection.ClearAllPools() + delete) in finally. The seam is
internal static string? LayoutPathOverride (line ~529 in MainViewModel.cs),
ytLive.csproj has InternalsVisibleTo("ytLive.Tests").
The layout DB is a full rewrite per save (delete all, re-insert from memory), so
save/load round trips are exact: an element removed in the UI (RemoveElement →
scene.Elements.Remove → OnElementsChanged → debounced ScheduleSave, plus Shutdown on
close) does not come back after reload (LayoutStorePersistenceTests guards this).
Session performance evidence
python3 scripts/session-stats.py mines every startup*.log (primary, rotated, and per-instance)
into one row per FramePump session: tier, scene, measured fps, drops, stalls, average/worst render,
capture rate — and the sink, which is the whole point:
| Sink | How it is recognised | What it proves |
|---|---|---|
RECORD |
Recording ended / Recording discarded |
the machine alone — no socket |
TEST |
Test session dock open / Test chat simulation |
the machine + socket, no audience |
LIVE |
Broadcast created / Broadcast bound |
everything, network included |
UNKNOWN |
no marker (crashed or paused) | nothing — investigate before quoting it |
Verdicts: holds = 0 drops, 0 stalls, average render under half the frame budget; strained =
under the full budget; otherwise over-budget. Worst render is printed but never judged — it is
dominated by one-off startup hitches. This is the evidence source for the minimum-configuration
table in product.md: recordings first, because the network cannot pollute them.
Persistence & external accounts (lookup)
- Layout:
%APPDATA%\ytLlive\ytLlive.db(SQLite, schema v8 — scenes, sources, assets by SHA-256 content hash, socials, settings; full rewrite per save). - Auth session:
%APPDATA%\ytLlive\ytLlive.auth(DPAPI, CurrentUser scope). - Startup log:
%APPDATA%\ytLlive\startup.log, capped at 5 MB with one rolled copy (Helpers/LogRotation.cs, TASK 55 slice 1). - Google Cloud project
198374416003— the OAuth ClientId prefix is the project number. It is owned byllamachile.shop@gmail.com("gramps"), which is NOT the Google account a creator signs into YouTube with; console access and channel identity are separate logins. Direct link:https://console.cloud.google.com/apis/credentials?project=198374416003— if the picker shows no match it defaults to the "Recent" tab, so switch to "All". - LAN credentials live in the gitignored
CREDENTIALS.md. ⚠️ It says Home Assistant is192.168.50.144; the real HA is192.168.50.145(.144isgordito). Fix that file when next editing it.