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