// 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> { const rows: Array> = []; 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; 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({ id: 'engine', worker: 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();