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