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