Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
107 lines
5.3 KiB
JavaScript
107 lines
5.3 KiB
JavaScript
// Adaptive waits for the walk. Every step used to sleep a fixed interval
|
|
// sized for its worst case — 414 seconds of them in the mock list and 223 in
|
|
// the phone's, over half of a round's twenty-three minutes. This polls a
|
|
// fingerprint of the page and the app's own state instead, and goes on once
|
|
// both have stopped moving.
|
|
//
|
|
// Applied on its own, after a chain clears, so it never rides inside a
|
|
// round's commit.
|
|
const fs = require('fs');
|
|
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
|
let p, s;
|
|
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => fs.writeFileSync(root + p, s);
|
|
const once = (a) => {
|
|
const i = s.indexOf(a);
|
|
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
|
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
|
return i;
|
|
};
|
|
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
|
|
|
load('tests/mobile/smoke.mjs');
|
|
if (s.includes('function settle(')) throw new Error('the walk already waits adaptively — nothing to do');
|
|
|
|
const A_LOOP = ` for (const s of steps) {`;
|
|
const A_SLEEP = ` await sleep(s.wait || 500);`;
|
|
once(A_LOOP);
|
|
once(A_SLEEP);
|
|
|
|
rep(A_LOOP, ` /* ── waiting only as long as it takes ───────────────────────────
|
|
Every step used to sleep its declared wait, sized for the worst
|
|
case it might ever meet. At 632 steps that was over half of a
|
|
round: 414 seconds declared in the mock list, 223 in the phone's.
|
|
|
|
So: poll something cheap until it stops changing, then go on. The
|
|
fingerprint reads the shape of the page AND the app's own moving
|
|
parts, because a step can be waiting on work that has not reached
|
|
the DOM yet — a resolve in flight, a transfer arriving, a command
|
|
that will only show itself in a toast.
|
|
|
|
Two properties keep this honest:
|
|
|
|
· It can only ever SHORTEN a wait, never lengthen one. The
|
|
declared wait is the ceiling, so a step that passes today still
|
|
gets every millisecond it got before.
|
|
· There is a floor. A page can be momentarily quiet in the middle
|
|
of work — a backend write with nothing visible behind it looks
|
|
settled the instant it is asked — so nothing is probed before a
|
|
third of its declared wait has passed.
|
|
|
|
\`hold: true\` on a step opts out and sleeps the whole wait, for
|
|
the handful that are waiting on something nothing here can see. */
|
|
const FINGERPRINT = \`(function () {
|
|
try {
|
|
var t = document.querySelector('.toast');
|
|
var ss = window.__ss || {};
|
|
var pl = ss.player || {}, rs = ss.resolver || {}, nt = ss.net || {}, lb = ss.lib || {};
|
|
return [
|
|
document.querySelectorAll('*').length,
|
|
document.images.length,
|
|
(document.querySelector('.screen') || {}).className || '',
|
|
(document.querySelector('.sheet') || {}).className || '',
|
|
(document.querySelector('.halfsheet') || {}).className || '',
|
|
t ? t.textContent.slice(0, 40) : '',
|
|
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,
|
|
(ss.transfers && ss.transfers.items ? ss.transfers.items.length : 0)
|
|
].join('|');
|
|
} catch (e) { return 'unreadable'; }
|
|
})()\`;
|
|
/* two identical readings 90ms apart, and nothing has moved */
|
|
const STABLE_READINGS = 2;
|
|
const POLL_MS = 90;
|
|
const FLOOR = 0.34;
|
|
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; }
|
|
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; }
|
|
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));
|
|
}
|
|
for (const s of steps) {`);
|
|
|
|
rep(A_SLEEP, ` await settle(s.wait || 500, s);`);
|
|
|
|
/* The tally, printed where `saved` and `waited` are provably in scope: the
|
|
comment below sits in the same block as the loop, at the same indent. */
|
|
rep(` /* ── the restart half of session restore, on the phone only ──────`,
|
|
` console.log("waits: " + Math.round(saved / 1000) + "s of " + Math.round(waited / 1000) + "s declared was not spent");
|
|
|
|
/* ── the restart half of session restore, on the phone only ──────`);
|
|
save();
|
|
|
|
console.log('the walk waits adaptively now');
|