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:
1 parent
9f0601027c
commit
9c3273e6af
1 file changed
+36
-8
+36
-8
@@ -721,15 +721,17 @@ const MOCK_STEPS = [
|
|||||||
{ do: `history.back()`, wait: 800 },
|
{ do: `history.back()`, wait: 800 },
|
||||||
{ do: `document.querySelector('.cat-song').click()`, name: "net-playing", wait: 5500 },
|
{ do: `document.querySelector('.cat-song').click()`, name: "net-playing", wait: 5500 },
|
||||||
{ do: `document.querySelector('.mini').click()`, name: "net-sheet", wait: 3000 },
|
{ 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
|
/* the now-playing sheet's ⋯: the streamed track saved into the library
|
||||||
(its cache is complete by now, so it files at once) */
|
(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 },
|
{ 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 —
|
/* the same cache, copied out as a plain file into a picked folder —
|
||||||
nothing downloaded again, no library row */
|
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: `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
|
/* the raw peers: a peer's sheet → their shares browsed → a folder opened
|
||||||
→ one of its files streams */
|
→ one of its files streams */
|
||||||
{ do: `__settle()`, wait: 800 },
|
{ do: `__settle()`, wait: 800 },
|
||||||
@@ -2774,7 +2776,19 @@ async function main() {
|
|||||||
var t = document.querySelector('.toast');
|
var t = document.querySelector('.toast');
|
||||||
var ss = window.__ss || {};
|
var ss = window.__ss || {};
|
||||||
var pl = ss.player || {}, rs = ss.resolver || {}, nt = ss.net || {}, lb = ss.lib || {};
|
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.querySelectorAll('*').length,
|
||||||
document.images.length,
|
document.images.length,
|
||||||
(document.querySelector('.screen') || {}).className || '',
|
(document.querySelector('.screen') || {}).className || '',
|
||||||
@@ -2784,32 +2798,46 @@ async function main() {
|
|||||||
document.title,
|
document.title,
|
||||||
pl.status, pl.phase, Math.round(pl.position || 0),
|
pl.status, pl.phase, Math.round(pl.position || 0),
|
||||||
(pl.queue || []).length, pl.qi,
|
(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)
|
(ss.transfers && ss.transfers.items ? ss.transfers.items.length : 0)
|
||||||
].join('|');
|
].join('|');
|
||||||
} catch (e) { return 'unreadable'; }
|
} catch (e) { return '1|unreadable'; }
|
||||||
})()`;
|
})()`;
|
||||||
/* two identical readings 90ms apart, and nothing has moved */
|
/* two identical readings 90ms apart, and nothing has moved */
|
||||||
const STABLE_READINGS = 2;
|
const STABLE_READINGS = 2;
|
||||||
const POLL_MS = 90;
|
const POLL_MS = 90;
|
||||||
const FLOOR = 0.34;
|
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 */
|
let waited = 0, saved = 0; /* what the walk actually spent, and what it did not */
|
||||||
async function settle(want, step) {
|
async function settle(want, step) {
|
||||||
waited += want;
|
waited += want;
|
||||||
if (step && step.hold) { await sleep(want); return; }
|
if (!ADAPTIVE || (step && step.hold)) { await sleep(want); return; }
|
||||||
const began = Date.now();
|
const began = Date.now();
|
||||||
await sleep(Math.min(want, Math.max(200, Math.round(want * FLOOR))));
|
await sleep(Math.min(want, Math.max(200, Math.round(want * FLOOR))));
|
||||||
let last = null, stable = 0;
|
let last = null, stable = 0;
|
||||||
while (Date.now() - began < want) {
|
while (Date.now() - began < want) {
|
||||||
let fp;
|
let fp;
|
||||||
try { fp = await cdp.evaluate(FINGERPRINT); } catch { break; }
|
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);
|
const left = want - (Date.now() - began);
|
||||||
if (left <= 0) break;
|
if (left <= 0) break;
|
||||||
await sleep(Math.min(POLL_MS, left));
|
await sleep(Math.min(POLL_MS, left));
|
||||||
}
|
}
|
||||||
saved += Math.max(0, want - (Date.now() - began));
|
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) {
|
for (const s of steps) {
|
||||||
/* a phone step can be an adb command — an intent, a key */
|
/* a phone step can be an adb command — an intent, a key */
|
||||||
let adbOut = "";
|
let adbOut = "";
|
||||||
|
|||||||
Reference in new issue
Block a user