library: the other window on the same origin draws the scan instead of running a second one
An installed app and a browser tab are one origin, one catalogue and one roll. Without a word between them the second window opens, finds nothing in flight, and reads the same frames again — two readers of one folder, two RAW decoders at 256MB apiece, for a progress the first window already has. The window holding the reading now announces it on every frame and on a heartbeat, and answers a window that has just opened and asks. A window that does not hold the reading takes it as its own: the same progress line, the folder marked as being read, the same STOP — which it relays rather than redirects — and the same jump. What it does not do is read the disk. A reading that is through leaves the catalogue behind, and a window watching it has nothing left to be told, so the screen reads the catalogue back once more when the session goes away: the frames that arrived with the last flush would otherwise never be drawn in the window that was only watching. Measured over one origin (two pages, 24 RAW frames): the second window read 25 frames before and 1 after (the open frame), the peak over an idle browser fell from 1412MB to 1155MB against 944MB for a single reader.
This commit is contained in:
@@ -703,10 +703,115 @@ const tell = () => {
|
||||
for (const fn of watchers) fn();
|
||||
};
|
||||
|
||||
// --- the other windows on this origin --------------------------------------
|
||||
|
||||
// An installed app and a browser tab share one origin, one catalogue and one
|
||||
// roll: without a word between them the second window opens, finds nothing in
|
||||
// flight here, and reads the same frames again — two readers of one folder, two
|
||||
// RAW decoders at 256MB apiece, for a progress the first window already has.
|
||||
// So the window holding the reading says so, on every frame and on a heartbeat,
|
||||
// and the windows that do not hold it take the reading as their own: they draw
|
||||
// the same progress, offer the same stop, and above all start nothing.
|
||||
const CHANNEL = 'recipescam-library';
|
||||
const SELF = Math.random().toString(36).slice(2);
|
||||
// A window that has heard nothing for this long has a peer that was closed, or
|
||||
// one that is wedged — either way the reading is nobody's again, and this window
|
||||
// may pick it up as it always could.
|
||||
const PEER_MS = 6000;
|
||||
const HEART_MS = 2000;
|
||||
// One round trip, for the window that has just opened: whether another window
|
||||
// holds the reading is what decides whether this one starts its own.
|
||||
const ASK_MS = 250;
|
||||
|
||||
interface Peer {
|
||||
folder: string;
|
||||
progress: ScanProgress;
|
||||
at: number;
|
||||
}
|
||||
|
||||
let peer: Peer | null = null;
|
||||
let peerTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let heart: ReturnType<typeof setInterval> | null = null;
|
||||
let askers: ((busy: boolean) => void)[] = [];
|
||||
// A browser without the channel (an old one, a worker) simply has no peers, and
|
||||
// everything below is a no-op there.
|
||||
const channel = typeof BroadcastChannel === 'function' ? new BroadcastChannel(CHANNEL) : null;
|
||||
|
||||
const say = (m: Record<string, unknown>): void => {
|
||||
channel?.postMessage({ ...m, from: SELF });
|
||||
};
|
||||
|
||||
const dropPeer = (): void => {
|
||||
if (peerTimer) {
|
||||
clearTimeout(peerTimer);
|
||||
peerTimer = null;
|
||||
}
|
||||
if (!peer) return;
|
||||
peer = null;
|
||||
tell();
|
||||
};
|
||||
|
||||
const holdPeer = (folder: string, progress: ScanProgress): void => {
|
||||
peer = { folder, progress, at: Date.now() };
|
||||
for (const fn of askers) fn(true);
|
||||
askers = [];
|
||||
if (peerTimer) clearTimeout(peerTimer);
|
||||
peerTimer = setTimeout(dropPeer, PEER_MS);
|
||||
tell();
|
||||
};
|
||||
|
||||
// A peer holds the reading the way this window holds one, as far as a screen is
|
||||
// concerned: the same shape, read the same way. Whether the reading is stopped
|
||||
// or jumped from here is answered by relaying it, which is why the window
|
||||
// watching can offer the buttons the window reading has.
|
||||
const peerSession = (): ScanSession | null =>
|
||||
peer ? { folder: peer.folder, progress: peer.progress, stop: false, jump: null } : null;
|
||||
|
||||
const announce = (): void => {
|
||||
if (live) say({ k: 'scan', folder: live.folder, progress: live.progress });
|
||||
};
|
||||
|
||||
if (channel) {
|
||||
channel.onmessage = (e: MessageEvent) => {
|
||||
const m = e.data as { k?: string; from?: string; folder?: string; progress?: ScanProgress; rel?: string | null } | null;
|
||||
if (!m || m.from === SELF) return;
|
||||
if (m.k === 'scan' && m.folder && m.progress) holdPeer(m.folder, m.progress);
|
||||
else if (m.k === 'end') {
|
||||
if (peer?.folder === m.folder) dropPeer();
|
||||
} else if (m.k === 'who') announce(); // a window just opened and asks who reads
|
||||
else if (m.k === 'stop' && live && live.folder === m.folder) {
|
||||
live.stop = true;
|
||||
tell();
|
||||
} else if (m.k === 'jump' && live && live.folder === m.folder) live.jump = m.rel ?? null;
|
||||
};
|
||||
}
|
||||
|
||||
// Whether another window is reading right now, asked rather than assumed: the
|
||||
// window that opens beside a running scan knows nothing until the reading
|
||||
// answers, and reading the disk twice over is exactly what the answer prevents.
|
||||
export function scanBusy(): Promise<boolean> {
|
||||
if (live) return Promise.resolve(true);
|
||||
if (!channel) return Promise.resolve(false);
|
||||
const answered = new Promise<boolean>((resolve) => {
|
||||
const settle = (busy: boolean): void => {
|
||||
askers = askers.filter((fn) => fn !== settle);
|
||||
resolve(busy);
|
||||
};
|
||||
askers.push(settle);
|
||||
// Nobody answered: no reading is running, and waiting longer only delays
|
||||
// this window's own.
|
||||
setTimeout(() => settle(false), ASK_MS);
|
||||
});
|
||||
say({ k: 'who' });
|
||||
return answered;
|
||||
}
|
||||
|
||||
// The scan in flight, or null. The same object for the whole of the scan, with
|
||||
// `progress` replaced on every pass — watchers are told, so they re-read it.
|
||||
// A reading another window holds answers the same way: the screens no longer
|
||||
// know or care which window the reading is in.
|
||||
export function scanSession(): ScanSession | null {
|
||||
return live;
|
||||
return live ?? peerSession();
|
||||
}
|
||||
|
||||
// Watch, and get the unsubscribe back: the shape an effect already has.
|
||||
@@ -718,13 +823,22 @@ export function watchScan(fn: () => void): () => void {
|
||||
}
|
||||
|
||||
export function stopScan(): void {
|
||||
if (!live) return;
|
||||
// A reading another window holds is stopped by telling it so: the button this
|
||||
// window drew is the same button, on the window that can act on it.
|
||||
if (!live) {
|
||||
if (peer) say({ k: 'stop', folder: peer.folder });
|
||||
return;
|
||||
}
|
||||
live.stop = true;
|
||||
tell();
|
||||
}
|
||||
|
||||
export function jumpScan(rel: string | null): void {
|
||||
if (live) live.jump = rel;
|
||||
if (live) {
|
||||
live.jump = rel;
|
||||
return;
|
||||
}
|
||||
if (peer) say({ k: 'jump', folder: peer.folder, rel });
|
||||
}
|
||||
|
||||
// Start reading a roll. Only one at a time: the walk reads one frame at a time on
|
||||
@@ -740,16 +854,27 @@ export function startScan(folder: LibraryFolder): Promise<ScanProgress> {
|
||||
};
|
||||
live = session;
|
||||
tell();
|
||||
announce();
|
||||
// Between two frames there is nothing to announce, and a window that hears
|
||||
// silence long enough would take the reading for abandoned and start its own.
|
||||
if (heart) clearInterval(heart);
|
||||
heart = setInterval(announce, HEART_MS);
|
||||
return scanFolder(
|
||||
folder,
|
||||
(p) => {
|
||||
session.progress = p;
|
||||
tell();
|
||||
announce();
|
||||
},
|
||||
() => session.stop,
|
||||
() => session.jump
|
||||
).finally(() => {
|
||||
if (heart) {
|
||||
clearInterval(heart);
|
||||
heart = null;
|
||||
}
|
||||
live = null;
|
||||
say({ k: 'end', folder: session.folder });
|
||||
tell();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user