android: the waits the walk could not see, and why they are off by default

The adaptive waits went in unmeasured — the comparison was still running when
work stopped. Measured against round 42's 255 step verdicts they are not green:
six steps that were `ok` at baseline fail — `artist-lanes-page`, `net-playing`,
`net-sheet`, `np-more`, `np-copied`, `np-saved`.

They were not six faults. `net-playing` threw `Cannot read properties of null
(reading 'click')` because the network row it wanted had not arrived, and every
step after it then worked the wrong track: `np-copied` and `np-saved` were
hunting for `Copy to a folder` on a LOCAL track's sheet, which does not have it.

The reading asked for three things and got one. `resolver.active` is real.
**`net.busy` and `lib.loading` are not fields on those stores** — `net` publishes
`searching`, `responses`, `files` and `groups`, `lib` publishes `loaded` — so
both read `undefined` on every poll, joined a constant into the fingerprint, and
left a search in flight indistinguishable from a settled page. The catalog was
not read at all, which is `artist-lanes-page`: an artist lookup sits in
`catalog.artists[name].status` until it turns `ready`, and the mock holds that
page for four seconds on purpose.

The names are corrected, the catalog's loading entries are counted, and the
counters that move as results land are in the reading, so partial progress
registers as progress.

The second half matters more than the names. **A stable fingerprint is not a
settled page.** A search that has sent nothing back yet holds every counter
still, so "two identical readings 90 ms apart" was satisfied in the middle of
the work it existed to wait for. Work in flight is its own leading field now,
and while the resolver is active, a search is running or any catalog entry is
loading, a still page does not count as a settled one. An unreadable page reads
as busy for the same reason. The declared wait was always the ceiling and still
is.

That fixed four of the six. What it did not fix is the reason this is now
**off by default**: across three full walks the catalog and network cluster
fails a *different subset* each time — one run lost `net-arrived`, `np-copied`
and `np-saved`; the next got all three back and lost `net-playing`,
`cat-album-row` and the three that hang off `net-playing`. A gate that is green
at random is worse than a slow one, and every expectation in this file was
written against the declared waits. So `settle()` sleeps the wait it was given
unless **`SS_ADAPTIVE=1`** asks otherwise, and the walk prints which mode it
ran in so no log is ambiguous about it. Turning that variable on and off is
also the A/B the comparison needed and never had.

With it on the saving is real — 162 of 414 declared seconds on the mock walk,
95 of 224 on a device, where the walk went 131 of 131 with no exceptions and no
console errors. It can have the default when it is green twice running.

Three steps carry `hold: true` either way, because they wait on the mock's own
clock rather than on the app: a streamed file handed over on a seven-second
timer, and the two writes whose only evidence is the notice that follows them.
This commit is contained in:
KeelrLeek committed 2026-09-14 10:11:53 -05:00
1 parent 9f0601027c
commit 9c3273e6af
1 file changed
+36 -8
+36 -8
View File
@@ -721,15 +721,17 @@ const MOCK_STEPS = [
{ do: `history.back()`, wait: 800 },
{ do: `document.querySelector('.cat-song').click()`, name: "net-playing", wait: 5500 },
{ do: `document.querySelector('.mini').click()`, name: "net-sheet", wait: 3000 },
{ name: "net-arrived", wait: 7000 },
/* the mock hands the finished file over on its own timer, with a still
page in front of it the whole way — nothing here can shorten this */
{ name: "net-arrived", wait: 7000, hold: true },
/* the now-playing sheet's ⋯: the streamed track saved into the library
(its cache is complete by now, so it files at once) */
{ do: `document.querySelectorAll('.sheet .np-head .iconbtn')[1].click()`, name: "np-more", wait: 700 },
/* the same cache, copied out as a plain file into a picked folder —
nothing downloaded again, no library row */
{ do: `__click('.halfsheet .row', 'Copy to a folder')`, name: "np-copied", wait: 900 },
{ do: `__click('.halfsheet .row', 'Copy to a folder')`, name: "np-copied", wait: 900, hold: true },
{ do: `document.querySelectorAll('.sheet .np-head .iconbtn')[1].click()`, wait: 700 },
{ do: `__click('.halfsheet .row', 'Save to library')`, name: "np-saved", wait: 900 },
{ do: `__click('.halfsheet .row', 'Save to library')`, name: "np-saved", wait: 900, hold: true },
/* the raw peers: a peer's sheet → their shares browsed → a folder opened
→ one of its files streams */
{ do: `__settle()`, wait: 800 },
@@ -2774,7 +2776,19 @@ async function main() {
var t = document.querySelector('.toast');
var ss = window.__ss || {};
var pl = ss.player || {}, rs = ss.resolver || {}, nt = ss.net || {}, lb = ss.lib || {};
return [
/* a catalog lookup never touches any of those: it sits in
catalog.artists[name].status until it turns 'ready', and
the artist page is waiting on exactly that. */
var ct = ss.catalog || {}, looking = 0;
['artists', 'albums', 'extras', 'profiles'].forEach(function (k) {
var m = ct[k] || {};
for (var n in m) { if (m[n] && m[n].status === 'loading') looking++; }
});
/* what is still running, as its own answer. A search that
has sent nothing back yet holds every counter still, so a
settled-looking page is not the same as a settled one. */
var busy = (rs.active || nt.searching || looking > 0) ? 1 : 0;
return busy + '|' + [
document.querySelectorAll('*').length,
document.images.length,
(document.querySelector('.screen') || {}).className || '',
@@ -2784,32 +2798,46 @@ async function main() {
document.title,
pl.status, pl.phase, Math.round(pl.position || 0),
(pl.queue || []).length, pl.qi,
rs.active ? 1 : 0, nt.busy ? 1 : 0, lb.loading ? 1 : 0,
rs.active ? 1 : 0, nt.searching ? 1 : 0, looking,
nt.responses || 0, nt.files || 0, (nt.groups || []).length,
lb.loaded ? 1 : 0, (lb.albums || []).length,
(ss.transfers && ss.transfers.items ? ss.transfers.items.length : 0)
].join('|');
} catch (e) { return 'unreadable'; }
} catch (e) { return '1|unreadable'; }
})()`;
/* two identical readings 90ms apart, and nothing has moved */
const STABLE_READINGS = 2;
const POLL_MS = 90;
const FLOOR = 0.34;
/* Off unless asked for. The declared waits are what round 42 was
verified on, and what every expectation was written against; the
short ones are an optimisation that has not earned the default
yet. SS_ADAPTIVE=1 turns them on — and turning it on and off is
the A/B this was missing. */
const ADAPTIVE = process.env.SS_ADAPTIVE === "1";
let waited = 0, saved = 0; /* what the walk actually spent, and what it did not */
async function settle(want, step) {
waited += want;
if (step && step.hold) { await sleep(want); return; }
if (!ADAPTIVE || (step && step.hold)) { await sleep(want); return; }
const began = Date.now();
await sleep(Math.min(want, Math.max(200, Math.round(want * FLOOR))));
let last = null, stable = 0;
while (Date.now() - began < want) {
let fp;
try { fp = await cdp.evaluate(FINGERPRINT); } catch { break; }
if (fp === last) { if (++stable >= STABLE_READINGS) break; } else { stable = 0; last = fp; }
/* the leading field is the busy flag: while anything the
app admits to is still running, a still page does not
count as a settled one. The declared wait is still the
ceiling, so this can only ever spend what it was given. */
const working = typeof fp === 'string' && fp.charAt(0) === '1';
if (fp === last && !working) { if (++stable >= STABLE_READINGS) break; } else { stable = 0; last = fp; }
const left = want - (Date.now() - began);
if (left <= 0) break;
await sleep(Math.min(POLL_MS, left));
}
saved += Math.max(0, want - (Date.now() - began));
}
console.log(ADAPTIVE ? "adaptive waits: on (SS_ADAPTIVE=1)" : "adaptive waits: off — sleeping every declared wait");
for (const s of steps) {
/* a phone step can be an adb command — an intent, a key */
let adbOut = "";