• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {Draft} from 'immer';
16
17import {SortDirection} from '../base/comparison_utils';
18import {assertExists, assertTrue} from '../base/logging';
19import {duration, time} from '../base/time';
20import {RecordConfig} from '../controller/record_config_types';
21import {randomColor} from '../core/colorizer';
22import {
23  GenericSliceDetailsTabConfig,
24  GenericSliceDetailsTabConfigBase,
25} from '../frontend/generic_slice_details_tab';
26import {
27  Aggregation,
28  AggregationFunction,
29  TableColumn,
30  tableColumnEquals,
31  toggleEnabled,
32} from '../frontend/pivot_table_types';
33
34import {
35  computeIntervals,
36  DropDirection,
37  performReordering,
38} from './dragndrop_logic';
39import {createEmptyState} from './empty_state';
40import {
41  MetatraceTrackId,
42  traceEventBegin,
43  traceEventEnd,
44  TraceEventScope,
45} from './metatracing';
46import {
47  AdbRecordingTarget,
48  EngineMode,
49  LoadedConfig,
50  NewEngineMode,
51  OmniboxMode,
52  OmniboxState,
53  PendingDeeplinkState,
54  PivotTableResult,
55  PrimaryTrackSortKey,
56  ProfileType,
57  RecordingTarget,
58  SCROLLING_TRACK_GROUP,
59  State,
60  Status,
61  ThreadTrackSortKey,
62  TrackSortKey,
63  UtidToTrackSortKey,
64  VisibleState,
65} from './state';
66
67type StateDraft = Draft<State>;
68
69export interface AddTrackArgs {
70  key?: string;
71  uri: string;
72  name: string;
73  labels?: string[];
74  trackSortKey: TrackSortKey;
75  trackGroup?: string;
76  closeable?: boolean;
77}
78
79export interface PostedTrace {
80  buffer: ArrayBuffer;
81  title: string;
82  fileName?: string;
83  url?: string;
84  uuid?: string;
85  localOnly?: boolean;
86  keepApiOpen?: boolean;
87
88  // Allows to pass extra arguments to plugins. This can be read by plugins
89  // onTraceLoad() and can be used to trigger plugin-specific-behaviours (e.g.
90  // allow dashboards like APC to pass extra data to materialize onto tracks).
91  // The format is the following:
92  // pluginArgs: {
93  //   'dev.perfetto.PluginFoo': { 'key1': 'value1', 'key2': 1234 }
94  //   'dev.perfetto.PluginBar': { 'key3': '...', 'key4': ... }
95  // }
96  pluginArgs?: {[pluginId: string]: {[key: string]: unknown}};
97}
98
99export interface PostedScrollToRange {
100  timeStart: number;
101  timeEnd: number;
102  viewPercentage?: number;
103}
104
105function clearTraceState(state: StateDraft) {
106  const nextId = state.nextId;
107  const recordConfig = state.recordConfig;
108  const recordingTarget = state.recordingTarget;
109  const fetchChromeCategories = state.fetchChromeCategories;
110  const extensionInstalled = state.extensionInstalled;
111  const availableAdbDevices = state.availableAdbDevices;
112  const chromeCategories = state.chromeCategories;
113  const newEngineMode = state.newEngineMode;
114
115  Object.assign(state, createEmptyState());
116  state.nextId = nextId;
117  state.recordConfig = recordConfig;
118  state.recordingTarget = recordingTarget;
119  state.fetchChromeCategories = fetchChromeCategories;
120  state.extensionInstalled = extensionInstalled;
121  state.availableAdbDevices = availableAdbDevices;
122  state.chromeCategories = chromeCategories;
123  state.newEngineMode = newEngineMode;
124}
125
126function generateNextId(draft: StateDraft): string {
127  const nextId = String(Number(draft.nextId) + 1);
128  draft.nextId = nextId;
129  return nextId;
130}
131
132// A helper to clean the state for a given removeable track.
133// This is not exported as action to make it clear that not all
134// tracks are removeable.
135function removeTrack(state: StateDraft, trackKey: string) {
136  const track = state.tracks[trackKey];
137  if (track === undefined) {
138    return;
139  }
140  delete state.tracks[trackKey];
141
142  const removeTrackId = (arr: string[]) => {
143    const index = arr.indexOf(trackKey);
144    if (index !== -1) arr.splice(index, 1);
145  };
146
147  if (track.trackGroup === SCROLLING_TRACK_GROUP) {
148    removeTrackId(state.scrollingTracks);
149  } else if (track.trackGroup !== undefined) {
150    const trackGroup = state.trackGroups[track.trackGroup];
151    if (trackGroup !== undefined) {
152      removeTrackId(trackGroup.tracks);
153    }
154  }
155  state.pinnedTracks = state.pinnedTracks.filter((key) => key !== trackKey);
156}
157
158let statusTraceEvent: TraceEventScope | undefined;
159
160export const StateActions = {
161  openTraceFromFile(state: StateDraft, args: {file: File}): void {
162    clearTraceState(state);
163    const id = generateNextId(state);
164    state.engine = {
165      id,
166      ready: false,
167      source: {type: 'FILE', file: args.file},
168    };
169  },
170
171  openTraceFromBuffer(state: StateDraft, args: PostedTrace): void {
172    clearTraceState(state);
173    const id = generateNextId(state);
174    state.engine = {
175      id,
176      ready: false,
177      source: {type: 'ARRAY_BUFFER', ...args},
178    };
179  },
180
181  openTraceFromUrl(state: StateDraft, args: {url: string}): void {
182    clearTraceState(state);
183    const id = generateNextId(state);
184    state.engine = {
185      id,
186      ready: false,
187      source: {type: 'URL', url: args.url},
188    };
189  },
190
191  openTraceFromHttpRpc(state: StateDraft, _args: {}): void {
192    clearTraceState(state);
193    const id = generateNextId(state);
194    state.engine = {
195      id,
196      ready: false,
197      source: {type: 'HTTP_RPC'},
198    };
199  },
200
201  setTraceUuid(state: StateDraft, args: {traceUuid: string}) {
202    state.traceUuid = args.traceUuid;
203  },
204
205  addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
206    args.tracks.forEach((track) => {
207      const trackKey =
208        track.key === undefined ? generateNextId(state) : track.key;
209      const name = track.name;
210      state.tracks[trackKey] = {
211        key: trackKey,
212        name,
213        trackSortKey: track.trackSortKey,
214        trackGroup: track.trackGroup,
215        labels: track.labels,
216        uri: track.uri,
217        closeable: track.closeable,
218      };
219      if (track.trackGroup === SCROLLING_TRACK_GROUP) {
220        state.scrollingTracks.push(trackKey);
221      } else if (track.trackGroup !== undefined) {
222        const group = state.trackGroups[track.trackGroup];
223        if (group !== undefined) {
224          group.tracks.push(trackKey);
225        }
226      }
227    });
228  },
229
230  // Note: While this action has traditionally been omitted, with more and more
231  // dynamic tracks being added and existing ones being moved to plugins, it
232  // makes sense to have a generic "removeTracks" action which is un-opinionated
233  // about what type of tracks we are removing.
234  // E.g. Once debug tracks have been moved to a plugin, it makes no sense to
235  // keep the "removeDebugTrack()" action, as the core should have no concept of
236  // what debug tracks are.
237  removeTracks(state: StateDraft, args: {trackKeys: string[]}) {
238    for (const trackKey of args.trackKeys) {
239      removeTrack(state, trackKey);
240    }
241  },
242
243  setUtidToTrackSortKey(
244    state: StateDraft,
245    args: {threadOrderingMetadata: UtidToTrackSortKey},
246  ) {
247    state.utidToThreadSortKey = args.threadOrderingMetadata;
248  },
249
250  addTrack(state: StateDraft, args: AddTrackArgs): void {
251    this.addTracks(state, {tracks: [args]});
252  },
253
254  addTrackGroup(
255    state: StateDraft,
256    // Define ID in action so a track group can be referred to without running
257    // the reducer.
258    args: {
259      name: string;
260      key: string;
261      summaryTrackKey?: string;
262      collapsed: boolean;
263      fixedOrdering?: boolean;
264    },
265  ): void {
266    state.trackGroups[args.key] = {
267      name: args.name,
268      key: args.key,
269      collapsed: args.collapsed,
270      tracks: [],
271      summaryTrack: args.summaryTrackKey,
272      fixedOrdering: args.fixedOrdering,
273    };
274  },
275
276  maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void {
277    const trackGroups = Object.values(state.trackGroups);
278    if (trackGroups.length === 1) {
279      trackGroups[0].collapsed = false;
280    }
281  },
282
283  sortThreadTracks(state: StateDraft, _: {}) {
284    const getFullKey = (a: string) => {
285      const track = state.tracks[a];
286      const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey;
287      if (threadTrackSortKey.utid === undefined) {
288        const sortKey = track.trackSortKey as PrimaryTrackSortKey;
289        return [sortKey, 0, 0, 0];
290      }
291      const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid];
292      return [
293        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
294        threadSortKey
295          ? threadSortKey.sortKey
296          : PrimaryTrackSortKey.ORDINARY_THREAD,
297        threadSortKey && threadSortKey.tid !== undefined
298          ? threadSortKey.tid
299          : Number.MAX_VALUE,
300        /* eslint-enable */
301        threadTrackSortKey.utid,
302        threadTrackSortKey.priority,
303      ];
304    };
305
306    // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
307    // rather than T1, T10, T11, ..., T2, T20, T21 .
308    const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
309    for (const group of Object.values(state.trackGroups)) {
310      if (group.fixedOrdering) continue;
311
312      group.tracks.sort((a: string, b: string) => {
313        const aRank = getFullKey(a);
314        const bRank = getFullKey(b);
315        for (let i = 0; i < aRank.length; i++) {
316          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
317        }
318
319        const aName = state.tracks[a].name.toLocaleLowerCase();
320        const bName = state.tracks[b].name.toLocaleLowerCase();
321        return coll.compare(aName, bName);
322      });
323    }
324  },
325
326  updateAggregateSorting(
327    state: StateDraft,
328    args: {id: string; column: string},
329  ) {
330    let prefs = state.aggregatePreferences[args.id];
331    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
332    if (!prefs) {
333      prefs = {id: args.id};
334      state.aggregatePreferences[args.id] = prefs;
335    }
336
337    if (!prefs.sorting || prefs.sorting.column !== args.column) {
338      // No sorting set for current column.
339      state.aggregatePreferences[args.id].sorting = {
340        column: args.column,
341        direction: 'DESC',
342      };
343    } else if (prefs.sorting.direction === 'DESC') {
344      // Toggle the direction if the column is currently sorted.
345      state.aggregatePreferences[args.id].sorting = {
346        column: args.column,
347        direction: 'ASC',
348      };
349    } else {
350      // If direction is currently 'ASC' toggle to no sorting.
351      state.aggregatePreferences[args.id].sorting = undefined;
352    }
353  },
354
355  moveTrack(
356    state: StateDraft,
357    args: {srcId: string; op: 'before' | 'after'; dstId: string},
358  ): void {
359    const moveWithinTrackList = (trackList: string[]) => {
360      const newList: string[] = [];
361      for (let i = 0; i < trackList.length; i++) {
362        const curTrackId = trackList[i];
363        if (curTrackId === args.dstId && args.op === 'before') {
364          newList.push(args.srcId);
365        }
366        if (curTrackId !== args.srcId) {
367          newList.push(curTrackId);
368        }
369        if (curTrackId === args.dstId && args.op === 'after') {
370          newList.push(args.srcId);
371        }
372      }
373      trackList.splice(0);
374      newList.forEach((x) => {
375        trackList.push(x);
376      });
377    };
378
379    moveWithinTrackList(state.pinnedTracks);
380    moveWithinTrackList(state.scrollingTracks);
381  },
382
383  toggleTrackPinned(state: StateDraft, args: {trackKey: string}): void {
384    const key = args.trackKey;
385    const isPinned = state.pinnedTracks.includes(key);
386    const trackGroup = assertExists(state.tracks[key]).trackGroup;
387
388    if (isPinned) {
389      state.pinnedTracks.splice(state.pinnedTracks.indexOf(key), 1);
390      if (trackGroup === SCROLLING_TRACK_GROUP) {
391        state.scrollingTracks.unshift(key);
392      }
393    } else {
394      if (trackGroup === SCROLLING_TRACK_GROUP) {
395        state.scrollingTracks.splice(state.scrollingTracks.indexOf(key), 1);
396      }
397      state.pinnedTracks.push(key);
398    }
399  },
400
401  toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
402    const trackGroup = assertExists(state.trackGroups[args.groupKey]);
403    trackGroup.collapsed = !trackGroup.collapsed;
404  },
405
406  requestTrackReload(state: StateDraft, _: {}) {
407    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
408    if (state.lastTrackReloadRequest) {
409      state.lastTrackReloadRequest++;
410    } else {
411      state.lastTrackReloadRequest = 1;
412    }
413  },
414
415  maybeSetPendingDeeplink(state: StateDraft, args: PendingDeeplinkState) {
416    state.pendingDeeplink = args;
417  },
418
419  clearPendingDeeplink(state: StateDraft, _: {}) {
420    state.pendingDeeplink = undefined;
421  },
422
423  // TODO(hjd): engine.ready should be a published thing. If it's part
424  // of the state it interacts badly with permalinks.
425  setEngineReady(
426    state: StateDraft,
427    args: {engineId: string; ready: boolean; mode: EngineMode},
428  ): void {
429    const engine = state.engine;
430    if (engine === undefined || engine.id !== args.engineId) {
431      return;
432    }
433    engine.ready = args.ready;
434    engine.mode = args.mode;
435  },
436
437  setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void {
438    state.newEngineMode = args.mode;
439  },
440
441  // Marks all engines matching the given |mode| as failed.
442  setEngineFailed(
443    state: StateDraft,
444    args: {mode: EngineMode; failure: string},
445  ): void {
446    if (state.engine !== undefined && state.engine.mode === args.mode) {
447      state.engine.failed = args.failure;
448    }
449  },
450
451  updateStatus(state: StateDraft, args: Status): void {
452    if (statusTraceEvent) {
453      traceEventEnd(statusTraceEvent);
454    }
455    statusTraceEvent = traceEventBegin(args.msg, {
456      track: MetatraceTrackId.kOmniboxStatus,
457    });
458    state.status = args;
459  },
460
461  // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
462  setState(state: StateDraft, args: {newState: State}): void {
463    for (const key of Object.keys(state)) {
464      // eslint-disable-next-line @typescript-eslint/no-explicit-any
465      delete (state as any)[key];
466    }
467    for (const key of Object.keys(args.newState)) {
468      // eslint-disable-next-line @typescript-eslint/no-explicit-any
469      (state as any)[key] = (args.newState as any)[key];
470    }
471
472    // If we're loading from a permalink then none of the engines can
473    // possibly be ready:
474    if (state.engine !== undefined) {
475      state.engine.ready = false;
476    }
477  },
478
479  setRecordConfig(
480    state: StateDraft,
481    args: {config: RecordConfig; configType?: LoadedConfig},
482  ): void {
483    state.recordConfig = args.config;
484    state.lastLoadedConfig = args.configType || {type: 'NONE'};
485  },
486
487  selectNote(state: StateDraft, args: {id: string}): void {
488    state.selection = {
489      kind: 'note',
490      id: args.id,
491    };
492  },
493
494  addNote(
495    state: StateDraft,
496    args: {timestamp: time; color: string; id?: string; text?: string},
497  ): void {
498    const {timestamp, color, id = generateNextId(state), text = ''} = args;
499    state.notes[id] = {
500      noteType: 'DEFAULT',
501      id,
502      timestamp,
503      color,
504      text,
505    };
506  },
507
508  addSpanNote(
509    state: StateDraft,
510    args: {start: time; end: time; id?: string; color?: string},
511  ): void {
512    const {
513      id = generateNextId(state),
514      color = randomColor(),
515      end,
516      start,
517    } = args;
518
519    state.notes[id] = {
520      noteType: 'SPAN',
521      start,
522      end,
523      color,
524      id,
525      text: '',
526    };
527  },
528
529  changeNoteColor(
530    state: StateDraft,
531    args: {id: string; newColor: string},
532  ): void {
533    const note = state.notes[args.id];
534    if (note === undefined) return;
535    note.color = args.newColor;
536  },
537
538  changeNoteText(state: StateDraft, args: {id: string; newText: string}): void {
539    const note = state.notes[args.id];
540    if (note === undefined) return;
541    note.text = args.newText;
542  },
543
544  removeNote(state: StateDraft, args: {id: string}): void {
545    delete state.notes[args.id];
546
547    // Clear the selection if this note was selected
548    if (state.selection.kind === 'note' && state.selection.id === args.id) {
549      state.selection = {kind: 'empty'};
550    }
551  },
552
553  selectHeapProfile(
554    state: StateDraft,
555    args: {id: number; upid: number; ts: time; type: ProfileType},
556  ): void {
557    state.selection = {
558      kind: 'legacy',
559      legacySelection: {
560        kind: 'HEAP_PROFILE',
561        id: args.id,
562        upid: args.upid,
563        ts: args.ts,
564        type: args.type,
565      },
566    };
567  },
568
569  selectPerfSamples(
570    state: StateDraft,
571    args: {
572      id: number;
573      upid: number;
574      leftTs: time;
575      rightTs: time;
576      type: ProfileType;
577    },
578  ): void {
579    state.selection = {
580      kind: 'legacy',
581      legacySelection: {
582        kind: 'PERF_SAMPLES',
583        id: args.id,
584        upid: args.upid,
585        leftTs: args.leftTs,
586        rightTs: args.rightTs,
587        type: args.type,
588      },
589    };
590  },
591
592  selectCpuProfileSample(
593    state: StateDraft,
594    args: {id: number; utid: number; ts: time},
595  ): void {
596    state.selection = {
597      kind: 'legacy',
598      legacySelection: {
599        kind: 'CPU_PROFILE_SAMPLE',
600        id: args.id,
601        utid: args.utid,
602        ts: args.ts,
603      },
604    };
605  },
606
607  selectSlice(
608    state: StateDraft,
609    args: {id: number; trackKey: string; table?: string; scroll?: boolean},
610  ): void {
611    state.selection = {
612      kind: 'legacy',
613      legacySelection: {
614        kind: 'SLICE',
615        id: args.id,
616        trackKey: args.trackKey,
617        table: args.table,
618      },
619    };
620    state.pendingScrollId = args.scroll ? args.id : undefined;
621  },
622
623  selectGenericSlice(
624    state: StateDraft,
625    args: {
626      id: number;
627      sqlTableName: string;
628      start: time;
629      duration: duration;
630      trackKey: string;
631      detailsPanelConfig: {
632        kind: string;
633        config: GenericSliceDetailsTabConfigBase;
634      };
635    },
636  ): void {
637    const detailsPanelConfig: GenericSliceDetailsTabConfig = {
638      id: args.id,
639      ...args.detailsPanelConfig.config,
640    };
641
642    state.selection = {
643      kind: 'legacy',
644      legacySelection: {
645        kind: 'GENERIC_SLICE',
646        id: args.id,
647        sqlTableName: args.sqlTableName,
648        start: args.start,
649        duration: args.duration,
650        trackKey: args.trackKey,
651        detailsPanelConfig: {
652          kind: args.detailsPanelConfig.kind,
653          config: detailsPanelConfig,
654        },
655      },
656    };
657  },
658
659  setPendingScrollId(state: StateDraft, args: {pendingScrollId: number}): void {
660    state.pendingScrollId = args.pendingScrollId;
661  },
662
663  clearPendingScrollId(state: StateDraft, _: {}): void {
664    state.pendingScrollId = undefined;
665  },
666
667  selectThreadState(
668    state: StateDraft,
669    args: {id: number; trackKey: string},
670  ): void {
671    state.selection = {
672      kind: 'legacy',
673      legacySelection: {
674        kind: 'THREAD_STATE',
675        id: args.id,
676        trackKey: args.trackKey,
677      },
678    };
679  },
680
681  startRecording(state: StateDraft, _: {}): void {
682    state.recordingInProgress = true;
683    state.lastRecordingError = undefined;
684    state.recordingCancelled = false;
685  },
686
687  stopRecording(state: StateDraft, _: {}): void {
688    state.recordingInProgress = false;
689  },
690
691  cancelRecording(state: StateDraft, _: {}): void {
692    state.recordingInProgress = false;
693    state.recordingCancelled = true;
694  },
695
696  setExtensionAvailable(state: StateDraft, args: {available: boolean}): void {
697    state.extensionInstalled = args.available;
698  },
699
700  setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void {
701    state.recordingTarget = args.target;
702  },
703
704  setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void {
705    state.fetchChromeCategories = args.fetch;
706  },
707
708  setAvailableAdbDevices(
709    state: StateDraft,
710    args: {devices: AdbRecordingTarget[]},
711  ): void {
712    state.availableAdbDevices = args.devices;
713  },
714
715  setOmnibox(state: StateDraft, args: OmniboxState): void {
716    state.omniboxState = args;
717  },
718
719  setOmniboxMode(state: StateDraft, args: {mode: OmniboxMode}): void {
720    state.omniboxState.mode = args.mode;
721  },
722
723  selectArea(
724    state: StateDraft,
725    args: {start: time; end: time; tracks: string[]},
726  ): void {
727    const {start, end, tracks} = args;
728    assertTrue(start <= end);
729    state.selection = {
730      kind: 'area',
731      start,
732      end,
733      tracks,
734    };
735  },
736
737  toggleTrackSelection(
738    state: StateDraft,
739    args: {key: string; isTrackGroup: boolean},
740  ) {
741    const selection = state.selection;
742    if (selection.kind !== 'area') {
743      return;
744    }
745
746    const index = selection.tracks.indexOf(args.key);
747    if (index > -1) {
748      selection.tracks.splice(index, 1);
749      if (args.isTrackGroup) {
750        // Also remove all child tracks.
751        for (const childTrack of state.trackGroups[args.key].tracks) {
752          const childIndex = selection.tracks.indexOf(childTrack);
753          if (childIndex > -1) {
754            selection.tracks.splice(childIndex, 1);
755          }
756        }
757      }
758    } else {
759      selection.tracks.push(args.key);
760      if (args.isTrackGroup) {
761        // Also add all child tracks.
762        for (const childTrack of state.trackGroups[args.key].tracks) {
763          if (!selection.tracks.includes(childTrack)) {
764            selection.tracks.push(childTrack);
765          }
766        }
767      }
768    }
769    // It's super unexpected that |toggleTrackSelection| does not cause
770    // selection to be updated and this leads to bugs for people who do:
771    // if (oldSelection !== state.selection) etc.
772    // To solve this re-create the selection object here:
773    state.selection = Object.assign({}, state.selection);
774  },
775
776  setVisibleTraceTime(state: StateDraft, args: VisibleState): void {
777    state.frontendLocalState.visibleState = {...args};
778  },
779
780  setChromeCategories(state: StateDraft, args: {categories: string[]}): void {
781    state.chromeCategories = args.categories;
782  },
783
784  setLastRecordingError(state: StateDraft, args: {error?: string}): void {
785    state.lastRecordingError = args.error;
786    state.recordingStatus = undefined;
787  },
788
789  setRecordingStatus(state: StateDraft, args: {status?: string}): void {
790    state.recordingStatus = args.status;
791    state.lastRecordingError = undefined;
792  },
793
794  togglePerfDebug(state: StateDraft, _: {}): void {
795    state.perfDebug = !state.perfDebug;
796  },
797
798  setSidebar(state: StateDraft, args: {visible: boolean}): void {
799    state.sidebarVisible = args.visible;
800  },
801
802  setHoveredUtidAndPid(state: StateDraft, args: {utid: number; pid: number}) {
803    state.hoveredPid = args.pid;
804    state.hoveredUtid = args.utid;
805  },
806
807  setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) {
808    state.highlightedSliceId = args.sliceId;
809  },
810
811  setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) {
812    state.focusedFlowIdLeft = args.flowId;
813  },
814
815  setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) {
816    state.focusedFlowIdRight = args.flowId;
817  },
818
819  setSearchIndex(state: StateDraft, args: {index: number}) {
820    state.searchIndex = args.index;
821  },
822
823  setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) {
824    state.hoverCursorTimestamp = args.ts;
825  },
826
827  setHoveredNoteTimestamp(state: StateDraft, args: {ts: time}) {
828    state.hoveredNoteTimestamp = args.ts;
829  },
830
831  // Add a tab with a given URI to the tab bar and show it.
832  // If the tab is already present in the tab bar, just show it.
833  showTab(state: StateDraft, args: {uri: string}) {
834    // Add tab, unless we're talking about the special current_selection tab
835    if (args.uri !== 'current_selection') {
836      // Add tab to tab list if not already
837      if (!state.tabs.openTabs.some((uri) => uri === args.uri)) {
838        state.tabs.openTabs.push(args.uri);
839      }
840    }
841    state.tabs.currentTab = args.uri;
842  },
843
844  // Hide a tab in the tab bar pick a new tab to show.
845  // Note: Attempting to hide the "current_selection" tab doesn't work. This tab
846  // is special and cannot be removed.
847  hideTab(state: StateDraft, args: {uri: string}) {
848    const tabs = state.tabs;
849    // If the removed tab is the "current" tab, we must find a new tab to focus
850    if (args.uri === tabs.currentTab) {
851      // Remember the index of the current tab
852      const currentTabIdx = tabs.openTabs.findIndex((uri) => uri === args.uri);
853
854      // Remove the tab
855      tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri);
856
857      if (currentTabIdx !== -1) {
858        if (tabs.openTabs.length === 0) {
859          // No more tabs, use current selection
860          tabs.currentTab = 'current_selection';
861        } else if (currentTabIdx < tabs.openTabs.length - 1) {
862          // Pick the tab to the right
863          tabs.currentTab = tabs.openTabs[currentTabIdx];
864        } else {
865          // Pick the last tab
866          const lastTab = tabs.openTabs[tabs.openTabs.length - 1];
867          tabs.currentTab = lastTab;
868        }
869      }
870    } else {
871      // Otherwise just remove the tab
872      tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri);
873    }
874  },
875
876  clearAllPinnedTracks(state: StateDraft, _: {}) {
877    const pinnedTracks = state.pinnedTracks.slice();
878    for (let index = pinnedTracks.length - 1; index >= 0; index--) {
879      const trackKey = pinnedTracks[index];
880      this.toggleTrackPinned(state, {trackKey});
881    }
882  },
883
884  togglePivotTable(
885    state: StateDraft,
886    args: {area?: {start: time; end: time; tracks: string[]}},
887  ) {
888    state.nonSerializableState.pivotTable.selectionArea = args.area;
889    state.nonSerializableState.pivotTable.queryResult = null;
890  },
891
892  setPivotStateQueryResult(
893    state: StateDraft,
894    args: {queryResult: PivotTableResult | null},
895  ) {
896    state.nonSerializableState.pivotTable.queryResult = args.queryResult;
897  },
898
899  setPivotTableConstrainToArea(state: StateDraft, args: {constrain: boolean}) {
900    state.nonSerializableState.pivotTable.constrainToArea = args.constrain;
901  },
902
903  dismissFlamegraphModal(state: StateDraft, _: {}) {
904    state.flamegraphModalDismissed = true;
905  },
906
907  addPivotTableAggregation(
908    state: StateDraft,
909    args: {aggregation: Aggregation; after: number},
910  ) {
911    state.nonSerializableState.pivotTable.selectedAggregations.splice(
912      args.after,
913      0,
914      args.aggregation,
915    );
916  },
917
918  removePivotTableAggregation(state: StateDraft, args: {index: number}) {
919    state.nonSerializableState.pivotTable.selectedAggregations.splice(
920      args.index,
921      1,
922    );
923  },
924
925  setPivotTableQueryRequested(
926    state: StateDraft,
927    args: {queryRequested: boolean},
928  ) {
929    state.nonSerializableState.pivotTable.queryRequested = args.queryRequested;
930  },
931
932  setPivotTablePivotSelected(
933    state: StateDraft,
934    args: {column: TableColumn; selected: boolean},
935  ) {
936    toggleEnabled(
937      tableColumnEquals,
938      state.nonSerializableState.pivotTable.selectedPivots,
939      args.column,
940      args.selected,
941    );
942  },
943
944  setPivotTableAggregationFunction(
945    state: StateDraft,
946    args: {index: number; function: AggregationFunction},
947  ) {
948    state.nonSerializableState.pivotTable.selectedAggregations[
949      args.index
950    ].aggregationFunction = args.function;
951  },
952
953  setPivotTableSortColumn(
954    state: StateDraft,
955    args: {aggregationIndex: number; order: SortDirection},
956  ) {
957    state.nonSerializableState.pivotTable.selectedAggregations =
958      state.nonSerializableState.pivotTable.selectedAggregations.map(
959        (agg, index) => ({
960          column: agg.column,
961          aggregationFunction: agg.aggregationFunction,
962          sortDirection:
963            index === args.aggregationIndex ? args.order : undefined,
964        }),
965      );
966  },
967
968  changePivotTablePivotOrder(
969    state: StateDraft,
970    args: {from: number; to: number; direction: DropDirection},
971  ) {
972    const pivots = state.nonSerializableState.pivotTable.selectedPivots;
973    state.nonSerializableState.pivotTable.selectedPivots = performReordering(
974      computeIntervals(pivots.length, args.from, args.to, args.direction),
975      pivots,
976    );
977  },
978
979  changePivotTableAggregationOrder(
980    state: StateDraft,
981    args: {from: number; to: number; direction: DropDirection},
982  ) {
983    const aggregations =
984      state.nonSerializableState.pivotTable.selectedAggregations;
985    state.nonSerializableState.pivotTable.selectedAggregations =
986      performReordering(
987        computeIntervals(
988          aggregations.length,
989          args.from,
990          args.to,
991          args.direction,
992        ),
993        aggregations,
994      );
995  },
996};
997
998// When we are on the frontend side, we don't really want to execute the
999// actions above, we just want to serialize them and marshal their
1000// arguments, send them over to the controller side and have them being
1001// executed there. The magic below takes care of turning each action into a
1002// function that returns the marshaled args.
1003
1004// A DeferredAction is a bundle of Args and a method name. This is the marshaled
1005// version of a StateActions method call.
1006export interface DeferredAction<Args = {}> {
1007  type: string;
1008  args: Args;
1009}
1010
1011// This type magic creates a type function DeferredActions<T> which takes a type
1012// T and 'maps' its attributes. For each attribute on T matching the signature:
1013// (state: StateDraft, args: Args) => void
1014// DeferredActions<T> has an attribute:
1015// (args: Args) => DeferredAction<Args>
1016type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
1017type DeferredActionFunc<T> = T extends ActionFunction<infer Args>
1018  ? (args: Args) => DeferredAction<Args>
1019  : never;
1020type DeferredActions<C> = {
1021  [P in keyof C]: DeferredActionFunc<C[P]>;
1022};
1023
1024// Actions is an implementation of DeferredActions<typeof StateActions>.
1025// (since StateActions is a variable not a type we have to do
1026// 'typeof StateActions' to access the (unnamed) type of StateActions).
1027// It's a Proxy such that any attribute access returns a function:
1028// (args) => {return {type: ATTRIBUTE_NAME, args};}
1029export const Actions =
1030  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1031  new Proxy<DeferredActions<typeof StateActions>>({} as any, {
1032    // eslint-disable-next-line @typescript-eslint/no-explicit-any
1033    get(_: any, prop: string, _2: any) {
1034      return (args: {}): DeferredAction<{}> => {
1035        return {
1036          type: prop,
1037          args,
1038        };
1039      };
1040    },
1041  });
1042