@@ -87,12 +87,28 @@ type Socket =
8787export type ReconnectMetadata = {
8888 connectionCount : number ;
8989 lastCloseReason : string | null ;
90+ clientTs : number ;
9091} ;
9192
9293export type OnMessageResponse = {
9394 hasSyncedPastLastReconnect : boolean ;
9495} ;
9596
97+ let firstTime : number | undefined ;
98+ function monotonicMillis ( ) {
99+ if ( firstTime === undefined ) {
100+ firstTime = Date . now ( ) ;
101+ }
102+ if ( typeof performance === "undefined" || ! performance . now ) {
103+ return Date . now ( ) ;
104+ }
105+ return Math . round ( firstTime + performance . now ( ) ) ;
106+ }
107+
108+ function prettyNow ( ) {
109+ return `t=${ Math . round ( ( monotonicMillis ( ) - firstTime ! ) / 100 ) / 10 } s` ;
110+ }
111+
96112const serverDisconnectErrors = {
97113 // A known error, e.g. during a restart or push
98114 InternalServerError : { timeout : 1000 } ,
@@ -264,11 +280,21 @@ export class WebSocketManager {
264280 this . onOpen ( {
265281 connectionCount : this . connectionCount ,
266282 lastCloseReason : this . lastCloseReason ,
283+ clientTs : monotonicMillis ( ) ,
267284 } ) ;
268285 }
269286
270287 if ( this . lastCloseReason !== "InitialConnect" ) {
271- this . logger . log ( "WebSocket reconnected" ) ;
288+ if ( this . lastCloseReason ) {
289+ this . logger . log (
290+ "WebSocket reconnected at" ,
291+ prettyNow ( ) ,
292+ "after disconnect due to" ,
293+ this . lastCloseReason ,
294+ ) ;
295+ } else {
296+ this . logger . log ( "WebSocket reconnected at" , prettyNow ( ) ) ;
297+ }
272298 }
273299
274300 this . connectionCount += 1 ;
@@ -277,12 +303,50 @@ export class WebSocketManager {
277303 // NB: The WebSocket API calls `onclose` even if connection fails, so we can route all error paths through `onclose`.
278304 ws . onerror = ( error ) => {
279305 const message = ( error as ErrorEvent ) . message ;
280- this . logger . log ( `WebSocket error: ${ message } ` ) ;
306+ if ( message ) {
307+ this . logger . log ( `WebSocket error message: ${ message } ` ) ;
308+ }
281309 } ;
282310 ws . onmessage = ( message ) => {
283311 this . resetServerInactivityTimeout ( ) ;
312+ const messageLength = message . data . length ;
284313 const serverMessage = parseServerMessage ( JSON . parse ( message . data ) ) ;
285314 this . _logVerbose ( `received ws message with type ${ serverMessage . type } ` ) ;
315+ if (
316+ serverMessage . type === "Transition" &&
317+ serverMessage . clientClockSkew !== undefined &&
318+ serverMessage . serverTs !== undefined
319+ ) {
320+ const transitionTransitTime =
321+ monotonicMillis ( ) - // client time now
322+ // clientClockSkew = (server time + upstream latency) - client time
323+ // clientClockSkew is "how many milliseconds behind (slow) is the client clock"
324+ // but the latency of the Connect message inflates this, making it appear further behind
325+ serverMessage . clientClockSkew -
326+ serverMessage . serverTs / 1_000_000 ; // server time when transition was sent
327+ const prettyTransitionTime = `${ Math . round ( transitionTransitTime ) } ms` ;
328+ const prettyMessageMB = `${ Math . round ( messageLength / 10_000 ) / 100 } MB` ;
329+ const bytesPerSecond = messageLength / ( transitionTransitTime / 1000 ) ;
330+ const prettyBytesPerSecond = `${ Math . round ( bytesPerSecond / 10_000 ) / 100 } MB per second` ;
331+ this . _logVerbose (
332+ `received ${ prettyMessageMB } transition in ${ prettyTransitionTime } at ${ prettyBytesPerSecond } ` ,
333+ ) ;
334+
335+ // Warnings that will show up for *all users*, so these are not very aggressive goals.
336+ if ( transitionTransitTime > 10_000 && messageLength > 10_000_000 ) {
337+ this . logger . log (
338+ `received query results totalling more than 10MB (${ prettyMessageMB } ) which took more than 10s (${ prettyTransitionTime } ) to arrive` ,
339+ ) ;
340+ } else if ( messageLength > 20_000_000 ) {
341+ this . logger . log (
342+ `received query results totalling more that 20MB (${ prettyMessageMB } ) which will take a long time to download on slower connections` ,
343+ ) ;
344+ } else if ( transitionTransitTime > 20_000 ) {
345+ this . logger . log (
346+ `received query results totalling ${ prettyMessageMB } which took more than 20s to arrive (${ prettyTransitionTime } )` ,
347+ ) ;
348+ }
349+ }
286350 const response = this . onMessage ( serverMessage ) ;
287351 if ( response . hasSyncedPastLastReconnect ) {
288352 // Reset backoff to 0 once all outstanding requests are complete.
@@ -293,7 +357,8 @@ export class WebSocketManager {
293357 ws . onclose = ( event ) => {
294358 this . _logVerbose ( "begin ws.onclose" ) ;
295359 if ( this . lastCloseReason === null ) {
296- this . lastCloseReason = event . reason ?? "OnCloseInvoked" ;
360+ // event.reason is often an empty string
361+ this . lastCloseReason = event . reason || `closed with code ${ event . code } ` ;
297362 }
298363 if (
299364 event . code !== CLOSE_NORMAL &&
@@ -342,17 +407,18 @@ export class WebSocketManager {
342407 if ( this . socket . state === "ready" && this . socket . paused === "no" ) {
343408 const encodedMessage = encodeClientMessage ( message ) ;
344409 const request = JSON . stringify ( encodedMessage ) ;
410+ let sent = false ;
345411 try {
346412 this . socket . ws . send ( request ) ;
413+ sent = true ;
347414 } catch ( error : any ) {
348415 this . logger . log (
349416 `Failed to send message on WebSocket, reconnecting: ${ error } ` ,
350417 ) ;
351418 this . closeAndReconnect ( "FailedToSendMessage" ) ;
352419 }
353- // We are not sure if this was sent or not.
354420 this . _logVerbose (
355- `sent message with type ${ message . type } : ${ JSON . stringify (
421+ `${ sent ? "sent" : "failed to send" } message with type ${ message . type } : ${ JSON . stringify (
356422 messageForLog ,
357423 ) } `,
358424 ) ;
@@ -385,7 +451,7 @@ export class WebSocketManager {
385451 this . socket = { state : "disconnected" } ;
386452 const backoff = this . nextBackoff ( reason ) ;
387453 this . markConnectionStateDirty ( ) ;
388- this . logger . log ( `Attempting reconnect in ${ backoff } ms` ) ;
454+ this . logger . log ( `Attempting reconnect in ${ Math . round ( backoff ) } ms` ) ;
389455 setTimeout ( ( ) => this . connect ( ) , backoff ) ;
390456 }
391457
@@ -568,6 +634,7 @@ export class WebSocketManager {
568634 this . onOpen ( {
569635 connectionCount : this . connectionCount ,
570636 lastCloseReason : this . lastCloseReason ,
637+ clientTs : monotonicMillis ( ) ,
571638 } ) ;
572639 } else if ( this . socket . paused === "yes" ) {
573640 this . socket = { ...this . socket , paused : "no" } ;
0 commit comments