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