|  | // 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, assertUnreachable} from '../base/logging'; | 
|  | import {duration, time} from '../base/time'; | 
|  | import {exists} from '../base/utils'; | 
|  | import {RecordConfig} from '../controller/record_config_types'; | 
|  | import { | 
|  | GenericSliceDetailsTabConfig, | 
|  | GenericSliceDetailsTabConfigBase, | 
|  | } from '../frontend/generic_slice_details_tab'; | 
|  | import {globals} from '../frontend/globals'; | 
|  | import { | 
|  | Aggregation, | 
|  | AggregationFunction, | 
|  | TableColumn, | 
|  | tableColumnEquals, | 
|  | toggleEnabled, | 
|  | } from '../frontend/pivot_table_types'; | 
|  | import {PrimaryTrackSortKey} from '../public/index'; | 
|  |  | 
|  | import {randomColor} from './colorizer'; | 
|  | import { | 
|  | computeIntervals, | 
|  | DropDirection, | 
|  | performReordering, | 
|  | } from './dragndrop_logic'; | 
|  | import {createEmptyState} from './empty_state'; | 
|  | import {defaultViewingOption} from './flamegraph_util'; | 
|  | import { | 
|  | MetatraceTrackId, | 
|  | traceEvent, | 
|  | traceEventBegin, | 
|  | traceEventEnd, | 
|  | TraceEventScope, | 
|  | } from './metatracing'; | 
|  | import { | 
|  | AdbRecordingTarget, | 
|  | Area, | 
|  | CallsiteInfo, | 
|  | EngineMode, | 
|  | FlamegraphStateViewingOption, | 
|  | FtraceFilterPatch, | 
|  | LoadedConfig, | 
|  | NewEngineMode, | 
|  | OmniboxMode, | 
|  | OmniboxState, | 
|  | Pagination, | 
|  | PendingDeeplinkState, | 
|  | PivotTableResult, | 
|  | ProfileType, | 
|  | RecordingTarget, | 
|  | SCROLLING_TRACK_GROUP, | 
|  | SortDirection, | 
|  | State, | 
|  | Status, | 
|  | ThreadTrackSortKey, | 
|  | TraceTime, | 
|  | TrackSortKey, | 
|  | TrackState, | 
|  | 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; | 
|  | } | 
|  |  | 
|  | export interface PostedTrace { | 
|  | buffer: ArrayBuffer; | 
|  | title: string; | 
|  | fileName?: string; | 
|  | url?: string; | 
|  | uuid?: string; | 
|  | localOnly?: boolean; | 
|  | keepApiOpen?: boolean; | 
|  | } | 
|  |  | 
|  | export interface PostedScrollToRange { | 
|  | timeStart: time; | 
|  | timeEnd: time; | 
|  | 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; | 
|  | }, | 
|  |  | 
|  | fillUiTrackIdByTraceTrackId( | 
|  | state: StateDraft, trackState: TrackState, trackKey: string) { | 
|  | const setTrackKey = (trackId: number, trackKey: string) => { | 
|  | if (state.trackKeyByTrackId[trackId] !== undefined && | 
|  | state.trackKeyByTrackId[trackId] !== trackKey) { | 
|  | throw new Error(`Trying to map track id ${trackId} to UI track ${ | 
|  | trackKey}, already mapped to ${state.trackKeyByTrackId[trackId]}`); | 
|  | } | 
|  | state.trackKeyByTrackId[trackId] = trackKey; | 
|  | }; | 
|  |  | 
|  | const {uri} = trackState; | 
|  | if (exists(uri)) { | 
|  | // If track is a new "plugin" type track (i.e. it has a uri), resolve the | 
|  | // track ids from through the pluginManager. | 
|  | const trackInfo = globals.trackManager.resolveTrackInfo(uri); | 
|  | if (trackInfo?.trackIds) { | 
|  | for (const trackId of trackInfo.trackIds) { | 
|  | setTrackKey(trackId, trackKey); | 
|  | } | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | 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, | 
|  | }; | 
|  | this.fillUiTrackIdByTraceTrackId(state, track as TrackState, trackKey); | 
|  | 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: [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.currentSelection = { | 
|  | 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.currentSelection === null || | 
|  | state.currentSelection.kind !== 'AREA') { | 
|  | return; | 
|  | } | 
|  | const id = args.persistent ? generateNextId(state) : '0'; | 
|  | const color = args.persistent ? args.color : '#344596'; | 
|  | state.notes[id] = { | 
|  | noteType: 'AREA', | 
|  | id, | 
|  | areaId: state.currentSelection.areaId, | 
|  | color, | 
|  | text: '', | 
|  | }; | 
|  | state.currentSelection.noteId = id; | 
|  | }, | 
|  |  | 
|  | toggleMarkCurrentArea(state: StateDraft, args: {persistent: boolean}) { | 
|  | const selection = state.currentSelection; | 
|  | if (selection != null && selection.kind === 'AREA' && | 
|  | selection.noteId !== undefined) { | 
|  | this.removeNote(state, {id: selection.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.currentSelection === null) return; | 
|  | if (state.currentSelection.kind === 'NOTE' && | 
|  | state.currentSelection.id === args.id) { | 
|  | state.currentSelection = null; | 
|  | } else if ( | 
|  | state.currentSelection.kind === 'AREA' && | 
|  | state.currentSelection.noteId === args.id) { | 
|  | state.currentSelection.noteId = undefined; | 
|  | } | 
|  | }, | 
|  |  | 
|  | selectSlice( | 
|  | state: StateDraft, | 
|  | args: {id: number, trackKey: string, scroll?: boolean}): void { | 
|  | state.currentSelection = { | 
|  | kind: 'SLICE', | 
|  | id: args.id, | 
|  | trackKey: args.trackKey, | 
|  | }; | 
|  | state.pendingScrollId = args.scroll ? args.id : undefined; | 
|  | }, | 
|  |  | 
|  | selectCounter( | 
|  | state: StateDraft, | 
|  | args: {leftTs: time, rightTs: time, id: number, trackKey: string}): void { | 
|  | state.currentSelection = { | 
|  | 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.currentSelection = { | 
|  | 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.currentSelection = { | 
|  | 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: '', | 
|  | }; | 
|  | }, | 
|  |  | 
|  | selectCpuProfileSample( | 
|  | state: StateDraft, args: {id: number, utid: number, ts: time}): void { | 
|  | state.currentSelection = { | 
|  | kind: 'CPU_PROFILE_SAMPLE', | 
|  | id: args.id, | 
|  | utid: args.utid, | 
|  | ts: args.ts, | 
|  | }; | 
|  | }, | 
|  |  | 
|  | expandFlamegraphState( | 
|  | state: StateDraft, args: {expandedCallsite?: CallsiteInfo}): void { | 
|  | if (state.currentFlamegraphState === null) return; | 
|  | state.currentFlamegraphState.expandedCallsite = 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.currentSelection = { | 
|  | 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.currentSelection = { | 
|  | 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}, | 
|  | }; | 
|  | }, | 
|  |  | 
|  | clearPendingScrollId(state: StateDraft, _: {}): void { | 
|  | state.pendingScrollId = undefined; | 
|  | }, | 
|  |  | 
|  | selectThreadState(state: StateDraft, args: {id: number, trackKey: string}): | 
|  | void { | 
|  | state.currentSelection = { | 
|  | kind: 'THREAD_STATE', | 
|  | id: args.id, | 
|  | trackKey: args.trackKey, | 
|  | }; | 
|  | }, | 
|  |  | 
|  | selectLog( | 
|  | state: StateDraft, | 
|  | args: {id: number, trackKey: string, scroll?: boolean}): void { | 
|  | state.currentSelection = { | 
|  | kind: 'LOG', | 
|  | id: args.id, | 
|  | trackKey: args.trackKey, | 
|  | }; | 
|  | state.pendingScrollId = args.scroll ? args.id : undefined; | 
|  | }, | 
|  |  | 
|  | deselect(state: StateDraft, _: {}): void { | 
|  | state.currentSelection = null; | 
|  | }, | 
|  |  | 
|  | updateLogsPagination(state: StateDraft, args: Pagination): void { | 
|  | state.logsPagination = args; | 
|  | }, | 
|  |  | 
|  | updateFtracePagination(state: StateDraft, args: Pagination): void { | 
|  | state.ftracePagination = args; | 
|  | }, | 
|  |  | 
|  | updateFtraceFilter(state: StateDraft, patch: FtraceFilterPatch) { | 
|  | const {excludedNames: diffs} = patch; | 
|  | const excludedNames = state.ftraceFilter.excludedNames; | 
|  | for (const [addRemove, name] of diffs) { | 
|  | switch (addRemove) { | 
|  | case 'add': | 
|  | if (!excludedNames.some((excluded: string) => excluded === name)) { | 
|  | excludedNames.push(name); | 
|  | } | 
|  | break; | 
|  | case 'remove': | 
|  | state.ftraceFilter.excludedNames = | 
|  | state.ftraceFilter.excludedNames.filter( | 
|  | (excluded: string) => excluded !== name); | 
|  | break; | 
|  | default: | 
|  | assertUnreachable(addRemove); | 
|  | break; | 
|  | } | 
|  | } | 
|  | }, | 
|  |  | 
|  | 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.currentSelection = {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.currentSelection = { | 
|  | kind: 'AREA', | 
|  | areaId: args.areaId, | 
|  | noteId: args.noteId, | 
|  | }; | 
|  | }, | 
|  |  | 
|  | toggleTrackSelection( | 
|  | state: StateDraft, args: {id: string, isTrackGroup: boolean}) { | 
|  | const selection = state.currentSelection; | 
|  | if (selection === null || selection.kind !== 'AREA') return; | 
|  | const areaId = selection.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.currentSelection = Object.assign({}, state.currentSelection); | 
|  | }, | 
|  |  | 
|  | 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; | 
|  | }, | 
|  |  | 
|  | // Tab V1 specific | 
|  | setCurrentTab(state: StateDraft, args: {tab: string|undefined}) { | 
|  | traceEvent('setCurrentTab', () => { | 
|  | state.currentTab = args.tab; | 
|  | }, { | 
|  | args: { | 
|  | tab: args.tab ?? '<undefined>', | 
|  | }, | 
|  | }); | 
|  | }, | 
|  |  | 
|  | // Specific to tabs V2. | 
|  | // 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; | 
|  | }, | 
|  |  | 
|  | // Specific to tabs V2. | 
|  | // 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); | 
|  | } | 
|  | }, | 
|  |  | 
|  | toggleAllTrackGroups(state: StateDraft, args: {collapsed: boolean}) { | 
|  | for (const group of Object.values(state.trackGroups)) { | 
|  | group.collapsed = args.collapsed; | 
|  | } | 
|  | }, | 
|  |  | 
|  | 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, | 
|  | })); | 
|  | }, | 
|  |  | 
|  | setPivotTableArgumentNames( | 
|  | state: StateDraft, args: {argumentNames: string[]}) { | 
|  | state.nonSerializableState.pivotTable.argumentNames = args.argumentNames; | 
|  | }, | 
|  |  | 
|  | 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); | 
|  | }, | 
|  |  | 
|  | setMinimumLogLevel(state: StateDraft, args: {minimumLevel: number}) { | 
|  | state.logFilteringCriteria.minimumLevel = args.minimumLevel; | 
|  | }, | 
|  |  | 
|  | addLogTag(state: StateDraft, args: {tag: string}) { | 
|  | if (!state.logFilteringCriteria.tags.includes(args.tag)) { | 
|  | state.logFilteringCriteria.tags.push(args.tag); | 
|  | } | 
|  | }, | 
|  |  | 
|  | removeLogTag(state: StateDraft, args: {tag: string}) { | 
|  | state.logFilteringCriteria.tags = | 
|  | state.logFilteringCriteria.tags.filter((t) => t !== args.tag); | 
|  | }, | 
|  |  | 
|  | updateLogFilterText(state: StateDraft, args: {textEntry: string}) { | 
|  | state.logFilteringCriteria.textEntry = args.textEntry; | 
|  | }, | 
|  |  | 
|  | toggleCollapseByTextEntry(state: StateDraft, _: {}) { | 
|  | state.logFilteringCriteria.hideNonMatching = | 
|  | !state.logFilteringCriteria.hideNonMatching; | 
|  | }, | 
|  | }; | 
|  |  | 
|  | // 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, | 
|  | }; | 
|  | }; | 
|  | }, | 
|  | }); |