Reaction: der Gleichlauf rechnet ohne Uhrenvergleich
Filipe: „die videos muessen perfekt laufen wenn ich die reaktions
mache."
GEMESSEN, NICHT VERMUTET. mess-reaktion hat einen neuen Abschnitt:
Er liest den Spielstand am ECHTEN <video>-Element in beiden Browsern,
rechnet beide auf denselben Augenblick um und sagt, wie weit Host und
Zuschauerin auseinanderliegen. Die vorhandene Pruefung fragte nur, ob
Start und Stopp ANKOMMEN -- zwei Videos koennen beide laufen und
trotzdem zwanzig Sekunden auseinander sein.
vorher nachher
Im Lauf 0,12 s -0,04 s
Wer dazukommt 6,91 s 0,26 s
Uhr des Geraets 30 s vor -29,97 s 0,26 s
DER SCHLIMMSTE FUND: DIE UHR DES TELEFONS
Der Zuschauer rechnete `Date.now() - stand.gesendet`. `gesendet` kam
vom SERVER, `Date.now()` vom Geraet -- zwei Uhren, die nie jemand
verglichen hat. Wessen Telefon dreissig Sekunden vorgeht, schaute
29,97 s weiter als alle anderen. Kein langsames Netz, kein schwaches
Handy. Oertlich faellt das nie auf: Auf einem Rechner sind beide
Uhren dieselbe.
Jetzt geht ein ALTER hinaus statt einer Uhrzeit -- die Differenz
zweier SERVERzeiten. Der Empfaenger braucht dafuer gar keine Uhr,
nur eine Stoppuhr (`performance.now()`), und die haelt Zeitumstellung
und Aufwachen aus dem Ruhezustand aus.
WEITER
* WER DAZUKOMMT, STEIGT RICHTIG EIN. Der gespeicherte Stand war so
alt wie der letzte Takt des Hosts; neue Spalte `sekunde_am` sagt,
wann er galt.
* DAS TEMPO GEHT IN DIE HOCHRECHNUNG EIN. Bei 2x laeuft die Videouhr
doppelt so schnell -- das fehlte ganz.
* JEDER RECHNET ALLE ZWEI SEKUNDEN SELBST NACH statt nur auf Zuruf.
Wer stockte, hing bis zu fuenf Sekunden hinterher.
* DER VORLAUF NACH EINEM SPRUNG WIRD GEREGELT, nicht geschaetzt --
gemessen wird der Rest, nicht die Ladezeit (siehe unten).
* PUFFERN IST AUCH IN DER TRANSPORTLEISTE KEINE PAUSE. Der
Start-Stopp-Knopf rief beim Puffern `playVideo()` -- wer Pause
drueckte, bekam „weiter". Lampe und Knopfbild flackerten bei jedem
Stocken. Die Lektion vom 29.09. war nur an einer Stelle eingeloest.
* DAS TEMPO MELDET UEBER `hostMelden()` statt ein zweites Mal von
Hand -- es hielt Puffern fuer Stillstand und hielt damit beim
Umschalten alle an.
* EIN VIDEO-RUNDRUF STATT DREI ABSCHRIFTEN. Beim Einbau hatte ich
zuerst nur eine nachgezogen; das Tauschen der beiden Videos haette
still die alte Rechnung weiterverschickt.
DREI EIGENE FEHLER UNTERWEGS, alle im Text festgehalten
1. ZWEI FUNKTIONEN HIESSEN `springen`. Meine „spring AUF die Stelle"
wurde von der vorhandenen „spring UM so viele Sekunden"
ueberschrieben -- lautlos, rueckwirkend fuer die ganze Datei. Ein
Zuschauer bei Sekunde 100, dem „geh auf 101" gesagt wird, waere
bei 201 gelandet. DREI BROWSERLAEUFE HABEN ES NICHT GEFUNDEN:
oertlich blieb der Abstand unter der Sprungschwelle, die Zeile
lief kein einziges Mal. Gefunden hat es erst eine Frage an die
ganze Datei -- die steht jetzt als Pruefung drin und wurde
nachgestellt (rot).
2. DER VORLAUF LERNTE NUR NACH OBEN. Gemessen wurde die Ladezeit
nach einem Sprung; ein Sprung in geladenes Material dauert aber
gar nicht, meldet nichts und lehrt nichts. Ergebnis: -0,95 s
stabil. Jetzt wird das ERGEBNIS geregelt statt der Ursache.
3. DER AUSREISSER VON 39,84 s WAR MEINE EIGENE MESSUNG -- der Block
davor schickt eine erfundene Position. Jetzt wird abgeklungen und
in Messreihenfolge ausgegeben.
GEPRUEFT
pruef-reaktion 421/0 (14 neue, darunter der Riegel gegen die
Rueckkehr der Uhrenrechnung und die Wache gegen doppelte
Funktionsnamen -- beide nachgestellt), mess-reaktion ohne ein
einziges ACHTUNG, pruef-buehne 38/0, pruef-struktur, pruef-
zwischenspeicher 34/0.
Schemaaenderung: ADD COLUMN sekunde_am. Datenbank vorher gesichert
und geprueft (integrity_check ok, 20 Personen).
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1660,6 +1660,166 @@ melde("=== 17. Tempo, Anordnung und das zweite Video ===");
|
||||
method: "POST", keks: k.admin, rumpf: { tempo: 1, laeuft: true, sekunde: 25 },
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
DER GLEICHLAUF: WIE ALT IST DER STAND? (30.09.2026)
|
||||
|
||||
Bis heute schickte der Server seine UHRZEIT mit (`gesendet`), und
|
||||
der Browser rechnete `Date.now() - gesendet`. Das vergleicht die
|
||||
Uhr des Servers mit der Uhr des Telefons -- zwei Uhren, die nie
|
||||
jemand abgeglichen hat.
|
||||
|
||||
Gemessen am echten Browser (mess-reaktion): Ein Geraet, dessen Uhr
|
||||
30 Sekunden vorgeht, spielte das Video 29,97 Sekunden weiter als
|
||||
alle anderen. Und wer mitten in der Sendung dazukam, stieg 6,91
|
||||
Sekunden hinter allen ein, weil der gespeicherte Stand so alt war
|
||||
wie der letzte Takt des Hosts.
|
||||
|
||||
Jetzt geht ein ALTER hinaus -- die Differenz zweier SERVERzeiten.
|
||||
Der Empfaenger braucht dafuer keine eigene Uhr.
|
||||
===================================================================== */
|
||||
{
|
||||
const jetztStellen = (sek, alter) => roh(CREW, "/workspace/api/reaktion/video", {
|
||||
method: "POST", keks: k.admin, rumpf: { laeuft: true, sekunde: sek, alter },
|
||||
});
|
||||
const holen = async () => (await roh(CREW, "/workspace/api/reaktion", { keks: k.gast })).j;
|
||||
|
||||
await jetztStellen(100, 0);
|
||||
const frisch = await holen();
|
||||
ok(Number.isFinite(frisch?.alter) && frisch.alter >= 0 && frisch.alter < 500,
|
||||
`ein frischer Stand ist fast null Millisekunden alt (${frisch?.alter})`);
|
||||
|
||||
/* ES IST WIRKLICH EIN ALTER UND KEINE FESTE ZAHL. Ohne diese
|
||||
Zeile hiesse „0 ms" oben womoeglich nur, dass dort immer 0
|
||||
steht -- und dann rechnete niemand je etwas dazu. */
|
||||
await new Promise((r) => setTimeout(r, 1300));
|
||||
const gereift = await holen();
|
||||
ok(gereift?.alter >= 1200 && gereift?.alter <= 2600,
|
||||
`nach 1,3 s ist derselbe Stand 1,3 s alt (${gereift?.alter} ms)`);
|
||||
|
||||
/* UND DIE RECHNUNG GEHT AUF. Das ist der eigentliche Zweck:
|
||||
sekunde + alter muss die Stelle ergeben, an der die Sendung
|
||||
jetzt steht. */
|
||||
const stelle = (gereift.sekunde || 0) + (gereift.alter || 0) / 1000;
|
||||
ok(Math.abs(stelle - 101.3) < 0.4,
|
||||
`daraus folgt die richtige Stelle: ${stelle.toFixed(2)} s (erwartet ~101,3)`);
|
||||
|
||||
/* ---- Was der Host ueber seine eigene Ablesung sagt ----------
|
||||
Er misst oertlich, wie alt sie beim Absenden war. Das gehoert
|
||||
dazugerechnet -- sonst saesse jeder Zuschauer genau um diese
|
||||
Spanne zu weit hinten. */
|
||||
await jetztStellen(200, 3000);
|
||||
const mitAlter = await holen();
|
||||
ok(mitAlter?.alter >= 3000 && mitAlter?.alter < 3600,
|
||||
`eine 3 s alte Ablesung kommt auch als 3 s alt an (${mitAlter?.alter})`);
|
||||
|
||||
/* GEDECKELT, WEIL ES VOM BROWSER KOMMT. Was hereinkommt, wird
|
||||
geprueft und nicht geglaubt -- sonst setzte eine einzige
|
||||
falsche Zahl alle Zuschauer an eine erfundene Stelle. */
|
||||
await jetztStellen(300, 999999);
|
||||
const frech = await holen();
|
||||
ok(frech?.alter !== null && frech.alter <= 5600,
|
||||
`ein unsinniges Alter wird auf 5 s gedeckelt (${frech?.alter})`);
|
||||
|
||||
await jetztStellen(310, -8000);
|
||||
const negativ = await holen();
|
||||
ok(negativ?.alter !== null && negativ.alter >= 0 && negativ.alter < 600,
|
||||
`ein negatives Alter gilt als null (${negativ?.alter})`);
|
||||
|
||||
/* ---- Der dritte Ausgang: „weiss ich nicht" -------------------
|
||||
Ein Stand ohne Zeitstempel (alte Zeile, frisch aufgeraeumte
|
||||
Sendung) darf NICHT als „null Millisekunden alt" durchgehen.
|
||||
`null` heisst beim Empfaenger „nicht hochrechnen"; eine 0
|
||||
hiesse „taufrisch" und schickte alle an eine alte Stelle. */
|
||||
/* EIGENES HANDLE. Das vom Anfang der Datei ist laengst
|
||||
geschlossen (Zeile 108) -- die Pruefung legt die Leute an und
|
||||
ueberlaesst die Datenbank danach dem Server. */
|
||||
const { DatabaseSync: DB3 } = await import("node:sqlite");
|
||||
const d3 = new DB3(process.env.WORKSPACE_DB);
|
||||
d3.prepare("UPDATE reaktion SET sekunde_am = 0 WHERE id = 1").run();
|
||||
const ohne = await holen();
|
||||
ok(ohne?.alter === null,
|
||||
`ohne Zeitstempel ist das Alter ausdruecklich unbekannt (${JSON.stringify(ohne?.alter)})`);
|
||||
|
||||
/* UND EIN UNMOEGLICH ALTER STAND EBENSO. Eine Zeile aus der
|
||||
vorigen Woche wuerde sonst „604800000 ms" melden, und der
|
||||
Browser rechnete sie brav dazu. */
|
||||
d3.prepare("UPDATE reaktion SET sekunde_am = ? WHERE id = 1")
|
||||
.run(Date.now() - 7 * 86400000);
|
||||
const uralt = await holen();
|
||||
ok(uralt?.alter === null,
|
||||
`ein eine Woche alter Stand gilt als unbekannt (${JSON.stringify(uralt?.alter)})`);
|
||||
|
||||
d3.close();
|
||||
|
||||
/* Und wieder in einen brauchbaren Zustand -- die Pruefungen
|
||||
danach sollen keinen Scherbenhaufen vorfinden. */
|
||||
await jetztStellen(0, 0);
|
||||
|
||||
/* ---- UND EIN RIEGEL GEGEN DIE RUECKKEHR ---------------------
|
||||
|
||||
Die alte Zeile sah harmlos aus:
|
||||
|
||||
const unterwegs = Date.now() - stand.gesendet;
|
||||
|
||||
Der Kommentar daneben erklaerte sogar richtig, warum die
|
||||
Laufzeit dazugehoert. Nur verglich sie die Uhr des Servers mit
|
||||
der Uhr des Telefons -- und das faellt oertlich NIE auf: Auf
|
||||
einem Rechner sind beide Uhren dieselbe. Erst als jemand die
|
||||
Uhr absichtlich verstellt hat, kam es heraus.
|
||||
|
||||
GEMESSEN WIRD AM KOMMENTARFREIEN TEXT. Im Kopf der Datei steht
|
||||
die alte Zeile als Beispiel, und ein Beispiel im Kommentar ist
|
||||
kein Programm. Genau diese Falle hat mich heute frueh beim
|
||||
Abzeichen schon einmal erwischt. */
|
||||
const { jsOhneKommentar: ohneK } = await import("./helfer-ohne-kommentar.mjs");
|
||||
const rjs = ohneK(readFileSync(
|
||||
join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8"));
|
||||
|
||||
ok(!/\bgesendet\b/.test(rjs),
|
||||
"der Browser rechnet nicht mehr mit der Uhrzeit des Servers");
|
||||
|
||||
const kern = rjs.slice(rjs.indexOf("function sollJetzt"));
|
||||
const rumpf = kern.slice(0, kern.indexOf("\n }"));
|
||||
ok(rumpf.length > 40 && /performance\.now\(\)/.test(rumpf),
|
||||
"die Soll-Stelle wird mit der Stoppuhr gerechnet, nicht mit dem Kalender");
|
||||
ok(/anker\.tempo/.test(rumpf),
|
||||
"und das Tempo geht in die Hochrechnung ein (bei 2x laeuft die Videouhr doppelt)");
|
||||
ok(!/Date\.now\(\)/.test(rumpf),
|
||||
"in der Rechnung selbst steckt keine Tagesuhr mehr");
|
||||
|
||||
/* GEGENPROBE: Die Suche liest wirklich etwas. Ohne sie hiessen
|
||||
die drei Zeilen darueber womoeglich nur, dass `rumpf` leer
|
||||
ist -- und ein leerer Text besteht jede Verbotspruefung. */
|
||||
ok(/const her =/.test(rumpf),
|
||||
`die Suche findet den Rumpf von sollJetzt (${rumpf.length} Zeichen)`);
|
||||
|
||||
/* ---- KEIN NAME ZWEIMAL ---------------------------------------
|
||||
|
||||
`reaktion.js` ist ein einziger Rumpf von 4400 Zeilen. Wer dort
|
||||
einen Funktionsnamen ein zweites Mal vergibt, ueberschreibt
|
||||
die erste -- und zwar RUECKWIRKEND fuer die ganze Datei, auch
|
||||
fuer Aufrufe weit davor. Es gibt keine Fehlermeldung.
|
||||
|
||||
GENAU DAS IST AM 30.09.2026 PASSIERT. Die neue Nachfuehrung
|
||||
hiess `springen(ziel)` („spring AUF diese Stelle"), der Host
|
||||
hatte laengst ein `springen(um)` („spring UM so viele
|
||||
Sekunden"). Gewonnen hat das zweite. Ein Zuschauer bei
|
||||
Sekunde 100, dem „geh auf 101" gesagt wird, waere bei 201
|
||||
gelandet.
|
||||
|
||||
DIE MESSUNG IM BROWSER HAT ES NICHT GEFUNDEN -- oertlich blieb
|
||||
der Abstand unter der Sprungschwelle, die Zeile lief in drei
|
||||
Laeufen kein einziges Mal. Gefunden hat es erst diese Frage an
|
||||
die ganze Datei. */
|
||||
const namen = [...rjs.matchAll(/^\s{0,4}(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm)]
|
||||
.map((t) => t[1]);
|
||||
const doppelt = [...new Set(namen.filter((n, i) => namen.indexOf(n) !== i))];
|
||||
ok(namen.length > 40 && doppelt.length === 0,
|
||||
doppelt.length
|
||||
? `ZWEIMAL VERGEBEN: ${doppelt.join(", ")} — die spaetere gewinnt, lautlos`
|
||||
: `alle ${namen.length} Funktionsnamen in reaktion.js sind einmalig`);
|
||||
}
|
||||
|
||||
/* ---- Das zweite Video ---- */
|
||||
const A = "dQw4w9WgXcQ";
|
||||
const B = "9bZkp7q19f0";
|
||||
|
||||
Reference in New Issue
Block a user