Skip to content

Commit 9d4e4aa

Browse files
authored
Add timings to the setting up of the web socket (#59)
1 parent 4d1247b commit 9d4e4aa

File tree

4 files changed

+60
-10
lines changed

4 files changed

+60
-10
lines changed

src/edge.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ async function handleApiCall(url, request, env) {
9494
// This is where the requests for the worker come in. They can either be pure API requests or
9595
// requests to set up a session with a Durable Object through a Yjs WebSocket.
9696
export async function handleApiRequest(request, env) {
97+
let timingDaAdminHeadDuration;
98+
const timingStartTime = Date.now();
99+
97100
// We've received a pure API request - handle it and return.
98101
const url = new URL(request.url);
99102
if (url.pathname.startsWith('/api/')) {
@@ -127,7 +130,9 @@ export async function handleApiRequest(request, env) {
127130
opts.headers = new Headers({ Authorization: auth });
128131
}
129132

133+
const timingBeforeDaAdminHead = Date.now();
130134
const initialReq = await env.daadmin.fetch(docName, opts);
135+
timingDaAdminHeadDuration = Date.now() - timingBeforeDaAdminHead;
131136

132137
if (!initialReq.ok && initialReq.status !== 404) {
133138
// eslint-disable-next-line no-console
@@ -140,6 +145,7 @@ export async function handleApiRequest(request, env) {
140145
return new Response('unable to get resource', { status: 500 });
141146
}
142147

148+
const timingBeforeDocRoomGet = Date.now();
143149
// Each Durable Object has a 256-bit unique ID. Route the request based on the path.
144150
const id = env.rooms.idFromName(docName);
145151

@@ -150,11 +156,17 @@ export async function handleApiRequest(request, env) {
150156
// created on-demand when the ID is first used, there's nothing to wait for anyway; we know
151157
// an object will be available somewhere to receive our requests.
152158
const roomObject = env.rooms.get(id);
159+
const timingDocRoomGetDuration = Date.now() - timingBeforeDocRoomGet;
153160

154161
// eslint-disable-next-line no-console
155162
console.log(`FETCHING: ${docName} ${id}`);
156163

157-
const headers = [...request.headers, ['X-collab-room', docName]];
164+
const headers = [...request.headers,
165+
['X-collab-room', docName],
166+
['X-timing-start', timingStartTime],
167+
['X-timing-da-admin-head-duration', timingDaAdminHeadDuration],
168+
['X-timing-docroom-get-duration', timingDocRoomGetDuration],
169+
];
158170
if (auth) {
159171
headers.push(['Authorization', auth]);
160172
}
@@ -248,6 +260,7 @@ export class DocRoom {
248260
return new Response('expected docName', { status: 400 });
249261
}
250262

263+
const timingBeforeSetupWebsocket = Date.now();
251264
// To accept the WebSocket request, we create a WebSocketPair (which is like a socketpair,
252265
// i.e. two WebSockets that talk to each other), we return one end of the pair in the
253266
// response, and we operate on the other end. Note that this API is not part of the
@@ -256,10 +269,20 @@ export class DocRoom {
256269
const pair = DocRoom.newWebSocketPair();
257270

258271
// We're going to take pair[1] as our end, and return pair[0] to the client.
259-
await this.handleSession(pair[1], docName, auth);
272+
const timingData = await this.handleSession(pair[1], docName, auth);
273+
const timingSetupWebSocketDuration = Date.now() - timingBeforeSetupWebsocket;
274+
275+
const reqHeaders = request.headers;
276+
const respheaders = new Headers();
277+
respheaders.set('X-1-timing-da-admin-head-duration', reqHeaders.get('X-timing-da-admin-head-duration'));
278+
respheaders.set('X-2-timing-docroom-get-duration', reqHeaders.get('X-timing-docroom-get-duration'));
279+
respheaders.set('X-4-timing-da-admin-get-duration', timingData.get('timingDaAdminGetDuration'));
280+
respheaders.set('X-5-timing-read-state-duration', timingData.get('timingReadStateDuration'));
281+
respheaders.set('X-7-timing-setup-websocket-duration', timingSetupWebSocketDuration);
282+
respheaders.set('X-9-timing-full-duration', Date.now() - reqHeaders.get('X-timing-start'));
260283

261284
// Now we return the other end of the pair to the client.
262-
return new Response(null, { status: successCode, webSocket: pair[0] });
285+
return new Response(null, { status: successCode, headers: respheaders, webSocket: pair[0] });
263286
}
264287

265288
/**
@@ -277,6 +300,7 @@ export class DocRoom {
277300
// eslint-disable-next-line no-console
278301
console.log(`setupWSConnection ${docName} with auth(${webSocket.auth
279302
? webSocket.auth.substring(0, webSocket.auth.indexOf(' ')) : 'none'})`);
280-
await setupWSConnection(webSocket, docName, this.env, this.storage);
303+
const timingData = await setupWSConnection(webSocket, docName, this.env, this.storage);
304+
return timingData;
281305
}
282306
}

src/shareddoc.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,16 @@ export const persistence = {
265265
* @param {TransactionalStorage} storage - the worker transactional storage object
266266
*/
267267
bindState: async (docName, ydoc, conn, storage) => {
268+
let timingReadStateDuration;
269+
let timingDaAdminGetDuration;
270+
268271
let current;
269272
let restored = false; // True if restored from worker storage
270273
try {
271274
let newDoc = false;
275+
const timingBeforeDaAdminGet = Date.now();
272276
current = await persistence.get(docName, conn.auth, ydoc.daadmin);
277+
timingDaAdminGetDuration = Date.now() - timingBeforeDaAdminGet;
273278
if (current === null) {
274279
// The document isn't there any more, clear the local storage
275280
await storage.deleteAll();
@@ -278,8 +283,11 @@ export const persistence = {
278283
newDoc = true;
279284
}
280285

286+
const timingBeforeReadState = Date.now();
281287
// Read the stored state from internal worker storage
282288
const stored = await readState(docName, storage);
289+
timingReadStateDuration = Date.now() - timingBeforeReadState;
290+
283291
if (stored && stored.length > 0) {
284292
Y.applyUpdate(ydoc, stored);
285293

@@ -344,6 +352,11 @@ export const persistence = {
344352
current = await persistence.update(ydoc, current);
345353
}
346354
}, 2000, { maxWait: 10000 }));
355+
356+
const timingMap = new Map();
357+
timingMap.set('timingReadStateDuration', timingReadStateDuration);
358+
timingMap.set('timingDaAdminGetDuration', timingDaAdminGetDuration);
359+
return timingMap;
347360
},
348361
};
349362

@@ -403,7 +416,7 @@ export class WSSharedDoc extends Y.Doc {
403416
* @param {boolean} gc - whether garbage collection is enabled
404417
* @returns The Yjs document object, which may be shared across multiple sockets.
405418
*/
406-
export const getYDoc = async (docname, conn, env, storage, gc = true) => {
419+
export const getYDoc = async (docname, conn, env, storage, timingData, gc = true) => {
407420
let doc = docs.get(docname);
408421
if (doc === undefined) {
409422
// The doc is not yet in the cache, create a new one.
@@ -426,7 +439,10 @@ export const getYDoc = async (docname, conn, env, storage, gc = true) => {
426439

427440
// We wait for the promise, for second and subsequent connections to the same doc, this will
428441
// already be resolved.
429-
await doc.promise;
442+
const timings = await doc.promise;
443+
if (timingData) {
444+
timings.forEach((v, k) => timingData.set(k, v));
445+
}
430446
return doc;
431447
};
432448

@@ -498,10 +514,12 @@ export const invalidateFromAdmin = async (docName) => {
498514
* @returns {Promise<void>} - The return value of this
499515
*/
500516
export const setupWSConnection = async (conn, docName, env, storage) => {
517+
const timingData = new Map();
518+
501519
// eslint-disable-next-line no-param-reassign
502520
conn.binaryType = 'arraybuffer';
503521
// get doc, initialize if it does not exist yet
504-
const doc = await getYDoc(docName, conn, env, storage, true);
522+
const doc = await getYDoc(docName, conn, env, storage, timingData, true);
505523

506524
// listen and reply to events
507525
conn.addEventListener('message', (message) => messageListener(conn, doc, new Uint8Array(message.data)));
@@ -527,4 +545,6 @@ export const setupWSConnection = async (conn, docName, env, storage) => {
527545
send(doc, conn, encoding.toUint8Array(encoder));
528546
}
529547
}
548+
549+
return timingData;
530550
};

test/edge.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ describe('Worker test suite', () => {
220220

221221
try {
222222
const bindCalled = [];
223-
persistence.bindState = async (nm, d, c) => bindCalled.push({nm, d, c});
223+
persistence.bindState = async (nm, d, c) => {
224+
bindCalled.push({nm, d, c});
225+
return new Map();
226+
}
224227

225228
const wspCalled = [];
226229
const wsp0 = {};

test/shareddoc.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,10 @@ describe('Collab Test Suite', () => {
781781

782782
try {
783783
const bindCalls = [];
784-
persistence.bindState = async (nm, d, c, s) => bindCalls.push({nm, d, c, s});
784+
persistence.bindState = async (nm, d, c, s) => {
785+
bindCalls.push({nm, d, c, s});
786+
return new Map();
787+
}
785788

786789
const docName = 'https://somewhere.com/somedoc.html';
787790
const eventListeners = new Map();
@@ -826,7 +829,7 @@ describe('Collab Test Suite', () => {
826829
const savedBind = persistence.bindState;
827830

828831
try {
829-
persistence.bindState = async (nm, d, c, s) => {};
832+
persistence.bindState = async (nm, d, c, s) => new Map();
830833

831834
const docName = 'https://somewhere.com/myotherdoc.html';
832835
const closeCalls = [];

0 commit comments

Comments
 (0)