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 {Patch, produce} from 'immer';
16
17import {assertExists} from '../base/logging';
18import {Remote} from '../base/remote';
19import {DeferredAction, StateActions} from '../common/actions';
20import {Engine} from '../common/engine';
21import {createEmptyState, State} from '../common/state';
22import {
23  createWasmEngine,
24  destroyWasmEngine,
25  WasmEngineProxy
26} from '../common/wasm_engine_proxy';
27
28import {ControllerAny} from './controller';
29
30
31export interface App {
32  state: State;
33  dispatch(action: DeferredAction): void;
34  publish(
35      what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'|
36            'SliceDetails',
37      data: {}, transferList?: Array<{}>): void;
38}
39
40/**
41 * Global accessors for state/dispatch in the controller.
42 */
43class Globals implements App {
44  private _state?: State;
45  private _rootController?: ControllerAny;
46  private _frontend?: Remote;
47  private _runningControllers = false;
48  private _queuedActions = new Array<DeferredAction>();
49
50  initialize(rootController: ControllerAny, frontendProxy: Remote) {
51    this._rootController = rootController;
52    this._frontend = frontendProxy;
53    this._state = createEmptyState();
54  }
55
56  dispatch(action: DeferredAction): void {
57    this.dispatchMultiple([action]);
58  }
59
60  dispatchMultiple(actions: DeferredAction[]): void {
61    this._queuedActions = this._queuedActions.concat(actions);
62
63    // If we are in the middle of running the controllers, queue the actions
64    // and run them at the end of the run, so the state is atomically updated
65    // only at the end and all controllers see the same state.
66    if (this._runningControllers) return;
67
68    this.runControllers();
69  }
70
71  private runControllers() {
72    if (this._runningControllers) throw new Error('Re-entrant call detected');
73
74    // Run controllers locally until all state machines reach quiescence.
75    let runAgain = false;
76    const patches: Patch[] = [];
77    for (let iter = 0; runAgain || this._queuedActions.length > 0; iter++) {
78      if (iter > 100) throw new Error('Controllers are stuck in a livelock');
79      const actions = this._queuedActions;
80      this._queuedActions = new Array<DeferredAction>();
81      for (const action of actions) {
82        patches.push(...this.applyAction(action));
83      }
84      this._runningControllers = true;
85      try {
86        runAgain = assertExists(this._rootController).invoke();
87      } finally {
88        this._runningControllers = false;
89      }
90    }
91    assertExists(this._frontend).send<void>('patchState', [patches]);
92  }
93
94  createEngine(): Engine {
95    const id = new Date().toUTCString();
96    const portAndId = {id, worker: createWasmEngine(id)};
97    return new WasmEngineProxy(portAndId);
98  }
99
100  destroyEngine(id: string): void {
101    destroyWasmEngine(id);
102  }
103
104  // TODO: this needs to be cleaned up.
105  publish(
106      what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'|
107            'SliceDetails',
108      data: {}, transferList?: Transferable[]) {
109    assertExists(this._frontend)
110        .send<void>(`publish${what}`, [data], transferList);
111  }
112
113  get state(): State {
114    return assertExists(this._state);
115  }
116
117  applyAction(action: DeferredAction): Patch[] {
118    assertExists(this._state);
119    const patches: Patch[] = [];
120
121    // 'produce' creates a immer proxy which wraps the current state turning
122    // all imperative mutations of the state done in the callback into
123    // immutable changes to the returned state.
124    this._state = produce(
125        this.state,
126        draft => {
127          // tslint:disable-next-line no-any
128          (StateActions as any)[action.type](draft, action.args);
129        },
130        (morePatches, _) => {
131          patches.push(...morePatches);
132        });
133    return patches;
134  }
135
136  resetForTesting() {
137    this._state = undefined;
138    this._rootController = undefined;
139  }
140}
141
142export const globals = new Globals();
143