From 127fa1b709e74d493c4b0d8f197f368f9a288cc3 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 2 Nov 2024 21:32:46 -0400 Subject: [PATCH] Add getProfileFlowInfo selector. --- src/actions/profile-view.js | 47 +++ src/profile-logic/marker-data.js | 569 +++++++++++++++++++++++++++++++ src/selectors/flow.js | 46 +++ src/selectors/index.js | 3 + src/utils/window-console.js | 40 +++ 5 files changed, 705 insertions(+) create mode 100644 src/selectors/flow.js diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 6fe354ece7..27aa99120d 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -999,6 +999,53 @@ export function showProvidedTracks( }; } +export function showProvidedThreads( + threadsToShow: Set +): ThunkAction { + return (dispatch, getState) => { + const globalTracks = getGlobalTracks(getState()); + const localTracksByPid = getLocalTracksByPid(getState()); + + const globalTracksToShow: Set = new Set(); + const localTracksByPidToShow: Map> = new Map(); + + for (const [globalTrackIndex, globalTrack] of globalTracks.entries()) { + if (globalTrack.type !== 'process') { + continue; + } + const { mainThreadIndex, pid } = globalTrack; + if (mainThreadIndex !== null && threadsToShow.has(mainThreadIndex)) { + globalTracksToShow.add(globalTrackIndex); + } + const localTracks = localTracksByPid.get(pid); + if (localTracks === undefined) { + continue; + } + + for (const [localTrackIndex, localTrack] of localTracks.entries()) { + if (localTrack.type !== 'thread') { + continue; + } + if (threadsToShow.has(localTrack.threadIndex)) { + const localTracksToShow = localTracksByPidToShow.get(pid); + if (localTracksToShow === undefined) { + localTracksByPidToShow.set(pid, new Set([localTrackIndex])); + } else { + localTracksToShow.add(localTrackIndex); + } + globalTracksToShow.add(globalTrackIndex); + } + } + } + + dispatch({ + type: 'SHOW_PROVIDED_TRACKS', + globalTracksToShow, + localTracksByPidToShow, + }); + }; +} + /** * This action makes the tracks that are provided hidden. */ diff --git a/src/profile-logic/marker-data.js b/src/profile-logic/marker-data.js index f05b326694..aaf4c6c571 100644 --- a/src/profile-logic/marker-data.js +++ b/src/profile-logic/marker-data.js @@ -7,6 +7,7 @@ import { getEmptyRawMarkerTable } from './data-structures'; import { getFriendlyThreadName } from './profile-data'; import { removeFilePath, removeURLs, stringsToRegExp } from '../utils/string'; import { ensureExists, assertExhaustiveCheck } from '../utils/flow'; +import { bisectionRightByKey } from '../utils/bisect'; import { INSTANT, INTERVAL, @@ -43,6 +44,7 @@ import type { MarkerSchemaByName, MarkerDisplayLocation, Tid, + Milliseconds, } from 'firefox-profiler/types'; import type { UniqueStringArray } from '../utils/unique-string-array'; @@ -1637,3 +1639,570 @@ export const stringsToMarkerRegExps = ( fieldMap, }; }; + +export type GlobalFlowMarkerHandle = {| + threadIndex: number, + flowMarkerIndex: number, +|}; + +// An index into the global flow table. +type IndexIntoFlowTable = number; + +export type Flow = {| + id: string, + startTime: Milliseconds, + endTime: Milliseconds | null, + // All markers which mention this flow, ordered by start time. + flowMarkers: GlobalFlowMarkerHandle[], +|}; + +export type ConnectedFlowInfo = {| + // Flows whose marker set has a non-empty intersection with our marker set. + directlyConnectedFlows: IndexIntoFlowTable[], + // Flows which have at least one marker in their marker set was a stack-based + // marker which was already running higher up on the stack when at least one + // of our stack-based or instant markers was running on the same thread. + // All flows in our incomingContextFlows set have this flow in their + // outgoingContextFlows set. + incomingContextFlows: IndexIntoFlowTable[], + // Flows which have at least one stack-based or instant marker in their marker + // set which was running when one of the stack-based markers in our set was + // running higher up on the same thread's stack. + // All flows in our outgoingContextFlows set have this flow in their + // incomingContextFlows set. + outgoingContextFlows: IndexIntoFlowTable[], +|}; + +type FlowIDAndTerminating = {| + flowID: string, + isTerminating: boolean, +|}; + +export type FlowMarker = {| + markerIndex: number, + time: Milliseconds, + // The index of the closest stack-based interval flow marker that encompasses + // this marker. ("Closest" means "with the most recent start time".) + // If non-null, parentContextFlowMarker is lower the index of this flow marker, + // i.e. this can only point "backwards" within the thread's flow markers array. + parentContextFlowMarker: number | null, + // The indexes of flow markers which have this flow marker as their parentContextFlowMarker. + // All indexes in this array after the index of this flow marker. + childContextFlowMarkers: number[], + flowIDs: FlowIDAndTerminating[], +|}; + +class MinHeap { + _keys: number[] = []; + _values: V[] = []; + + size(): number { + return this._values.length; + } + insert(key: number, value: V) { + this._keys.push(key); + this._values.push(value); + } + delete(handle: number) { + this._keys.splice(handle, 1); + this._values.splice(handle, 1); + } + first(): number | null { + if (this._values.length === 0) { + return null; + } + + let minI = 0; + let minKey = this._keys[0]; + for (let i = 1; i < this._keys.length; i++) { + const k = this._keys[i]; + if (k < minKey) { + minI = i; + minKey = k; + } + } + return minI; + } + get(handle: number): V { + return this._values[handle]; + } + reorder(handle: number, newKey: number) { + this._keys[handle] = newKey; + } +} + +export type FlowFieldDescriptor = {| + key: string, + isTerminating: boolean, +|}; + +export type FlowSchema = {| + flowFields: FlowFieldDescriptor[], + isStackBased: boolean, +|}; + +export type FlowSchemasByName = Map; + +export function computeFlowSchemasByName( + markerSchemas: MarkerSchema[] +): FlowSchemasByName { + const flowSchemasByName = new Map(); + for (const schema of markerSchemas) { + const flowFields = []; + for (const field of schema.data) { + if (!field.key) { + continue; + } + const key = field.key; + if (field.format === 'flow-id') { + flowFields.push({ key, isTerminating: false }); + } else if (field.format === 'terminating-flow-id') { + flowFields.push({ key, isTerminating: true }); + } + } + if (flowFields.length !== 0) { + flowSchemasByName.set(schema.name, { + flowFields, + isStackBased: schema.isStackBased === true, + }); + } + } + return flowSchemasByName; +} + +export function computeFlowMarkers( + fullMarkerList: Marker[], + stringTable: UniqueStringArray, + flowSchemasByName: FlowSchemasByName +): FlowMarker[] { + const flowMarkers: FlowMarker[] = []; + const currentContextFlowMarkers: number[] = []; + const currentContextEndTimes: Milliseconds[] = []; + for ( + let markerIndex = 0; + markerIndex < fullMarkerList.length; + markerIndex++ + ) { + const marker = fullMarkerList[markerIndex]; + const markerData = marker.data; + if (!markerData) { + continue; + } + const schemaName = markerData.type; + if (!schemaName) { + continue; + } + const flowSchema = flowSchemasByName.get(schemaName); + if (flowSchema === undefined) { + continue; + } + + const time = marker.start; + + while ( + currentContextEndTimes.length !== 0 && + currentContextEndTimes[currentContextEndTimes.length - 1] < time + ) { + currentContextEndTimes.pop(); + currentContextFlowMarkers.pop(); + } + + const flowIDs = []; + for (const { key, isTerminating } of flowSchema.flowFields) { + const flowIDStringIndex = markerData[key]; + if (flowIDStringIndex !== undefined && flowIDStringIndex !== null) { + const flowID = stringTable.getString(flowIDStringIndex); + flowIDs.push({ flowID, isTerminating }); + } + } + if (flowIDs.length === 0) { + continue; + } + + const thisFlowMarkerIndex = flowMarkers.length; + const parentContextFlowMarker = + currentContextFlowMarkers.length !== 0 + ? currentContextFlowMarkers[currentContextFlowMarkers.length - 1] + : null; + flowMarkers.push({ + parentContextFlowMarker, + childContextFlowMarkers: [], + flowIDs, + time, + markerIndex, + }); + if (flowSchema.isStackBased || marker.end === null) { + if (parentContextFlowMarker !== null) { + flowMarkers[parentContextFlowMarker].childContextFlowMarkers.push( + thisFlowMarkerIndex + ); + } + } + if (flowSchema.isStackBased && marker.end !== null) { + currentContextEndTimes.push(marker.end); + currentContextFlowMarkers.push(thisFlowMarkerIndex); + } + } + return flowMarkers; +} + +export type ProfileFlowInfo = {| + flowTable: Flow[], + flowsByID: Map, + flowMarkersPerThread: FlowMarker[][], + flowMarkerFlowsPerThread: IndexIntoFlowTable[][][], + flowSchemasByName: FlowSchemasByName, +|}; + +export function computeProfileFlowInfo( + fullMarkerListPerThread: Marker[][], + threads: Thread[], + markerSchemas: MarkerSchema[] +): ProfileFlowInfo { + const flowSchemasByName = computeFlowSchemasByName(markerSchemas); + + const flowMarkersPerThread: FlowMarker[][] = fullMarkerListPerThread.map( + (fullMarkerList, threadIndex) => { + const { stringTable } = threads[threadIndex]; + return computeFlowMarkers(fullMarkerList, stringTable, flowSchemasByName); + } + ); + + const threadCount = flowMarkersPerThread.length; + const nextEntryHeap = new MinHeap(); + for (let threadIndex = 0; threadIndex < threadCount; threadIndex++) { + const flowMarkers = flowMarkersPerThread[threadIndex]; + if (flowMarkers.length !== 0) { + nextEntryHeap.insert(flowMarkers[0].time, { + threadIndex, + nextIndex: 0, + }); + } + } + + const flowMarkerFlowsPerThread = flowMarkersPerThread.map(() => []); + + const flowTable = []; + const currentActiveFlows = new Map(); + const flowsByID = new Map(); + + while (true) { + const handle = nextEntryHeap.first(); + if (handle === null) { + break; + } + + const nextEntry = nextEntryHeap.get(handle); + const { threadIndex, nextIndex } = nextEntry; + const flowMarkerIndex = nextIndex; + const flowMarkers = flowMarkersPerThread[threadIndex]; + const flowReference = flowMarkers[nextIndex]; + + const { flowIDs, time } = flowReference; + const flowMarkerHandle = { threadIndex, flowMarkerIndex }; + const flowsForThisFlowMarker = []; + for (const { flowID, isTerminating } of flowIDs) { + let flowIndex = currentActiveFlows.get(flowID); + if (flowIndex === undefined) { + flowIndex = flowTable.length; + flowTable.push({ + id: flowID, + startTime: time, + endTime: time, + flowMarkers: [flowMarkerHandle], + }); + if (!isTerminating) { + currentActiveFlows.set(flowID, flowIndex); + } + const flowsByIDEntry = flowsByID.get(flowID); + if (flowsByIDEntry === undefined) { + flowsByID.set(flowID, [flowIndex]); + } else { + flowsByIDEntry.push(flowIndex); + } + } else { + const flow = flowTable[flowIndex]; + flow.flowMarkers.push(flowMarkerHandle); + flow.endTime = time; + if (isTerminating) { + currentActiveFlows.delete(flowID); + } + } + flowsForThisFlowMarker.push(flowIndex); + } + sortAndDedup(flowsForThisFlowMarker); + flowMarkerFlowsPerThread[threadIndex][flowMarkerIndex] = + flowsForThisFlowMarker; + + const newNextIndex = nextIndex + 1; + if (newNextIndex < flowMarkers.length) { + nextEntry.nextIndex = newNextIndex; + nextEntryHeap.reorder(handle, flowMarkers[newNextIndex].time); + } else { + nextEntryHeap.delete(handle); + } + } + + return { + flowTable, + flowsByID, + flowMarkersPerThread, + flowMarkerFlowsPerThread, + flowSchemasByName, + }; +} + +export function getConnectedFlowInfo( + flowIndex: IndexIntoFlowTable, + profileFlowInfo: ProfileFlowInfo +): ConnectedFlowInfo { + const { flowTable, flowMarkersPerThread, flowMarkerFlowsPerThread } = + profileFlowInfo; + const directlyConnectedFlows: IndexIntoFlowTable[] = []; + const incomingContextFlows: IndexIntoFlowTable[] = []; + const outgoingContextFlows: IndexIntoFlowTable[] = []; + + const flow = flowTable[flowIndex]; + for (const { threadIndex, flowMarkerIndex } of flow.flowMarkers) { + const thisMarkerFlows = + flowMarkerFlowsPerThread[threadIndex][flowMarkerIndex]; + for (const directlyConnectedFlowIndex of thisMarkerFlows) { + if (directlyConnectedFlowIndex !== flowIndex) { + directlyConnectedFlows.push(directlyConnectedFlowIndex); + } + } + + const flowMarker: FlowMarker = + flowMarkersPerThread[threadIndex][flowMarkerIndex]; + + const incomingFlowMarkerIndex = flowMarker.parentContextFlowMarker; + if (incomingFlowMarkerIndex !== null) { + const incomingMarkerFlows = + flowMarkerFlowsPerThread[threadIndex][incomingFlowMarkerIndex]; + for (const incomingContextFlowIndex of incomingMarkerFlows) { + if (incomingContextFlowIndex !== flowIndex) { + incomingContextFlows.push(incomingContextFlowIndex); + } + } + } + + for (const outgoingFlowMarkerIndex of flowMarker.childContextFlowMarkers) { + const outgoingMarkerFlows = + flowMarkerFlowsPerThread[threadIndex][outgoingFlowMarkerIndex]; + for (const outgoingContextFlowIndex of outgoingMarkerFlows) { + if (outgoingContextFlowIndex !== flowIndex) { + outgoingContextFlows.push(outgoingContextFlowIndex); + } + } + } + } + sortAndDedup(directlyConnectedFlows); + sortAndDedup(incomingContextFlows); + sortAndDedup(outgoingContextFlows); + return { + directlyConnectedFlows, + incomingContextFlows, + outgoingContextFlows, + }; +} + +export function lookupFlow( + flowID: string, + time: Milliseconds, + profileFlowInfo: ProfileFlowInfo +): IndexIntoFlowTable | null { + const { flowsByID, flowTable } = profileFlowInfo; + const candidateFlows = flowsByID.get(flowID); + if (candidateFlows === undefined) { + return null; + } + const index = + bisectionRightByKey( + candidateFlows, + time, + (flowIndex) => flowTable[flowIndex].startTime + ) - 1; + if (index === -1) { + return null; + } + return candidateFlows[index]; +} + +export function computeMarkerFlowsForConsole( + threadIndex: number, + markerIndex: MarkerIndex, + profileFlowInfo: ProfileFlowInfo, + threads: Thread[], + fullMarkerListPerThread: Marker[][] +): IndexIntoFlowTable[] | null { + const marker = fullMarkerListPerThread[threadIndex][markerIndex]; + const markerData = marker.data; + if (!markerData) { + return null; + } + const markerType = markerData.type; + if (!markerType) { + return null; + } + const flowSchema = profileFlowInfo.flowSchemasByName.get(markerType); + if (flowSchema === undefined) { + return null; + } + + const stringTable = threads[threadIndex].stringTable; + + const flowIndexes = []; + for (const { key } of flowSchema.flowFields) { + const fieldValue = markerData[key]; + if (fieldValue === undefined || fieldValue === null) { + continue; + } + const flowID = stringTable.getString(fieldValue); + const flowIndex = lookupFlow(flowID, marker.start, profileFlowInfo); + if (flowIndex === null) { + console.error( + `Could not find flow for ID ${flowID} at time ${marker.start}!` + ); + continue; + } + flowIndexes.push(flowIndex); + } + dedupConsecutive(flowIndexes); + return flowIndexes.length !== 0 ? flowIndexes : null; +} + +function sortAndDedup(array: number[]) { + array.sort((a, b) => a - b); + dedupConsecutive(array); +} + +function dedupConsecutive(array: T[]) { + if (array.length === 0) { + return; + } + + let prev = array[0]; + for (let i = 1; i < array.length; i++) { + const curr = array[i]; + if (prev === curr) { + array.splice(i, 1); + i--; + } else { + prev = curr; + } + } +} + +export function printMarkerFlows( + markerThreadIndex: number, + markerIndex: MarkerIndex, + profileFlowInfo: ProfileFlowInfo, + threads: Thread[], + fullMarkerListPerThread: Marker[][] +) { + const markerFlows = computeMarkerFlowsForConsole( + markerThreadIndex, + markerIndex, + profileFlowInfo, + threads, + fullMarkerListPerThread + ); + if (markerFlows === null) { + console.log('This marker is not part of any flows.'); + return; + } + + const { flowTable } = profileFlowInfo; + const flowCount = markerFlows.length; + if (flowCount === 1) { + const flowIndex = markerFlows[0]; + console.log( + `This marker is part of one flow: ${flowTable[flowIndex].id} (index ${flowIndex})` + ); + } else { + console.log( + `This marker is part of ${flowCount} flows:`, + markerFlows.map((flowIndex) => flowTable[flowIndex].id) + ); + } + + for (const flowIndex of markerFlows) { + printFlow(flowIndex, profileFlowInfo, threads, fullMarkerListPerThread); + } +} + +export function printFlow( + flowIndex: IndexIntoFlowTable, + profileFlowInfo: ProfileFlowInfo, + threads: Thread[], + fullMarkerListPerThread: Marker[][] +) { + const { flowTable, flowMarkersPerThread, flowMarkerFlowsPerThread } = + profileFlowInfo; + + const flow = flowTable[flowIndex]; + console.log(`Flow ${flow.id} (index ${flowIndex}):`, flow); + console.log(`This flow contains ${flow.flowMarkers.length} markers:`); + for (const { threadIndex, flowMarkerIndex } of flow.flowMarkers) { + const flowMarker = flowMarkersPerThread[threadIndex][flowMarkerIndex]; + const otherMarkerIndex = flowMarker.markerIndex; + const thread = threads[threadIndex]; + const marker = fullMarkerListPerThread[threadIndex][otherMarkerIndex]; + console.log( + ` - marker ${otherMarkerIndex} (thread index: ${threadIndex}) at time ${flowMarker.time} on thread ${thread.name} (pid: ${thread.pid}, tid: ${thread.tid}):`, + marker + ); + const directlyConnectedFlows = flowMarkerFlowsPerThread[threadIndex][ + flowMarkerIndex + ].filter((otherFlowIndex) => otherFlowIndex !== flowIndex); + const incomingContextFlows = + flowMarker.parentContextFlowMarker !== null + ? flowMarkerFlowsPerThread[threadIndex][ + flowMarker.parentContextFlowMarker + ].filter((otherFlowIndex) => otherFlowIndex !== flowIndex) + : []; + const outgoingContextFlows = []; + for (const childFlowMarkerIndex of flowMarker.childContextFlowMarkers) { + for (const outgoingFlow of flowMarkerFlowsPerThread[threadIndex][ + childFlowMarkerIndex + ]) { + if (outgoingFlow !== flowIndex) { + outgoingContextFlows.push(outgoingFlow); + } + } + } + sortAndDedup(outgoingContextFlows); + if (directlyConnectedFlows.length !== 0) { + console.log( + `Directly connected flows on this marker: ${directlyConnectedFlows.join(', ')}` + ); + } + if (incomingContextFlows.length !== 0) { + console.log( + `Incoming context flows on this marker: ${incomingContextFlows.join(', ')}` + ); + } + if (outgoingContextFlows.length !== 0) { + console.log( + `Outgoing context flows on this marker: ${outgoingContextFlows.join(', ')}` + ); + } + } + + // const connections = getConnectedFlowInfo(flowIndex, profileFlowInfo); + // if (connections.directlyConnectedFlows.length !== 0) { + // console.log( + // `Directly connected flows: ${connections.directlyConnectedFlows.join(', ')}` + // ); + // } + // if (connections.incomingContextFlows.length !== 0) { + // console.log( + // `Incoming context flows: ${connections.incomingContextFlows.join(', ')}` + // ); + // } + // if (connections.outgoingContextFlows.length !== 0) { + // console.log( + // `Outgoing context flows: ${connections.outgoingContextFlows.join(', ')}` + // ); + // } +} diff --git a/src/selectors/flow.js b/src/selectors/flow.js new file mode 100644 index 0000000000..f9c6d45e6e --- /dev/null +++ b/src/selectors/flow.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow +import { createSelector } from 'reselect'; + +import { computeProfileFlowInfo } from '../profile-logic/marker-data'; +import type { ProfileFlowInfo } from '../profile-logic/marker-data'; +import { getThreadSelectors } from './per-thread'; +import { getThreads, getMarkerSchema } from './profile'; + +import type { Selector, State, Marker } from 'firefox-profiler/types'; + +let _cachedFullMarkerListPerThread = []; +export const getFullMarkerListPerThread: Selector = ( + state: State +) => { + const threads = getThreads(state); + const fullMarkerListPerThread = threads.map((_thread, i) => { + const { getFullMarkerList } = getThreadSelectors(i); + return getFullMarkerList(state); + }); + + // getFullMarkerList is a selector which caches the per-thread marker list. + // It is likely that the values in fullMarkerListPerThread haven't changed + // compared to last time. If that's the case, make sure that the array we + // return is the same object as the one we returned last time, so that + // our callers can memoize based on the array object identity. + if ( + _cachedFullMarkerListPerThread.length !== fullMarkerListPerThread.length || + !_cachedFullMarkerListPerThread.every( + (val, index) => val === fullMarkerListPerThread[index] + ) + ) { + _cachedFullMarkerListPerThread = fullMarkerListPerThread; + } + return _cachedFullMarkerListPerThread; +}; + +export const getProfileFlowInfo: Selector = createSelector( + getFullMarkerListPerThread, + getThreads, + getMarkerSchema, + computeProfileFlowInfo +); diff --git a/src/selectors/index.js b/src/selectors/index.js index 4dfce0f72e..a34763ecb3 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -13,6 +13,7 @@ export * from './publish'; export * from './zipped-profiles'; export * from './cpu'; export * from './code'; +export * from './flow'; import * as app from './app'; import { @@ -27,6 +28,7 @@ import * as zippedProfiles from './zipped-profiles'; import * as l10n from './l10n'; import * as cpu from './cpu'; import * as code from './code'; +import * as flow from './flow'; const _selectorsForConsole = { app, @@ -40,6 +42,7 @@ const _selectorsForConsole = { l10n, cpu, code, + flow, }; // Exports require explicit typing. Deduce the type with typeof. diff --git a/src/utils/window-console.js b/src/utils/window-console.js index 35ae508c64..3f057d3938 100644 --- a/src/utils/window-console.js +++ b/src/utils/window-console.js @@ -8,6 +8,10 @@ import { selectorsForConsole } from 'firefox-profiler/selectors'; import actions from 'firefox-profiler/actions'; import { shortenUrl } from 'firefox-profiler/utils/shorten-url'; import { createBrowserConnection } from 'firefox-profiler/app-logic/browser-connection'; +import { + printMarkerFlows, + printFlow, +} from 'firefox-profiler/profile-logic/marker-data'; // Despite providing a good libdef for Object.defineProperty, Flow still // special-cases the `value` property: if it's missing it throws an error. Using @@ -68,6 +72,42 @@ export function addDataToWindowObject( }, }); + target.printFlows = function () { + const threadIndex = + selectorsForConsole.urlState.getFirstSelectedThreadIndex(getState()); + const markerIndex = + selectorsForConsole.selectedThread.getSelectedMarkerIndex(getState()); + const profileFlowInfo = + selectorsForConsole.flow.getProfileFlowInfo(getState()); + const threads = selectorsForConsole.profile.getThreads(getState()); + const fullMarkerListPerThread = + selectorsForConsole.flow.getFullMarkerListPerThread(getState()); + if (markerIndex === null) { + console.log('No marker is selected.'); + } else { + printMarkerFlows( + threadIndex, + markerIndex, + profileFlowInfo, + threads, + fullMarkerListPerThread + ); + } + }; + target.selectMarkerOnThread = function (markerIndex, threadIndex) { + dispatch(actions.showProvidedThreads(new Set([threadIndex]))); + dispatch(actions.changeSelectedThreads(new Set([threadIndex]))); + dispatch(actions.changeSelectedMarker(threadIndex, markerIndex)); + }; + target.printFlow = function (flowIndex) { + const profileFlowInfo = + selectorsForConsole.flow.getProfileFlowInfo(getState()); + const threads = selectorsForConsole.profile.getThreads(getState()); + const fullMarkerListPerThread = + selectorsForConsole.flow.getFullMarkerListPerThread(getState()); + printFlow(flowIndex, profileFlowInfo, threads, fullMarkerListPerThread); + }; + target.experimental = { enableEventDelayTracks() { const areEventDelayTracksEnabled = dispatch(