// 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 {Patch, produce} from 'immer'; import {assertExists} from '../base/logging'; import {Remote} from '../base/remote'; import {DeferredAction, StateActions} from '../common/actions'; import {Engine} from '../common/engine'; import {createEmptyState, State} from '../common/state'; import { createWasmEngine, destroyWasmEngine, WasmEngineProxy } from '../common/wasm_engine_proxy'; import {ControllerAny} from './controller'; export interface App { state: State; dispatch(action: DeferredAction): void; publish( what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'| 'SliceDetails', data: {}, transferList?: Array<{}>): void; } /** * Global accessors for state/dispatch in the controller. */ class Globals implements App { private _state?: State; private _rootController?: ControllerAny; private _frontend?: Remote; private _runningControllers = false; private _queuedActions = new Array(); initialize(rootController: ControllerAny, frontendProxy: Remote) { this._rootController = rootController; this._frontend = frontendProxy; this._state = createEmptyState(); } dispatch(action: DeferredAction): void { this.dispatchMultiple([action]); } dispatchMultiple(actions: DeferredAction[]): void { this._queuedActions = this._queuedActions.concat(actions); // If we are in the middle of running the controllers, queue the actions // and run them at the end of the run, so the state is atomically updated // only at the end and all controllers see the same state. if (this._runningControllers) return; this.runControllers(); } private runControllers() { if (this._runningControllers) throw new Error('Re-entrant call detected'); // Run controllers locally until all state machines reach quiescence. let runAgain = false; const patches: Patch[] = []; for (let iter = 0; runAgain || this._queuedActions.length > 0; iter++) { if (iter > 100) throw new Error('Controllers are stuck in a livelock'); const actions = this._queuedActions; this._queuedActions = new Array(); for (const action of actions) { patches.push(...this.applyAction(action)); } this._runningControllers = true; try { runAgain = assertExists(this._rootController).invoke(); } finally { this._runningControllers = false; } } assertExists(this._frontend).send('patchState', [patches]); } createEngine(): Engine { const id = new Date().toUTCString(); const portAndId = {id, worker: createWasmEngine(id)}; return new WasmEngineProxy(portAndId); } destroyEngine(id: string): void { destroyWasmEngine(id); } // TODO: this needs to be cleaned up. publish( what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace'| 'SliceDetails', data: {}, transferList?: Transferable[]) { assertExists(this._frontend) .send(`publish${what}`, [data], transferList); } get state(): State { return assertExists(this._state); } applyAction(action: DeferredAction): Patch[] { assertExists(this._state); const patches: Patch[] = []; // 'produce' creates a immer proxy which wraps the current state turning // all imperative mutations of the state done in the callback into // immutable changes to the returned state. this._state = produce( this.state, draft => { // tslint:disable-next-line no-any (StateActions as any)[action.type](draft, action.args); }, (morePatches, _) => { patches.push(...morePatches); }); return patches; } resetForTesting() { this._state = undefined; this._rootController = undefined; } } export const globals = new Globals();