Skip to content

Instantly share code, notes, and snippets.

@djnugent
Created September 2, 2023 05:08
Show Gist options
  • Save djnugent/57ee73177920bc590d8f50fc8fe6d0ae to your computer and use it in GitHub Desktop.
Save djnugent/57ee73177920bc590d8f50fc8fe6d0ae to your computer and use it in GitHub Desktop.
Time sync implementation in Javascript
// Function to generate normally distributed random numbers
function normalDistribution(mean, stdDev) {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return mean + stdDev * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
function time_ns() {
const nanos = process.hrtime.bigint();
return Number(nanos);
}
var createRingBuffer = function (length) {
/* https://stackoverflow.com/a/4774081 */
var pointer = 0, buffer = [];
return {
get: function (key) {
if (key < 0) {
return buffer[pointer + key];
} else if (key === false) {
return buffer[pointer - 1];
} else {
return buffer[key];
}
},
push: function (item) {
buffer[pointer] = item;
pointer = (pointer + 1) % length;
return item;
},
prev: function () {
var tmp_pointer = (pointer - 1) % length;
if (buffer[tmp_pointer]) {
pointer = tmp_pointer;
return buffer[pointer];
}
},
next: function () {
if (buffer[pointer]) {
pointer = (pointer + 1) % length;
return buffer[pointer];
}
},
buffer: function () {
return buffer;
},
};
};
/*
Sync to a single time source
Use a low pass filter to adjust to the time source
Use a proportional controller to monotonicly converge to the time source
Terms:
true time: the time that the time source is reporting
system time: the time that the system is reporting - assumed to be monotonic
adjusted time: the system time adjusted to the true time - non-monotonic
synced time: the system time adjusted to the true time and monotonic
*/
class TimeSync {
constructor(alpha = 0.1, dilation_p = 0.3, max_dilation_per_sec = 0.2, jump_on_first_measurement = false) {
this.alpha = alpha; // exponential moving average alpha for adjusted time offset
this.dilation_p = dilation_p; // proportional gain for sync time tracking adjusted time
this.max_dilation_per_sec = max_dilation_per_sec; // max dilation(in seconds) per second
this.jump_on_first_measurement = jump_on_first_measurement; // jump the synced time to the adjusted time on the first measurement
this.offsets = createRingBuffer(30);
this.adjustedTimeOffset_ns = 0;
this.syncedTime_ns = null;
this.lastSyncUpdate_ns = null;
this.lastMeasurement_ns = null;
this.lastMeasurementTime_ns = null;
this.stdDev = 0;
}
addMeasurement(trueTime_ns, now_ns = null) {
if (now_ns === null) {
now_ns = time_ns();
}
this.lastMeasurement_ns = trueTime_ns;
const offset_ns = trueTime_ns - now_ns;
this.offsets.push(offset_ns);
// If this is the first measurement, set the adjusted time offset to the measured offset
// and jump the synced time to the adjusted time if jump_on_first_measurement is true
if (this.lastMeasurementTime_ns === null) {
this.adjustedTimeOffset_ns = offset_ns;
if (this.jump_on_first_measurement) {
this.jump();
}
}
// Adjust the time offset using a low pass filter
this.adjustedTimeOffset_ns = (1 - this.alpha) * this.adjustedTimeOffset_ns + this.alpha * offset_ns;
// Calculate the standard deviation of the offsets
this.stdDev = Math.sqrt(this.offsets.buffer().reduce((acc, val) => acc + Math.pow(val - this.adjustedTimeOffset_ns, 2), 0) / this.offsets.buffer().length);
this.lastMeasurementTime_ns = now_ns;
}
// Monotonically increasing time - dilates to adjusted time
getSyncedTime_ns(now_ns = null) {
if (now_ns === null) {
now_ns = time_ns();
}
const adjustedTime_ns = now_ns + this.adjustedTimeOffset_ns;
// return the system time if we have no synced time
if (this.lastSyncUpdate_ns === null) {
this.lastSyncUpdate_ns = now_ns;
this.syncedTime_ns = now_ns;
return this.syncedTime_ns;
}
// Tick the synced time forward
const elapsed_ns = now_ns - this.lastSyncUpdate_ns;
const syncedTimeNoCorrection_ns = this.syncedTime_ns + elapsed_ns;
// Calculate the error between the adjusted time and the synced time
// And dilate the synced time to reduce the error
const error_ns = adjustedTime_ns - syncedTimeNoCorrection_ns;
const max_correction_ns = this.max_dilation_per_sec * elapsed_ns;
var correction_ns = (this.dilation_p * error_ns) * (elapsed_ns / 1e9);
correction_ns = Math.max(Math.min(correction_ns, max_correction_ns), -max_correction_ns); // Clamp the correction
correction_ns = Math.max(correction_ns, -elapsed_ns / 2); // if the correction is negative it cant be more than half the magnitude of the elapsed time
if (syncedTimeNoCorrection_ns + correction_ns < this.syncedTime_ns) {
throw new Error("Time went backwards!");
}
this.syncedTime_ns = syncedTimeNoCorrection_ns + correction_ns;
this.lastSyncUpdate_ns = now_ns;
return this.syncedTime_ns;
}
// Time that is adjusted to the true time, converges to true time
// No monotonicity guarantee
getAdjustedTime_ns(now_ns = null) {
if (now_ns === null) {
now_ns = time_ns();
}
return now_ns + this.adjustedTimeOffset_ns;
}
// Jump the synced time to the adjusted time
jump(now_ns = null) {
if (now_ns === null) {
now_ns = time_ns();
}
this.syncedTime_ns = now_ns + this.adjustedTimeOffset_ns;
this.lastSyncUpdate_ns = now_ns;
}
getStats(now_ns = null) {
if (now_ns === null) {
now_ns = time_ns();
}
const systemTime_ns = now_ns;
const adjustedTime_ns = this.getAdjustedTime_ns(now_ns);
const syncedTime_ns = this.getSyncedTime_ns(now_ns);
const adjustedTimeOffset = this.adjustedTimeOffset_ns / 1e9;
const syncedTimeError = (syncedTime_ns - adjustedTime_ns) / 1e9;
const ageOfLastMeasurement = this.lastMeasurementTime_ns && (now_ns - this.lastMeasurementTime_ns) / 1e9;
return {
systemTime_ns: systemTime_ns,
adjustedTime_ns: adjustedTime_ns,
syncedTime_ns: syncedTime_ns,
adjustedTimeOffset: adjustedTimeOffset,
syncedTimeError: syncedTimeError,
lastMeasurement_ns: this.lastMeasurement_ns,
ageOfLastMeasurement: ageOfLastMeasurement,
stdDev: this.stdDev / 1e9,
};
}
}
// Create a time synchronizer
const alpha = 0.03;
const dilation_p = 0.4;
const max_dilation_per_sec = 0.2;
const jump_on_first_measurement = true
const timeSync = new TimeSync(alpha, dilation_p, max_dilation_per_sec, jump_on_first_measurement);
// Simulate a time source like GPS
setInterval(() => {
const systemTime_ns = time_ns();
const trueTime_ns = systemTime_ns + normalDistribution(50 * 1e9, 0.5 * 1e9); // Simulating system time with noise
timeSync.addMeasurement(trueTime_ns, systemTime_ns);
}, 1000);
// Simulate a system that is using the synced time
setInterval(() => {
console.log("Stats:", timeSync.getStats());
}, 50);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment