[ui] Port cpu_freq tracks.
CpuFreqTrackController does some async stuff in it's onSetup function.
Previously this function was being called fire-n-forget, which meant
that onBoundsChange could be called before onSetup was complete. Thus,
this CL adds a more strict lifecycle to tracks where onCreate() is
guaranteed to complete before any of the other methods are called.
Change-Id: I5998fea769741c3218b28817f60cbfb279c3381c
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 2c2d5e7..64cfd63 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -15,6 +15,7 @@
import {hex} from 'color-convert';
import m from 'mithril';
+import {Disposable} from '../base/disposable';
import {currentTargetOffset} from '../base/dom_utils';
import {Icons} from '../base/semantic_icons';
import {duration, Span, time} from '../base/time';
@@ -101,7 +102,7 @@
}
interface TrackShellAttrs {
- track: TrackLike;
+ track: TrackLifecycleContainer;
trackState: TrackState;
}
@@ -233,7 +234,7 @@
}
export interface TrackContentAttrs {
- track: TrackLike;
+ track: TrackLifecycleContainer;
}
export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
private mouseDownX?: number;
@@ -292,7 +293,7 @@
interface TrackComponentAttrs {
trackState: TrackState;
- track: TrackLike;
+ track: TrackLifecycleContainer;
}
class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
view({attrs}: m.CVnode<TrackComponentAttrs>) {
@@ -352,12 +353,121 @@
selectable: boolean;
}
+enum TrackLifecycleState {
+ Initializing,
+ Initialized,
+ DestroyPending,
+ Destroying,
+ Destroyed,
+}
+
+export class TrackLifecycleContainer implements Disposable {
+ private state = TrackLifecycleState.Initializing;
+
+ constructor(private track: TrackLike) {
+ track.onCreate().then(() => {
+ if (this.state === TrackLifecycleState.DestroyPending) {
+ track.onDestroy();
+ this.state = TrackLifecycleState.Destroying;
+ } else {
+ this.state = TrackLifecycleState.Initialized;
+ }
+ });
+ }
+
+ onFullRedraw(): void {
+ if (this.state === TrackLifecycleState.Initialized) {
+ this.track.onFullRedraw();
+ }
+ }
+
+ getSliceRect(
+ visibleTimeScale: TimeScale, visibleWindow: Span<time, bigint>,
+ windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
+ |undefined {
+ if (this.state === TrackLifecycleState.Initialized) {
+ return this.track.getSliceRect(
+ visibleTimeScale, visibleWindow, windowSpan, tStart, tEnd, depth);
+ } else {
+ return undefined;
+ }
+ }
+
+ getHeight(): number {
+ if (this.state === TrackLifecycleState.Initialized) {
+ return this.track.getHeight();
+ } else {
+ return 18;
+ }
+ }
+
+ getTrackShellButtons(): m.Vnode<TrackButtonAttrs, {}>[] {
+ if (this.state === TrackLifecycleState.Initialized) {
+ return this.track.getTrackShellButtons();
+ } else {
+ return [];
+ }
+ }
+
+ getContextMenu(): m.Vnode<any, {}>|null {
+ if (this.state === TrackLifecycleState.Initialized) {
+ return this.track.getContextMenu();
+ } else {
+ return null;
+ }
+ }
+
+ onMouseMove(position: {x: number; y: number;}): void {
+ if (this.state === TrackLifecycleState.Initialized) {
+ this.track.onMouseMove(position);
+ }
+ }
+
+ onMouseClick(position: {x: number; y: number;}): boolean {
+ if (this.state === TrackLifecycleState.Initialized) {
+ return this.track.onMouseClick(position);
+ } else {
+ return false;
+ }
+ }
+
+ onMouseOut(): void {
+ if (this.state === TrackLifecycleState.Initialized) {
+ this.track.onMouseOut();
+ }
+ }
+
+ render(ctx: CanvasRenderingContext2D) {
+ if (this.state === TrackLifecycleState.Initialized) {
+ this.track.render(ctx);
+ }
+ }
+
+ dispose() {
+ switch (this.state) {
+ case TrackLifecycleState.Initializing:
+ this.state = TrackLifecycleState.DestroyPending;
+ break;
+ case TrackLifecycleState.Initialized:
+ this.state = TrackLifecycleState.Destroying;
+ this.track.onDestroy().then(() => {
+ this.state = TrackLifecycleState.Destroyed;
+ });
+ break;
+ case TrackLifecycleState.DestroyPending:
+ case TrackLifecycleState.Destroying:
+ case TrackLifecycleState.Destroyed:
+ break;
+ default:
+ const x: never = this.state;
+ throw new Error(`Invalid state "${x}"`);
+ }
+ }
+}
+
export class TrackPanel extends Panel<TrackPanelAttrs> {
- // TODO(hjd): It would be nicer if these could not be undefined here.
- // We should implement a NullTrack which can be used if the trackState
- // has disappeared.
- private track: TrackLike|undefined;
- private trackState: TrackState|undefined;
+ private track?: TrackLifecycleContainer;
+ private trackState?: TrackState;
private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) {
const trackId = vnode.attrs.id;
@@ -379,10 +489,13 @@
},
};
- this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
- loadTrack(trackState, id);
- this.track?.onCreate();
- this.trackState = trackState;
+ const track = uri ? pluginManager.createTrack(uri, trackCtx) :
+ loadTrack(trackState, id);
+
+ if (track) {
+ this.track = new TrackLifecycleContainer(track);
+ this.trackState = trackState;
+ }
}
view(vnode: m.CVnode<TrackPanelAttrs>) {
@@ -410,7 +523,7 @@
onremove() {
if (this.track !== undefined) {
- this.track.onDestroy();
+ this.track.dispose();
this.track = undefined;
}
}