| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {Draft} from 'immer'; |
| |
| import {assertExists, assertTrue} from '../base/logging'; |
| import {duration, time} from '../base/time'; |
| import {RecordConfig} from '../controller/record_config_types'; |
| import { |
| GenericSliceDetailsTabConfig, |
| GenericSliceDetailsTabConfigBase, |
| } from '../frontend/generic_slice_details_tab'; |
| import { |
| Aggregation, |
| AggregationFunction, |
| TableColumn, |
| tableColumnEquals, |
| toggleEnabled, |
| } from '../frontend/pivot_table_types'; |
| import {PrimaryTrackSortKey} from '../public/index'; |
| |
| import {randomColor} from '../core/colorizer'; |
| import { |
| computeIntervals, |
| DropDirection, |
| performReordering, |
| } from './dragndrop_logic'; |
| import {createEmptyState} from './empty_state'; |
| import {defaultViewingOption} from './flamegraph_util'; |
| import { |
| MetatraceTrackId, |
| traceEventBegin, |
| traceEventEnd, |
| TraceEventScope, |
| } from './metatracing'; |
| import { |
| AdbRecordingTarget, |
| Area, |
| CallsiteInfo, |
| EngineMode, |
| FlamegraphStateViewingOption, |
| LoadedConfig, |
| NewEngineMode, |
| OmniboxMode, |
| OmniboxState, |
| PendingDeeplinkState, |
| PivotTableResult, |
| ProfileType, |
| RecordingTarget, |
| SCROLLING_TRACK_GROUP, |
| SortDirection, |
| State, |
| Status, |
| ThreadTrackSortKey, |
| TraceTime, |
| TrackSortKey, |
| UtidToTrackSortKey, |
| VisibleState, |
| } from './state'; |
| |
| type StateDraft = Draft<State>; |
| |
| export interface AddTrackArgs { |
| key?: string; |
| uri: string; |
| name: string; |
| labels?: string[]; |
| trackSortKey: TrackSortKey; |
| trackGroup?: string; |
| params?: unknown; |
| closeable?: boolean; |
| } |
| |
| export interface PostedTrace { |
| buffer: ArrayBuffer; |
| title: string; |
| fileName?: string; |
| url?: string; |
| uuid?: string; |
| localOnly?: boolean; |
| keepApiOpen?: boolean; |
| } |
| |
| export interface PostedScrollToRange { |
| timeStart: number; |
| timeEnd: number; |
| viewPercentage?: number; |
| } |
| |
| function clearTraceState(state: StateDraft) { |
| const nextId = state.nextId; |
| const recordConfig = state.recordConfig; |
| const recordingTarget = state.recordingTarget; |
| const fetchChromeCategories = state.fetchChromeCategories; |
| const extensionInstalled = state.extensionInstalled; |
| const availableAdbDevices = state.availableAdbDevices; |
| const chromeCategories = state.chromeCategories; |
| const newEngineMode = state.newEngineMode; |
| |
| Object.assign(state, createEmptyState()); |
| state.nextId = nextId; |
| state.recordConfig = recordConfig; |
| state.recordingTarget = recordingTarget; |
| state.fetchChromeCategories = fetchChromeCategories; |
| state.extensionInstalled = extensionInstalled; |
| state.availableAdbDevices = availableAdbDevices; |
| state.chromeCategories = chromeCategories; |
| state.newEngineMode = newEngineMode; |
| } |
| |
| function generateNextId(draft: StateDraft): string { |
| const nextId = String(Number(draft.nextId) + 1); |
| draft.nextId = nextId; |
| return nextId; |
| } |
| |
| // A helper to clean the state for a given removeable track. |
| // This is not exported as action to make it clear that not all |
| // tracks are removeable. |
| function removeTrack(state: StateDraft, trackKey: string) { |
| const track = state.tracks[trackKey]; |
| if (track === undefined) { |
| return; |
| } |
| delete state.tracks[trackKey]; |
| |
| const removeTrackId = (arr: string[]) => { |
| const index = arr.indexOf(trackKey); |
| if (index !== -1) arr.splice(index, 1); |
| }; |
| |
| if (track.trackGroup === SCROLLING_TRACK_GROUP) { |
| removeTrackId(state.scrollingTracks); |
| } else if (track.trackGroup !== undefined) { |
| const trackGroup = state.trackGroups[track.trackGroup]; |
| if (trackGroup !== undefined) { |
| removeTrackId(trackGroup.tracks); |
| } |
| } |
| state.pinnedTracks = state.pinnedTracks.filter((key) => key !== trackKey); |
| } |
| |
| let statusTraceEvent: TraceEventScope | undefined; |
| |
| export const StateActions = { |
| openTraceFromFile(state: StateDraft, args: {file: File}): void { |
| clearTraceState(state); |
| const id = generateNextId(state); |
| state.engine = { |
| id, |
| ready: false, |
| source: {type: 'FILE', file: args.file}, |
| }; |
| }, |
| |
| openTraceFromBuffer(state: StateDraft, args: PostedTrace): void { |
| clearTraceState(state); |
| const id = generateNextId(state); |
| state.engine = { |
| id, |
| ready: false, |
| source: {type: 'ARRAY_BUFFER', ...args}, |
| }; |
| }, |
| |
| openTraceFromUrl(state: StateDraft, args: {url: string}): void { |
| clearTraceState(state); |
| const id = generateNextId(state); |
| state.engine = { |
| id, |
| ready: false, |
| source: {type: 'URL', url: args.url}, |
| }; |
| }, |
| |
| openTraceFromHttpRpc(state: StateDraft, _args: {}): void { |
| clearTraceState(state); |
| const id = generateNextId(state); |
| state.engine = { |
| id, |
| ready: false, |
| source: {type: 'HTTP_RPC'}, |
| }; |
| }, |
| |
| setTraceUuid(state: StateDraft, args: {traceUuid: string}) { |
| state.traceUuid = args.traceUuid; |
| }, |
| |
| addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) { |
| args.tracks.forEach((track) => { |
| const trackKey = |
| track.key === undefined ? generateNextId(state) : track.key; |
| const name = track.name; |
| state.tracks[trackKey] = { |
| key: trackKey, |
| name, |
| trackSortKey: track.trackSortKey, |
| trackGroup: track.trackGroup, |
| labels: track.labels, |
| uri: track.uri, |
| params: track.params, |
| closeable: track.closeable, |
| }; |
| if (track.trackGroup === SCROLLING_TRACK_GROUP) { |
| state.scrollingTracks.push(trackKey); |
| } else if (track.trackGroup !== undefined) { |
| const group = state.trackGroups[track.trackGroup]; |
| if (group !== undefined) { |
| group.tracks.push(trackKey); |
| } |
| } |
| }); |
| }, |
| |
| // Note: While this action has traditionally been omitted, with more and more |
| // dynamic tracks being added and existing ones being moved to plugins, it |
| // makes sense to have a generic "removeTracks" action which is un-opinionated |
| // about what type of tracks we are removing. |
| // E.g. Once debug tracks have been moved to a plugin, it makes no sense to |
| // keep the "removeDebugTrack()" action, as the core should have no concept of |
| // what debug tracks are. |
| removeTracks(state: StateDraft, args: {trackKeys: string[]}) { |
| for (const trackKey of args.trackKeys) { |
| removeTrack(state, trackKey); |
| } |
| }, |
| |
| setUtidToTrackSortKey( |
| state: StateDraft, |
| args: {threadOrderingMetadata: UtidToTrackSortKey}, |
| ) { |
| state.utidToThreadSortKey = args.threadOrderingMetadata; |
| }, |
| |
| addTrack(state: StateDraft, args: AddTrackArgs): void { |
| this.addTracks(state, {tracks: [args]}); |
| }, |
| |
| addTrackGroup( |
| state: StateDraft, |
| // Define ID in action so a track group can be referred to without running |
| // the reducer. |
| args: { |
| name: string; |
| id: string; |
| summaryTrackKey?: string; |
| collapsed: boolean; |
| fixedOrdering?: boolean; |
| }, |
| ): void { |
| state.trackGroups[args.id] = { |
| name: args.name, |
| id: args.id, |
| collapsed: args.collapsed, |
| tracks: [], |
| summaryTrack: args.summaryTrackKey, |
| fixedOrdering: args.fixedOrdering, |
| }; |
| }, |
| |
| maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void { |
| const trackGroups = Object.values(state.trackGroups); |
| if (trackGroups.length === 1) { |
| trackGroups[0].collapsed = false; |
| } |
| }, |
| |
| sortThreadTracks(state: StateDraft, _: {}) { |
| const getFullKey = (a: string) => { |
| const track = state.tracks[a]; |
| const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey; |
| if (threadTrackSortKey.utid === undefined) { |
| const sortKey = track.trackSortKey as PrimaryTrackSortKey; |
| return [sortKey, 0, 0, 0]; |
| } |
| const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid]; |
| return [ |
| /* eslint-disable @typescript-eslint/strict-boolean-expressions */ |
| threadSortKey |
| ? threadSortKey.sortKey |
| : PrimaryTrackSortKey.ORDINARY_THREAD, |
| threadSortKey && threadSortKey.tid !== undefined |
| ? threadSortKey.tid |
| : Number.MAX_VALUE, |
| /* eslint-enable */ |
| threadTrackSortKey.utid, |
| threadTrackSortKey.priority, |
| ]; |
| }; |
| |
| // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11, |
| // rather than T1, T10, T11, ..., T2, T20, T21 . |
| const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true}); |
| for (const group of Object.values(state.trackGroups)) { |
| if (group.fixedOrdering) continue; |
| |
| group.tracks.sort((a: string, b: string) => { |
| const aRank = getFullKey(a); |
| const bRank = getFullKey(b); |
| for (let i = 0; i < aRank.length; i++) { |
| if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i]; |
| } |
| |
| const aName = state.tracks[a].name.toLocaleLowerCase(); |
| const bName = state.tracks[b].name.toLocaleLowerCase(); |
| return coll.compare(aName, bName); |
| }); |
| } |
| }, |
| |
| updateAggregateSorting( |
| state: StateDraft, |
| args: {id: string; column: string}, |
| ) { |
| let prefs = state.aggregatePreferences[args.id]; |
| // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions |
| if (!prefs) { |
| prefs = {id: args.id}; |
| state.aggregatePreferences[args.id] = prefs; |
| } |
| |
| if (!prefs.sorting || prefs.sorting.column !== args.column) { |
| // No sorting set for current column. |
| state.aggregatePreferences[args.id].sorting = { |
| column: args.column, |
| direction: 'DESC', |
| }; |
| } else if (prefs.sorting.direction === 'DESC') { |
| // Toggle the direction if the column is currently sorted. |
| state.aggregatePreferences[args.id].sorting = { |
| column: args.column, |
| direction: 'ASC', |
| }; |
| } else { |
| // If direction is currently 'ASC' toggle to no sorting. |
| state.aggregatePreferences[args.id].sorting = undefined; |
| } |
| }, |
| |
| moveTrack( |
| state: StateDraft, |
| args: {srcId: string; op: 'before' | 'after'; dstId: string}, |
| ): void { |
| const moveWithinTrackList = (trackList: string[]) => { |
| const newList: string[] = []; |
| for (let i = 0; i < trackList.length; i++) { |
| const curTrackId = trackList[i]; |
| if (curTrackId === args.dstId && args.op === 'before') { |
| newList.push(args.srcId); |
| } |
| if (curTrackId !== args.srcId) { |
| newList.push(curTrackId); |
| } |
| if (curTrackId === args.dstId && args.op === 'after') { |
| newList.push(args.srcId); |
| } |
| } |
| trackList.splice(0); |
| newList.forEach((x) => { |
| trackList.push(x); |
| }); |
| }; |
| |
| moveWithinTrackList(state.pinnedTracks); |
| moveWithinTrackList(state.scrollingTracks); |
| }, |
| |
| toggleTrackPinned(state: StateDraft, args: {trackKey: string}): void { |
| const key = args.trackKey; |
| const isPinned = state.pinnedTracks.includes(key); |
| const trackGroup = assertExists(state.tracks[key]).trackGroup; |
| |
| if (isPinned) { |
| state.pinnedTracks.splice(state.pinnedTracks.indexOf(key), 1); |
| if (trackGroup === SCROLLING_TRACK_GROUP) { |
| state.scrollingTracks.unshift(key); |
| } |
| } else { |
| if (trackGroup === SCROLLING_TRACK_GROUP) { |
| state.scrollingTracks.splice(state.scrollingTracks.indexOf(key), 1); |
| } |
| state.pinnedTracks.push(key); |
| } |
| }, |
| |
| toggleTrackGroupCollapsed( |
| state: StateDraft, |
| args: {trackGroupId: string}, |
| ): void { |
| const id = args.trackGroupId; |
| const trackGroup = assertExists(state.trackGroups[id]); |
| trackGroup.collapsed = !trackGroup.collapsed; |
| }, |
| |
| requestTrackReload(state: StateDraft, _: {}) { |
| // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions |
| if (state.lastTrackReloadRequest) { |
| state.lastTrackReloadRequest++; |
| } else { |
| state.lastTrackReloadRequest = 1; |
| } |
| }, |
| |
| maybeSetPendingDeeplink(state: StateDraft, args: PendingDeeplinkState) { |
| state.pendingDeeplink = args; |
| }, |
| |
| clearPendingDeeplink(state: StateDraft, _: {}) { |
| state.pendingDeeplink = undefined; |
| }, |
| |
| // TODO(hjd): engine.ready should be a published thing. If it's part |
| // of the state it interacts badly with permalinks. |
| setEngineReady( |
| state: StateDraft, |
| args: {engineId: string; ready: boolean; mode: EngineMode}, |
| ): void { |
| const engine = state.engine; |
| if (engine === undefined || engine.id !== args.engineId) { |
| return; |
| } |
| engine.ready = args.ready; |
| engine.mode = args.mode; |
| }, |
| |
| setNewEngineMode(state: StateDraft, args: {mode: NewEngineMode}): void { |
| state.newEngineMode = args.mode; |
| }, |
| |
| // Marks all engines matching the given |mode| as failed. |
| setEngineFailed( |
| state: StateDraft, |
| args: {mode: EngineMode; failure: string}, |
| ): void { |
| if (state.engine !== undefined && state.engine.mode === args.mode) { |
| state.engine.failed = args.failure; |
| } |
| }, |
| |
| createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void { |
| state.permalink = { |
| requestId: generateNextId(state), |
| hash: undefined, |
| isRecordingConfig: args.isRecordingConfig, |
| }; |
| }, |
| |
| setPermalink( |
| state: StateDraft, |
| args: {requestId: string; hash: string}, |
| ): void { |
| // Drop any links for old requests. |
| if (state.permalink.requestId !== args.requestId) return; |
| state.permalink = args; |
| }, |
| |
| loadPermalink(state: StateDraft, args: {hash: string}): void { |
| state.permalink = {requestId: generateNextId(state), hash: args.hash}; |
| }, |
| |
| clearPermalink(state: StateDraft, _: {}): void { |
| state.permalink = {}; |
| }, |
| |
| setTraceTime(state: StateDraft, args: TraceTime): void { |
| state.traceTime = args; |
| }, |
| |
| updateStatus(state: StateDraft, args: Status): void { |
| if (statusTraceEvent) { |
| traceEventEnd(statusTraceEvent); |
| } |
| statusTraceEvent = traceEventBegin(args.msg, { |
| track: MetatraceTrackId.kOmniboxStatus, |
| }); |
| state.status = args; |
| }, |
| |
| // TODO(hjd): Remove setState - it causes problems due to reuse of ids. |
| setState(state: StateDraft, args: {newState: State}): void { |
| for (const key of Object.keys(state)) { |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| delete (state as any)[key]; |
| } |
| for (const key of Object.keys(args.newState)) { |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| (state as any)[key] = (args.newState as any)[key]; |
| } |
| |
| // If we're loading from a permalink then none of the engines can |
| // possibly be ready: |
| if (state.engine !== undefined) { |
| state.engine.ready = false; |
| } |
| }, |
| |
| setRecordConfig( |
| state: StateDraft, |
| args: {config: RecordConfig; configType?: LoadedConfig}, |
| ): void { |
| state.recordConfig = args.config; |
| state.lastLoadedConfig = args.configType || {type: 'NONE'}; |
| }, |
| |
| selectNote(state: StateDraft, args: {id: string}): void { |
| if (args.id) { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'NOTE', |
| id: args.id, |
| }, |
| }; |
| } |
| }, |
| |
| addAutomaticNote( |
| state: StateDraft, |
| args: {timestamp: time; color: string; text: string}, |
| ): void { |
| const id = generateNextId(state); |
| state.notes[id] = { |
| noteType: 'DEFAULT', |
| id, |
| timestamp: args.timestamp, |
| color: args.color, |
| text: args.text, |
| }; |
| }, |
| |
| addNote(state: StateDraft, args: {timestamp: time; color: string}): void { |
| const id = generateNextId(state); |
| state.notes[id] = { |
| noteType: 'DEFAULT', |
| id, |
| timestamp: args.timestamp, |
| color: args.color, |
| text: '', |
| }; |
| this.selectNote(state, {id}); |
| }, |
| |
| markCurrentArea( |
| state: StateDraft, |
| args: {color: string; persistent: boolean}, |
| ): void { |
| if (state.selection.kind !== 'legacy') { |
| return; |
| } |
| if (state.selection.legacySelection.kind !== 'AREA') { |
| return; |
| } |
| const legacySelection = state.selection.legacySelection; |
| const id = args.persistent ? generateNextId(state) : '0'; |
| const color = args.persistent ? args.color : '#344596'; |
| state.notes[id] = { |
| noteType: 'AREA', |
| id, |
| areaId: legacySelection.areaId, |
| color, |
| text: '', |
| }; |
| legacySelection.noteId = id; |
| }, |
| |
| toggleMarkCurrentArea(state: StateDraft, args: {persistent: boolean}) { |
| const selection = state.selection; |
| if ( |
| selection.kind === 'legacy' && |
| selection.legacySelection.kind === 'AREA' && |
| selection.legacySelection.noteId !== undefined |
| ) { |
| this.removeNote(state, {id: selection.legacySelection.noteId}); |
| } else { |
| const color = randomColor(); |
| this.markCurrentArea(state, {color, persistent: args.persistent}); |
| } |
| }, |
| |
| markArea(state: StateDraft, args: {area: Area; persistent: boolean}): void { |
| const {start, end, tracks} = args.area; |
| assertTrue(start <= end); |
| const areaId = generateNextId(state); |
| state.areas[areaId] = {id: areaId, start, end, tracks}; |
| const noteId = args.persistent ? generateNextId(state) : '0'; |
| const color = args.persistent ? randomColor() : '#344596'; |
| state.notes[noteId] = { |
| noteType: 'AREA', |
| id: noteId, |
| areaId, |
| color, |
| text: '', |
| }; |
| }, |
| |
| changeNoteColor( |
| state: StateDraft, |
| args: {id: string; newColor: string}, |
| ): void { |
| const note = state.notes[args.id]; |
| if (note === undefined) return; |
| note.color = args.newColor; |
| }, |
| |
| changeNoteText(state: StateDraft, args: {id: string; newText: string}): void { |
| const note = state.notes[args.id]; |
| if (note === undefined) return; |
| note.text = args.newText; |
| }, |
| |
| removeNote(state: StateDraft, args: {id: string}): void { |
| if (state.notes[args.id] === undefined) return; |
| delete state.notes[args.id]; |
| // For regular notes, we clear the current selection but for an area note |
| // we only want to clear the note/marking and leave the area selected. |
| if (state.selection.kind !== 'legacy') return; |
| if ( |
| state.selection.legacySelection.kind === 'NOTE' && |
| state.selection.legacySelection.id === args.id |
| ) { |
| state.selection = { |
| kind: 'empty', |
| }; |
| } else if ( |
| state.selection.legacySelection.kind === 'AREA' && |
| state.selection.legacySelection.noteId === args.id |
| ) { |
| state.selection.legacySelection.noteId = undefined; |
| } |
| }, |
| |
| selectCounter( |
| state: StateDraft, |
| args: {leftTs: time; rightTs: time; id: number; trackKey: string}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'COUNTER', |
| leftTs: args.leftTs, |
| rightTs: args.rightTs, |
| id: args.id, |
| trackKey: args.trackKey, |
| }, |
| }; |
| }, |
| |
| selectHeapProfile( |
| state: StateDraft, |
| args: {id: number; upid: number; ts: time; type: ProfileType}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'HEAP_PROFILE', |
| id: args.id, |
| upid: args.upid, |
| ts: args.ts, |
| type: args.type, |
| }, |
| }; |
| this.openFlamegraph(state, { |
| type: args.type, |
| start: state.traceTime.start as time, // TODO(stevegolton): Avoid type assertion here. |
| end: args.ts, |
| upids: [args.upid], |
| viewingOption: defaultViewingOption(args.type), |
| }); |
| }, |
| |
| selectPerfSamples( |
| state: StateDraft, |
| args: { |
| id: number; |
| upid: number; |
| leftTs: time; |
| rightTs: time; |
| type: ProfileType; |
| }, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'PERF_SAMPLES', |
| id: args.id, |
| upid: args.upid, |
| leftTs: args.leftTs, |
| rightTs: args.rightTs, |
| type: args.type, |
| }, |
| }; |
| this.openFlamegraph(state, { |
| type: args.type, |
| start: args.leftTs, |
| end: args.rightTs, |
| upids: [args.upid], |
| viewingOption: defaultViewingOption(args.type), |
| }); |
| }, |
| |
| openFlamegraph( |
| state: StateDraft, |
| args: { |
| upids: number[]; |
| start: time; |
| end: time; |
| type: ProfileType; |
| viewingOption: FlamegraphStateViewingOption; |
| }, |
| ): void { |
| state.currentFlamegraphState = { |
| kind: 'FLAMEGRAPH_STATE', |
| upids: args.upids, |
| start: args.start, |
| end: args.end, |
| type: args.type, |
| viewingOption: args.viewingOption, |
| focusRegex: '', |
| expandedCallsiteByViewingOption: {}, |
| }; |
| }, |
| |
| selectCpuProfileSample( |
| state: StateDraft, |
| args: {id: number; utid: number; ts: time}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'CPU_PROFILE_SAMPLE', |
| id: args.id, |
| utid: args.utid, |
| ts: args.ts, |
| }, |
| }; |
| }, |
| |
| expandFlamegraphState( |
| state: StateDraft, |
| args: { |
| expandedCallsite?: CallsiteInfo; |
| viewingOption: FlamegraphStateViewingOption; |
| }, |
| ): void { |
| if (state.currentFlamegraphState === null) return; |
| state.currentFlamegraphState.expandedCallsiteByViewingOption[ |
| args.viewingOption |
| ] = args.expandedCallsite; |
| }, |
| |
| changeViewFlamegraphState( |
| state: StateDraft, |
| args: {viewingOption: FlamegraphStateViewingOption}, |
| ): void { |
| if (state.currentFlamegraphState === null) return; |
| state.currentFlamegraphState.viewingOption = args.viewingOption; |
| }, |
| |
| changeFocusFlamegraphState( |
| state: StateDraft, |
| args: {focusRegex: string}, |
| ): void { |
| if (state.currentFlamegraphState === null) return; |
| state.currentFlamegraphState.focusRegex = args.focusRegex; |
| }, |
| |
| selectChromeSlice( |
| state: StateDraft, |
| args: {id: number; trackKey: string; table?: string; scroll?: boolean}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'CHROME_SLICE', |
| id: args.id, |
| trackKey: args.trackKey, |
| table: args.table, |
| }, |
| }; |
| state.pendingScrollId = args.scroll ? args.id : undefined; |
| }, |
| |
| selectGenericSlice( |
| state: StateDraft, |
| args: { |
| id: number; |
| sqlTableName: string; |
| start: time; |
| duration: duration; |
| trackKey: string; |
| detailsPanelConfig: { |
| kind: string; |
| config: GenericSliceDetailsTabConfigBase; |
| }; |
| }, |
| ): void { |
| const detailsPanelConfig: GenericSliceDetailsTabConfig = { |
| id: args.id, |
| ...args.detailsPanelConfig.config, |
| }; |
| |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'GENERIC_SLICE', |
| id: args.id, |
| sqlTableName: args.sqlTableName, |
| start: args.start, |
| duration: args.duration, |
| trackKey: args.trackKey, |
| detailsPanelConfig: { |
| kind: args.detailsPanelConfig.kind, |
| config: detailsPanelConfig, |
| }, |
| }, |
| }; |
| }, |
| |
| setPendingScrollId(state: StateDraft, args: {pendingScrollId: number}): void { |
| state.pendingScrollId = args.pendingScrollId; |
| }, |
| |
| clearPendingScrollId(state: StateDraft, _: {}): void { |
| state.pendingScrollId = undefined; |
| }, |
| |
| selectThreadState( |
| state: StateDraft, |
| args: {id: number; trackKey: string}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'THREAD_STATE', |
| id: args.id, |
| trackKey: args.trackKey, |
| }, |
| }; |
| }, |
| |
| startRecording(state: StateDraft, _: {}): void { |
| state.recordingInProgress = true; |
| state.lastRecordingError = undefined; |
| state.recordingCancelled = false; |
| }, |
| |
| stopRecording(state: StateDraft, _: {}): void { |
| state.recordingInProgress = false; |
| }, |
| |
| cancelRecording(state: StateDraft, _: {}): void { |
| state.recordingInProgress = false; |
| state.recordingCancelled = true; |
| }, |
| |
| setExtensionAvailable(state: StateDraft, args: {available: boolean}): void { |
| state.extensionInstalled = args.available; |
| }, |
| |
| setRecordingTarget(state: StateDraft, args: {target: RecordingTarget}): void { |
| state.recordingTarget = args.target; |
| }, |
| |
| setFetchChromeCategories(state: StateDraft, args: {fetch: boolean}): void { |
| state.fetchChromeCategories = args.fetch; |
| }, |
| |
| setAvailableAdbDevices( |
| state: StateDraft, |
| args: {devices: AdbRecordingTarget[]}, |
| ): void { |
| state.availableAdbDevices = args.devices; |
| }, |
| |
| setOmnibox(state: StateDraft, args: OmniboxState): void { |
| state.omniboxState = args; |
| }, |
| |
| setOmniboxMode(state: StateDraft, args: {mode: OmniboxMode}): void { |
| state.omniboxState.mode = args.mode; |
| }, |
| |
| selectArea(state: StateDraft, args: {area: Area}): void { |
| const {start, end, tracks} = args.area; |
| assertTrue(start <= end); |
| const areaId = generateNextId(state); |
| state.areas[areaId] = {id: areaId, start, end, tracks}; |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: {kind: 'AREA', areaId}, |
| }; |
| }, |
| |
| editArea(state: StateDraft, args: {area: Area; areaId: string}): void { |
| const {start, end, tracks} = args.area; |
| assertTrue(start <= end); |
| state.areas[args.areaId] = {id: args.areaId, start, end, tracks}; |
| }, |
| |
| reSelectArea( |
| state: StateDraft, |
| args: {areaId: string; noteId: string}, |
| ): void { |
| state.selection = { |
| kind: 'legacy', |
| legacySelection: { |
| kind: 'AREA', |
| areaId: args.areaId, |
| noteId: args.noteId, |
| }, |
| }; |
| }, |
| |
| toggleTrackSelection( |
| state: StateDraft, |
| args: {id: string; isTrackGroup: boolean}, |
| ) { |
| const selection = state.selection; |
| if ( |
| selection.kind !== 'legacy' || |
| selection.legacySelection.kind !== 'AREA' |
| ) { |
| return; |
| } |
| const areaId = selection.legacySelection.areaId; |
| const index = state.areas[areaId].tracks.indexOf(args.id); |
| if (index > -1) { |
| state.areas[areaId].tracks.splice(index, 1); |
| if (args.isTrackGroup) { |
| // Also remove all child tracks. |
| for (const childTrack of state.trackGroups[args.id].tracks) { |
| const childIndex = state.areas[areaId].tracks.indexOf(childTrack); |
| if (childIndex > -1) { |
| state.areas[areaId].tracks.splice(childIndex, 1); |
| } |
| } |
| } |
| } else { |
| state.areas[areaId].tracks.push(args.id); |
| if (args.isTrackGroup) { |
| // Also add all child tracks. |
| for (const childTrack of state.trackGroups[args.id].tracks) { |
| if (!state.areas[areaId].tracks.includes(childTrack)) { |
| state.areas[areaId].tracks.push(childTrack); |
| } |
| } |
| } |
| } |
| // It's super unexpected that |toggleTrackSelection| does not cause |
| // selection to be updated and this leads to bugs for people who do: |
| // if (oldSelection !== state.selection) etc. |
| // To solve this re-create the selection object here: |
| state.selection = Object.assign({}, state.selection); |
| }, |
| |
| setVisibleTraceTime(state: StateDraft, args: VisibleState): void { |
| state.frontendLocalState.visibleState = {...args}; |
| }, |
| |
| setChromeCategories(state: StateDraft, args: {categories: string[]}): void { |
| state.chromeCategories = args.categories; |
| }, |
| |
| setLastRecordingError(state: StateDraft, args: {error?: string}): void { |
| state.lastRecordingError = args.error; |
| state.recordingStatus = undefined; |
| }, |
| |
| setRecordingStatus(state: StateDraft, args: {status?: string}): void { |
| state.recordingStatus = args.status; |
| state.lastRecordingError = undefined; |
| }, |
| |
| togglePerfDebug(state: StateDraft, _: {}): void { |
| state.perfDebug = !state.perfDebug; |
| }, |
| |
| setSidebar(state: StateDraft, args: {visible: boolean}): void { |
| state.sidebarVisible = args.visible; |
| }, |
| |
| setHoveredUtidAndPid(state: StateDraft, args: {utid: number; pid: number}) { |
| state.hoveredPid = args.pid; |
| state.hoveredUtid = args.utid; |
| }, |
| |
| setHighlightedSliceId(state: StateDraft, args: {sliceId: number}) { |
| state.highlightedSliceId = args.sliceId; |
| }, |
| |
| setHighlightedFlowLeftId(state: StateDraft, args: {flowId: number}) { |
| state.focusedFlowIdLeft = args.flowId; |
| }, |
| |
| setHighlightedFlowRightId(state: StateDraft, args: {flowId: number}) { |
| state.focusedFlowIdRight = args.flowId; |
| }, |
| |
| setSearchIndex(state: StateDraft, args: {index: number}) { |
| state.searchIndex = args.index; |
| }, |
| |
| setHoverCursorTimestamp(state: StateDraft, args: {ts: time}) { |
| state.hoverCursorTimestamp = args.ts; |
| }, |
| |
| setHoveredNoteTimestamp(state: StateDraft, args: {ts: time}) { |
| state.hoveredNoteTimestamp = args.ts; |
| }, |
| |
| // Add a tab with a given URI to the tab bar and show it. |
| // If the tab is already present in the tab bar, just show it. |
| showTab(state: StateDraft, args: {uri: string}) { |
| // Add tab, unless we're talking about the special current_selection tab |
| if (args.uri !== 'current_selection') { |
| // Add tab to tab list if not already |
| if (!state.tabs.openTabs.some((uri) => uri === args.uri)) { |
| state.tabs.openTabs.push(args.uri); |
| } |
| } |
| state.tabs.currentTab = args.uri; |
| }, |
| |
| // Hide a tab in the tab bar pick a new tab to show. |
| // Note: Attempting to hide the "current_selection" tab doesn't work. This tab |
| // is special and cannot be removed. |
| hideTab(state: StateDraft, args: {uri: string}) { |
| const tabs = state.tabs; |
| // If the removed tab is the "current" tab, we must find a new tab to focus |
| if (args.uri === tabs.currentTab) { |
| // Remember the index of the current tab |
| const currentTabIdx = tabs.openTabs.findIndex((uri) => uri === args.uri); |
| |
| // Remove the tab |
| tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri); |
| |
| if (currentTabIdx !== -1) { |
| if (tabs.openTabs.length === 0) { |
| // No more tabs, use current selection |
| tabs.currentTab = 'current_selection'; |
| } else if (currentTabIdx < tabs.openTabs.length - 1) { |
| // Pick the tab to the right |
| tabs.currentTab = tabs.openTabs[currentTabIdx]; |
| } else { |
| // Pick the last tab |
| const lastTab = tabs.openTabs[tabs.openTabs.length - 1]; |
| tabs.currentTab = lastTab; |
| } |
| } |
| } else { |
| // Otherwise just remove the tab |
| tabs.openTabs = tabs.openTabs.filter((uri) => uri !== args.uri); |
| } |
| }, |
| |
| clearAllPinnedTracks(state: StateDraft, _: {}) { |
| const pinnedTracks = state.pinnedTracks.slice(); |
| for (let index = pinnedTracks.length - 1; index >= 0; index--) { |
| const trackKey = pinnedTracks[index]; |
| this.toggleTrackPinned(state, {trackKey}); |
| } |
| }, |
| |
| togglePivotTable(state: StateDraft, args: {areaId: string | null}) { |
| state.nonSerializableState.pivotTable.selectionArea = |
| args.areaId === null |
| ? undefined |
| : {areaId: args.areaId, tracks: state.areas[args.areaId].tracks}; |
| if ( |
| args.areaId !== |
| state.nonSerializableState.pivotTable.selectionArea?.areaId |
| ) { |
| state.nonSerializableState.pivotTable.queryResult = null; |
| } |
| }, |
| |
| setPivotStateQueryResult( |
| state: StateDraft, |
| args: {queryResult: PivotTableResult | null}, |
| ) { |
| state.nonSerializableState.pivotTable.queryResult = args.queryResult; |
| }, |
| |
| setPivotTableConstrainToArea(state: StateDraft, args: {constrain: boolean}) { |
| state.nonSerializableState.pivotTable.constrainToArea = args.constrain; |
| }, |
| |
| dismissFlamegraphModal(state: StateDraft, _: {}) { |
| state.flamegraphModalDismissed = true; |
| }, |
| |
| addPivotTableAggregation( |
| state: StateDraft, |
| args: {aggregation: Aggregation; after: number}, |
| ) { |
| state.nonSerializableState.pivotTable.selectedAggregations.splice( |
| args.after, |
| 0, |
| args.aggregation, |
| ); |
| }, |
| |
| removePivotTableAggregation(state: StateDraft, args: {index: number}) { |
| state.nonSerializableState.pivotTable.selectedAggregations.splice( |
| args.index, |
| 1, |
| ); |
| }, |
| |
| setPivotTableQueryRequested( |
| state: StateDraft, |
| args: {queryRequested: boolean}, |
| ) { |
| state.nonSerializableState.pivotTable.queryRequested = args.queryRequested; |
| }, |
| |
| setPivotTablePivotSelected( |
| state: StateDraft, |
| args: {column: TableColumn; selected: boolean}, |
| ) { |
| toggleEnabled( |
| tableColumnEquals, |
| state.nonSerializableState.pivotTable.selectedPivots, |
| args.column, |
| args.selected, |
| ); |
| }, |
| |
| setPivotTableAggregationFunction( |
| state: StateDraft, |
| args: {index: number; function: AggregationFunction}, |
| ) { |
| state.nonSerializableState.pivotTable.selectedAggregations[ |
| args.index |
| ].aggregationFunction = args.function; |
| }, |
| |
| setPivotTableSortColumn( |
| state: StateDraft, |
| args: {aggregationIndex: number; order: SortDirection}, |
| ) { |
| state.nonSerializableState.pivotTable.selectedAggregations = |
| state.nonSerializableState.pivotTable.selectedAggregations.map( |
| (agg, index) => ({ |
| column: agg.column, |
| aggregationFunction: agg.aggregationFunction, |
| sortDirection: |
| index === args.aggregationIndex ? args.order : undefined, |
| }), |
| ); |
| }, |
| |
| changePivotTablePivotOrder( |
| state: StateDraft, |
| args: {from: number; to: number; direction: DropDirection}, |
| ) { |
| const pivots = state.nonSerializableState.pivotTable.selectedPivots; |
| state.nonSerializableState.pivotTable.selectedPivots = performReordering( |
| computeIntervals(pivots.length, args.from, args.to, args.direction), |
| pivots, |
| ); |
| }, |
| |
| changePivotTableAggregationOrder( |
| state: StateDraft, |
| args: {from: number; to: number; direction: DropDirection}, |
| ) { |
| const aggregations = |
| state.nonSerializableState.pivotTable.selectedAggregations; |
| state.nonSerializableState.pivotTable.selectedAggregations = |
| performReordering( |
| computeIntervals( |
| aggregations.length, |
| args.from, |
| args.to, |
| args.direction, |
| ), |
| aggregations, |
| ); |
| }, |
| }; |
| |
| // When we are on the frontend side, we don't really want to execute the |
| // actions above, we just want to serialize them and marshal their |
| // arguments, send them over to the controller side and have them being |
| // executed there. The magic below takes care of turning each action into a |
| // function that returns the marshaled args. |
| |
| // A DeferredAction is a bundle of Args and a method name. This is the marshaled |
| // version of a StateActions method call. |
| export interface DeferredAction<Args = {}> { |
| type: string; |
| args: Args; |
| } |
| |
| // This type magic creates a type function DeferredActions<T> which takes a type |
| // T and 'maps' its attributes. For each attribute on T matching the signature: |
| // (state: StateDraft, args: Args) => void |
| // DeferredActions<T> has an attribute: |
| // (args: Args) => DeferredAction<Args> |
| type ActionFunction<Args> = (state: StateDraft, args: Args) => void; |
| type DeferredActionFunc<T> = T extends ActionFunction<infer Args> |
| ? (args: Args) => DeferredAction<Args> |
| : never; |
| type DeferredActions<C> = { |
| [P in keyof C]: DeferredActionFunc<C[P]>; |
| }; |
| |
| // Actions is an implementation of DeferredActions<typeof StateActions>. |
| // (since StateActions is a variable not a type we have to do |
| // 'typeof StateActions' to access the (unnamed) type of StateActions). |
| // It's a Proxy such that any attribute access returns a function: |
| // (args) => {return {type: ATTRIBUTE_NAME, args};} |
| export const Actions = |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| new Proxy<DeferredActions<typeof StateActions>>({} as any, { |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| get(_: any, prop: string, _2: any) { |
| return (args: {}): DeferredAction<{}> => { |
| return { |
| type: prop, |
| args, |
| }; |
| }; |
| }, |
| }); |