Spaces:
Sleeping
Sleeping
File size: 4,783 Bytes
90cbf22 |
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 |
import { Doc } from '../../convex/_generated/dataModel';
import { useEffect, useRef, useState } from 'react';
export function useHistoricalTime(engineStatus?: Doc<'engines'>) {
const timeManager = useRef(new HistoricalTimeManager());
const rafRef = useRef<number>();
const [historicalTime, setHistoricalTime] = useState<number | undefined>(undefined);
if (engineStatus) {
timeManager.current.receive(engineStatus);
}
const updateTime = (performanceNow: number) => {
// We don't need sub-millisecond precision for interpolation, so just use `Date.now()`.
const now = Date.now();
setHistoricalTime(timeManager.current.historicalServerTime(now));
rafRef.current = requestAnimationFrame(updateTime);
};
useEffect(() => {
rafRef.current = requestAnimationFrame(updateTime);
return () => cancelAnimationFrame(rafRef.current!);
}, []);
return { historicalTime, timeManager: timeManager.current };
}
type ServerTimeInterval = {
startTs: number;
endTs: number;
};
export class HistoricalTimeManager {
intervals: Array<ServerTimeInterval> = [];
prevClientTs?: number;
prevServerTs?: number;
totalDuration: number = 0;
latestEngineStatus?: Doc<'engines'>;
receive(engineStatus: Doc<'engines'>) {
this.latestEngineStatus = engineStatus;
if (!engineStatus.currentTime || !engineStatus.lastStepTs) {
return;
}
const latest = this.intervals[this.intervals.length - 1];
if (latest) {
if (latest.endTs === engineStatus.currentTime) {
return;
}
if (latest.endTs > engineStatus.currentTime) {
throw new Error(`Received out-of-order engine status`);
}
}
const newInterval = {
startTs: engineStatus.lastStepTs,
endTs: engineStatus.currentTime,
};
this.intervals.push(newInterval);
this.totalDuration += newInterval.endTs - newInterval.startTs;
}
historicalServerTime(clientNow: number): number | undefined {
if (this.intervals.length == 0) {
return undefined;
}
if (clientNow === this.prevClientTs) {
return this.prevServerTs;
}
// If this is our first time simulating, start at the beginning of the buffer.
const prevClientTs = this.prevClientTs ?? clientNow;
const prevServerTs = this.prevServerTs ?? this.intervals[0].startTs;
const lastServerTs = this.intervals[this.intervals.length - 1].endTs;
// Simple rate adjustment: run time at 1.2 speed if we're more than 1s behind and
// 0.8 speed if we only have 100ms of buffer left. A more sophisticated approach
// would be to continuously adjust the rate based on the size of the buffer.
const bufferDuration = lastServerTs - prevServerTs;
let rate = 1;
if (bufferDuration < SOFT_MIN_SERVER_BUFFER_AGE) {
rate = 0.8;
} else if (bufferDuration > SOFT_MAX_SERVER_BUFFER_AGE) {
rate = 1.2;
}
let serverTs = Math.max(
prevServerTs + (clientNow - prevClientTs) * rate,
// Jump forward if we're too far behind.
lastServerTs - MAX_SERVER_BUFFER_AGE,
);
let chosen = null;
for (let i = 0; i < this.intervals.length; i++) {
const snapshot = this.intervals[i];
// We're past this snapshot, continue to the next one.
if (snapshot.endTs < serverTs) {
continue;
}
// We're cleanly within this snapshot.
if (serverTs >= snapshot.startTs) {
chosen = i;
break;
}
// We've gone past the desired timestamp, which implies a gap in our server state.
// Jump time forward to the beginning of this snapshot.
if (serverTs < snapshot.startTs) {
serverTs = snapshot.startTs;
chosen = i;
}
}
if (chosen === null) {
serverTs = this.intervals.at(-1)!.endTs;
chosen = this.intervals.length - 1;
}
// Time only moves forward, so we can trim all of the snapshots before our chosen one.
const toTrim = Math.max(chosen - 1, 0);
if (toTrim > 0) {
for (const snapshot of this.intervals.slice(0, toTrim)) {
this.totalDuration -= snapshot.endTs - snapshot.startTs;
}
this.intervals = this.intervals.slice(toTrim);
}
this.prevClientTs = clientNow;
this.prevServerTs = serverTs;
return serverTs;
}
bufferHealth(): number {
if (!this.intervals.length) {
return 0;
}
const lastServerTs = this.prevServerTs ?? this.intervals[0].startTs;
return this.intervals[this.intervals.length - 1].endTs - lastServerTs;
}
clockSkew(): number {
if (!this.prevClientTs || !this.prevServerTs) {
return 0;
}
return this.prevClientTs - this.prevServerTs;
}
}
const MAX_SERVER_BUFFER_AGE = 1500;
const SOFT_MAX_SERVER_BUFFER_AGE = 1250;
const SOFT_MIN_SERVER_BUFFER_AGE = 250;
|