CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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