• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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