• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 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
15import {defer, Deferred} from '../base/deferred';
16import {assertExists, assertTrue} from '../base/logging';
17import {exists} from '../base/utils';
18
19const SLICE_SIZE = 32 * 1024 * 1024;
20
21// The object returned by TraceStream.readChunk() promise.
22export interface TraceChunk {
23  data: Uint8Array;
24  eof: boolean;
25  bytesRead: number;
26  bytesTotal: number;
27}
28
29// Base interface for loading trace data in chunks.
30// The caller has to call readChunk() until TraceChunk.eof == true.
31export interface TraceStream {
32  readChunk(): Promise<TraceChunk>;
33}
34
35// Loads a trace from a File object. For the "open file" use case.
36export class TraceFileStream implements TraceStream {
37  private traceFile: Blob;
38  private reader: FileReader;
39  private pendingRead?: Deferred<TraceChunk>;
40  private bytesRead = 0;
41
42  constructor(traceFile: Blob) {
43    this.traceFile = traceFile;
44    this.reader = new FileReader();
45    this.reader.onloadend = () => this.onLoad();
46  }
47
48  private onLoad() {
49    const pendingRead = assertExists(this.pendingRead);
50    this.pendingRead = undefined;
51    if (this.reader.error) {
52      pendingRead.reject(this.reader.error);
53      return;
54    }
55    const res = assertExists(this.reader.result) as ArrayBuffer;
56    this.bytesRead += res.byteLength;
57    pendingRead.resolve({
58      data: new Uint8Array(res),
59      eof: this.bytesRead >= this.traceFile.size,
60      bytesRead: this.bytesRead,
61      bytesTotal: this.traceFile.size,
62    });
63  }
64
65  readChunk(): Promise<TraceChunk> {
66    const sliceEnd = Math.min(this.bytesRead + SLICE_SIZE, this.traceFile.size);
67    const slice = this.traceFile.slice(this.bytesRead, sliceEnd);
68    this.pendingRead = defer<TraceChunk>();
69    this.reader.readAsArrayBuffer(slice);
70    return this.pendingRead;
71  }
72}
73
74// Loads a trace from an ArrayBuffer. For the window.open() + postMessage
75// use-case, used by other dashboards (see post_message_handler.ts).
76export class TraceBufferStream implements TraceStream {
77  private traceBuf: ArrayBuffer;
78  private bytesRead = 0;
79
80  constructor(traceBuf: ArrayBuffer) {
81    // This is subtle. Technically speaking a Uint8Array is type-compatible with
82    // ArrayBuffer, so you can pass here a Uint8Array rather than an ArrayBufer.
83    // However, the new Uint8Array(buf, off, len) below works only if the arg
84    // is an ArrayBuffer, and silently ignores the (off,len) if the argument is
85    // a Uint8Array. We could try to deal gracefully with both cases in this
86    // class, but then other parts of the code (e.g. cache_manager.ts) will have
87    // similar bugs if traceInfo.source is not a pure ArrayBuffer.
88    // See b/390473162.
89    assertTrue(traceBuf instanceof ArrayBuffer);
90    this.traceBuf = traceBuf;
91  }
92
93  readChunk(): Promise<TraceChunk> {
94    assertTrue(this.bytesRead <= this.traceBuf.byteLength);
95    const len = Math.min(SLICE_SIZE, this.traceBuf.byteLength - this.bytesRead);
96    const data = new Uint8Array(this.traceBuf, this.bytesRead, len);
97    this.bytesRead += len;
98    return Promise.resolve({
99      data,
100      eof: this.bytesRead >= this.traceBuf.byteLength,
101      bytesRead: this.bytesRead,
102      bytesTotal: this.traceBuf.byteLength,
103    });
104  }
105}
106
107// Loads a stream from a URL via fetch(). For the permalink (?s=UUID) and
108// open url (?url=http://...) cases.
109export class TraceHttpStream implements TraceStream {
110  private bytesRead = 0;
111  private bytesTotal = 0;
112  private uri: string;
113  private httpStream?: ReadableStreamDefaultReader<Uint8Array>;
114
115  constructor(uri: string) {
116    assertTrue(uri.startsWith('http://') || uri.startsWith('https://'));
117    this.uri = uri;
118  }
119
120  async readChunk(): Promise<TraceChunk> {
121    // Initialize the fetch() job on the first read request.
122    if (this.httpStream === undefined) {
123      const response = await fetch(this.uri);
124      if (response.status !== 200) {
125        throw new Error(`HTTP ${response.status} - ${response.statusText}`);
126      }
127      const len = response.headers.get('Content-Length');
128      this.bytesTotal = exists(len) ? Number.parseInt(len, 10) : 0;
129      this.httpStream = response.body!.getReader();
130    }
131
132    let eof = false;
133    let bytesRead = 0;
134    const chunks = [];
135
136    // httpStream can return very small chunks which can slow down
137    // TraceProcessor. Here we accumulate chunks until we get at least 32mb
138    // or hit EOF.
139    while (!eof && bytesRead < 32 * 1024 * 1024) {
140      const res = await this.httpStream.read();
141      if (res.value) {
142        chunks.push(res.value);
143        bytesRead += res.value.length;
144      }
145      eof = res.done;
146    }
147
148    let data;
149    if (chunks.length === 1) {
150      data = chunks[0];
151    } else {
152      // Stitch all the chunks into one big array:
153      data = new Uint8Array(bytesRead);
154      let offset = 0;
155      for (const chunk of chunks) {
156        data.set(chunk, offset);
157        offset += chunk.length;
158      }
159    }
160
161    this.bytesRead += data.length;
162
163    return {
164      data,
165      eof,
166      bytesRead: this.bytesRead,
167      bytesTotal: this.bytesTotal,
168    };
169  }
170}
171