• 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 {assertExists, assertTrue, assertUnreachable} from '../base/logging';
18import {RecordConfig} from '../controller/record_config_types';
19import {globals} from '../frontend/globals';
20import {
21  Aggregation,
22  AggregationFunction,
23  TableColumn,
24  tableColumnEquals,
25  toggleEnabled,
26} from '../frontend/pivot_table_types';
27import {DebugTrackV2Config} from '../tracks/debug/slice_track';
28
29import {randomColor} from './colorizer';
30import {
31  computeIntervals,
32  DropDirection,
33  performReordering,
34} from './dragndrop_logic';
35import {createEmptyState} from './empty_state';
36import {DEFAULT_VIEWING_OPTION, PERF_SAMPLES_KEY} from './flamegraph_util';
37import {traceEventBegin, traceEventEnd, TraceEventScope} from './metatracing';
38import {
39  AdbRecordingTarget,
40  Area,
41  CallsiteInfo,
42  EngineMode,
43  FlamegraphStateViewingOption,
44  FtraceFilterPatch,
45  LoadedConfig,
46  NewEngineMode,
47  OmniboxState,
48  Pagination,
49  PivotTableResult,
50  PrimaryTrackSortKey,
51  ProfileType,
52  RecordingTarget,
53  SCROLLING_TRACK_GROUP,
54  SortDirection,
55  State,
56  Status,
57  ThreadTrackSortKey,
58  TraceTime,
59  TrackSortKey,
60  TrackState,
61  UtidToTrackSortKey,
62  VisibleState,
63} from './state';
64import {TPDuration, TPTime} from './time';
65
66export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
67
68type StateDraft = Draft<State>;
69
70export interface AddTrackArgs {
71  id?: string;
72  engineId: string;
73  kind: string;
74  name: string;
75  labels?: string[];
76  trackSortKey: TrackSortKey;
77  trackGroup?: string;
78  config: {};
79}
80
81export interface PostedTrace {
82  buffer: ArrayBuffer;
83  title: string;
84  fileName?: string;
85  url?: string;
86  uuid?: string;
87  localOnly?: boolean;
88  keepApiOpen?: boolean;
89}
90
91export interface PostedScrollToRange {
92  timeStart: TPTime;
93  timeEnd: TPTime;
94  viewPercentage?: number;
95}
96
97function clearTraceState(state: StateDraft) {
98  const nextId = state.nextId;
99  const recordConfig = state.recordConfig;
100  const recordingTarget = state.recordingTarget;
101  const fetchChromeCategories = state.fetchChromeCategories;
102  const extensionInstalled = state.extensionInstalled;
103  const availableAdbDevices = state.availableAdbDevices;
104  const chromeCategories = state.chromeCategories;
105  const newEngineMode = state.newEngineMode;
106
107  Object.assign(state, createEmptyState());
108  state.nextId = nextId;
109  state.recordConfig = recordConfig;
110  state.recordingTarget = recordingTarget;
111  state.fetchChromeCategories = fetchChromeCategories;
112  state.extensionInstalled = extensionInstalled;
113  state.availableAdbDevices = availableAdbDevices;
114  state.chromeCategories = chromeCategories;
115  state.newEngineMode = newEngineMode;
116}
117
118function generateNextId(draft: StateDraft): string {
119  const nextId = String(Number(draft.nextId) + 1);
120  draft.nextId = nextId;
121  return nextId;
122}
123
124// A helper to clean the state for a given removeable track.
125// This is not exported as action to make it clear that not all
126// tracks are removeable.
127function removeTrack(state: StateDraft, trackId: string) {
128  const track = state.tracks[trackId];
129  delete state.tracks[trackId];
130
131  const removeTrackId = (arr: string[]) => {
132    const index = arr.indexOf(trackId);
133    if (index !== -1) arr.splice(index, 1);
134  };
135
136  if (track.trackGroup === SCROLLING_TRACK_GROUP) {
137    removeTrackId(state.scrollingTracks);
138  } else if (track.trackGroup !== undefined) {
139    removeTrackId(state.trackGroups[track.trackGroup].tracks);
140  }
141  state.pinnedTracks = state.pinnedTracks.filter((id) => id !== trackId);
142}
143
144let statusTraceEvent: TraceEventScope|undefined;
145
146export const StateActions = {
147
148  openTraceFromFile(state: StateDraft, args: {file: File}): void {
149    clearTraceState(state);
150    const id = generateNextId(state);
151    state.engine = {
152      id,
153      ready: false,
154      source: {type: 'FILE', file: args.file},
155    };
156  },
157
158  openTraceFromBuffer(state: StateDraft, args: PostedTrace): void {
159    clearTraceState(state);
160    const id = generateNextId(state);
161    state.engine = {
162      id,
163      ready: false,
164      source: {type: 'ARRAY_BUFFER', ...args},
165    };
166  },
167
168  openTraceFromUrl(state: StateDraft, args: {url: string}): void {
169    clearTraceState(state);
170    const id = generateNextId(state);
171    state.engine = {
172      id,
173      ready: false,
174      source: {type: 'URL', url: args.url},
175    };
176  },
177
178  openTraceFromHttpRpc(state: StateDraft, _args: {}): void {
179    clearTraceState(state);
180    const id = generateNextId(state);
181    state.engine = {
182      id,
183      ready: false,
184      source: {type: 'HTTP_RPC'},
185    };
186  },
187
188  setTraceUuid(state: StateDraft, args: {traceUuid: string}) {
189    state.traceUuid = args.traceUuid;
190  },
191
192  fillUiTrackIdByTraceTrackId(
193      state: StateDraft, trackState: TrackState, uiTrackId: string) {
194    const namespace = (trackState.config as {namespace?: string}).namespace;
195    if (namespace !== undefined) return;
196
197    const setUiTrackId = (trackId: number, uiTrackId: string) => {
198      if (state.uiTrackIdByTraceTrackId[trackId] !== undefined &&
199          state.uiTrackIdByTraceTrackId[trackId] !== uiTrackId) {
200        throw new Error(`Trying to map track id ${trackId} to UI track ${
201            uiTrackId}, already mapped to ${
202            state.uiTrackIdByTraceTrackId[trackId]}`);
203      }
204      state.uiTrackIdByTraceTrackId[trackId] = uiTrackId;
205    };
206
207    const config = trackState.config as {trackId: number};
208    if (config.trackId !== undefined) {
209      setUiTrackId(config.trackId, uiTrackId);
210      return;
211    }
212
213    const multiple = trackState.config as {trackIds: number[]};
214    if (multiple.trackIds !== undefined) {
215      for (const trackId of multiple.trackIds) {
216        setUiTrackId(trackId, uiTrackId);
217      }
218    }
219  },
220
221  addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
222    args.tracks.forEach((track) => {
223      const id = track.id === undefined ? generateNextId(state) : track.id;
224      track.id = id;
225      state.tracks[id] = track as TrackState;
226      this.fillUiTrackIdByTraceTrackId(state, track as TrackState, id);
227      if (track.trackGroup === SCROLLING_TRACK_GROUP) {
228        state.scrollingTracks.push(id);
229      } else if (track.trackGroup !== undefined) {
230        assertExists(state.trackGroups[track.trackGroup]).tracks.push(id);
231      }
232    });
233  },
234
235  setUtidToTrackSortKey(
236      state: StateDraft, args: {threadOrderingMetadata: UtidToTrackSortKey}) {
237    state.utidToThreadSortKey = args.threadOrderingMetadata;
238  },
239
240  addTrack(state: StateDraft, args: {
241    id?: string; engineId: string; kind: string; name: string;
242    trackGroup?: string; config: {}; trackSortKey: TrackSortKey;
243  }): void {
244    const id = args.id !== undefined ? args.id : generateNextId(state);
245    state.tracks[id] = {
246      id,
247      engineId: args.engineId,
248      kind: args.kind,
249      name: args.name,
250      trackSortKey: args.trackSortKey,
251      trackGroup: args.trackGroup,
252      config: args.config,
253    };
254    this.fillUiTrackIdByTraceTrackId(state, state.tracks[id], id);
255    if (args.trackGroup === SCROLLING_TRACK_GROUP) {
256      state.scrollingTracks.push(id);
257    } else if (args.trackGroup !== undefined) {
258      assertExists(state.trackGroups[args.trackGroup]).tracks.push(id);
259    }
260  },
261
262  addTrackGroup(
263      state: StateDraft,
264      // Define ID in action so a track group can be referred to without running
265      // the reducer.
266      args: {
267        engineId: string; name: string; id: string; summaryTrackId: string;
268        collapsed: boolean;
269      }): void {
270    state.trackGroups[args.id] = {
271      engineId: args.engineId,
272      name: args.name,
273      id: args.id,
274      collapsed: args.collapsed,
275      tracks: [args.summaryTrackId],
276    };
277  },
278
279  addDebugTrack(
280      state: StateDraft,
281      args: {engineId: string, name: string, config: DebugTrackV2Config}):
282      void {
283        if (state.debugTrackId !== undefined) return;
284        const trackId = generateNextId(state);
285        this.addTrack(state, {
286          id: trackId,
287          engineId: args.engineId,
288          kind: DEBUG_SLICE_TRACK_KIND,
289          name: args.name,
290          trackSortKey: PrimaryTrackSortKey.DEBUG_SLICE_TRACK,
291          trackGroup: SCROLLING_TRACK_GROUP,
292          config: args.config,
293        });
294        this.toggleTrackPinned(state, {trackId});
295      },
296
297  removeDebugTrack(state: StateDraft, args: {trackId: string}): void {
298    const track = state.tracks[args.trackId];
299    assertTrue(track.kind === DEBUG_SLICE_TRACK_KIND);
300    removeTrack(state, args.trackId);
301  },
302
303  removeVisualisedArgTracks(state: StateDraft, args: {trackIds: string[]}) {
304    for (const trackId of args.trackIds) {
305      const track = state.tracks[trackId];
306
307      const namespace = (track.config as {namespace?: string}).namespace;
308      if (namespace === undefined) {
309        throw new Error(
310            'All visualised arg tracks should have non-empty namespace');
311      }
312
313      removeTrack(state, trackId);
314    }
315  },
316
317  maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void {
318    const trackGroups = Object.values(state.trackGroups);
319    if (trackGroups.length === 1) {
320      trackGroups[0].collapsed = false;
321    }
322  },
323
324  sortThreadTracks(state: StateDraft, _: {}) {
325    const getFullKey = (a: string) => {
326      const track = state.tracks[a];
327      const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey;
328      if (threadTrackSortKey.utid === undefined) {
329        const sortKey = track.trackSortKey as PrimaryTrackSortKey;
330        return [
331          sortKey,
332          0,
333          0,
334          0,
335        ];
336      }
337      const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid];
338      return [
339        threadSortKey ? threadSortKey.sortKey :
340                        PrimaryTrackSortKey.ORDINARY_THREAD,
341        threadSortKey && threadSortKey.tid !== undefined ? threadSortKey.tid :
342                                                           Number.MAX_VALUE,
343        threadTrackSortKey.utid,
344        threadTrackSortKey.priority,
345      ];
346    };
347
348    // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
349    // rather than T1, T10, T11, ..., T2, T20, T21 .
350    const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
351    for (const group of Object.values(state.trackGroups)) {
352      group.tracks.sort((a: string, b: string) => {
353        const aRank = getFullKey(a);
354        const bRank = getFullKey(b);
355        for (let i = 0; i < aRank.length; i++) {
356          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
357        }
358
359        const aName = state.tracks[a].name.toLocaleLowerCase();
360        const bName = state.tracks[b].name.toLocaleLowerCase();
361        return coll.compare(aName, bName);
362      });
363    }
364  },
365
366  updateAggregateSorting(
367      state: StateDraft, args: {id: string, column: string}) {
368    let prefs = state.aggregatePreferences[args.id];
369    if (!prefs) {
370      prefs = {id: args.id};
371      state.aggregatePreferences[args.id] = prefs;
372    }
373
374    if (!prefs.sorting || prefs.sorting.column !== args.column) {
375      // No sorting set for current column.
376      state.aggregatePreferences[args.id].sorting = {
377        column: args.column,
378        direction: 'DESC',
379      };
380    } else if (prefs.sorting.direction === 'DESC') {
381      // Toggle the direction if the column is currently sorted.
382      state.aggregatePreferences[args.id].sorting = {
383        column: args.column,
384        direction: 'ASC',
385      };
386    } else {
387      // If direction is currently 'ASC' toggle to no sorting.
388      state.aggregatePreferences[args.id].sorting = undefined;
389    }
390  },
391
392  setVisibleTracks(state: StateDraft, args: {tracks: string[]}) {
393    state.visibleTracks = args.tracks;
394  },
395
396  updateTrackConfig(state: StateDraft, args: {id: string, config: {}}) {
397    if (state.tracks[args.id] === undefined) return;
398    state.tracks[args.id].config = args.config;
399  },
400
401  moveTrack(
402      state: StateDraft,
403      args: {srcId: string; op: 'before' | 'after', dstId: string}): void {
404    const moveWithinTrackList = (trackList: string[]) => {
405      const newList: string[] = [];
406      for (let i = 0; i < trackList.length; i++) {
407        const curTrackId = trackList[i];
408        if (curTrackId === args.dstId && args.op === 'before') {
409          newList.push(args.srcId);
410        }
411        if (curTrackId !== args.srcId) {
412          newList.push(curTrackId);
413        }
414        if (curTrackId === args.dstId && args.op === 'after') {
415          newList.push(args.srcId);
416        }
417      }
418      trackList.splice(0);
419      newList.forEach((x) => {
420        trackList.push(x);
421      });
422    };
423
424    moveWithinTrackList(state.pinnedTracks);
425    moveWithinTrackList(state.scrollingTracks);
426  },
427
428  toggleTrackPinned(state: StateDraft, args: {trackId: string}): void {
429    const id = args.trackId;
430    const isPinned = state.pinnedTracks.includes(id);
431    const trackGroup = assertExists(state.tracks[id]).trackGroup;
432
433    if (isPinned) {
434      state.pinnedTracks.splice(state.pinnedTracks.indexOf(id), 1);
435      if (trackGroup === SCROLLING_TRACK_GROUP) {
436        state.scrollingTracks.unshift(id);
437      }
438    } else {
439      if (trackGroup === SCROLLING_TRACK_GROUP) {
440        state.scrollingTracks.splice(state.scrollingTracks.indexOf(id), 1);
441      }
442      state.pinnedTracks.push(id);
443    }
444  },
445
446  toggleTrackGroupCollapsed(state: StateDraft, args: {trackGroupId: string}):
447      void {
448        const id = args.trackGroupId;
449        const trackGroup = assertExists(state.trackGroups[id]);
450        trackGroup.collapsed = !trackGroup.collapsed;
451      },
452
453  requestTrackReload(state: StateDraft, _: {}) {
454    if (state.lastTrackReloadRequest) {
455      state.lastTrackReloadRequest++;
456    } else {
457      state.lastTrackReloadRequest = 1;
458    }
459  },
460
461  // TODO(hjd): engine.ready should be a published thing. If it's part
462  // of the state it interacts badly with permalinks.
463  setEngineReady(
464      state: StateDraft,
465      args: {engineId: string; ready: boolean, mode: EngineMode}): void {
466    const engine = state.engine;
467    if (engine === undefined || engine.id !== args.engineId) {
468      return;
469    }
470    engine.ready = args.ready;
471    engine.mode = args.mode;
472  },
473
474  setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void {
475    state.newEngineMode = args.mode;
476  },
477
478  // Marks all engines matching the given |mode| as failed.
479  setEngineFailed(state: StateDraft, args: {mode: EngineMode; failure: string}):
480      void {
481        if (state.engine !== undefined && state.engine.mode === args.mode) {
482          state.engine.failed = args.failure;
483        }
484      },
485
486  createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void {
487    state.permalink = {
488      requestId: generateNextId(state),
489      hash: undefined,
490      isRecordingConfig: args.isRecordingConfig,
491    };
492  },
493
494  setPermalink(state: StateDraft, args: {requestId: string; hash: string}):
495      void {
496        // Drop any links for old requests.
497        if (state.permalink.requestId !== args.requestId) return;
498        state.permalink = args;
499      },
500
501  loadPermalink(state: StateDraft, args: {hash: string}): void {
502    state.permalink = {requestId: generateNextId(state), hash: args.hash};
503  },
504
505  clearPermalink(state: StateDraft, _: {}): void {
506    state.permalink = {};
507  },
508
509  setTraceTime(state: StateDraft, args: TraceTime): void {
510    state.traceTime = args;
511  },
512
513  updateStatus(state: StateDraft, args: Status): void {
514    if (statusTraceEvent) {
515      traceEventEnd(statusTraceEvent);
516    }
517    statusTraceEvent = traceEventBegin(args.msg);
518    state.status = args;
519  },
520
521  // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
522  setState(state: StateDraft, args: {newState: State}): void {
523    for (const key of Object.keys(state)) {
524      delete (state as any)[key];
525    }
526    for (const key of Object.keys(args.newState)) {
527      (state as any)[key] = (args.newState as any)[key];
528    }
529
530    // If we're loading from a permalink then none of the engines can
531    // possibly be ready:
532    if (state.engine !== undefined) {
533      state.engine.ready = false;
534    }
535  },
536
537  setRecordConfig(
538      state: StateDraft,
539      args: {config: RecordConfig, configType?: LoadedConfig}): void {
540    state.recordConfig = args.config;
541    state.lastLoadedConfig = args.configType || {type: 'NONE'};
542  },
543
544  selectNote(state: StateDraft, args: {id: string}): void {
545    if (args.id) {
546      state.currentSelection = {
547        kind: 'NOTE',
548        id: args.id,
549      };
550    }
551  },
552
553  addAutomaticNote(
554      state: StateDraft,
555      args: {timestamp: TPTime, color: string, text: string}): void {
556    const id = generateNextId(state);
557    state.notes[id] = {
558      noteType: 'DEFAULT',
559      id,
560      timestamp: args.timestamp,
561      color: args.color,
562      text: args.text,
563    };
564  },
565
566  addNote(state: StateDraft, args: {timestamp: TPTime, color: string}): void {
567    const id = generateNextId(state);
568    state.notes[id] = {
569      noteType: 'DEFAULT',
570      id,
571      timestamp: args.timestamp,
572      color: args.color,
573      text: '',
574    };
575    this.selectNote(state, {id});
576  },
577
578  markCurrentArea(
579      state: StateDraft, args: {color: string, persistent: boolean}):
580      void {
581        if (state.currentSelection === null ||
582            state.currentSelection.kind !== 'AREA') {
583          return;
584        }
585        const id = args.persistent ? generateNextId(state) : '0';
586        const color = args.persistent ? args.color : '#344596';
587        state.notes[id] = {
588          noteType: 'AREA',
589          id,
590          areaId: state.currentSelection.areaId,
591          color,
592          text: '',
593        };
594        state.currentSelection.noteId = id;
595      },
596
597  toggleMarkCurrentArea(state: StateDraft, args: {persistent: boolean}) {
598    const selection = state.currentSelection;
599    if (selection != null && selection.kind === 'AREA' &&
600        selection.noteId !== undefined) {
601      this.removeNote(state, {id: selection.noteId});
602    } else {
603      const color = randomColor();
604      this.markCurrentArea(state, {color, persistent: args.persistent});
605    }
606  },
607
608  markArea(state: StateDraft, args: {area: Area, persistent: boolean}): void {
609    const {start, end, tracks} = args.area;
610    assertTrue(start <= end);
611    const areaId = generateNextId(state);
612    state.areas[areaId] = {id: areaId, start, end, tracks};
613    const noteId = args.persistent ? generateNextId(state) : '0';
614    const color = args.persistent ? randomColor() : '#344596';
615    state.notes[noteId] = {
616      noteType: 'AREA',
617      id: noteId,
618      areaId,
619      color,
620      text: '',
621    };
622  },
623
624  changeNoteColor(state: StateDraft, args: {id: string, newColor: string}):
625      void {
626        const note = state.notes[args.id];
627        if (note === undefined) return;
628        note.color = args.newColor;
629      },
630
631  changeNoteText(state: StateDraft, args: {id: string, newText: string}): void {
632    const note = state.notes[args.id];
633    if (note === undefined) return;
634    note.text = args.newText;
635  },
636
637  removeNote(state: StateDraft, args: {id: string}): void {
638    if (state.notes[args.id] === undefined) return;
639    delete state.notes[args.id];
640    // For regular notes, we clear the current selection but for an area note
641    // we only want to clear the note/marking and leave the area selected.
642    if (state.currentSelection === null) return;
643    if (state.currentSelection.kind === 'NOTE' &&
644        state.currentSelection.id === args.id) {
645      state.currentSelection = null;
646    } else if (
647        state.currentSelection.kind === 'AREA' &&
648        state.currentSelection.noteId === args.id) {
649      state.currentSelection.noteId = undefined;
650    }
651  },
652
653  selectSlice(
654      state: StateDraft,
655      args: {id: number, trackId: string, scroll?: boolean}): void {
656    state.currentSelection = {
657      kind: 'SLICE',
658      id: args.id,
659      trackId: args.trackId,
660    };
661    state.pendingScrollId = args.scroll ? args.id : undefined;
662  },
663
664  selectCounter(
665      state: StateDraft,
666      args: {leftTs: TPTime, rightTs: TPTime, id: number, trackId: string}):
667      void {
668        state.currentSelection = {
669          kind: 'COUNTER',
670          leftTs: args.leftTs,
671          rightTs: args.rightTs,
672          id: args.id,
673          trackId: args.trackId,
674        };
675      },
676
677  selectHeapProfile(
678      state: StateDraft,
679      args: {id: number, upid: number, ts: TPTime, type: ProfileType}): void {
680    state.currentSelection = {
681      kind: 'HEAP_PROFILE',
682      id: args.id,
683      upid: args.upid,
684      ts: args.ts,
685      type: args.type,
686    };
687    this.openFlamegraph(state, {
688      type: args.type,
689      start: state.traceTime.start,
690      end: args.ts,
691      upids: [args.upid],
692      viewingOption: DEFAULT_VIEWING_OPTION,
693    });
694  },
695
696  selectPerfSamples(state: StateDraft, args: {
697    id: number,
698    upid: number,
699    leftTs: TPTime,
700    rightTs: TPTime,
701    type: ProfileType
702  }): void {
703    state.currentSelection = {
704      kind: 'PERF_SAMPLES',
705      id: args.id,
706      upid: args.upid,
707      leftTs: args.leftTs,
708      rightTs: args.rightTs,
709      type: args.type,
710    };
711    this.openFlamegraph(state, {
712      type: args.type,
713      start: args.leftTs,
714      end: args.rightTs,
715      upids: [args.upid],
716      viewingOption: PERF_SAMPLES_KEY,
717    });
718  },
719
720  openFlamegraph(state: StateDraft, args: {
721    upids: number[],
722    start: TPTime,
723    end: TPTime,
724    type: ProfileType,
725    viewingOption: FlamegraphStateViewingOption
726  }): void {
727    state.currentFlamegraphState = {
728      kind: 'FLAMEGRAPH_STATE',
729      upids: args.upids,
730      start: args.start,
731      end: args.end,
732      type: args.type,
733      viewingOption: args.viewingOption,
734      focusRegex: '',
735    };
736  },
737
738  selectCpuProfileSample(
739      state: StateDraft, args: {id: number, utid: number, ts: number}): void {
740    state.currentSelection = {
741      kind: 'CPU_PROFILE_SAMPLE',
742      id: args.id,
743      utid: args.utid,
744      ts: args.ts,
745    };
746  },
747
748  expandFlamegraphState(
749      state: StateDraft, args: {expandedCallsite?: CallsiteInfo}): void {
750    if (state.currentFlamegraphState === null) return;
751    state.currentFlamegraphState.expandedCallsite = args.expandedCallsite;
752  },
753
754  changeViewFlamegraphState(
755      state: StateDraft, args: {viewingOption: FlamegraphStateViewingOption}):
756      void {
757        if (state.currentFlamegraphState === null) return;
758        state.currentFlamegraphState.viewingOption = args.viewingOption;
759      },
760
761  changeFocusFlamegraphState(state: StateDraft, args: {focusRegex: string}):
762      void {
763        if (state.currentFlamegraphState === null) return;
764        state.currentFlamegraphState.focusRegex = args.focusRegex;
765      },
766
767  selectChromeSlice(
768      state: StateDraft,
769      args: {id: number, trackId: string, table: string, scroll?: boolean}):
770      void {
771        state.currentSelection = {
772          kind: 'CHROME_SLICE',
773          id: args.id,
774          trackId: args.trackId,
775          table: args.table,
776        };
777        state.pendingScrollId = args.scroll ? args.id : undefined;
778      },
779
780  selectDebugSlice(state: StateDraft, args: {
781    id: number,
782    sqlTableName: string,
783    start: TPTime,
784    duration: TPDuration,
785    trackId: string,
786  }): void {
787    state.currentSelection = {
788      kind: 'DEBUG_SLICE',
789      id: args.id,
790      sqlTableName: args.sqlTableName,
791      start: args.start,
792      duration: args.duration,
793      trackId: args.trackId,
794    };
795  },
796
797  selectTopLevelScrollSlice(state: StateDraft, args: {
798    id: number,
799    sqlTableName: string,
800    start: TPTime,
801    duration: TPTime,
802    trackId: string,
803  }): void {
804    state.currentSelection = {
805      kind: 'TOP_LEVEL_SCROLL',
806      id: args.id,
807      sqlTableName: args.sqlTableName,
808      start: args.start,
809      duration: args.duration,
810      trackId: args.trackId,
811    };
812  },
813
814  clearPendingScrollId(state: StateDraft, _: {}): void {
815    state.pendingScrollId = undefined;
816  },
817
818  selectThreadState(state: StateDraft, args: {id: number, trackId: string}):
819      void {
820        state.currentSelection = {
821          kind: 'THREAD_STATE',
822          id: args.id,
823          trackId: args.trackId,
824        };
825      },
826
827  selectLog(
828      state: StateDraft, args: {id: number, trackId: string, scroll?: boolean}):
829      void {
830        state.currentSelection = {
831          kind: 'LOG',
832          id: args.id,
833          trackId: args.trackId,
834        };
835        state.pendingScrollId = args.scroll ? args.id : undefined;
836      },
837
838  deselect(state: StateDraft, _: {}): void {
839    state.currentSelection = null;
840  },
841
842  updateLogsPagination(state: StateDraft, args: Pagination): void {
843    state.logsPagination = args;
844  },
845
846  updateFtracePagination(state: StateDraft, args: Pagination): void {
847    state.ftracePagination = args;
848  },
849
850  updateFtraceFilter(state: StateDraft, patch: FtraceFilterPatch) {
851    const {excludedNames: diffs} = patch;
852    const excludedNames = state.ftraceFilter.excludedNames;
853    for (const [addRemove, name] of diffs) {
854      switch (addRemove) {
855        case 'add':
856          if (!excludedNames.some((excluded: string) => excluded === name)) {
857            excludedNames.push(name);
858          }
859          break;
860        case 'remove':
861          state.ftraceFilter.excludedNames =
862              state.ftraceFilter.excludedNames.filter(
863                  (excluded: string) => excluded !== name);
864          break;
865        default:
866          assertUnreachable(addRemove);
867          break;
868      }
869    }
870  },
871
872  startRecording(state: StateDraft, _: {}): void {
873    state.recordingInProgress = true;
874    state.lastRecordingError = undefined;
875    state.recordingCancelled = false;
876  },
877
878  stopRecording(state: StateDraft, _: {}): void {
879    state.recordingInProgress = false;
880  },
881
882  cancelRecording(state: StateDraft, _: {}): void {
883    state.recordingInProgress = false;
884    state.recordingCancelled = true;
885  },
886
887  setExtensionAvailable(state: StateDraft, args: {available: boolean}): void {
888    state.extensionInstalled = args.available;
889  },
890
891  setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void {
892    state.recordingTarget = args.target;
893  },
894
895  setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void {
896    state.fetchChromeCategories = args.fetch;
897  },
898
899  setAvailableAdbDevices(
900      state: StateDraft, args: {devices: AdbRecordingTarget[]}): void {
901    state.availableAdbDevices = args.devices;
902  },
903
904  setOmnibox(state: StateDraft, args: OmniboxState): void {
905    state.omniboxState = args;
906  },
907
908  selectArea(state: StateDraft, args: {area: Area}): void {
909    const {start, end, tracks} = args.area;
910    assertTrue(start <= end);
911    const areaId = generateNextId(state);
912    state.areas[areaId] = {id: areaId, start, end, tracks};
913    state.currentSelection = {kind: 'AREA', areaId};
914  },
915
916  editArea(state: StateDraft, args: {area: Area, areaId: string}): void {
917    const {start, end, tracks} = args.area;
918    assertTrue(start <= end);
919    state.areas[args.areaId] = {id: args.areaId, start, end, tracks};
920  },
921
922  reSelectArea(state: StateDraft, args: {areaId: string, noteId: string}):
923      void {
924        state.currentSelection = {
925          kind: 'AREA',
926          areaId: args.areaId,
927          noteId: args.noteId,
928        };
929      },
930
931  toggleTrackSelection(
932      state: StateDraft, args: {id: string, isTrackGroup: boolean}) {
933    const selection = state.currentSelection;
934    if (selection === null || selection.kind !== 'AREA') return;
935    const areaId = selection.areaId;
936    const index = state.areas[areaId].tracks.indexOf(args.id);
937    if (index > -1) {
938      state.areas[areaId].tracks.splice(index, 1);
939      if (args.isTrackGroup) {  // Also remove all child tracks.
940        for (const childTrack of state.trackGroups[args.id].tracks) {
941          const childIndex = state.areas[areaId].tracks.indexOf(childTrack);
942          if (childIndex > -1) {
943            state.areas[areaId].tracks.splice(childIndex, 1);
944          }
945        }
946      }
947    } else {
948      state.areas[areaId].tracks.push(args.id);
949      if (args.isTrackGroup) {  // Also add all child tracks.
950        for (const childTrack of state.trackGroups[args.id].tracks) {
951          if (!state.areas[areaId].tracks.includes(childTrack)) {
952            state.areas[areaId].tracks.push(childTrack);
953          }
954        }
955      }
956    }
957    // It's super unexpected that |toggleTrackSelection| does not cause
958    // selection to be updated and this leads to bugs for people who do:
959    // if (oldSelection !== state.selection) etc.
960    // To solve this re-create the selection object here:
961    state.currentSelection = Object.assign({}, state.currentSelection);
962  },
963
964  setVisibleTraceTime(state: StateDraft, args: VisibleState): void {
965    state.frontendLocalState.visibleState = {...args};
966  },
967
968  setChromeCategories(state: StateDraft, args: {categories: string[]}): void {
969    state.chromeCategories = args.categories;
970  },
971
972  setLastRecordingError(state: StateDraft, args: {error?: string}): void {
973    state.lastRecordingError = args.error;
974    state.recordingStatus = undefined;
975  },
976
977  setRecordingStatus(state: StateDraft, args: {status?: string}): void {
978    state.recordingStatus = args.status;
979    state.lastRecordingError = undefined;
980  },
981
982  requestSelectedMetric(state: StateDraft, _: {}): void {
983    if (!state.metrics.availableMetrics) throw Error('No metrics available');
984    if (state.metrics.selectedIndex === undefined) {
985      throw Error('No metric selected');
986    }
987    state.metrics.requestedMetric =
988        state.metrics.availableMetrics[state.metrics.selectedIndex];
989  },
990
991  resetMetricRequest(state: StateDraft, args: {name: string}): void {
992    if (state.metrics.requestedMetric !== args.name) return;
993    state.metrics.requestedMetric = undefined;
994  },
995
996  setAvailableMetrics(state: StateDraft, args: {availableMetrics: string[]}):
997      void {
998        state.metrics.availableMetrics = args.availableMetrics;
999        if (args.availableMetrics.length > 0) state.metrics.selectedIndex = 0;
1000      },
1001
1002  setMetricSelectedIndex(state: StateDraft, args: {index: number}): void {
1003    if (!state.metrics.availableMetrics ||
1004        args.index >= state.metrics.availableMetrics.length) {
1005      throw Error('metric selection out of bounds');
1006    }
1007    state.metrics.selectedIndex = args.index;
1008  },
1009
1010  togglePerfDebug(state: StateDraft, _: {}): void {
1011    state.perfDebug = !state.perfDebug;
1012  },
1013
1014  toggleSidebar(state: StateDraft, _: {}): void {
1015    state.sidebarVisible = !state.sidebarVisible;
1016  },
1017
1018  setHoveredUtidAndPid(state: StateDraft, args: {utid: number, pid: number}) {
1019    state.hoveredPid = args.pid;
1020    state.hoveredUtid = args.utid;
1021  },
1022
1023  setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) {
1024    state.highlightedSliceId = args.sliceId;
1025  },
1026
1027  setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) {
1028    state.focusedFlowIdLeft = args.flowId;
1029  },
1030
1031  setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) {
1032    state.focusedFlowIdRight = args.flowId;
1033  },
1034
1035  setSearchIndex(state: StateDraft, args: {index: number}) {
1036    state.searchIndex = args.index;
1037  },
1038
1039  setHoverCursorTimestamp(state: StateDraft, args: {ts: TPTime}) {
1040    state.hoverCursorTimestamp = args.ts;
1041  },
1042
1043  setHoveredNoteTimestamp(state: StateDraft, args: {ts: TPTime}) {
1044    state.hoveredNoteTimestamp = args.ts;
1045  },
1046
1047  setCurrentTab(state: StateDraft, args: {tab: string|undefined}) {
1048    state.currentTab = args.tab;
1049  },
1050
1051  toggleAllTrackGroups(state: StateDraft, args: {collapsed: boolean}) {
1052    for (const group of Object.values(state.trackGroups)) {
1053      group.collapsed = args.collapsed;
1054    }
1055  },
1056
1057  clearAllPinnedTracks(state: StateDraft, _: {}) {
1058    if (state.pinnedTracks.length > 0) {
1059      // Clear pinnedTracks array
1060      state.pinnedTracks.length = 0;
1061    }
1062  },
1063
1064  togglePivotTable(state: StateDraft, args: {areaId: string|null}) {
1065    state.nonSerializableState.pivotTable.selectionArea = args.areaId === null ?
1066        undefined :
1067        {areaId: args.areaId, tracks: globals.state.areas[args.areaId].tracks};
1068    if (args.areaId !==
1069        state.nonSerializableState.pivotTable.selectionArea?.areaId) {
1070      state.nonSerializableState.pivotTable.queryResult = null;
1071    }
1072  },
1073
1074  setPivotStateQueryResult(
1075      state: StateDraft, args: {queryResult: PivotTableResult|null}) {
1076    state.nonSerializableState.pivotTable.queryResult = args.queryResult;
1077  },
1078
1079  setPivotTableConstrainToArea(state: StateDraft, args: {constrain: boolean}) {
1080    state.nonSerializableState.pivotTable.constrainToArea = args.constrain;
1081  },
1082
1083  dismissFlamegraphModal(state: StateDraft, _: {}) {
1084    state.flamegraphModalDismissed = true;
1085  },
1086
1087  addPivotTableAggregation(
1088      state: StateDraft, args: {aggregation: Aggregation, after: number}) {
1089    state.nonSerializableState.pivotTable.selectedAggregations.splice(
1090        args.after, 0, args.aggregation);
1091  },
1092
1093  removePivotTableAggregation(state: StateDraft, args: {index: number}) {
1094    state.nonSerializableState.pivotTable.selectedAggregations.splice(
1095        args.index, 1);
1096  },
1097
1098  setPivotTableQueryRequested(
1099      state: StateDraft, args: {queryRequested: boolean}) {
1100    state.nonSerializableState.pivotTable.queryRequested = args.queryRequested;
1101  },
1102
1103  setPivotTablePivotSelected(
1104      state: StateDraft, args: {column: TableColumn, selected: boolean}) {
1105    toggleEnabled(
1106        tableColumnEquals,
1107        state.nonSerializableState.pivotTable.selectedPivots,
1108        args.column,
1109        args.selected);
1110  },
1111
1112  setPivotTableAggregationFunction(
1113      state: StateDraft, args: {index: number, function: AggregationFunction}) {
1114    state.nonSerializableState.pivotTable.selectedAggregations[args.index]
1115        .aggregationFunction = args.function;
1116  },
1117
1118  setPivotTableSortColumn(
1119      state: StateDraft,
1120      args: {aggregationIndex: number, order: SortDirection}) {
1121    state.nonSerializableState.pivotTable.selectedAggregations =
1122        state.nonSerializableState.pivotTable.selectedAggregations.map(
1123            (agg, index) => ({
1124              column: agg.column,
1125              aggregationFunction: agg.aggregationFunction,
1126              sortDirection: (index === args.aggregationIndex) ? args.order :
1127                                                                 undefined,
1128            }));
1129  },
1130
1131  addVisualisedArg(state: StateDraft, args: {argName: string}) {
1132    if (!state.visualisedArgs.includes(args.argName)) {
1133      state.visualisedArgs.push(args.argName);
1134    }
1135  },
1136
1137  removeVisualisedArg(state: StateDraft, args: {argName: string}) {
1138    state.visualisedArgs =
1139        state.visualisedArgs.filter((val) => val !== args.argName);
1140  },
1141
1142  setPivotTableArgumentNames(
1143      state: StateDraft, args: {argumentNames: string[]}) {
1144    state.nonSerializableState.pivotTable.argumentNames = args.argumentNames;
1145  },
1146
1147  changePivotTablePivotOrder(
1148      state: StateDraft,
1149      args: {from: number, to: number, direction: DropDirection}) {
1150    const pivots = state.nonSerializableState.pivotTable.selectedPivots;
1151    state.nonSerializableState.pivotTable.selectedPivots = performReordering(
1152        computeIntervals(pivots.length, args.from, args.to, args.direction),
1153        pivots);
1154  },
1155
1156  changePivotTableAggregationOrder(
1157      state: StateDraft,
1158      args: {from: number, to: number, direction: DropDirection}) {
1159    const aggregations =
1160        state.nonSerializableState.pivotTable.selectedAggregations;
1161    state.nonSerializableState.pivotTable.selectedAggregations =
1162        performReordering(
1163            computeIntervals(
1164                aggregations.length, args.from, args.to, args.direction),
1165            aggregations);
1166  },
1167
1168  setMinimumLogLevel(state: StateDraft, args: {minimumLevel: number}) {
1169    state.logFilteringCriteria.minimumLevel = args.minimumLevel;
1170  },
1171
1172  addLogTag(state: StateDraft, args: {tag: string}) {
1173    if (!state.logFilteringCriteria.tags.includes(args.tag)) {
1174      state.logFilteringCriteria.tags.push(args.tag);
1175    }
1176  },
1177
1178  removeLogTag(state: StateDraft, args: {tag: string}) {
1179    state.logFilteringCriteria.tags =
1180        state.logFilteringCriteria.tags.filter((t) => t !== args.tag);
1181  },
1182
1183  updateLogFilterText(state: StateDraft, args: {textEntry: string}) {
1184    state.logFilteringCriteria.textEntry = args.textEntry;
1185  },
1186
1187  toggleCollapseByTextEntry(state: StateDraft, _: {}) {
1188    state.logFilteringCriteria.hideNonMatching =
1189        !state.logFilteringCriteria.hideNonMatching;
1190  },
1191};
1192
1193// When we are on the frontend side, we don't really want to execute the
1194// actions above, we just want to serialize them and marshal their
1195// arguments, send them over to the controller side and have them being
1196// executed there. The magic below takes care of turning each action into a
1197// function that returns the marshaled args.
1198
1199// A DeferredAction is a bundle of Args and a method name. This is the marshaled
1200// version of a StateActions method call.
1201export interface DeferredAction<Args = {}> {
1202  type: string;
1203  args: Args;
1204}
1205
1206// This type magic creates a type function DeferredActions<T> which takes a type
1207// T and 'maps' its attributes. For each attribute on T matching the signature:
1208// (state: StateDraft, args: Args) => void
1209// DeferredActions<T> has an attribute:
1210// (args: Args) => DeferredAction<Args>
1211type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
1212type DeferredActionFunc<T> = T extends ActionFunction<infer Args>?
1213    (args: Args) => DeferredAction<Args>:
1214    never;
1215type DeferredActions<C> = {
1216  [P in keyof C]: DeferredActionFunc<C[P]>;
1217};
1218
1219// Actions is an implementation of DeferredActions<typeof StateActions>.
1220// (since StateActions is a variable not a type we have to do
1221// 'typeof StateActions' to access the (unnamed) type of StateActions).
1222// It's a Proxy such that any attribute access returns a function:
1223// (args) => {return {type: ATTRIBUTE_NAME, args};}
1224export const Actions =
1225    new Proxy<DeferredActions<typeof StateActions>>({} as any, {
1226      get(_: any, prop: string, _2: any) {
1227        return (args: {}): DeferredAction<{}> => {
1228          return {
1229            type: prop,
1230            args,
1231          };
1232        };
1233      },
1234    });
1235