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';
17
18const SLICE_SIZE = 32 * 1024 * 1024;
19
20// The object returned by TraceStream.readChunk() promise.
21export interface TraceChunk {
22  data: Uint8Array;
23  eof: boolean;
24  bytesRead: number;
25  bytesTotal: number;
26}
27
28// Base interface for loading trace data in chunks.
29// The caller has to call readChunk() until TraceChunk.eof == true.
30export interface TraceStream {
31  readChunk(): Promise<TraceChunk>;
32}
33
34// Loads a trace from a File object. For the "open file" use case.
35export class TraceFileStream implements TraceStream {
36  private traceFile: Blob;
37  private reader: FileReader;
38  private pendingRead?: Deferred<TraceChunk>;
39  private bytesRead = 0;
40
41  constructor(traceFile: Blob) {
42    this.traceFile = traceFile;
43    this.reader = new FileReader();
44    this.reader.onloadend = () => this.onLoad();
45  }
46
47  onLoad() {
48    const res = assertExists(this.reader.result) as ArrayBuffer;
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    this.bytesRead += res.byteLength;
56    pendingRead.resolve({
57      data: new Uint8Array(res),
58      eof: this.bytesRead >= this.traceFile.size,
59      bytesRead: this.bytesRead,
60      bytesTotal: this.traceFile.size,
61    });
62  }
63
64  readChunk(): Promise<TraceChunk> {
65    const sliceEnd = Math.min(this.bytesRead + SLICE_SIZE, this.traceFile.size);
66    const slice = this.traceFile.slice(this.bytesRead, sliceEnd);
67    this.pendingRead = defer<TraceChunk>();
68    this.reader.readAsArrayBuffer(slice);
69    return this.pendingRead;
70  }
71}
72
73// Loads a trace from an ArrayBuffer. For the window.open() + postMessage
74// use-case, used by other dashboards (see post_message_handler.ts).
75export class TraceBufferStream implements TraceStream {
76  private traceBuf: ArrayBuffer;
77  private bytesRead = 0;
78
79  constructor(traceBuf: ArrayBuffer) {
80    this.traceBuf = traceBuf;
81  }
82
83  readChunk(): Promise<TraceChunk> {
84    assertTrue(this.bytesRead <= this.traceBuf.byteLength);
85    const len = Math.min(SLICE_SIZE, this.traceBuf.byteLength - this.bytesRead);
86    const data = new Uint8Array(this.traceBuf, this.bytesRead, len);
87    this.bytesRead += len;
88    return Promise.resolve({
89      data,
90      eof: this.bytesRead >= this.traceBuf.byteLength,
91      bytesRead: this.bytesRead,
92      bytesTotal: this.traceBuf.byteLength,
93    });
94  }
95}
96
97// Loads a stream from a URL via fetch(). For the permalink (?s=UUID) and
98// open url (?url=http://...) cases.
99export class TraceHttpStream implements TraceStream {
100  private bytesRead = 0;
101  private bytesTotal = 0;
102  private uri: string;
103  private httpStream?: ReadableStreamReader;
104
105  constructor(uri: string) {
106    assertTrue(uri.startsWith('http://') || uri.startsWith('https://'));
107    this.uri = uri;
108  }
109
110  async readChunk(): Promise<TraceChunk> {
111    // Initialize the fetch() job on the first read request.
112    if (this.httpStream === undefined) {
113      const response = await fetch(this.uri);
114      if (response.status !== 200) {
115        throw new Error(`HTTP ${response.status} - ${response.statusText}`);
116      }
117      const len = response.headers.get('Content-Length');
118      this.bytesTotal = len ? Number.parseInt(len, 10) : 0;
119      // tslint:disable-next-line no-any
120      this.httpStream = (response.body as any).getReader();
121    }
122
123    let eof = false;
124    let bytesRead = 0;
125    const chunks = [];
126
127    // httpStream can return very small chunks which can slow down
128    // TraceProcessor. Here we accumulate chunks until we get at least 32mb
129    // or hit EOF.
130    while (!eof && bytesRead < 32 * 1024 * 1024) {
131      const res = (await this.httpStream!.read()) as
132          {value?: Uint8Array, done: boolean};
133      if (res.value) {
134        chunks.push(res.value);
135        bytesRead += res.value.length;
136      }
137      eof = res.done;
138    }
139
140    let data;
141    if (chunks.length === 1) {
142      data = chunks[0];
143    } else {
144      // Stitch all the chunks into one big array:
145      data = new Uint8Array(bytesRead);
146      let offset = 0;
147      for (const chunk of chunks) {
148        data.set(chunk, offset);
149        offset += chunk.length;
150      }
151    }
152
153    this.bytesRead += data.length;
154
155    return {
156      data,
157      eof,
158      bytesRead: this.bytesRead,
159      bytesTotal: this.bytesTotal,
160    };
161  }
162}
163