blob: 606dab40017632294c9ce4ac5c82adaac7d8c1dc [file] [log] [blame]
// 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 {createEmptyState, State} from '../common/state';
import {ControllerAny} from './controller';
type PublishKinds = 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|
'LegacyTrace'|'SliceDetails'|'CounterDetails'|'HeapProfileDetails'|
'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
'RecordingLog'|'SearchResult'|'AggregateData'|'CpuProfileDetails'|
'TraceErrors'|'UpdateChromeCategories'|'ConnectedFlows'|'SelectedFlows'|
'ThreadStateDetails'|'MetricError'|'MetricResult';
export interface App {
state: State;
dispatch(action: DeferredAction): void;
publish(what: PublishKinds, 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<DeferredAction>();
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<DeferredAction>();
for (const action of actions) {
const originalLength = patches.length;
const morePatches = this.applyAction(action);
patches.length += morePatches.length;
for (let i = 0; i < morePatches.length; ++i) {
patches[i + originalLength] = morePatches[i];
}
}
this._runningControllers = true;
try {
runAgain = assertExists(this._rootController).invoke();
} finally {
this._runningControllers = false;
}
}
assertExists(this._frontend).send<void>('patchState', [patches]);
}
// TODO: this needs to be cleaned up.
publish(what: PublishKinds, data: {}, transferList?: Transferable[]) {
assertExists(this._frontend)
.send<void>(`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, _) => {
const originalLength = patches.length;
patches.length += morePatches.length;
for (let i = 0; i < morePatches.length; ++i) {
patches[i + originalLength] = morePatches[i];
}
});
return patches;
}
resetForTesting() {
this._state = undefined;
this._rootController = undefined;
}
}
export const globals = new Globals();