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 {SortDirection} from '../base/comparison_utils'; 18import {assertExists, assertTrue} from '../base/logging'; 19import {duration, time} from '../base/time'; 20import {RecordConfig} from '../controller/record_config_types'; 21import {randomColor} from '../core/colorizer'; 22import { 23 GenericSliceDetailsTabConfig, 24 GenericSliceDetailsTabConfigBase, 25} from '../frontend/generic_slice_details_tab'; 26import { 27 Aggregation, 28 AggregationFunction, 29 TableColumn, 30 tableColumnEquals, 31 toggleEnabled, 32} from '../frontend/pivot_table_types'; 33 34import { 35 computeIntervals, 36 DropDirection, 37 performReordering, 38} from './dragndrop_logic'; 39import {createEmptyState} from './empty_state'; 40import { 41 MetatraceTrackId, 42 traceEventBegin, 43 traceEventEnd, 44 TraceEventScope, 45} from './metatracing'; 46import { 47 AdbRecordingTarget, 48 EngineMode, 49 LoadedConfig, 50 NewEngineMode, 51 OmniboxMode, 52 OmniboxState, 53 PendingDeeplinkState, 54 PivotTableResult, 55 PrimaryTrackSortKey, 56 ProfileType, 57 RecordingTarget, 58 SCROLLING_TRACK_GROUP, 59 State, 60 Status, 61 ThreadTrackSortKey, 62 TrackSortKey, 63 UtidToTrackSortKey, 64 VisibleState, 65} from './state'; 66 67type StateDraft = Draft<State>; 68 69export interface AddTrackArgs { 70 key?: string; 71 uri: string; 72 name: string; 73 labels?: string[]; 74 trackSortKey: TrackSortKey; 75 trackGroup?: string; 76 closeable?: boolean; 77} 78 79export interface PostedTrace { 80 buffer: ArrayBuffer; 81 title: string; 82 fileName?: string; 83 url?: string; 84 uuid?: string; 85 localOnly?: boolean; 86 keepApiOpen?: boolean; 87 88 // Allows to pass extra arguments to plugins. This can be read by plugins 89 // onTraceLoad() and can be used to trigger plugin-specific-behaviours (e.g. 90 // allow dashboards like APC to pass extra data to materialize onto tracks). 91 // The format is the following: 92 // pluginArgs: { 93 // 'dev.perfetto.PluginFoo': { 'key1': 'value1', 'key2': 1234 } 94 // 'dev.perfetto.PluginBar': { 'key3': '...', 'key4': ... } 95 // } 96 pluginArgs?: {[pluginId: string]: {[key: string]: unknown}}; 97} 98 99export interface PostedScrollToRange { 100 timeStart: number; 101 timeEnd: number; 102 viewPercentage?: number; 103} 104 105function clearTraceState(state: StateDraft) { 106 const nextId = state.nextId; 107 const recordConfig = state.recordConfig; 108 const recordingTarget = state.recordingTarget; 109 const fetchChromeCategories = state.fetchChromeCategories; 110 const extensionInstalled = state.extensionInstalled; 111 const availableAdbDevices = state.availableAdbDevices; 112 const chromeCategories = state.chromeCategories; 113 const newEngineMode = state.newEngineMode; 114 115 Object.assign(state, createEmptyState()); 116 state.nextId = nextId; 117 state.recordConfig = recordConfig; 118 state.recordingTarget = recordingTarget; 119 state.fetchChromeCategories = fetchChromeCategories; 120 state.extensionInstalled = extensionInstalled; 121 state.availableAdbDevices = availableAdbDevices; 122 state.chromeCategories = chromeCategories; 123 state.newEngineMode = newEngineMode; 124} 125 126function generateNextId(draft: StateDraft): string { 127 const nextId = String(Number(draft.nextId) + 1); 128 draft.nextId = nextId; 129 return nextId; 130} 131 132// A helper to clean the state for a given removeable track. 133// This is not exported as action to make it clear that not all 134// tracks are removeable. 135function removeTrack(state: StateDraft, trackKey: string) { 136 const track = state.tracks[trackKey]; 137 if (track === undefined) { 138 return; 139 } 140 delete state.tracks[trackKey]; 141 142 const removeTrackId = (arr: string[]) => { 143 const index = arr.indexOf(trackKey); 144 if (index !== -1) arr.splice(index, 1); 145 }; 146 147 if (track.trackGroup === SCROLLING_TRACK_GROUP) { 148 removeTrackId(state.scrollingTracks); 149 } else if (track.trackGroup !== undefined) { 150 const trackGroup = state.trackGroups[track.trackGroup]; 151 if (trackGroup !== undefined) { 152 removeTrackId(trackGroup.tracks); 153 } 154 } 155 state.pinnedTracks = state.pinnedTracks.filter((key) => key !== trackKey); 156} 157 158let statusTraceEvent: TraceEventScope | undefined; 159 160export const StateActions = { 161 openTraceFromFile(state: StateDraft, args: {file: File}): void { 162 clearTraceState(state); 163 const id = generateNextId(state); 164 state.engine = { 165 id, 166 ready: false, 167 source: {type: 'FILE', file: args.file}, 168 }; 169 }, 170 171 openTraceFromBuffer(state: StateDraft, args: PostedTrace): void { 172 clearTraceState(state); 173 const id = generateNextId(state); 174 state.engine = { 175 id, 176 ready: false, 177 source: {type: 'ARRAY_BUFFER', ...args}, 178 }; 179 }, 180 181 openTraceFromUrl(state: StateDraft, args: {url: string}): void { 182 clearTraceState(state); 183 const id = generateNextId(state); 184 state.engine = { 185 id, 186 ready: false, 187 source: {type: 'URL', url: args.url}, 188 }; 189 }, 190 191 openTraceFromHttpRpc(state: StateDraft, _args: {}): void { 192 clearTraceState(state); 193 const id = generateNextId(state); 194 state.engine = { 195 id, 196 ready: false, 197 source: {type: 'HTTP_RPC'}, 198 }; 199 }, 200 201 setTraceUuid(state: StateDraft, args: {traceUuid: string}) { 202 state.traceUuid = args.traceUuid; 203 }, 204 205 addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) { 206 args.tracks.forEach((track) => { 207 const trackKey = 208 track.key === undefined ? generateNextId(state) : track.key; 209 const name = track.name; 210 state.tracks[trackKey] = { 211 key: trackKey, 212 name, 213 trackSortKey: track.trackSortKey, 214 trackGroup: track.trackGroup, 215 labels: track.labels, 216 uri: track.uri, 217 closeable: track.closeable, 218 }; 219 if (track.trackGroup === SCROLLING_TRACK_GROUP) { 220 state.scrollingTracks.push(trackKey); 221 } else if (track.trackGroup !== undefined) { 222 const group = state.trackGroups[track.trackGroup]; 223 if (group !== undefined) { 224 group.tracks.push(trackKey); 225 } 226 } 227 }); 228 }, 229 230 // Note: While this action has traditionally been omitted, with more and more 231 // dynamic tracks being added and existing ones being moved to plugins, it 232 // makes sense to have a generic "removeTracks" action which is un-opinionated 233 // about what type of tracks we are removing. 234 // E.g. Once debug tracks have been moved to a plugin, it makes no sense to 235 // keep the "removeDebugTrack()" action, as the core should have no concept of 236 // what debug tracks are. 237 removeTracks(state: StateDraft, args: {trackKeys: string[]}) { 238 for (const trackKey of args.trackKeys) { 239 removeTrack(state, trackKey); 240 } 241 }, 242 243 setUtidToTrackSortKey( 244 state: StateDraft, 245 args: {threadOrderingMetadata: UtidToTrackSortKey}, 246 ) { 247 state.utidToThreadSortKey = args.threadOrderingMetadata; 248 }, 249 250 addTrack(state: StateDraft, args: AddTrackArgs): void { 251 this.addTracks(state, {tracks: [args]}); 252 }, 253 254 addTrackGroup( 255 state: StateDraft, 256 // Define ID in action so a track group can be referred to without running 257 // the reducer. 258 args: { 259 name: string; 260 key: string; 261 summaryTrackKey?: string; 262 collapsed: boolean; 263 fixedOrdering?: boolean; 264 }, 265 ): void { 266 state.trackGroups[args.key] = { 267 name: args.name, 268 key: args.key, 269 collapsed: args.collapsed, 270 tracks: [], 271 summaryTrack: args.summaryTrackKey, 272 fixedOrdering: args.fixedOrdering, 273 }; 274 }, 275 276 maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void { 277 const trackGroups = Object.values(state.trackGroups); 278 if (trackGroups.length === 1) { 279 trackGroups[0].collapsed = false; 280 } 281 }, 282 283 sortThreadTracks(state: StateDraft, _: {}) { 284 const getFullKey = (a: string) => { 285 const track = state.tracks[a]; 286 const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey; 287 if (threadTrackSortKey.utid === undefined) { 288 const sortKey = track.trackSortKey as PrimaryTrackSortKey; 289 return [sortKey, 0, 0, 0]; 290 } 291 const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid]; 292 return [ 293 /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 294 threadSortKey 295 ? threadSortKey.sortKey 296 : PrimaryTrackSortKey.ORDINARY_THREAD, 297 threadSortKey && threadSortKey.tid !== undefined 298 ? threadSortKey.tid 299 : Number.MAX_VALUE, 300 /* eslint-enable */ 301 threadTrackSortKey.utid, 302 threadTrackSortKey.priority, 303 ]; 304 }; 305 306 // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11, 307 // rather than T1, T10, T11, ..., T2, T20, T21 . 308 const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true}); 309 for (const group of Object.values(state.trackGroups)) { 310 if (group.fixedOrdering) continue; 311 312 group.tracks.sort((a: string, b: string) => { 313 const aRank = getFullKey(a); 314 const bRank = getFullKey(b); 315 for (let i = 0; i < aRank.length; i++) { 316 if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i]; 317 } 318 319 const aName = state.tracks[a].name.toLocaleLowerCase(); 320 const bName = state.tracks[b].name.toLocaleLowerCase(); 321 return coll.compare(aName, bName); 322 }); 323 } 324 }, 325 326 updateAggregateSorting( 327 state: StateDraft, 328 args: {id: string; column: string}, 329 ) { 330 let prefs = state.aggregatePreferences[args.id]; 331 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 332 if (!prefs) { 333 prefs = {id: args.id}; 334 state.aggregatePreferences[args.id] = prefs; 335 } 336 337 if (!prefs.sorting || prefs.sorting.column !== args.column) { 338 // No sorting set for current column. 339 state.aggregatePreferences[args.id].sorting = { 340 column: args.column, 341 direction: 'DESC', 342 }; 343 } else if (prefs.sorting.direction === 'DESC') { 344 // Toggle the direction if the column is currently sorted. 345 state.aggregatePreferences[args.id].sorting = { 346 column: args.column, 347 direction: 'ASC', 348 }; 349 } else { 350 // If direction is currently 'ASC' toggle to no sorting. 351 state.aggregatePreferences[args.id].sorting = undefined; 352 } 353 }, 354 355 moveTrack( 356 state: StateDraft, 357 args: {srcId: string; op: 'before' | 'after'; dstId: string}, 358 ): void { 359 const moveWithinTrackList = (trackList: string[]) => { 360 const newList: string[] = []; 361 for (let i = 0; i < trackList.length; i++) { 362 const curTrackId = trackList[i]; 363 if (curTrackId === args.dstId && args.op === 'before') { 364 newList.push(args.srcId); 365 } 366 if (curTrackId !== args.srcId) { 367 newList.push(curTrackId); 368 } 369 if (curTrackId === args.dstId && args.op === 'after') { 370 newList.push(args.srcId); 371 } 372 } 373 trackList.splice(0); 374 newList.forEach((x) => { 375 trackList.push(x); 376 }); 377 }; 378 379 moveWithinTrackList(state.pinnedTracks); 380 moveWithinTrackList(state.scrollingTracks); 381 }, 382 383 toggleTrackPinned(state: StateDraft, args: {trackKey: string}): void { 384 const key = args.trackKey; 385 const isPinned = state.pinnedTracks.includes(key); 386 const trackGroup = assertExists(state.tracks[key]).trackGroup; 387 388 if (isPinned) { 389 state.pinnedTracks.splice(state.pinnedTracks.indexOf(key), 1); 390 if (trackGroup === SCROLLING_TRACK_GROUP) { 391 state.scrollingTracks.unshift(key); 392 } 393 } else { 394 if (trackGroup === SCROLLING_TRACK_GROUP) { 395 state.scrollingTracks.splice(state.scrollingTracks.indexOf(key), 1); 396 } 397 state.pinnedTracks.push(key); 398 } 399 }, 400 401 toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void { 402 const trackGroup = assertExists(state.trackGroups[args.groupKey]); 403 trackGroup.collapsed = !trackGroup.collapsed; 404 }, 405 406 requestTrackReload(state: StateDraft, _: {}) { 407 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 408 if (state.lastTrackReloadRequest) { 409 state.lastTrackReloadRequest++; 410 } else { 411 state.lastTrackReloadRequest = 1; 412 } 413 }, 414 415 maybeSetPendingDeeplink(state: StateDraft, args: PendingDeeplinkState) { 416 state.pendingDeeplink = args; 417 }, 418 419 clearPendingDeeplink(state: StateDraft, _: {}) { 420 state.pendingDeeplink = undefined; 421 }, 422 423 // TODO(hjd): engine.ready should be a published thing. If it's part 424 // of the state it interacts badly with permalinks. 425 setEngineReady( 426 state: StateDraft, 427 args: {engineId: string; ready: boolean; mode: EngineMode}, 428 ): void { 429 const engine = state.engine; 430 if (engine === undefined || engine.id !== args.engineId) { 431 return; 432 } 433 engine.ready = args.ready; 434 engine.mode = args.mode; 435 }, 436 437 setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void { 438 state.newEngineMode = args.mode; 439 }, 440 441 // Marks all engines matching the given |mode| as failed. 442 setEngineFailed( 443 state: StateDraft, 444 args: {mode: EngineMode; failure: string}, 445 ): void { 446 if (state.engine !== undefined && state.engine.mode === args.mode) { 447 state.engine.failed = args.failure; 448 } 449 }, 450 451 updateStatus(state: StateDraft, args: Status): void { 452 if (statusTraceEvent) { 453 traceEventEnd(statusTraceEvent); 454 } 455 statusTraceEvent = traceEventBegin(args.msg, { 456 track: MetatraceTrackId.kOmniboxStatus, 457 }); 458 state.status = args; 459 }, 460 461 // TODO(hjd): Remove setState - it causes problems due to reuse of ids. 462 setState(state: StateDraft, args: {newState: State}): void { 463 for (const key of Object.keys(state)) { 464 // eslint-disable-next-line @typescript-eslint/no-explicit-any 465 delete (state as any)[key]; 466 } 467 for (const key of Object.keys(args.newState)) { 468 // eslint-disable-next-line @typescript-eslint/no-explicit-any 469 (state as any)[key] = (args.newState as any)[key]; 470 } 471 472 // If we're loading from a permalink then none of the engines can 473 // possibly be ready: 474 if (state.engine !== undefined) { 475 state.engine.ready = false; 476 } 477 }, 478 479 setRecordConfig( 480 state: StateDraft, 481 args: {config: RecordConfig; configType?: LoadedConfig}, 482 ): void { 483 state.recordConfig = args.config; 484 state.lastLoadedConfig = args.configType || {type: 'NONE'}; 485 }, 486 487 selectNote(state: StateDraft, args: {id: string}): void { 488 state.selection = { 489 kind: 'note', 490 id: args.id, 491 }; 492 }, 493 494 addNote( 495 state: StateDraft, 496 args: {timestamp: time; color: string; id?: string; text?: string}, 497 ): void { 498 const {timestamp, color, id = generateNextId(state), text = ''} = args; 499 state.notes[id] = { 500 noteType: 'DEFAULT', 501 id, 502 timestamp, 503 color, 504 text, 505 }; 506 }, 507 508 addSpanNote( 509 state: StateDraft, 510 args: {start: time; end: time; id?: string; color?: string}, 511 ): void { 512 const { 513 id = generateNextId(state), 514 color = randomColor(), 515 end, 516 start, 517 } = args; 518 519 state.notes[id] = { 520 noteType: 'SPAN', 521 start, 522 end, 523 color, 524 id, 525 text: '', 526 }; 527 }, 528 529 changeNoteColor( 530 state: StateDraft, 531 args: {id: string; newColor: string}, 532 ): void { 533 const note = state.notes[args.id]; 534 if (note === undefined) return; 535 note.color = args.newColor; 536 }, 537 538 changeNoteText(state: StateDraft, args: {id: string; newText: string}): void { 539 const note = state.notes[args.id]; 540 if (note === undefined) return; 541 note.text = args.newText; 542 }, 543 544 removeNote(state: StateDraft, args: {id: string}): void { 545 delete state.notes[args.id]; 546 547 // Clear the selection if this note was selected 548 if (state.selection.kind === 'note' && state.selection.id === args.id) { 549 state.selection = {kind: 'empty'}; 550 } 551 }, 552 553 selectHeapProfile( 554 state: StateDraft, 555 args: {id: number; upid: number; ts: time; type: ProfileType}, 556 ): void { 557 state.selection = { 558 kind: 'legacy', 559 legacySelection: { 560 kind: 'HEAP_PROFILE', 561 id: args.id, 562 upid: args.upid, 563 ts: args.ts, 564 type: args.type, 565 }, 566 }; 567 }, 568 569 selectPerfSamples( 570 state: StateDraft, 571 args: { 572 id: number; 573 upid: number; 574 leftTs: time; 575 rightTs: time; 576 type: ProfileType; 577 }, 578 ): void { 579 state.selection = { 580 kind: 'legacy', 581 legacySelection: { 582 kind: 'PERF_SAMPLES', 583 id: args.id, 584 upid: args.upid, 585 leftTs: args.leftTs, 586 rightTs: args.rightTs, 587 type: args.type, 588 }, 589 }; 590 }, 591 592 selectCpuProfileSample( 593 state: StateDraft, 594 args: {id: number; utid: number; ts: time}, 595 ): void { 596 state.selection = { 597 kind: 'legacy', 598 legacySelection: { 599 kind: 'CPU_PROFILE_SAMPLE', 600 id: args.id, 601 utid: args.utid, 602 ts: args.ts, 603 }, 604 }; 605 }, 606 607 selectSlice( 608 state: StateDraft, 609 args: {id: number; trackKey: string; table?: string; scroll?: boolean}, 610 ): void { 611 state.selection = { 612 kind: 'legacy', 613 legacySelection: { 614 kind: 'SLICE', 615 id: args.id, 616 trackKey: args.trackKey, 617 table: args.table, 618 }, 619 }; 620 state.pendingScrollId = args.scroll ? args.id : undefined; 621 }, 622 623 selectGenericSlice( 624 state: StateDraft, 625 args: { 626 id: number; 627 sqlTableName: string; 628 start: time; 629 duration: duration; 630 trackKey: string; 631 detailsPanelConfig: { 632 kind: string; 633 config: GenericSliceDetailsTabConfigBase; 634 }; 635 }, 636 ): void { 637 const detailsPanelConfig: GenericSliceDetailsTabConfig = { 638 id: args.id, 639 ...args.detailsPanelConfig.config, 640 }; 641 642 state.selection = { 643 kind: 'legacy', 644 legacySelection: { 645 kind: 'GENERIC_SLICE', 646 id: args.id, 647 sqlTableName: args.sqlTableName, 648 start: args.start, 649 duration: args.duration, 650 trackKey: args.trackKey, 651 detailsPanelConfig: { 652 kind: args.detailsPanelConfig.kind, 653 config: detailsPanelConfig, 654 }, 655 }, 656 }; 657 }, 658 659 setPendingScrollId(state: StateDraft, args: {pendingScrollId: number}): void { 660 state.pendingScrollId = args.pendingScrollId; 661 }, 662 663 clearPendingScrollId(state: StateDraft, _: {}): void { 664 state.pendingScrollId = undefined; 665 }, 666 667 selectThreadState( 668 state: StateDraft, 669 args: {id: number; trackKey: string}, 670 ): void { 671 state.selection = { 672 kind: 'legacy', 673 legacySelection: { 674 kind: 'THREAD_STATE', 675 id: args.id, 676 trackKey: args.trackKey, 677 }, 678 }; 679 }, 680 681 startRecording(state: StateDraft, _: {}): void { 682 state.recordingInProgress = true; 683 state.lastRecordingError = undefined; 684 state.recordingCancelled = false; 685 }, 686 687 stopRecording(state: StateDraft, _: {}): void { 688 state.recordingInProgress = false; 689 }, 690 691 cancelRecording(state: StateDraft, _: {}): void { 692 state.recordingInProgress = false; 693 state.recordingCancelled = true; 694 }, 695 696 setExtensionAvailable(state: StateDraft, args: {available: boolean}): void { 697 state.extensionInstalled = args.available; 698 }, 699 700 setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void { 701 state.recordingTarget = args.target; 702 }, 703 704 setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void { 705 state.fetchChromeCategories = args.fetch; 706 }, 707 708 setAvailableAdbDevices( 709 state: StateDraft, 710 args: {devices: AdbRecordingTarget[]}, 711 ): void { 712 state.availableAdbDevices = args.devices; 713 }, 714 715 setOmnibox(state: StateDraft, args: OmniboxState): void { 716 state.omniboxState = args; 717 }, 718 719 setOmniboxMode(state: StateDraft, args: {mode: OmniboxMode}): void { 720 state.omniboxState.mode = args.mode; 721 }, 722 723 selectArea( 724 state: StateDraft, 725 args: {start: time; end: time; tracks: string[]}, 726 ): void { 727 const {start, end, tracks} = args; 728 assertTrue(start <= end); 729 state.selection = { 730 kind: 'area', 731 start, 732 end, 733 tracks, 734 }; 735 }, 736 737 toggleTrackSelection( 738 state: StateDraft, 739 args: {key: string; isTrackGroup: boolean}, 740 ) { 741 const selection = state.selection; 742 if (selection.kind !== 'area') { 743 return; 744 } 745 746 const index = selection.tracks.indexOf(args.key); 747 if (index > -1) { 748 selection.tracks.splice(index, 1); 749 if (args.isTrackGroup) { 750 // Also remove all child tracks. 751 for (const childTrack of state.trackGroups[args.key].tracks) { 752 const childIndex = selection.tracks.indexOf(childTrack); 753 if (childIndex > -1) { 754 selection.tracks.splice(childIndex, 1); 755 } 756 } 757 } 758 } else { 759 selection.tracks.push(args.key); 760 if (args.isTrackGroup) { 761 // Also add all child tracks. 762 for (const childTrack of state.trackGroups[args.key].tracks) { 763 if (!selection.tracks.includes(childTrack)) { 764 selection.tracks.push(childTrack); 765 } 766 } 767 } 768 } 769 // It's super unexpected that |toggleTrackSelection| does not cause 770 // selection to be updated and this leads to bugs for people who do: 771 // if (oldSelection !== state.selection) etc. 772 // To solve this re-create the selection object here: 773 state.selection = Object.assign({}, state.selection); 774 }, 775 776 setVisibleTraceTime(state: StateDraft, args: VisibleState): void { 777 state.frontendLocalState.visibleState = {...args}; 778 }, 779 780 setChromeCategories(state: StateDraft, args: {categories: string[]}): void { 781 state.chromeCategories = args.categories; 782 }, 783 784 setLastRecordingError(state: StateDraft, args: {error?: string}): void { 785 state.lastRecordingError = args.error; 786 state.recordingStatus = undefined; 787 }, 788 789 setRecordingStatus(state: StateDraft, args: {status?: string}): void { 790 state.recordingStatus = args.status; 791 state.lastRecordingError = undefined; 792 }, 793 794 togglePerfDebug(state: StateDraft, _: {}): void { 795 state.perfDebug = !state.perfDebug; 796 }, 797 798 setSidebar(state: StateDraft, args: {visible: boolean}): void { 799 state.sidebarVisible = args.visible; 800 }, 801 802 setHoveredUtidAndPid(state: StateDraft, args: {utid: number; pid: number}) { 803 state.hoveredPid = args.pid; 804 state.hoveredUtid = args.utid; 805 }, 806 807 setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) { 808 state.highlightedSliceId = args.sliceId; 809 }, 810 811 setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) { 812 state.focusedFlowIdLeft = args.flowId; 813 }, 814 815 setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) { 816 state.focusedFlowIdRight = args.flowId; 817 }, 818 819 setSearchIndex(state: StateDraft, args: {index: number}) { 820 state.searchIndex = args.index; 821 }, 822 823 setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) { 824 state.hoverCursorTimestamp = args.ts; 825 }, 826 827 setHoveredNoteTimestamp(state: StateDraft, args: {ts: time}) { 828 state.hoveredNoteTimestamp = args.ts; 829 }, 830 831 // Add a tab with a given URI to the tab bar and show it. 832 // If the tab is already present in the tab bar, just show it. 833 showTab(state: StateDraft, args: {uri: string}) { 834 // Add tab, unless we're talking about the special current_selection tab 835 if (args.uri !== 'current_selection') { 836 // Add tab to tab list if not already 837 if (!state.tabs.openTabs.some((uri) => uri === args.uri)) { 838 state.tabs.openTabs.push(args.uri); 839 } 840 } 841 state.tabs.currentTab = args.uri; 842 }, 843 844 // Hide a tab in the tab bar pick a new tab to show. 845 // Note: Attempting to hide the "current_selection" tab doesn't work. This tab 846 // is special and cannot be removed. 847 hideTab(state: StateDraft, args: {uri: string}) { 848 const tabs = state.tabs; 849 // If the removed tab is the "current" tab, we must find a new tab to focus 850 if (args.uri === tabs.currentTab) { 851 // Remember the index of the current tab 852 const currentTabIdx = tabs.openTabs.findIndex((uri) => uri === args.uri); 853 854 // Remove the tab 855 tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri); 856 857 if (currentTabIdx !== -1) { 858 if (tabs.openTabs.length === 0) { 859 // No more tabs, use current selection 860 tabs.currentTab = 'current_selection'; 861 } else if (currentTabIdx < tabs.openTabs.length - 1) { 862 // Pick the tab to the right 863 tabs.currentTab = tabs.openTabs[currentTabIdx]; 864 } else { 865 // Pick the last tab 866 const lastTab = tabs.openTabs[tabs.openTabs.length - 1]; 867 tabs.currentTab = lastTab; 868 } 869 } 870 } else { 871 // Otherwise just remove the tab 872 tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri); 873 } 874 }, 875 876 clearAllPinnedTracks(state: StateDraft, _: {}) { 877 const pinnedTracks = state.pinnedTracks.slice(); 878 for (let index = pinnedTracks.length - 1; index >= 0; index--) { 879 const trackKey = pinnedTracks[index]; 880 this.toggleTrackPinned(state, {trackKey}); 881 } 882 }, 883 884 togglePivotTable( 885 state: StateDraft, 886 args: {area?: {start: time; end: time; tracks: string[]}}, 887 ) { 888 state.nonSerializableState.pivotTable.selectionArea = args.area; 889 state.nonSerializableState.pivotTable.queryResult = null; 890 }, 891 892 setPivotStateQueryResult( 893 state: StateDraft, 894 args: {queryResult: PivotTableResult | null}, 895 ) { 896 state.nonSerializableState.pivotTable.queryResult = args.queryResult; 897 }, 898 899 setPivotTableConstrainToArea(state: StateDraft, args: {constrain: boolean}) { 900 state.nonSerializableState.pivotTable.constrainToArea = args.constrain; 901 }, 902 903 dismissFlamegraphModal(state: StateDraft, _: {}) { 904 state.flamegraphModalDismissed = true; 905 }, 906 907 addPivotTableAggregation( 908 state: StateDraft, 909 args: {aggregation: Aggregation; after: number}, 910 ) { 911 state.nonSerializableState.pivotTable.selectedAggregations.splice( 912 args.after, 913 0, 914 args.aggregation, 915 ); 916 }, 917 918 removePivotTableAggregation(state: StateDraft, args: {index: number}) { 919 state.nonSerializableState.pivotTable.selectedAggregations.splice( 920 args.index, 921 1, 922 ); 923 }, 924 925 setPivotTableQueryRequested( 926 state: StateDraft, 927 args: {queryRequested: boolean}, 928 ) { 929 state.nonSerializableState.pivotTable.queryRequested = args.queryRequested; 930 }, 931 932 setPivotTablePivotSelected( 933 state: StateDraft, 934 args: {column: TableColumn; selected: boolean}, 935 ) { 936 toggleEnabled( 937 tableColumnEquals, 938 state.nonSerializableState.pivotTable.selectedPivots, 939 args.column, 940 args.selected, 941 ); 942 }, 943 944 setPivotTableAggregationFunction( 945 state: StateDraft, 946 args: {index: number; function: AggregationFunction}, 947 ) { 948 state.nonSerializableState.pivotTable.selectedAggregations[ 949 args.index 950 ].aggregationFunction = args.function; 951 }, 952 953 setPivotTableSortColumn( 954 state: StateDraft, 955 args: {aggregationIndex: number; order: SortDirection}, 956 ) { 957 state.nonSerializableState.pivotTable.selectedAggregations = 958 state.nonSerializableState.pivotTable.selectedAggregations.map( 959 (agg, index) => ({ 960 column: agg.column, 961 aggregationFunction: agg.aggregationFunction, 962 sortDirection: 963 index === args.aggregationIndex ? args.order : undefined, 964 }), 965 ); 966 }, 967 968 changePivotTablePivotOrder( 969 state: StateDraft, 970 args: {from: number; to: number; direction: DropDirection}, 971 ) { 972 const pivots = state.nonSerializableState.pivotTable.selectedPivots; 973 state.nonSerializableState.pivotTable.selectedPivots = performReordering( 974 computeIntervals(pivots.length, args.from, args.to, args.direction), 975 pivots, 976 ); 977 }, 978 979 changePivotTableAggregationOrder( 980 state: StateDraft, 981 args: {from: number; to: number; direction: DropDirection}, 982 ) { 983 const aggregations = 984 state.nonSerializableState.pivotTable.selectedAggregations; 985 state.nonSerializableState.pivotTable.selectedAggregations = 986 performReordering( 987 computeIntervals( 988 aggregations.length, 989 args.from, 990 args.to, 991 args.direction, 992 ), 993 aggregations, 994 ); 995 }, 996}; 997 998// When we are on the frontend side, we don't really want to execute the 999// actions above, we just want to serialize them and marshal their 1000// arguments, send them over to the controller side and have them being 1001// executed there. The magic below takes care of turning each action into a 1002// function that returns the marshaled args. 1003 1004// A DeferredAction is a bundle of Args and a method name. This is the marshaled 1005// version of a StateActions method call. 1006export interface DeferredAction<Args = {}> { 1007 type: string; 1008 args: Args; 1009} 1010 1011// This type magic creates a type function DeferredActions<T> which takes a type 1012// T and 'maps' its attributes. For each attribute on T matching the signature: 1013// (state: StateDraft, args: Args) => void 1014// DeferredActions<T> has an attribute: 1015// (args: Args) => DeferredAction<Args> 1016type ActionFunction<Args> = (state: StateDraft, args: Args) => void; 1017type DeferredActionFunc<T> = T extends ActionFunction<infer Args> 1018 ? (args: Args) => DeferredAction<Args> 1019 : never; 1020type DeferredActions<C> = { 1021 [P in keyof C]: DeferredActionFunc<C[P]>; 1022}; 1023 1024// Actions is an implementation of DeferredActions<typeof StateActions>. 1025// (since StateActions is a variable not a type we have to do 1026// 'typeof StateActions' to access the (unnamed) type of StateActions). 1027// It's a Proxy such that any attribute access returns a function: 1028// (args) => {return {type: ATTRIBUTE_NAME, args};} 1029export const Actions = 1030 // eslint-disable-next-line @typescript-eslint/no-explicit-any 1031 new Proxy<DeferredActions<typeof StateActions>>({} as any, { 1032 // eslint-disable-next-line @typescript-eslint/no-explicit-any 1033 get(_: any, prop: string, _2: any) { 1034 return (args: {}): DeferredAction<{}> => { 1035 return { 1036 type: prop, 1037 args, 1038 }; 1039 }; 1040 }, 1041 }); 1042