Home | History | Annotate | Download | only in controller
      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 
     15 import {Patch, produce} from 'immer';
     16 
     17 import {assertExists} from '../base/logging';
     18 import {Remote} from '../base/remote';
     19 import {DeferredAction, StateActions} from '../common/actions';
     20 import {Engine} from '../common/engine';
     21 import {createEmptyState, State} from '../common/state';
     22 import {
     23   createWasmEngine,
     24   destroyWasmEngine,
     25   WasmEngineProxy
     26 } from '../common/wasm_engine_proxy';
     27 
     28 import {ControllerAny} from './controller';
     29 
     30 
     31 export 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  */
     43 class 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 
    142 export const globals = new Globals();
    143