diff --git a/services/web/frontend/js/serviceWorker.js b/services/web/frontend/js/serviceWorker.js index 8ee1c37409..18123e6e5e 100644 --- a/services/web/frontend/js/serviceWorker.js +++ b/services/web/frontend/js/serviceWorker.js @@ -3,6 +3,25 @@ const MIN_CHUNK_SIZE = 128 * 1024 const PDF_FILES = new Map() +const METRICS = { + epoch: Date.now(), + cachedBytes: 0, + fetchedBytes: 0, + requestedBytes: 0, +} + +/** + * + * @param {number} size + * @param {number} cachedBytes + * @param {number} fetchedBytes + */ +function trackStats({ size, cachedBytes, fetchedBytes }) { + METRICS.cachedBytes += cachedBytes + METRICS.fetchedBytes += fetchedBytes + METRICS.requestedBytes += size +} + /** * @param {FetchEvent} event */ @@ -18,6 +37,7 @@ function onFetch(event) { if (ctx) { return processPdfRequest(event, ctx) } + // other request, ignore } @@ -27,8 +47,9 @@ function processCompileRequest(event) { if (response.status !== 200) return response return response.json().then(body => { - handleCompileResponse(body) - // The response body is consumed, serialize it again. + handleCompileResponse(response, body) + // Send the service workers metrics to the frontend. + body.serviceWorkerMetrics = METRICS return new Response(JSON.stringify(body), response) }) }) @@ -41,8 +62,12 @@ function processCompileRequest(event) { * @param {Object} file * @param {string} clsiServerId * @param {string} compileGroup + * @param {Date} pdfCreatedAt */ -function processPdfRequest(event, { file, clsiServerId, compileGroup }) { +function processPdfRequest( + event, + { file, clsiServerId, compileGroup, pdfCreatedAt } +) { if (!event.request.headers.has('Range') && file.size > MIN_CHUNK_SIZE) { // skip probe request const headers = new Headers() @@ -96,6 +121,8 @@ function processPdfRequest(event, { file, clsiServerId, compileGroup }) { }) ) const size = end - start + let cachedBytes = 0 + let fetchedBytes = 0 const reAssembledBlob = new Uint8Array(size) event.respondWith( Promise.all( @@ -109,6 +136,22 @@ function processPdfRequest(event, { file, clsiServerId, compileGroup }) { }` ) } + const blobFetchDate = getServerTime(response) + const blobSize = getResponseSize(response) + if (blobFetchDate && blobSize) { + const chunkSize = + Math.min(end, chunk.end) - Math.max(start, chunk.start) + // Example: 2MB PDF, 1MB image, 128KB PDF.js chunk. + // | pdf.js chunk | + // | A BIG IMAGE BLOB | + // | THE FULL PDF | + if (blobFetchDate < pdfCreatedAt) { + cachedBytes += chunkSize + } else { + // Blobs are fetched in bulk. + fetchedBytes += blobSize + } + } return response.arrayBuffer() }) .then(arrayBuffer => { @@ -134,6 +177,8 @@ function processPdfRequest(event, { file, clsiServerId, compileGroup }) { const insertPosition = Math.max(chunk.start - start, 0) reAssembledBlob.set(new Uint8Array(arrayBuffer), insertPosition) }) + + trackStats({ size, cachedBytes, fetchedBytes }) return new Response(reAssembledBlob, { status: 206, headers: { @@ -152,16 +197,40 @@ function processPdfRequest(event, { file, clsiServerId, compileGroup }) { } /** + * + * @param {Response} response + */ +function getServerTime(response) { + const raw = response.headers.get('Date') + if (!raw) return undefined + return new Date(raw) +} + +/** + * + * @param {Response} response + */ +function getResponseSize(response) { + const raw = response.headers.get('Content-Length') + if (!raw) return 0 + return parseInt(raw, 10) +} + +/** + * @param {Response} response * @param {Object} body */ -function handleCompileResponse(body) { +function handleCompileResponse(response, body) { if (!body || body.status !== 'success') return + const pdfCreatedAt = getServerTime(response) + for (const file of body.outputFiles) { if (file.path !== 'output.pdf') continue // not the pdf used for rendering if (file.ranges) { const { clsiServerId, compileGroup } = body PDF_FILES.set(file.url, { + pdfCreatedAt, file, clsiServerId, compileGroup,