xref: /aosp_15_r20/external/perfetto/ui/src/core/cache_manager.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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
19 * a long time) or when the user accidentally hits reload.
20 */
21import {TraceArrayBufferSource, TraceSource} from './trace_source';
22
23const TRACE_CACHE_NAME = 'cached_traces';
24const TRACE_CACHE_SIZE = 10;
25
26let LAZY_CACHE: Cache | undefined = undefined;
27
28async function getCache(): Promise<Cache | undefined> {
29  if (self.caches === undefined) {
30    // The browser doesn't support cache storage or the page is opened from
31    // a non-secure origin.
32    return undefined;
33  }
34  if (LAZY_CACHE !== undefined) {
35    return LAZY_CACHE;
36  }
37  LAZY_CACHE = await caches.open(TRACE_CACHE_NAME);
38  return LAZY_CACHE;
39}
40
41async function cacheDelete(key: Request): Promise<boolean> {
42  try {
43    const cache = await getCache();
44    if (cache === undefined) return false; // Cache storage not supported.
45    return await cache.delete(key);
46  } catch (_) {
47    // TODO(288483453): Reinstate:
48    // return ignoreCacheUnactionableErrors(e, false);
49    return false;
50  }
51}
52
53async function cachePut(key: string, value: Response): Promise<void> {
54  try {
55    const cache = await getCache();
56    if (cache === undefined) return; // Cache storage not supported.
57    await cache.put(key, value);
58  } catch (_) {
59    // TODO(288483453): Reinstate:
60    // ignoreCacheUnactionableErrors(e, undefined);
61  }
62}
63
64async function cacheMatch(
65  key: Request | string,
66): Promise<Response | undefined> {
67  try {
68    const cache = await getCache();
69    if (cache === undefined) return undefined; // Cache storage not supported.
70    return await cache.match(key);
71  } catch (_) {
72    // TODO(288483453): Reinstate:
73    // ignoreCacheUnactionableErrors(e, undefined);
74    return undefined;
75  }
76}
77
78async function cacheKeys(): Promise<readonly Request[]> {
79  try {
80    const cache = await getCache();
81    if (cache === undefined) return []; // Cache storage not supported.
82    return await cache.keys();
83  } catch (e) {
84    // TODO(288483453): Reinstate:
85    // return ignoreCacheUnactionableErrors(e, []);
86    return [];
87  }
88}
89
90export async function cacheTrace(
91  traceSource: TraceSource,
92  traceUuid: string,
93): Promise<boolean> {
94  let trace;
95  let title = '';
96  let fileName = '';
97  let url = '';
98  let contentLength = 0;
99  let localOnly = false;
100  switch (traceSource.type) {
101    case 'ARRAY_BUFFER':
102      trace = traceSource.buffer;
103      title = traceSource.title;
104      fileName = traceSource.fileName ?? '';
105      url = traceSource.url ?? '';
106      contentLength = traceSource.buffer.byteLength;
107      localOnly = traceSource.localOnly || false;
108      break;
109    case 'FILE':
110      trace = await traceSource.file.arrayBuffer();
111      title = traceSource.file.name;
112      contentLength = traceSource.file.size;
113      break;
114    default:
115      return false;
116  }
117
118  const headers = new Headers([
119    ['x-trace-title', encodeURI(title)],
120    ['x-trace-url', url],
121    ['x-trace-filename', fileName],
122    ['x-trace-local-only', `${localOnly}`],
123    ['content-type', 'application/octet-stream'],
124    ['content-length', `${contentLength}`],
125    [
126      'expires',
127      // Expires in a week from now (now = upload time)
128      new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 7).toUTCString(),
129    ],
130  ]);
131  await deleteStaleEntries();
132  await cachePut(
133    `/_${TRACE_CACHE_NAME}/${traceUuid}`,
134    new Response(trace, {headers}),
135  );
136  return true;
137}
138
139export async function tryGetTrace(
140  traceUuid: string,
141): Promise<TraceArrayBufferSource | undefined> {
142  await deleteStaleEntries();
143  const response = await cacheMatch(`/_${TRACE_CACHE_NAME}/${traceUuid}`);
144
145  if (!response) return undefined;
146  return {
147    type: 'ARRAY_BUFFER',
148    buffer: await response.arrayBuffer(),
149    title: decodeURI(response.headers.get('x-trace-title') ?? ''),
150    fileName: response.headers.get('x-trace-filename') ?? undefined,
151    url: response.headers.get('x-trace-url') ?? undefined,
152    uuid: traceUuid,
153    localOnly: response.headers.get('x-trace-local-only') === 'true',
154  };
155}
156
157async function deleteStaleEntries() {
158  // Loop through stored traces and invalidate all but the most recent
159  // TRACE_CACHE_SIZE.
160  const keys = await cacheKeys();
161  const storedTraces: Array<{key: Request; date: Date}> = [];
162  const now = new Date();
163  const deletions = [];
164  for (const key of keys) {
165    const existingTrace = await cacheMatch(key);
166    if (existingTrace === undefined) {
167      continue;
168    }
169    const expires = existingTrace.headers.get('expires');
170    if (expires === undefined || expires === null) {
171      // Missing `expires`, so give up and delete which is better than
172      // keeping it around forever.
173      deletions.push(cacheDelete(key));
174      continue;
175    }
176    const expiryDate = new Date(expires);
177    if (expiryDate < now) {
178      deletions.push(cacheDelete(key));
179    } else {
180      storedTraces.push({key, date: expiryDate});
181    }
182  }
183
184  // Sort the traces descending by time, such that most recent ones are placed
185  // at the beginning. Then, take traces from TRACE_CACHE_SIZE onwards and
186  // delete them from cache.
187  const oldTraces = storedTraces
188    .sort((a, b) => b.date.getTime() - a.date.getTime())
189    .slice(TRACE_CACHE_SIZE);
190  for (const oldTrace of oldTraces) {
191    deletions.push(cacheDelete(oldTrace.key));
192  }
193
194  // TODO(hjd): Wrong Promise.all here, should use the one that
195  // ignores failures but need to upgrade TypeScript for that.
196  await Promise.all(deletions);
197}
198