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