• 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} from '../base/logging';
18import {randomColor} from '../common/colorizer';
19import {RecordConfig} from '../controller/record_config_types';
20import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames/common';
21import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/common';
22import {COUNTER_TRACK_KIND} from '../tracks/counter/common';
23import {DEBUG_SLICE_TRACK_KIND} from '../tracks/debug_slices/common';
24import {
25  EXPECTED_FRAMES_SLICE_TRACK_KIND
26} from '../tracks/expected_frames/common';
27import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile/common';
28import {
29  PERF_SAMPLES_PROFILE_TRACK_KIND
30} from '../tracks/perf_samples_profile/common';
31import {
32  PROCESS_SCHEDULING_TRACK_KIND
33} from '../tracks/process_scheduling/common';
34import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
35
36import {createEmptyState} from './empty_state';
37import {DEFAULT_VIEWING_OPTION, PERF_SAMPLES_KEY} from './flamegraph_util';
38import {
39  AggregationAttrs,
40  PivotAttrs,
41  SubQueryAttrs,
42  TableAttrs
43} from './pivot_table_common';
44import {
45  AdbRecordingTarget,
46  Area,
47  CallsiteInfo,
48  EngineMode,
49  FlamegraphStateViewingOption,
50  LoadedConfig,
51  LogsPagination,
52  NewEngineMode,
53  OmniboxState,
54  PivotTableReduxState,
55  RecordingTarget,
56  SCROLLING_TRACK_GROUP,
57  State,
58  Status,
59  TraceTime,
60  TrackKindPriority,
61  TrackState,
62  VisibleState,
63} from './state';
64import {toNs} from './time';
65
66type StateDraft = Draft<State>;
67
68const highPriorityTrackOrder = [
69  PROCESS_SCHEDULING_TRACK_KIND,
70  PROCESS_SUMMARY_TRACK,
71  EXPECTED_FRAMES_SLICE_TRACK_KIND,
72  ACTUAL_FRAMES_SLICE_TRACK_KIND
73];
74
75const lowPriorityTrackOrder = [
76  PERF_SAMPLES_PROFILE_TRACK_KIND,
77  HEAP_PROFILE_TRACK_KIND,
78  COUNTER_TRACK_KIND,
79  ASYNC_SLICE_TRACK_KIND
80];
81
82export interface AddTrackArgs {
83  id?: string;
84  engineId: string;
85  kind: string;
86  name: string;
87  labels?: string[];
88  trackKindPriority: TrackKindPriority;
89  trackGroup?: string;
90  config: {};
91}
92
93export interface PostedTrace {
94  buffer: ArrayBuffer;
95  title: string;
96  fileName?: string;
97  url?: string;
98  uuid?: string;
99  localOnly?: boolean;
100}
101
102function clearTraceState(state: StateDraft) {
103  const nextId = state.nextId;
104  const recordConfig = state.recordConfig;
105  const recordingTarget = state.recordingTarget;
106  const fetchChromeCategories = state.fetchChromeCategories;
107  const extensionInstalled = state.extensionInstalled;
108  const availableAdbDevices = state.availableAdbDevices;
109  const chromeCategories = state.chromeCategories;
110  const newEngineMode = state.newEngineMode;
111
112  Object.assign(state, createEmptyState());
113  state.nextId = nextId;
114  state.recordConfig = recordConfig;
115  state.recordingTarget = recordingTarget;
116  state.fetchChromeCategories = fetchChromeCategories;
117  state.extensionInstalled = extensionInstalled;
118  state.availableAdbDevices = availableAdbDevices;
119  state.chromeCategories = chromeCategories;
120  state.newEngineMode = newEngineMode;
121}
122
123function rank(ts: TrackState): number[] {
124  const hpRank = rankIndex(ts.kind, highPriorityTrackOrder);
125  const lpRank = rankIndex(ts.kind, lowPriorityTrackOrder);
126  // TODO(hjd): Create sortBy object on TrackState to avoid this cast.
127  const tid = (ts.config as {tid?: number}).tid || 0;
128  return [hpRank, ts.trackKindPriority.valueOf(), lpRank, tid];
129}
130
131function rankIndex<T>(element: T, array: T[]): number {
132  const index = array.indexOf(element);
133  if (index === -1) return array.length;
134  return index;
135}
136
137export const StateActions = {
138
139  openTraceFromFile(state: StateDraft, args: {file: File}): void {
140    clearTraceState(state);
141    const id = `${state.nextId++}`;
142    state.engines[id] = {
143      id,
144      ready: false,
145      source: {type: 'FILE', file: args.file},
146    };
147  },
148
149  openTraceFromBuffer(state: StateDraft, args: PostedTrace): void {
150    clearTraceState(state);
151    const id = `${state.nextId++}`;
152    state.engines[id] = {
153      id,
154      ready: false,
155      source: {type: 'ARRAY_BUFFER', ...args},
156    };
157  },
158
159  openTraceFromUrl(state: StateDraft, args: {url: string}): void {
160    clearTraceState(state);
161    const id = `${state.nextId++}`;
162    state.engines[id] = {
163      id,
164      ready: false,
165      source: {type: 'URL', url: args.url},
166    };
167  },
168
169  openTraceFromHttpRpc(state: StateDraft, _args: {}): void {
170    clearTraceState(state);
171    const id = `${state.nextId++}`;
172    state.engines[id] = {
173      id,
174      ready: false,
175      source: {type: 'HTTP_RPC'},
176    };
177  },
178
179  setTraceUuid(state: StateDraft, args: {traceUuid: string}) {
180    state.traceUuid = args.traceUuid;
181  },
182
183  fillUiTrackIdByTraceTrackId(
184      state: StateDraft, trackState: TrackState, uiTrackId: string) {
185    const config = trackState.config as {trackId: number};
186    if (config.trackId !== undefined) {
187      state.uiTrackIdByTraceTrackId[config.trackId] = uiTrackId;
188      return;
189    }
190
191    const multiple = trackState.config as {trackIds: number[]};
192    if (multiple.trackIds !== undefined) {
193      for (const trackId of multiple.trackIds) {
194        state.uiTrackIdByTraceTrackId[trackId] = uiTrackId;
195      }
196    }
197  },
198
199  addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
200    args.tracks.forEach(track => {
201      const id = track.id === undefined ? `${state.nextId++}` : track.id;
202      track.id = id;
203      state.tracks[id] = track as TrackState;
204      this.fillUiTrackIdByTraceTrackId(state, track as TrackState, id);
205      if (track.trackGroup === SCROLLING_TRACK_GROUP) {
206        state.scrollingTracks.push(id);
207      } else if (track.trackGroup !== undefined) {
208        assertExists(state.trackGroups[track.trackGroup]).tracks.push(id);
209      }
210    });
211  },
212
213  addTrack(state: StateDraft, args: {
214    id?: string; engineId: string; kind: string; name: string;
215    trackGroup?: string; config: {}; trackKindPriority: TrackKindPriority;
216  }): void {
217    const id = args.id !== undefined ? args.id : `${state.nextId++}`;
218    state.tracks[id] = {
219      id,
220      engineId: args.engineId,
221      kind: args.kind,
222      name: args.name,
223      trackKindPriority: args.trackKindPriority,
224      trackGroup: args.trackGroup,
225      config: args.config,
226    };
227    this.fillUiTrackIdByTraceTrackId(state, state.tracks[id], id);
228    if (args.trackGroup === SCROLLING_TRACK_GROUP) {
229      state.scrollingTracks.push(id);
230    } else if (args.trackGroup !== undefined) {
231      assertExists(state.trackGroups[args.trackGroup]).tracks.push(id);
232    }
233  },
234
235  addTrackGroup(
236      state: StateDraft,
237      // Define ID in action so a track group can be referred to without running
238      // the reducer.
239      args: {
240        engineId: string; name: string; id: string; summaryTrackId: string;
241        collapsed: boolean;
242      }): void {
243    state.trackGroups[args.id] = {
244      engineId: args.engineId,
245      name: args.name,
246      id: args.id,
247      collapsed: args.collapsed,
248      tracks: [args.summaryTrackId],
249    };
250  },
251
252  addDebugTrack(state: StateDraft, args: {engineId: string, name: string}):
253      void {
254        if (state.debugTrackId !== undefined) return;
255        const trackId = `${state.nextId++}`;
256        state.debugTrackId = trackId;
257        this.addTrack(state, {
258          id: trackId,
259          engineId: args.engineId,
260          kind: DEBUG_SLICE_TRACK_KIND,
261          name: args.name,
262          trackKindPriority: TrackKindPriority.ORDINARY,
263          trackGroup: SCROLLING_TRACK_GROUP,
264          config: {
265            maxDepth: 1,
266          }
267        });
268        this.toggleTrackPinned(state, {trackId});
269      },
270
271  removeDebugTrack(state: StateDraft, _: {}): void {
272    const {debugTrackId} = state;
273    if (debugTrackId === undefined) return;
274    delete state.tracks[debugTrackId];
275    state.scrollingTracks =
276        state.scrollingTracks.filter(id => id !== debugTrackId);
277    state.pinnedTracks = state.pinnedTracks.filter(id => id !== debugTrackId);
278    state.debugTrackId = undefined;
279  },
280
281  sortThreadTracks(state: StateDraft, _: {}): void {
282    // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
283    // rather than T1, T10, T11, ..., T2, T20, T21 .
284    const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
285    for (const group of Object.values(state.trackGroups)) {
286      group.tracks.sort((a: string, b: string) => {
287        const aRank = rank(state.tracks[a]);
288        const bRank = rank(state.tracks[b]);
289        for (let i = 0; i < aRank.length; i++) {
290          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
291        }
292
293        const aName = state.tracks[a].name.toLocaleLowerCase();
294        const bName = state.tracks[b].name.toLocaleLowerCase();
295        return coll.compare(aName, bName);
296      });
297    }
298  },
299
300  updateAggregateSorting(
301      state: StateDraft, args: {id: string, column: string}) {
302    let prefs = state.aggregatePreferences[args.id];
303    if (!prefs) {
304      prefs = {id: args.id};
305      state.aggregatePreferences[args.id] = prefs;
306    }
307
308    if (!prefs.sorting || prefs.sorting.column !== args.column) {
309      // No sorting set for current column.
310      state.aggregatePreferences[args.id].sorting = {
311        column: args.column,
312        direction: 'DESC'
313      };
314    } else if (prefs.sorting.direction === 'DESC') {
315      // Toggle the direction if the column is currently sorted.
316      state.aggregatePreferences[args.id].sorting = {
317        column: args.column,
318        direction: 'ASC'
319      };
320    } else {
321      // If direction is currently 'ASC' toggle to no sorting.
322      state.aggregatePreferences[args.id].sorting = undefined;
323    }
324  },
325
326  setVisibleTracks(state: StateDraft, args: {tracks: string[]}) {
327    state.visibleTracks = args.tracks;
328  },
329
330  updateTrackConfig(state: StateDraft, args: {id: string, config: {}}) {
331    if (state.tracks[args.id] === undefined) return;
332    state.tracks[args.id].config = args.config;
333  },
334
335  executeQuery(
336      state: StateDraft,
337      args: {queryId: string; engineId: string; query: string}): void {
338    state.queries[args.queryId] = {
339      id: args.queryId,
340      engineId: args.engineId,
341      query: args.query,
342    };
343  },
344
345  deleteQuery(state: StateDraft, args: {queryId: string}): void {
346    delete state.queries[args.queryId];
347  },
348
349  moveTrack(
350      state: StateDraft,
351      args: {srcId: string; op: 'before' | 'after', dstId: string}): void {
352    const moveWithinTrackList = (trackList: string[]) => {
353      const newList: string[] = [];
354      for (let i = 0; i < trackList.length; i++) {
355        const curTrackId = trackList[i];
356        if (curTrackId === args.dstId && args.op === 'before') {
357          newList.push(args.srcId);
358        }
359        if (curTrackId !== args.srcId) {
360          newList.push(curTrackId);
361        }
362        if (curTrackId === args.dstId && args.op === 'after') {
363          newList.push(args.srcId);
364        }
365      }
366      trackList.splice(0);
367      newList.forEach(x => {
368        trackList.push(x);
369      });
370    };
371
372    moveWithinTrackList(state.pinnedTracks);
373    moveWithinTrackList(state.scrollingTracks);
374  },
375
376  toggleTrackPinned(state: StateDraft, args: {trackId: string}): void {
377    const id = args.trackId;
378    const isPinned = state.pinnedTracks.includes(id);
379    const trackGroup = assertExists(state.tracks[id]).trackGroup;
380
381    if (isPinned) {
382      state.pinnedTracks.splice(state.pinnedTracks.indexOf(id), 1);
383      if (trackGroup === SCROLLING_TRACK_GROUP) {
384        state.scrollingTracks.unshift(id);
385      }
386    } else {
387      if (trackGroup === SCROLLING_TRACK_GROUP) {
388        state.scrollingTracks.splice(state.scrollingTracks.indexOf(id), 1);
389      }
390      state.pinnedTracks.push(id);
391    }
392  },
393
394  toggleTrackGroupCollapsed(state: StateDraft, args: {trackGroupId: string}):
395      void {
396        const id = args.trackGroupId;
397        const trackGroup = assertExists(state.trackGroups[id]);
398        trackGroup.collapsed = !trackGroup.collapsed;
399      },
400
401  requestTrackReload(state: StateDraft, _: {}) {
402    if (state.lastTrackReloadRequest) {
403      state.lastTrackReloadRequest++;
404    } else {
405      state.lastTrackReloadRequest = 1;
406    }
407  },
408
409  // TODO(hjd): engine.ready should be a published thing. If it's part
410  // of the state it interacts badly with permalinks.
411  setEngineReady(
412      state: StateDraft,
413      args: {engineId: string; ready: boolean, mode: EngineMode}): void {
414    const engine = state.engines[args.engineId];
415    if (engine === undefined) {
416      return;
417    }
418    engine.ready = args.ready;
419    engine.mode = args.mode;
420  },
421
422  setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void {
423    state.newEngineMode = args.mode;
424  },
425
426  // Marks all engines matching the given |mode| as failed.
427  setEngineFailed(state: StateDraft, args: {mode: EngineMode; failure: string}):
428      void {
429        for (const engine of Object.values(state.engines)) {
430          if (engine.mode === args.mode) engine.failed = args.failure;
431        }
432      },
433
434  createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void {
435    state.permalink = {
436      requestId: `${state.nextId++}`,
437      hash: undefined,
438      isRecordingConfig: args.isRecordingConfig
439    };
440  },
441
442  setPermalink(state: StateDraft, args: {requestId: string; hash: string}):
443      void {
444        // Drop any links for old requests.
445        if (state.permalink.requestId !== args.requestId) return;
446        state.permalink = args;
447      },
448
449  loadPermalink(state: StateDraft, args: {hash: string}): void {
450    state.permalink = {requestId: `${state.nextId++}`, hash: args.hash};
451  },
452
453  clearPermalink(state: StateDraft, _: {}): void {
454    state.permalink = {};
455  },
456
457  setTraceTime(state: StateDraft, args: TraceTime): void {
458    state.traceTime = args;
459  },
460
461  updateStatus(state: StateDraft, args: Status): void {
462    state.status = args;
463  },
464
465  // TODO(hjd): Remove setState - it causes problems due to reuse of ids.
466  setState(state: StateDraft, args: {newState: State}): void {
467    for (const key of Object.keys(state)) {
468      // tslint:disable-next-line no-any
469      delete (state as any)[key];
470    }
471    for (const key of Object.keys(args.newState)) {
472      // tslint:disable-next-line no-any
473      (state as any)[key] = (args.newState as any)[key];
474    }
475
476    // If we're loading from a permalink then none of the engines can
477    // possibly be ready:
478    for (const engine of Object.values(state.engines)) {
479      engine.ready = false;
480    }
481  },
482
483  setRecordConfig(
484      state: StateDraft,
485      args: {config: RecordConfig, configType?: LoadedConfig}): void {
486    state.recordConfig = args.config;
487    state.lastLoadedConfig = args.configType || {type: 'NONE'};
488  },
489
490  selectNote(state: StateDraft, args: {id: string}): void {
491    if (args.id) {
492      state.currentSelection = {
493        kind: 'NOTE',
494        id: args.id
495      };
496    }
497  },
498
499  addNote(state: StateDraft, args: {timestamp: number, color: string}): void {
500    const id = `${state.nextNoteId++}`;
501    state.notes[id] = {
502      noteType: 'DEFAULT',
503      id,
504      timestamp: args.timestamp,
505      color: args.color,
506      text: '',
507    };
508    this.selectNote(state, {id});
509  },
510
511  markCurrentArea(
512      state: StateDraft, args: {color: string, persistent: boolean}):
513      void {
514        if (state.currentSelection === null ||
515            state.currentSelection.kind !== 'AREA') {
516          return;
517        }
518        const id = args.persistent ? `${state.nextNoteId++}` : '0';
519        const color = args.persistent ? args.color : '#344596';
520        state.notes[id] = {
521          noteType: 'AREA',
522          id,
523          areaId: state.currentSelection.areaId,
524          color,
525          text: '',
526        };
527        state.currentSelection.noteId = id;
528      },
529
530  toggleMarkCurrentArea(state: StateDraft, args: {persistent: boolean}) {
531    const selection = state.currentSelection;
532    if (selection != null && selection.kind === 'AREA' &&
533        selection.noteId !== undefined) {
534      this.removeNote(state, {id: selection.noteId});
535    } else {
536      const color = randomColor();
537      this.markCurrentArea(state, {color, persistent: args.persistent});
538    }
539  },
540
541  markArea(state: StateDraft, args: {area: Area, persistent: boolean}): void {
542    const areaId = `${state.nextAreaId++}`;
543    assertTrue(args.area.endSec >= args.area.startSec);
544    state.areas[areaId] = {
545      id: areaId,
546      startSec: args.area.startSec,
547      endSec: args.area.endSec,
548      tracks: args.area.tracks
549    };
550    const id = args.persistent ? `${state.nextNoteId++}` : '0';
551    const color = args.persistent ? randomColor() : '#344596';
552    state.notes[id] = {
553      noteType: 'AREA',
554      id,
555      areaId,
556      color,
557      text: '',
558    };
559  },
560
561  changeNoteColor(state: StateDraft, args: {id: string, newColor: string}):
562      void {
563        const note = state.notes[args.id];
564        if (note === undefined) return;
565        note.color = args.newColor;
566      },
567
568  changeNoteText(state: StateDraft, args: {id: string, newText: string}): void {
569    const note = state.notes[args.id];
570    if (note === undefined) return;
571    note.text = args.newText;
572  },
573
574  removeNote(state: StateDraft, args: {id: string}): void {
575    if (state.notes[args.id] === undefined) return;
576    delete state.notes[args.id];
577    // For regular notes, we clear the current selection but for an area note
578    // we only want to clear the note/marking and leave the area selected.
579    if (state.currentSelection === null) return;
580    if (state.currentSelection.kind === 'NOTE' &&
581        state.currentSelection.id === args.id) {
582      state.currentSelection = null;
583    } else if (
584        state.currentSelection.kind === 'AREA' &&
585        state.currentSelection.noteId === args.id) {
586      state.currentSelection.noteId = undefined;
587    }
588  },
589
590  selectSlice(state: StateDraft, args: {id: number, trackId: string}): void {
591    state.currentSelection = {
592      kind: 'SLICE',
593      id: args.id,
594      trackId: args.trackId,
595    };
596  },
597
598  selectCounter(
599      state: StateDraft,
600      args: {leftTs: number, rightTs: number, id: number, trackId: string}):
601      void {
602        state.currentSelection = {
603          kind: 'COUNTER',
604          leftTs: args.leftTs,
605          rightTs: args.rightTs,
606          id: args.id,
607          trackId: args.trackId,
608        };
609      },
610
611  selectHeapProfile(
612      state: StateDraft,
613      args: {id: number, upid: number, ts: number, type: string}): void {
614    state.currentSelection = {
615      kind: 'HEAP_PROFILE',
616      id: args.id,
617      upid: args.upid,
618      ts: args.ts,
619      type: args.type,
620    };
621    this.openFlamegraph(state, {
622      type: args.type,
623      startNs: toNs(state.traceTime.startSec),
624      endNs: args.ts,
625      upids: [args.upid],
626      viewingOption: DEFAULT_VIEWING_OPTION
627    });
628  },
629
630  selectPerfSamples(
631      state: StateDraft,
632      args: {id: number, upid: number, ts: number, type: string}): void {
633    state.currentSelection = {
634      kind: 'PERF_SAMPLES',
635      id: args.id,
636      upid: args.upid,
637      ts: args.ts,
638      type: args.type,
639    };
640    this.openFlamegraph(state, {
641      type: args.type,
642      startNs: toNs(state.traceTime.startSec),
643      endNs: args.ts,
644      upids: [args.upid],
645      viewingOption: PERF_SAMPLES_KEY
646    });
647  },
648
649  openFlamegraph(state: StateDraft, args: {
650    upids: number[],
651    startNs: number,
652    endNs: number,
653    type: string,
654    viewingOption: FlamegraphStateViewingOption
655  }): void {
656    state.currentFlamegraphState = {
657      kind: 'FLAMEGRAPH_STATE',
658      upids: args.upids,
659      startNs: args.startNs,
660      endNs: args.endNs,
661      type: args.type,
662      viewingOption: args.viewingOption,
663      focusRegex: ''
664    };
665  },
666
667  selectCpuProfileSample(
668      state: StateDraft, args: {id: number, utid: number, ts: number}): void {
669    state.currentSelection = {
670      kind: 'CPU_PROFILE_SAMPLE',
671      id: args.id,
672      utid: args.utid,
673      ts: args.ts,
674    };
675  },
676
677  expandFlamegraphState(
678      state: StateDraft, args: {expandedCallsite?: CallsiteInfo}): void {
679    if (state.currentFlamegraphState === null) return;
680    state.currentFlamegraphState.expandedCallsite = args.expandedCallsite;
681  },
682
683  changeViewFlamegraphState(
684      state: StateDraft, args: {viewingOption: FlamegraphStateViewingOption}):
685      void {
686        if (state.currentFlamegraphState === null) return;
687        state.currentFlamegraphState.viewingOption = args.viewingOption;
688      },
689
690  changeFocusFlamegraphState(state: StateDraft, args: {focusRegex: string}):
691      void {
692        if (state.currentFlamegraphState === null) return;
693        state.currentFlamegraphState.focusRegex = args.focusRegex;
694      },
695
696  selectChromeSlice(
697      state: StateDraft,
698      args: {id: number, trackId: string, table: string, scroll?: boolean}):
699      void {
700        state.currentSelection = {
701          kind: 'CHROME_SLICE',
702          id: args.id,
703          trackId: args.trackId,
704          table: args.table
705        };
706        state.pendingScrollId = args.scroll ? args.id : undefined;
707      },
708
709  clearPendingScrollId(state: StateDraft, _: {}): void {
710    state.pendingScrollId = undefined;
711  },
712
713  selectThreadState(state: StateDraft, args: {id: number, trackId: string}):
714      void {
715        state.currentSelection = {
716          kind: 'THREAD_STATE',
717          id: args.id,
718          trackId: args.trackId,
719        };
720      },
721
722  deselect(state: StateDraft, _: {}): void {
723    state.currentSelection = null;
724  },
725
726  updateLogsPagination(state: StateDraft, args: LogsPagination): void {
727    state.logsPagination = args;
728  },
729
730  startRecording(state: StateDraft, _: {}): void {
731    state.recordingInProgress = true;
732    state.lastRecordingError = undefined;
733    state.recordingCancelled = false;
734  },
735
736  stopRecording(state: StateDraft, _: {}): void {
737    state.recordingInProgress = false;
738  },
739
740  cancelRecording(state: StateDraft, _: {}): void {
741    state.recordingInProgress = false;
742    state.recordingCancelled = true;
743  },
744
745  setExtensionAvailable(state: StateDraft, args: {available: boolean}): void {
746    state.extensionInstalled = args.available;
747  },
748
749  updateBufferUsage(state: StateDraft, args: {percentage: number}): void {
750    state.bufferUsage = args.percentage;
751  },
752
753  setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void {
754    state.recordingTarget = args.target;
755  },
756
757  setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void {
758    state.fetchChromeCategories = args.fetch;
759  },
760
761  setAvailableAdbDevices(
762      state: StateDraft, args: {devices: AdbRecordingTarget[]}): void {
763    state.availableAdbDevices = args.devices;
764  },
765
766  setOmnibox(state: StateDraft, args: OmniboxState): void {
767    state.frontendLocalState.omniboxState = args;
768  },
769
770  selectArea(state: StateDraft, args: {area: Area}): void {
771    const areaId = `${state.nextAreaId++}`;
772    assertTrue(args.area.endSec >= args.area.startSec);
773    state.areas[areaId] = {
774      id: areaId,
775      startSec: args.area.startSec,
776      endSec: args.area.endSec,
777      tracks: args.area.tracks
778    };
779    state.currentSelection = {kind: 'AREA', areaId};
780  },
781
782  editArea(state: StateDraft, args: {area: Area, areaId: string}): void {
783    assertTrue(args.area.endSec >= args.area.startSec);
784    state.areas[args.areaId] = {
785      id: args.areaId,
786      startSec: args.area.startSec,
787      endSec: args.area.endSec,
788      tracks: args.area.tracks
789    };
790  },
791
792  reSelectArea(state: StateDraft, args: {areaId: string, noteId: string}):
793      void {
794        state.currentSelection = {
795          kind: 'AREA',
796          areaId: args.areaId,
797          noteId: args.noteId
798        };
799      },
800
801  toggleTrackSelection(
802      state: StateDraft, args: {id: string, isTrackGroup: boolean}) {
803    const selection = state.currentSelection;
804    if (selection === null || selection.kind !== 'AREA') return;
805    const areaId = selection.areaId;
806    const index = state.areas[areaId].tracks.indexOf(args.id);
807    if (index > -1) {
808      state.areas[areaId].tracks.splice(index, 1);
809      if (args.isTrackGroup) {  // Also remove all child tracks.
810        for (const childTrack of state.trackGroups[args.id].tracks) {
811          const childIndex = state.areas[areaId].tracks.indexOf(childTrack);
812          if (childIndex > -1) {
813            state.areas[areaId].tracks.splice(childIndex, 1);
814          }
815        }
816      }
817    } else {
818      state.areas[areaId].tracks.push(args.id);
819      if (args.isTrackGroup) {  // Also add all child tracks.
820        for (const childTrack of state.trackGroups[args.id].tracks) {
821          if (!state.areas[areaId].tracks.includes(childTrack)) {
822            state.areas[areaId].tracks.push(childTrack);
823          }
824        }
825      }
826    }
827  },
828
829  setVisibleTraceTime(state: StateDraft, args: VisibleState): void {
830    state.frontendLocalState.visibleState = {...args};
831  },
832
833  setChromeCategories(state: StateDraft, args: {categories: string[]}): void {
834    state.chromeCategories = args.categories;
835  },
836
837  setLastRecordingError(state: StateDraft, args: {error?: string}): void {
838    state.lastRecordingError = args.error;
839    state.recordingStatus = undefined;
840  },
841
842  setRecordingStatus(state: StateDraft, args: {status?: string}): void {
843    state.recordingStatus = args.status;
844    state.lastRecordingError = undefined;
845  },
846
847  setAnalyzePageQuery(state: StateDraft, args: {query: string}): void {
848    state.analyzePageQuery = args.query;
849  },
850
851  requestSelectedMetric(state: StateDraft, _: {}): void {
852    if (!state.metrics.availableMetrics) throw Error('No metrics available');
853    if (state.metrics.selectedIndex === undefined) {
854      throw Error('No metric selected');
855    }
856    state.metrics.requestedMetric =
857        state.metrics.availableMetrics[state.metrics.selectedIndex];
858  },
859
860  resetMetricRequest(state: StateDraft, args: {name: string}): void {
861    if (state.metrics.requestedMetric !== args.name) return;
862    state.metrics.requestedMetric = undefined;
863  },
864
865  setAvailableMetrics(state: StateDraft, args: {availableMetrics: string[]}):
866      void {
867        state.metrics.availableMetrics = args.availableMetrics;
868        if (args.availableMetrics.length > 0) state.metrics.selectedIndex = 0;
869      },
870
871  setMetricSelectedIndex(state: StateDraft, args: {index: number}): void {
872    if (!state.metrics.availableMetrics ||
873        args.index >= state.metrics.availableMetrics.length) {
874      throw Error('metric selection out of bounds');
875    }
876    state.metrics.selectedIndex = args.index;
877  },
878
879  togglePerfDebug(state: StateDraft, _: {}): void {
880    state.perfDebug = !state.perfDebug;
881  },
882
883  toggleSidebar(state: StateDraft, _: {}): void {
884    state.sidebarVisible = !state.sidebarVisible;
885  },
886
887  setHoveredUtidAndPid(state: StateDraft, args: {utid: number, pid: number}) {
888    state.hoveredPid = args.pid;
889    state.hoveredUtid = args.utid;
890  },
891
892  setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) {
893    state.highlightedSliceId = args.sliceId;
894  },
895
896  setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) {
897    state.focusedFlowIdLeft = args.flowId;
898  },
899
900  setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) {
901    state.focusedFlowIdRight = args.flowId;
902  },
903
904  setSearchIndex(state: StateDraft, args: {index: number}) {
905    state.searchIndex = args.index;
906  },
907
908  setHoveredLogsTimestamp(state: StateDraft, args: {ts: number}) {
909    state.hoveredLogsTimestamp = args.ts;
910  },
911
912  setHoveredNoteTimestamp(state: StateDraft, args: {ts: number}) {
913    state.hoveredNoteTimestamp = args.ts;
914  },
915
916  setCurrentTab(state: StateDraft, args: {tab: string|undefined}) {
917    state.currentTab = args.tab;
918  },
919
920  toggleAllTrackGroups(state: StateDraft, args: {collapsed: boolean}) {
921    for (const [_, group] of Object.entries(state.trackGroups)) {
922      group.collapsed = args.collapsed;
923    }
924  },
925
926  togglePivotTableRedux(state: StateDraft, args: {selectionArea: Area|null}) {
927    state.pivotTableRedux.selectionArea = args.selectionArea;
928  },
929
930  addNewPivotTable(state: StateDraft, args: {
931    name: string,
932    pivotTableId: string,
933    selectedPivots: PivotAttrs[],
934    selectedAggregations: AggregationAttrs[],
935    traceTime?: TraceTime,
936    selectedTrackIds?: number[]
937  }): void {
938    state.pivotTable[args.pivotTableId] = {
939      id: args.pivotTableId,
940      name: args.name,
941      selectedPivots: args.selectedPivots,
942      selectedAggregations: args.selectedAggregations,
943      isLoadingQuery: false,
944      traceTime: args.traceTime,
945      selectedTrackIds: args.selectedTrackIds
946    };
947  },
948
949  deletePivotTable(state: StateDraft, args: {pivotTableId: string}): void {
950    delete state.pivotTable[args.pivotTableId];
951  },
952
953  resetPivotTableRequest(state: StateDraft, args: {pivotTableId: string}):
954      void {
955        if (state.pivotTable[args.pivotTableId] !== undefined) {
956          state.pivotTable[args.pivotTableId].requestedAction = undefined;
957        }
958      },
959
960  setPivotTableRequest(
961      state: StateDraft,
962      args: {pivotTableId: string, action: string, attrs?: SubQueryAttrs}):
963      void {
964        state.pivotTable[args.pivotTableId].requestedAction = {
965          action: args.action,
966          attrs: args.attrs
967        };
968      },
969
970  setAvailablePivotTableColumns(
971      state: StateDraft,
972      args: {availableColumns: TableAttrs[], availableAggregations: string[]}):
973      void {
974        state.pivotTableConfig.availableColumns = args.availableColumns;
975        state.pivotTableConfig.availableAggregations =
976            args.availableAggregations;
977      },
978
979  toggleQueryLoading(state: StateDraft, args: {pivotTableId: string}): void {
980    state.pivotTable[args.pivotTableId].isLoadingQuery =
981        !state.pivotTable[args.pivotTableId].isLoadingQuery;
982  },
983
984  setSelectedPivotsAndAggregations(state: StateDraft, args: {
985    pivotTableId: string,
986    selectedPivots: PivotAttrs[],
987    selectedAggregations: AggregationAttrs[]
988  }) {
989    state.pivotTable[args.pivotTableId].selectedPivots =
990        args.selectedPivots.map(pivot => Object.assign({}, pivot));
991    state.pivotTable[args.pivotTableId].selectedAggregations =
992        args.selectedAggregations.map(
993            aggregation => Object.assign({}, aggregation));
994  },
995
996  setPivotTableRange(state: StateDraft, args: {
997    pivotTableId: string,
998    traceTime?: TraceTime,
999    selectedTrackIds?: number[]
1000  }) {
1001    const pivotTable = state.pivotTable[args.pivotTableId];
1002    pivotTable.traceTime = args.traceTime;
1003    pivotTable.selectedTrackIds = args.selectedTrackIds;
1004  },
1005
1006  setPivotStateReduxState(
1007      state: StateDraft, args: {pivotTableState: PivotTableReduxState}) {
1008    state.pivotTableRedux = args.pivotTableState;
1009  },
1010
1011  dismissFlamegraphModal(state: StateDraft, _: {}) {
1012    state.flamegraphModalDismissed = true;
1013  }
1014};
1015
1016// When we are on the frontend side, we don't really want to execute the
1017// actions above, we just want to serialize them and marshal their
1018// arguments, send them over to the controller side and have them being
1019// executed there. The magic below takes care of turning each action into a
1020// function that returns the marshaled args.
1021
1022// A DeferredAction is a bundle of Args and a method name. This is the marshaled
1023// version of a StateActions method call.
1024export interface DeferredAction<Args = {}> {
1025  type: string;
1026  args: Args;
1027}
1028
1029// This type magic creates a type function DeferredActions<T> which takes a type
1030// T and 'maps' its attributes. For each attribute on T matching the signature:
1031// (state: StateDraft, args: Args) => void
1032// DeferredActions<T> has an attribute:
1033// (args: Args) => DeferredAction<Args>
1034type ActionFunction<Args> = (state: StateDraft, args: Args) => void;
1035type DeferredActionFunc<T> = T extends ActionFunction<infer Args>?
1036    (args: Args) => DeferredAction<Args>:
1037    never;
1038type DeferredActions<C> = {
1039  [P in keyof C]: DeferredActionFunc<C[P]>;
1040};
1041
1042// Actions is an implementation of DeferredActions<typeof StateActions>.
1043// (since StateActions is a variable not a type we have to do
1044// 'typeof StateActions' to access the (unnamed) type of StateActions).
1045// It's a Proxy such that any attribute access returns a function:
1046// (args) => {return {type: ATTRIBUTE_NAME, args};}
1047export const Actions =
1048    // tslint:disable-next-line no-any
1049    new Proxy<DeferredActions<typeof StateActions>>({} as any, {
1050      // tslint:disable-next-line no-any
1051      get(_: any, prop: string, _2: any) {
1052        return (args: {}): DeferredAction<{}> => {
1053          return {
1054            type: prop,
1055            args,
1056          };
1057        };
1058      },
1059    });
1060