blob: bbb1f2cb87abae8bb876b90dbff35e9b36428690 [file] [log] [blame]
// 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();