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