Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | 5x 5x 5x 164x 164x 5x 5x 145x 145x 145x 76x 76x 5x 5x 15x 15x 15x 15x 15x 15x 24x 24x 24x 24x 24x 24x 24x 24x 130x 130x 130x 130x 130x 130x 160x 160x 160x 119x 31x 156x 156x 160x 160x 160x 160x 160x 160x 160x 125x 14x 14x 125x 125x 125x 35x 160x 45x 45x 115x 115x 65x 1x 11x 118x 118x 88x 88x 88x 35x 35x 6x 35x 35x 519x 519x 5x 5x 359x 359x 359x 387x 387x 387x 311x 42x 42x 311x 387x 387x 387x 387x 387x 359x | // The machine that orders a shared session's inputs: it runs as usual, applies the keys it is
// sent at the cycles they were stamped with, and sends each execute out as a commit for every
// other machine to replay. The host is the sequencer unless someone else has taken control.
import { cycleCount, LockstepHost } from "../lockstep.js";
import { rounded } from "./session-log.js";
// A key stamped further ahead than this is applied at once; an honest stamp, a round trip and the pause since
// the last commit, comes nowhere near it.
const MaxStampAheadSeconds = 2;
// A source with more keys than this waiting for their cycles has them all applied at once instead.
export const MaxWaitingKeys = 64;
// While keys are catching up, on a guest stamping later than it would or on a sequencer applying them later than
// stamped, a gap between two keys longer than this shrinks to it: four of the OS's 10 ms keyboard scans, so each
// key is still seen, while keys pressed closer together stay as close.
const CatchUpGapMs = 40;
/**
* When the next of a run of keys goes, `at` and no earlier than `earliest`, after the last went at `last.went`
* `gap` before: as far after it as it was, but with a gap over `catchUp` shortened to it, so a run that has
* fallen behind its own times makes the lag up a little with each key.
*/
function nextInRun(at, earliest, last, gap, catchUp) {
const after = last ? last.went + Math.min(gap, catchUp) : -Infinity;
return Math.max(at, earliest, after, last?.went ?? -Infinity);
}
// A stamp allows this much on top of the round trip for the trip taking longer than it did.
const JitterMarginMs = 20;
// Commits come every few milliseconds from a sequencer running in real time; one that has gone quiet for longer is
// running slowly (hidden, say), and counting the whole pause would stamp keys far beyond where it will be.
const MaxSinceCommitMs = 100;
/**
* Stamps keys sent to the sequencer with a cycle it will not have passed when they reach it. The sequencer was at
* `upTo`, the end of the last commit seen here, when it sent it, has run on since it came, and runs on for the trip
* back, `roundTripMs` in all. When the round trip shrinks, the stamps come down to it as nextInRun says, never going
* backwards and never squeezing keys pressed close together.
*/
export class KeyStamper {
constructor(cyclesPerSecond) {
this.cyclesPerMs = cyclesPerSecond / 1000;
this.last = null;
// Whether this page's keys have fallen behind when they were pressed: one was stamped ahead of where the
// machine was, or held back while control came to it. Until one is stamped where the machine is (and the
// sequencer has none of them waiting, which it may still hold later), they follow on.
this.catchingUp = false;
}
/** Starts afresh, the machine the stamps counted on having jumped. */
reset() {
this.last = null;
this.catchingUp = false;
}
/** Notes a key that went in at `cycle` unstamped, for the next that follows it to keep its gap from. */
mark({ cycle, nowMs }) {
this.last = { at: cycle, ms: nowMs };
this.catchingUp = false;
}
/**
* Stamps a key for this machine's own sequencer at `cycle`, where it is now, but no nearer the last key stamped
* than they were pressed apart (as nextInRun shortens a long gap), so a key pressed just before taking control,
* stamped a round trip ahead or held back until control came, keeps its length when its release is pressed after.
*/
follow({ cycle, nowMs, heldBack = false }) {
const { cyclesPerMs, last } = this;
const gap = last ? Math.round((nowMs - last.ms) * cyclesPerMs) : 0;
const stamp = nextInRun(cycle, -Infinity, last && { went: last.at }, gap, CatchUpGapMs * cyclesPerMs);
this.last = { at: stamp, ms: nowMs };
this.catchingUp = heldBack || stamp > cycle;
return stamp;
}
stamp({ upTo, roundTripMs, sinceCommitMs, nowMs }) {
const { cyclesPerMs, last } = this;
const aheadMs = roundTripMs + Math.min(sinceCommitMs, MaxSinceCommitMs) + JitterMarginMs;
const at = upTo + Math.round(aheadMs * cyclesPerMs);
const gap = last ? Math.round((nowMs - last.ms) * cyclesPerMs) : 0;
const stamp = nextInRun(at, -Infinity, last && { went: last.at }, gap, CatchUpGapMs * cyclesPerMs);
this.last = { at: stamp, ms: nowMs };
this.catchingUp = true;
return stamp;
}
}
export class Sequencer {
/**
* @param {object} processor
* @param {object} options
* @param {import("./session-log.js").SessionLog} options.log where each input is logged as it is applied
* @param {function(): import("./session-log.js").IntervalStats} options.stats the interval being gathered
* @param {function(object): void} options.send takes each commit
* @param {function(): void} options.onJump told when the machine has moved by itself, before the next commit
* @param {number} [options.rtcBaseMs] the wall time the session's clock counts from, if it has one already
*/
constructor(processor, { log, stats, send, onJump, rtcBaseMs }) {
this.processor = processor;
this.log = log;
this.stats = stats;
this.lockstep = new LockstepHost(processor, send, onJump, { rtcBaseMs });
this.reachedAt = cycleCount(processor);
// For each source of keys, those waiting for the cycle queue gave them, in the order they came, and the
// stamp and cycle due of its last stamped key, which the next keeps its gap from.
this.sources = new Map();
}
source(id) {
let source = this.sources.get(id);
if (!source) this.sources.set(id, (source = { scheduled: [], lastKey: null }));
return source;
}
snapshot() {
return this.lockstep.snapshot();
}
get rtcBaseMs() {
return this.lockstep.rtcBaseMs;
}
// Logged with the cycle it is applied at: the machine is between two of the lockstep's executes.
input(input, fields = {}) {
this.log.record("input", { ...fields, ...input, cycle: cycleCount(this.processor) });
this.lockstep.input(input);
}
/**
* Applies a key from source `id` at the cycle `at` it was stamped with, or at once if it has none. A key too
* late for its cycle goes in as soon as it can, and the ones after it keep their gaps from it, as nextInRun
* says, so a bunch held up past its cycles goes in as far apart as it was typed and the lag is made up over
* the keys that follow.
*/
queue(id, input, fields, at) {
this.followJump();
const source = this.source(id);
const now = cycleCount(this.processor);
const cyclesPerMs = this.processor.model.cyclesPerSecond / 1000;
const stamped = at !== undefined && at <= now + MaxStampAheadSeconds * 1000 * cyclesPerMs;
let due = Math.max(now, source.scheduled.at(-1)?.at ?? now);
if (stamped) {
if (at < now) {
this.stats().count("lateKeys");
this.stats().peak("lateKeyMaxMs", rounded((now - at) / cyclesPerMs));
}
const last = source.lastKey;
due = nextInRun(at, due, last && { went: last.due }, last ? at - last.at : 0, CatchUpGapMs * cyclesPerMs);
source.lastKey = { at, due };
} else {
source.lastKey = null;
}
if (due === now && source.scheduled.length === 0) {
this.input(input, fields);
return;
}
source.scheduled.push({ input, at: due, fields: { ...fields, arrivedMs: this.log.elapsed() } });
if (source.scheduled.length > MaxWaitingKeys) {
for (const key of source.scheduled.splice(0)) this.input(key.input, key.fields);
source.lastKey = null;
}
}
/** Whether source `id` has keys waiting for their cycles. */
waiting(id) {
return (this.sources.get(id)?.scheduled.length ?? 0) > 0;
}
/** Starts source `id`'s next key afresh, its stamps now counting on a machine that has been replaced. */
forget(id) {
const source = this.sources.get(id);
if (source) source.lastKey = null;
}
/** The keys source `id` still has waiting, each with the cycle it was due at, which are dropped with it. */
release(id) {
const waiting = this.sources.get(id)?.scheduled ?? [];
this.sources.delete(id);
return waiting;
}
/**
* Commits what is due where the machine is now (BREAK, say), and gives up every key still waiting for its cycle,
* as `{ source, input, at }`, for another sequencer to apply.
*/
releaseAll() {
if (this.lockstep.pending.length > 0) this.lockstep.execute(0);
const waiting = [...this.sources].flatMap(([source, { scheduled }]) =>
scheduled.map(({ input, at }) => ({ source, input, at })),
);
this.sources.clear();
return waiting;
}
// The machine only moves between executes by jumping (a reset, a loaded state), and the keys still
// waiting, due on the old cycle count, move with it.
followJump() {
const jump = cycleCount(this.processor) - this.reachedAt;
if (jump === 0) return;
this.reachedAt += jump;
for (const source of this.sources.values()) for (const key of source.scheduled) key.at += jump;
}
// While keys are waiting, the cycles asked for run in pieces that end where the next is due, each its
// own commit, so a long execute cannot bring keys spaced apart back onto one cycle. The pieces add up
// to what was asked, so the processor's overshoot carries over as it does in one execute.
execute(cycles) {
this.followJump();
let remaining = cycles;
let running;
for (;;) {
const now = cycleCount(this.processor);
let nextAt = Infinity;
for (const source of this.sources.values()) {
while (source.scheduled.length > 0 && source.scheduled[0].at <= now) {
const { input, fields } = source.scheduled.shift();
this.input(input, fields);
}
if (source.scheduled.length > 0) nextAt = Math.min(nextAt, source.scheduled[0].at);
}
const piece = Math.min(remaining, nextAt - now);
running = this.lockstep.execute(piece);
this.reachedAt = cycleCount(this.processor);
remaining -= piece;
if (!running || remaining <= 0) break;
}
return running;
}
}
|