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