1// Copyright (C) 2018 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 * as m from 'mithril'; 16 17import {Engine} from '../common/engine'; 18import { 19 RawQueryResult, 20 rawQueryResultColumns, 21 rawQueryResultIter 22} from '../common/protos'; 23import { 24 createWasmEngine, 25 destroyWasmEngine, 26 warmupWasmEngine, 27 WasmEngineProxy 28} from '../common/wasm_engine_proxy'; 29 30const kEngineId = 'engine'; 31const kSliceSize = 1024 * 1024; 32 33 34interface OnReadSlice { 35 (blob: Blob, end: number, slice: ArrayBuffer): void; 36} 37 38function readSlice( 39 blob: Blob, start: number, end: number, callback: OnReadSlice) { 40 const slice = blob.slice(start, end); 41 const reader = new FileReader(); 42 reader.onerror = e => { 43 console.error(e); 44 }; 45 reader.onloadend = _ => { 46 callback(blob, end, reader.result as ArrayBuffer); 47 }; 48 reader.readAsArrayBuffer(slice); 49} 50 51 52// Represents an in flight or resolved query. 53type QueryState = QueryPendingState|QueryResultState|QueryErrorState; 54 55interface QueryResultState { 56 kind: 'QueryResultState'; 57 id: number; 58 query: string; 59 result: RawQueryResult; 60 executionTimeNs: number; 61} 62 63interface QueryErrorState { 64 kind: 'QueryErrorState'; 65 id: number; 66 query: string; 67 error: string; 68} 69 70interface QueryPendingState { 71 kind: 'QueryPendingState'; 72 id: number; 73 query: string; 74} 75 76function isPending(q: QueryState): q is QueryPendingState { 77 return q.kind === 'QueryPendingState'; 78} 79 80function isError(q: QueryState): q is QueryErrorState { 81 return q.kind === 'QueryErrorState'; 82} 83 84function isResult(q: QueryState): q is QueryResultState { 85 return q.kind === 'QueryResultState'; 86} 87 88 89// Helpers for accessing a query result 90function columns(result: RawQueryResult): string[] { 91 return [...rawQueryResultColumns(result)]; 92} 93 94function rows(result: RawQueryResult, offset: number, count: number): 95 Array<Array<number|string>> { 96 const rows: Array<Array<number|string>> = []; 97 98 let i = 0; 99 for (const value of rawQueryResultIter(result)) { 100 if (i < offset) continue; 101 if (i > offset + count) break; 102 rows.push(Object.values(value)); 103 i++; 104 } 105 return rows; 106} 107 108 109// State machine controller for the UI. 110type Input = NewFile|NewQuery|MoreData|QuerySuccess|QueryFailure; 111 112interface NewFile { 113 kind: 'NewFile'; 114 file: File; 115} 116 117interface MoreData { 118 kind: 'MoreData'; 119 end: number; 120 source: Blob; 121 buffer: ArrayBuffer; 122} 123 124interface NewQuery { 125 kind: 'NewQuery'; 126 query: string; 127} 128 129interface QuerySuccess { 130 kind: 'QuerySuccess'; 131 id: number; 132 result: RawQueryResult; 133} 134 135interface QueryFailure { 136 kind: 'QueryFailure'; 137 id: number; 138 error: string; 139} 140 141class QueryController { 142 engine: Engine|undefined; 143 file: File|undefined; 144 state: 'initial'|'loading'|'ready'; 145 render: (state: QueryController) => void; 146 nextQueryId: number; 147 queries: Map<number, QueryState>; 148 149 constructor(render: (state: QueryController) => void) { 150 this.render = render; 151 this.state = 'initial'; 152 this.nextQueryId = 0; 153 this.queries = new Map(); 154 this.render(this); 155 } 156 157 onInput(input: Input) { 158 // tslint:disable-next-line no-any 159 const f = (this as any)[`${this.state}On${input.kind}`]; 160 if (f === undefined) { 161 throw new Error(`No edge for input '${input.kind}' in '${this.state}'`); 162 } 163 f.call(this, input); 164 this.render(this); 165 } 166 167 initialOnNewFile(input: NewFile) { 168 this.state = 'loading'; 169 if (this.engine) { 170 destroyWasmEngine(kEngineId); 171 } 172 this.engine = new WasmEngineProxy({ 173 id: 'engine', 174 worker: createWasmEngine(kEngineId), 175 }); 176 177 this.file = input.file; 178 this.readNextSlice(0); 179 } 180 181 loadingOnMoreData(input: MoreData) { 182 if (input.source !== this.file) return; 183 this.engine!.parse(new Uint8Array(input.buffer)); 184 if (input.end === this.file.size) { 185 this.engine!.notifyEof(); 186 this.state = 'ready'; 187 } else { 188 this.readNextSlice(input.end); 189 } 190 } 191 192 readyOnNewQuery(input: NewQuery) { 193 const id = this.nextQueryId++; 194 this.queries.set(id, { 195 kind: 'QueryPendingState', 196 id, 197 query: input.query, 198 }); 199 200 this.engine!.query(input.query) 201 .then(result => { 202 if (result.error) { 203 this.onInput({ 204 kind: 'QueryFailure', 205 id, 206 error: result.error, 207 }); 208 } else { 209 this.onInput({ 210 kind: 'QuerySuccess', 211 id, 212 result, 213 }); 214 } 215 }) 216 .catch(error => { 217 this.onInput({ 218 kind: 'QueryFailure', 219 id, 220 error, 221 }); 222 }); 223 } 224 225 readyOnQuerySuccess(input: QuerySuccess) { 226 const oldQueryState = this.queries.get(input.id); 227 console.log('sucess', input); 228 if (!oldQueryState) return; 229 this.queries.set(input.id, { 230 kind: 'QueryResultState', 231 id: oldQueryState.id, 232 query: oldQueryState.query, 233 result: input.result, 234 executionTimeNs: +input.result.executionTimeNs, 235 }); 236 } 237 238 readyOnQueryFailure(input: QueryFailure) { 239 const oldQueryState = this.queries.get(input.id); 240 console.log('failure', input); 241 if (!oldQueryState) return; 242 this.queries.set(input.id, { 243 kind: 'QueryErrorState', 244 id: oldQueryState.id, 245 query: oldQueryState.query, 246 error: input.error, 247 }); 248 } 249 250 readNextSlice(start: number) { 251 const end = Math.min(this.file!.size, start + kSliceSize); 252 readSlice(this.file!, start, end, (source, end, buffer) => { 253 this.onInput({ 254 kind: 'MoreData', 255 end, 256 source, 257 buffer, 258 }); 259 }); 260 } 261} 262 263function render(root: Element, controller: QueryController) { 264 const queries = [...controller.queries.values()].sort((a, b) => b.id - a.id); 265 m.render(root, [ 266 m('h1', controller.state), 267 m('input[type=file]', { 268 onchange: (e: Event) => { 269 if (!(e.target instanceof HTMLInputElement)) return; 270 if (!e.target.files) return; 271 if (!e.target.files[0]) return; 272 const file = e.target.files[0]; 273 controller.onInput({ 274 kind: 'NewFile', 275 file, 276 }); 277 }, 278 }), 279 m('input[type=text]', { 280 disabled: controller.state !== 'ready', 281 onchange: (e: Event) => { 282 controller.onInput({ 283 kind: 'NewQuery', 284 query: (e.target as HTMLInputElement).value, 285 }); 286 } 287 }), 288 m('.query-list', 289 queries.map( 290 q => 291 m('.query', 292 { 293 key: q.id, 294 }, 295 m('.query-text', q.query), 296 m('.query-time', 297 isResult(q) ? `${q.executionTimeNs / 1000000}ms` : ''), 298 isResult(q) ? m('.query-content', renderTable(q.result)) : null, 299 isError(q) ? m('.query-content', q.error) : null, 300 isPending(q) ? m('.query-content') : null, ))), 301 ]); 302} 303 304function renderTable(result: RawQueryResult) { 305 return m( 306 'table', 307 m('tr', columns(result).map(c => m('th', c))), 308 rows(result, 0, 1000).map(r => { 309 return m('tr', Object.values(r).map(d => m('td', d))); 310 }), ); 311} 312 313function main() { 314 warmupWasmEngine(); 315 const root = document.querySelector('#root'); 316 if (!root) throw new Error('Could not find root element'); 317 new QueryController(ctrl => render(root, ctrl)); 318} 319 320main(); 321