Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/client/time.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { delay } from "awaiting";67import {8get_local_storage,9set_local_storage,10} from "@cocalc/frontend/misc/local-storage";11import * as message from "@cocalc/util/message";1213export class TimeClient {14private client: any;15private ping_interval_ms: number = 30000; // interval in ms between pings16private last_ping: Date = new Date(0);17private last_pong?: { server: Date; local: Date };18private clock_skew_ms?: number;19private last_server_time?: Date;20private closed: boolean = false;2122constructor(client: any) {23this.client = client;24}2526close(): void {27this.closed = true;28}2930// Ping server and also use the ping to determine clock skew.31public async ping(noLoop: boolean = false): Promise<void> {32if (this.closed) return;33const start = (this.last_ping = new Date());34let pong;35try {36pong = await this.client.async_call({37allow_post: false,38message: message.ping(),39timeout: 10, // CRITICAL that this timeout be less than the @_ping_interval40});41} catch (err) {42if (!noLoop) {43// try again **sooner**44setTimeout(this.ping.bind(this), this.ping_interval_ms / 2);45}46return;47}48const now = new Date();49// Only record something if success, got a pong, and the round trip is short!50// If user messes with their clock during a ping and we don't do this, then51// bad things will happen.52if (53pong?.event == "pong" &&54now.valueOf() - this.last_ping.valueOf() <= 1000 * 1555) {56if (pong.now == null) {57console.warn("pong must have a now field");58} else {59this.last_pong = { server: pong.now, local: now };60// See the function server_time below; subtract this.clock_skew_ms from local61// time to get a better estimate for server time.62this.clock_skew_ms =63this.last_ping.valueOf() +64(this.last_pong.local.valueOf() - this.last_ping.valueOf()) / 2 -65this.last_pong.server.valueOf();66set_local_storage("clock_skew", `${this.clock_skew_ms}`);67}68}6970this.emit_latency(now.valueOf() - start.valueOf());7172if (!noLoop) {73// periodically ping the server, to ensure clocks stay in sync.74setTimeout(this.ping.bind(this), this.ping_interval_ms);75}76}7778private emit_latency(latency: number) {79if (!window.document.hasFocus()) {80// console.log("latency: not in focus")81return;82}83// networking/pinging slows down when browser not in focus...84if (latency > 10000) {85// console.log("latency: discarding huge latency", latency)86// We get some ridiculous values from Primus when the browser87// tab gains focus after not being in focus for a while (say on ipad but on many browsers)88// that throttle. Just discard them, since otherwise they lead to ridiculous false89// numbers displayed in the browser.90return;91}92this.client.emit("ping", latency, this.clock_skew_ms);93}9495// Returns (approximate) time in ms since epoch on the server.96// NOTE:97// Once the clock has synced ever with the server, this is guaranteed98// to be an *increasing* function, with an arbitrary99// ms added on in case of multiple calls at once, to guarantee uniqueness.100// Also, if the user changes their clock back a little, this will still101// increase... very slowly until things catch up. This avoids102// weird random re-ordering of patches within a given session.103// NOTE: we do not force this to be increasing until sync, since this104// gets called immediately during startup, and forcing it to increase105// would make cocalc-with-a-broken-clock be completely broken until106// the user refreshes their browser.107public server_time(): Date {108let t = this.unskewed_server_time();109const last = this.last_server_time;110if (last != null && last >= t) {111// That's annoying -- time is not marching forward... let's fake it until it does.112t = new Date(last.valueOf() + 1);113}114if (115this.last_pong != null &&116Date.now() - this.last_pong.local.valueOf() < 5 * this.ping_interval_ms117) {118// We have synced the clock **recently successfully**, so119// we now ensure the time is increasing.120// This first sync should happen with ms of the user connecting.121// We do NOT trust if the sync was a long time ago, e.g., due to122// a long network outage or laptop suspend/resume.123this.last_server_time = t;124} else {125delete this.last_server_time;126}127return t;128}129130private unskewed_server_time(): Date {131// Add clock_skew_ms to our local time to get a better estimate of the actual time on the server.132// This can help compensate in case the user's clock is wildly wrong, e.g., by several minutes,133// or even hours due to totally wrong time (e.g. ignoring time zone), which is relevant for134// some algorithms including sync which uses time. Getting the clock right up to a small multiple135// of ping times is fine for our application.136if (this.clock_skew_ms == null) {137const x = get_local_storage("clock_skew");138if (x != null) {139this.clock_skew_ms = typeof x === "string" ? parseFloat(x) : 0;140}141}142if (this.clock_skew_ms != null) {143return new Date(Date.now() - this.clock_skew_ms);144} else {145return new Date();146}147}148149public async ping_test(opts: {150packets?: number;151timeout?: number; // any ping that takes this long in seconds is considered a fail152delay_ms?: number; // wait this long between doing pings153log?: Function; // if set, use this to log output154}) {155if (opts.packets == null) opts.packets = 20;156if (opts.timeout == null) opts.timeout = 5;157if (opts.delay_ms == null) opts.delay_ms = 200;158159/*160Use like this in a the console:161162smc.client.time_client.ping_test(delay_ms:100, packets:40, log:print)163*/164const ping_times: number[] = [];165const do_ping: (i: number) => Promise<void> = async (i) => {166const t = new Date();167const heading = `${i}/${opts.packets}: `;168let bar, mesg, pong, ping_time;169try {170pong = await this.client.async_call({171message: message.ping(),172timeout: opts.timeout,173});174ping_time = Date.now() - t.valueOf();175bar = "";176for (let j = 0; j <= Math.floor(ping_time / 10); j++) {177bar += "*";178}179mesg = `${heading}time=${ping_time}ms`;180} catch (err) {181bar = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";182mesg = `${heading}Request error -- ${err}, ${JSON.stringify(pong)}`;183ping_time = Infinity;184}185186while (mesg.length < 40) {187mesg += " ";188}189mesg += bar;190if (opts.log != null) {191opts.log(mesg);192} else {193console.log(mesg);194}195ping_times.push(ping_time);196await delay(opts.delay_ms);197};198199for (let i = 0; i < opts.packets; i++) {200await do_ping.bind(this)(i);201}202203return ping_times;204}205}206207208