|  | // Copyright (C) 2018 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import * as m from 'mithril'; | 
|  |  | 
|  | import {Engine} from '../common/engine'; | 
|  | import { | 
|  | RawQueryResult, | 
|  | rawQueryResultColumns, | 
|  | rawQueryResultIter | 
|  | } from '../common/protos'; | 
|  | import { | 
|  | createWasmEngine, | 
|  | destroyWasmEngine, | 
|  | warmupWasmEngine, | 
|  | WasmEngineProxy | 
|  | } from '../common/wasm_engine_proxy'; | 
|  |  | 
|  | const kEngineId = 'engine'; | 
|  | const kSliceSize = 1024 * 1024; | 
|  |  | 
|  |  | 
|  | interface OnReadSlice { | 
|  | (blob: Blob, end: number, slice: ArrayBuffer): void; | 
|  | } | 
|  |  | 
|  | function readSlice( | 
|  | blob: Blob, start: number, end: number, callback: OnReadSlice) { | 
|  | const slice = blob.slice(start, end); | 
|  | const reader = new FileReader(); | 
|  | reader.onerror = e => { | 
|  | console.error(e); | 
|  | }; | 
|  | reader.onloadend = _ => { | 
|  | callback(blob, end, reader.result as ArrayBuffer); | 
|  | }; | 
|  | reader.readAsArrayBuffer(slice); | 
|  | } | 
|  |  | 
|  |  | 
|  | // Represents an in flight or resolved query. | 
|  | type QueryState = QueryPendingState|QueryResultState|QueryErrorState; | 
|  |  | 
|  | interface QueryResultState { | 
|  | kind: 'QueryResultState'; | 
|  | id: number; | 
|  | query: string; | 
|  | result: RawQueryResult; | 
|  | executionTimeNs: number; | 
|  | } | 
|  |  | 
|  | interface QueryErrorState { | 
|  | kind: 'QueryErrorState'; | 
|  | id: number; | 
|  | query: string; | 
|  | error: string; | 
|  | } | 
|  |  | 
|  | interface QueryPendingState { | 
|  | kind: 'QueryPendingState'; | 
|  | id: number; | 
|  | query: string; | 
|  | } | 
|  |  | 
|  | function isPending(q: QueryState): q is QueryPendingState { | 
|  | return q.kind === 'QueryPendingState'; | 
|  | } | 
|  |  | 
|  | function isError(q: QueryState): q is QueryErrorState { | 
|  | return q.kind === 'QueryErrorState'; | 
|  | } | 
|  |  | 
|  | function isResult(q: QueryState): q is QueryResultState { | 
|  | return q.kind === 'QueryResultState'; | 
|  | } | 
|  |  | 
|  |  | 
|  | // Helpers for accessing a query result | 
|  | function columns(result: RawQueryResult): string[] { | 
|  | return [...rawQueryResultColumns(result)]; | 
|  | } | 
|  |  | 
|  | function rows(result: RawQueryResult, offset: number, count: number): | 
|  | Array<Array<number|string>> { | 
|  | const rows: Array<Array<number|string>> = []; | 
|  |  | 
|  | let i = 0; | 
|  | for (const value of rawQueryResultIter(result)) { | 
|  | if (i < offset) continue; | 
|  | if (i > offset + count) break; | 
|  | rows.push(Object.values(value)); | 
|  | i++; | 
|  | } | 
|  | return rows; | 
|  | } | 
|  |  | 
|  |  | 
|  | // State machine controller for the UI. | 
|  | type Input = NewFile|NewQuery|MoreData|QuerySuccess|QueryFailure; | 
|  |  | 
|  | interface NewFile { | 
|  | kind: 'NewFile'; | 
|  | file: File; | 
|  | } | 
|  |  | 
|  | interface MoreData { | 
|  | kind: 'MoreData'; | 
|  | end: number; | 
|  | source: Blob; | 
|  | buffer: ArrayBuffer; | 
|  | } | 
|  |  | 
|  | interface NewQuery { | 
|  | kind: 'NewQuery'; | 
|  | query: string; | 
|  | } | 
|  |  | 
|  | interface QuerySuccess { | 
|  | kind: 'QuerySuccess'; | 
|  | id: number; | 
|  | result: RawQueryResult; | 
|  | } | 
|  |  | 
|  | interface QueryFailure { | 
|  | kind: 'QueryFailure'; | 
|  | id: number; | 
|  | error: string; | 
|  | } | 
|  |  | 
|  | class QueryController { | 
|  | engine: Engine|undefined; | 
|  | file: File|undefined; | 
|  | state: 'initial'|'loading'|'ready'; | 
|  | render: (state: QueryController) => void; | 
|  | nextQueryId: number; | 
|  | queries: Map<number, QueryState>; | 
|  |  | 
|  | constructor(render: (state: QueryController) => void) { | 
|  | this.render = render; | 
|  | this.state = 'initial'; | 
|  | this.nextQueryId = 0; | 
|  | this.queries = new Map(); | 
|  | this.render(this); | 
|  | } | 
|  |  | 
|  | onInput(input: Input) { | 
|  | // tslint:disable-next-line no-any | 
|  | const f = (this as any)[`${this.state}On${input.kind}`]; | 
|  | if (f === undefined) { | 
|  | throw new Error(`No edge for input '${input.kind}' in '${this.state}'`); | 
|  | } | 
|  | f.call(this, input); | 
|  | this.render(this); | 
|  | } | 
|  |  | 
|  | initialOnNewFile(input: NewFile) { | 
|  | this.state = 'loading'; | 
|  | if (this.engine) { | 
|  | destroyWasmEngine(kEngineId); | 
|  | } | 
|  | this.engine = new WasmEngineProxy('engine', createWasmEngine(kEngineId)); | 
|  |  | 
|  | this.file = input.file; | 
|  | this.readNextSlice(0); | 
|  | } | 
|  |  | 
|  | loadingOnMoreData(input: MoreData) { | 
|  | if (input.source !== this.file) return; | 
|  | this.engine!.parse(new Uint8Array(input.buffer)); | 
|  | if (input.end === this.file.size) { | 
|  | this.engine!.notifyEof(); | 
|  | this.state = 'ready'; | 
|  | } else { | 
|  | this.readNextSlice(input.end); | 
|  | } | 
|  | } | 
|  |  | 
|  | readyOnNewQuery(input: NewQuery) { | 
|  | const id = this.nextQueryId++; | 
|  | this.queries.set(id, { | 
|  | kind: 'QueryPendingState', | 
|  | id, | 
|  | query: input.query, | 
|  | }); | 
|  |  | 
|  | this.engine!.query(input.query) | 
|  | .then(result => { | 
|  | if (result.error) { | 
|  | this.onInput({ | 
|  | kind: 'QueryFailure', | 
|  | id, | 
|  | error: result.error, | 
|  | }); | 
|  | } else { | 
|  | this.onInput({ | 
|  | kind: 'QuerySuccess', | 
|  | id, | 
|  | result, | 
|  | }); | 
|  | } | 
|  | }) | 
|  | .catch(error => { | 
|  | this.onInput({ | 
|  | kind: 'QueryFailure', | 
|  | id, | 
|  | error, | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | readyOnQuerySuccess(input: QuerySuccess) { | 
|  | const oldQueryState = this.queries.get(input.id); | 
|  | console.log('sucess', input); | 
|  | if (!oldQueryState) return; | 
|  | this.queries.set(input.id, { | 
|  | kind: 'QueryResultState', | 
|  | id: oldQueryState.id, | 
|  | query: oldQueryState.query, | 
|  | result: input.result, | 
|  | executionTimeNs: +input.result.executionTimeNs, | 
|  | }); | 
|  | } | 
|  |  | 
|  | readyOnQueryFailure(input: QueryFailure) { | 
|  | const oldQueryState = this.queries.get(input.id); | 
|  | console.log('failure', input); | 
|  | if (!oldQueryState) return; | 
|  | this.queries.set(input.id, { | 
|  | kind: 'QueryErrorState', | 
|  | id: oldQueryState.id, | 
|  | query: oldQueryState.query, | 
|  | error: input.error, | 
|  | }); | 
|  | } | 
|  |  | 
|  | readNextSlice(start: number) { | 
|  | const end = Math.min(this.file!.size, start + kSliceSize); | 
|  | readSlice(this.file!, start, end, (source, end, buffer) => { | 
|  | this.onInput({ | 
|  | kind: 'MoreData', | 
|  | end, | 
|  | source, | 
|  | buffer, | 
|  | }); | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | function render(root: Element, controller: QueryController) { | 
|  | const queries = [...controller.queries.values()].sort((a, b) => b.id - a.id); | 
|  | m.render(root, [ | 
|  | m('h1', controller.state), | 
|  | m('input[type=file]', { | 
|  | onchange: (e: Event) => { | 
|  | if (!(e.target instanceof HTMLInputElement)) return; | 
|  | if (!e.target.files) return; | 
|  | if (!e.target.files[0]) return; | 
|  | const file = e.target.files[0]; | 
|  | controller.onInput({ | 
|  | kind: 'NewFile', | 
|  | file, | 
|  | }); | 
|  | }, | 
|  | }), | 
|  | m('input[type=text]', { | 
|  | disabled: controller.state !== 'ready', | 
|  | onchange: (e: Event) => { | 
|  | controller.onInput({ | 
|  | kind: 'NewQuery', | 
|  | query: (e.target as HTMLInputElement).value, | 
|  | }); | 
|  | } | 
|  | }), | 
|  | m('.query-list', | 
|  | queries.map( | 
|  | q => | 
|  | m('.query', | 
|  | { | 
|  | key: q.id, | 
|  | }, | 
|  | m('.query-text', q.query), | 
|  | m('.query-time', | 
|  | isResult(q) ? `${q.executionTimeNs / 1000000}ms` : ''), | 
|  | isResult(q) ? m('.query-content', renderTable(q.result)) : null, | 
|  | isError(q) ? m('.query-content', q.error) : null, | 
|  | isPending(q) ? m('.query-content') : null))), | 
|  | ]); | 
|  | } | 
|  |  | 
|  | function renderTable(result: RawQueryResult) { | 
|  | return m( | 
|  | 'table', | 
|  | m('tr', columns(result).map(c => m('th', c))), | 
|  | rows(result, 0, 1000).map(r => { | 
|  | return m('tr', Object.values(r).map(d => m('td', d))); | 
|  | })); | 
|  | } | 
|  |  | 
|  | function main() { | 
|  | warmupWasmEngine(); | 
|  | const root = document.querySelector('#root'); | 
|  | if (!root) throw new Error('Could not find root element'); | 
|  | new QueryController(ctrl => render(root, ctrl)); | 
|  | } | 
|  |  | 
|  | main(); |