1// Copyright (C) 2021 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15/** 16 * This file deals with caching traces in the browser's Cache storage. The 17 * traces are cached so that the UI can gracefully reload a trace when the tab 18 * containing it is discarded by Chrome(e.g. because the tab was not used for a 19 * long time) or when the user accidentally hits reload. 20 */ 21import {assertExists} from '../base/logging'; 22import {TraceArrayBufferSource, TraceSource} from './state'; 23 24const TRACE_CACHE_NAME = 'cached_traces'; 25const TRACE_CACHE_SIZE = 10; 26 27export async function cacheTrace( 28 traceSource: TraceSource, traceUuid: string): Promise<boolean> { 29 let trace, title = '', fileName = '', url = '', contentLength = 0, 30 localOnly = false; 31 switch (traceSource.type) { 32 case 'ARRAY_BUFFER': 33 trace = traceSource.buffer; 34 title = traceSource.title; 35 fileName = traceSource.fileName || ''; 36 url = traceSource.url || ''; 37 contentLength = traceSource.buffer.byteLength; 38 localOnly = traceSource.localOnly || false; 39 break; 40 case 'FILE': 41 trace = await traceSource.file.arrayBuffer(); 42 title = traceSource.file.name; 43 contentLength = traceSource.file.size; 44 break; 45 default: 46 return false; 47 } 48 assertExists(trace); 49 50 const headers = new Headers([ 51 ['x-trace-title', title], 52 ['x-trace-url', url], 53 ['x-trace-filename', fileName], 54 ['x-trace-local-only', `${localOnly}`], 55 ['content-type', 'application/octet-stream'], 56 ['content-length', `${contentLength}`], 57 [ 58 'expires', 59 // Expires in a week from now (now = upload time) 60 (new Date((new Date()).getTime() + (1000 * 60 * 60 * 24 * 7))) 61 .toUTCString() 62 ] 63 ]); 64 const traceCache = await caches.open(TRACE_CACHE_NAME); 65 await deleteStaleEntries(traceCache); 66 await traceCache.put( 67 `/_${TRACE_CACHE_NAME}/${traceUuid}`, new Response(trace, {headers})); 68 return true; 69} 70 71export async function tryGetTrace(traceUuid: string): 72 Promise<TraceArrayBufferSource|undefined> { 73 await deleteStaleEntries(await caches.open(TRACE_CACHE_NAME)); 74 const response = await caches.match( 75 `/_${TRACE_CACHE_NAME}/${traceUuid}`, {cacheName: TRACE_CACHE_NAME}); 76 77 if (!response) return undefined; 78 return { 79 type: 'ARRAY_BUFFER', 80 buffer: await response.arrayBuffer(), 81 title: response.headers.get('x-trace-title') || '', 82 fileName: response.headers.get('x-trace-filename') || undefined, 83 url: response.headers.get('x-trace-url') || undefined, 84 uuid: traceUuid, 85 localOnly: response.headers.get('x-trace-local-only') === 'true' 86 }; 87} 88 89async function deleteStaleEntries(traceCache: Cache) { 90 /* 91 * Loop through stored caches and invalidate all but the most recent 10. 92 */ 93 const keys = await traceCache.keys(); 94 const storedTraces: Array<{key: Request, date: Date}> = []; 95 for (const key of keys) { 96 const existingTrace = assertExists(await traceCache.match(key)); 97 const expiryDate = 98 new Date(assertExists(existingTrace.headers.get('expires'))); 99 if (expiryDate < new Date()) { 100 await traceCache.delete(key); 101 } else { 102 storedTraces.push({key, date: expiryDate}); 103 } 104 } 105 106 if (storedTraces.length <= TRACE_CACHE_SIZE) return; 107 108 /* 109 * Sort the traces descending by time, such that most recent ones are placed 110 * at the beginning. Then, take traces from TRACE_CACHE_SIZE onwards and 111 * delete them from cache. 112 */ 113 const oldTraces = 114 storedTraces.sort((a, b) => b.date.getTime() - a.date.getTime()) 115 .slice(TRACE_CACHE_SIZE); 116 for (const oldTrace of oldTraces) { 117 await traceCache.delete(oldTrace.key); 118 } 119} 120