From a30bb43b5928d199f4ce2b89d9d5c76328ac0a57 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 2 Nov 2024 21:32:46 -0400 Subject: [PATCH 1/3] Add getProfileFlowInfo selector. --- src/actions/profile-view.js | 47 +++ src/profile-logic/marker-data.js | 569 +++++++++++++++++++++++++++++++ src/selectors/flow.js | 48 +++ src/selectors/index.js | 3 + src/utils/window-console.js | 40 +++ 5 files changed, 707 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 e0cddc2c82..4647e0b97e 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'; @@ -1638,3 +1640,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..f72273679f --- /dev/null +++ b/src/selectors/flow.js @@ -0,0 +1,48 @@ +/* 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 type { ThreadSelectors } from './per-thread'; +import { getThreads, getMarkerSchema } from './profile'; + +import type { Selector, State, Marker } from 'firefox-profiler/types'; + +function _arraysShallowEqual(arr1: any[], arr2: any[]): boolean { + return arr1.length === arr2.length && arr1.every((val, i) => val === arr2[i]); +} + +function _createSelectorForAllThreads( + f: (ThreadSelectors, State) => T +): Selector { + let previousOutputPerThread = []; + return function recomputeSelectorForAllThreads(state: State): T[] { + const threads = getThreads(state); + let outputPerThread = threads.map((_thread, i) => { + const threadSelectors = getThreadSelectors(i); + return f(threadSelectors, state); + }); + if (_arraysShallowEqual(outputPerThread, previousOutputPerThread)) { + outputPerThread = previousOutputPerThread; + } + previousOutputPerThread = outputPerThread; + return outputPerThread; + }; +} + +export const getFullMarkerListPerThread: Selector = + _createSelectorForAllThreads(({ getFullMarkerList }, state) => + getFullMarkerList(state) + ); + +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( From c383ce0bbc2bcac69d39056b3013901183f9fbbf Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 9 Nov 2024 16:49:48 +0100 Subject: [PATCH 2/3] Group stack-based and instant markers at the top of the marker chart. --- src/profile-logic/marker-timing.js | 101 +++++++++++++++++++--------- src/selectors/per-thread/markers.js | 1 + 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/src/profile-logic/marker-timing.js b/src/profile-logic/marker-timing.js index 9bf6b13f0d..24997a07f7 100644 --- a/src/profile-logic/marker-timing.js +++ b/src/profile-logic/marker-timing.js @@ -8,12 +8,31 @@ import type { MarkerIndex, MarkerTiming, MarkerTimingAndBuckets, + MarkerSchemaByName, } from 'firefox-profiler/types'; +import { getSchemaFromMarker } from './marker-schema'; + // Arbitrarily set an upper limit for adding marker depths, avoiding very long // overlapping marker timings. const MAX_STACKING_DEPTH = 300; +function _createEmptyTiming( + name: string, + bucket: string, + instantOnly: boolean +): MarkerTiming { + return { + start: [], + end: [], + index: [], + name, + bucket, + instantOnly, + length: 0, + }; +} + /** * This function computes the timing information for laying out the markers in the * MarkerChart component. Each marker is put into a single row based on its name. In @@ -86,13 +105,15 @@ export function getMarkerTiming( markerIndexes: MarkerIndex[], // Categories can be null for things like Network Markers, where we don't care to // break things up by category. - categories: ?CategoryList + categories: ?CategoryList, + markerSchemaByName: ?MarkerSchemaByName ): MarkerTiming[] { - // Each marker type will have it's own timing information, later collapse these into + // Each marker type will have its own timing information, later collapse these into // a single array. const intervalMarkerTimingsMap: Map = new Map(); // Instant markers are on separate lines. const instantMarkerTimingsMap: Map = new Map(); + const stackBasedMarkerTimings: MarkerTiming[] = []; // Go through all of the markers. for (const markerIndex of markerIndexes) { @@ -109,31 +130,41 @@ export function getMarkerTiming( markerTiming.length++; }; - const bucketName = categories ? categories[marker.category].name : 'None'; + const schema = markerSchemaByName + ? getSchemaFromMarker(markerSchemaByName, marker.name, marker.data) + : null; + let isStackBased = schema !== null && schema.isStackBased === true; + if (marker.end === null) { + // XXXmstange let's see how we like this + isStackBased = true; + } + + // eslint-disable-next-line no-nested-ternary + const bucketName = isStackBased + ? 'Stack' + : categories + ? categories[marker.category].name + : 'None'; // We want to group all network requests in the same line. Indeed they all // have different names and they'd end up with one single request in each // line without this special handling. - const markerLineName = - marker.data && marker.data.type === 'Network' + // eslint-disable-next-line no-nested-ternary + const markerLineName = isStackBased + ? 'Stack' + : marker.data && marker.data.type === 'Network' ? 'Network Requests' : marker.name; - const emptyTiming = ({ instantOnly }): MarkerTiming => ({ - start: [], - end: [], - index: [], - name: markerLineName, - bucket: bucketName, - instantOnly, - length: 0, - }); - - if (marker.end === null) { + if (marker.end === null && !isStackBased) { // This is an instant marker. let instantMarkerTiming = instantMarkerTimingsMap.get(markerLineName); if (!instantMarkerTiming) { - instantMarkerTiming = emptyTiming({ instantOnly: true }); + instantMarkerTiming = _createEmptyTiming( + markerLineName, + bucketName, + true + ); instantMarkerTimingsMap.set(markerLineName, instantMarkerTiming); } addCurrentMarkerToMarkerTiming(instantMarkerTiming); @@ -141,16 +172,18 @@ export function getMarkerTiming( } // This is an interval marker. - let markerTimingsForName = intervalMarkerTimingsMap.get(markerLineName); - if (markerTimingsForName === undefined) { - markerTimingsForName = []; - intervalMarkerTimingsMap.set(markerLineName, markerTimingsForName); + let timingRows = isStackBased + ? stackBasedMarkerTimings + : intervalMarkerTimingsMap.get(markerLineName); + if (timingRows === undefined) { + timingRows = []; + intervalMarkerTimingsMap.set(markerLineName, timingRows); } // Find the first row where the new marker fits. // Since the markers are sorted, look at the last added marker in this row. If // the new marker fits, go ahead and insert it. - const foundMarkerTiming = markerTimingsForName.find( + const foundMarkerTiming = timingRows.find( (markerTiming) => markerTiming.end[markerTiming.length - 1] <= marker.start ); @@ -160,29 +193,35 @@ export function getMarkerTiming( continue; } - if (markerTimingsForName.length >= MAX_STACKING_DEPTH) { + if (timingRows.length >= MAX_STACKING_DEPTH) { // There are too many markers stacked around the same time already, let's // ignore this marker. continue; } // Otherwise, let's add a new row! - const newTiming = emptyTiming({ instantOnly: false }); + const newTiming = _createEmptyTiming(markerLineName, bucketName, false); addCurrentMarkerToMarkerTiming(newTiming); - markerTimingsForName.push(newTiming); + timingRows.push(newTiming); continue; } // Flatten out the maps into a single array. // One item in this array is one line in the drawn canvas. - const allMarkerTimings = [...instantMarkerTimingsMap.values()].concat( - ...intervalMarkerTimingsMap.values() - ); + const allMarkerTimings = [...instantMarkerTimingsMap.values()] + .concat(...intervalMarkerTimingsMap.values()) + .concat(...stackBasedMarkerTimings); // Sort all the marker timings in place, first by the bucket, then by their names. allMarkerTimings.sort((a, b) => { if (a.bucket !== b.bucket) { // Sort by buckets first. + if (a.bucket === 'Stack') { + return -1; + } + if (b.bucket === 'Stack') { + return 1; + } // Show the 'Test' category first. Test markers are almost guaranteed to // be the most relevant when they exist. if (a.bucket === 'Test') { @@ -286,12 +325,14 @@ export function getMarkerTimingAndBuckets( markerIndexes: MarkerIndex[], // Categories can be null for things like Network Markers, where we don't care to // break things up by category. - categories: ?CategoryList + categories: ?CategoryList, + markerSchemaByName: ?MarkerSchemaByName ): MarkerTimingAndBuckets { const allMarkerTimings = getMarkerTiming( getMarker, markerIndexes, - categories + categories, + markerSchemaByName ); // Interleave the bucket names in between the marker timings. diff --git a/src/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 393ee3f168..0fd7a03bdb 100644 --- a/src/selectors/per-thread/markers.js +++ b/src/selectors/per-thread/markers.js @@ -488,6 +488,7 @@ export function getMarkerSelectorsPerThread( getMarkerGetter, getMarkerChartMarkerIndexes, ProfileSelectors.getCategories, + ProfileSelectors.getMarkerSchemaByName, MarkerTimingLogic.getMarkerTimingAndBuckets ); From f6a6a44992e41f2aa8e1a36f840b41a84f74b6aa Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 14 Nov 2024 01:25:56 +0100 Subject: [PATCH 3/3] Add a flow panel. --- src/actions/profile-view.js | 38 + src/app-logic/url-handling.js | 7 + src/components/app/Details.js | 4 +- src/components/app/DetailsContainer.css | 8 +- src/components/flow-panel/Canvas.js | 934 ++++++++++++++++++++++ src/components/flow-panel/index.css | 13 + src/components/flow-panel/index.js | 182 +++++ src/components/marker-chart-tab/index.css | 9 + src/components/marker-chart-tab/index.js | 36 + src/components/marker-chart/Canvas.js | 38 +- src/components/marker-chart/index.js | 13 +- src/profile-logic/marker-data.js | 352 ++++++-- src/reducers/url-state.js | 13 + src/selectors/flow.js | 34 +- src/selectors/url-state.js | 3 + src/types/actions.js | 5 + src/types/profile-derived.js | 114 +++ src/types/state.js | 2 + src/utils/window-console.js | 3 + 19 files changed, 1679 insertions(+), 129 deletions(-) create mode 100644 src/components/flow-panel/Canvas.js create mode 100644 src/components/flow-panel/index.css create mode 100644 src/components/flow-panel/index.js create mode 100644 src/components/marker-chart-tab/index.css create mode 100644 src/components/marker-chart-tab/index.js diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 27aa99120d..64aec703df 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -25,6 +25,11 @@ import { getThreadSelectorsFromThreadsKey, selectedThreadSelectors, } from 'firefox-profiler/selectors/per-thread'; +import { + getProfileFlowInfo, + getStringTablePerThread, + getFullMarkerListPerThread, +} from 'firefox-profiler/selectors/flow'; import { getAllCommittedRanges, getImplementationFilter, @@ -79,11 +84,13 @@ import type { TableViewOptions, SelectionContext, BottomBoxInfo, + IndexIntoFlowTable, } from 'firefox-profiler/types'; import { funcHasDirectRecursiveCall, funcHasRecursiveCall, } from '../profile-logic/transforms'; +import { computeMarkerFlows } from '../profile-logic/marker-data'; import { changeStoredProfileNameInDb } from 'firefox-profiler/app-logic/uploaded-profiles-db'; import type { TabSlug } from '../app-logic/tabs-handling'; import { intersectSets } from 'firefox-profiler/utils/set'; @@ -1727,6 +1734,37 @@ export function changeHoveredMarker( }; } +export function changeActiveFlows(activeFlows: IndexIntoFlowTable[]): Action { + return { + type: 'CHANGE_ACTIVE_FLOWS', + activeFlows, + }; +} + +export function activateFlowsForMarker( + threadIndex: ThreadIndex, + markerIndex: MarkerIndex +): ThunkAction { + console.log('yo'); + return (dispatch, getState) => { + console.log('aha'); + const profileFlowInfo = getProfileFlowInfo(getState()); + const stringTablePerThread = getStringTablePerThread(getState()); + const fullMarkerListPerThread = getFullMarkerListPerThread(getState()); + console.log('aha2'); + const flows = + computeMarkerFlows( + threadIndex, + markerIndex, + profileFlowInfo, + stringTablePerThread, + fullMarkerListPerThread + ) ?? []; + console.log({ flows }); + dispatch(changeActiveFlows(flows)); + }; +} + /** * This action is used when the user right clicks a marker, and is especially * used to display its context menu. diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index f2ce716633..b90790e3ec 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -186,6 +186,7 @@ type BaseQuery = {| timelineType: string, sourceView: string, assemblyView: string, + activeFlows: string, ...FullProfileSpecificBaseQuery, ...ActiveTabProfileSpecificBaseQuery, ...OriginsProfileSpecificBaseQuery, @@ -436,6 +437,9 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { query = (baseQuery: MarkersQueryShape); query.markerSearch = urlState.profileSpecific.markersSearchString || undefined; + query.activeFlows = + encodeUintArrayForUrlComponent(urlState.profileSpecific.activeFlows) || + undefined; break; case 'network-chart': query = (baseQuery: NetworkQueryShape); @@ -578,6 +582,8 @@ export function stateFromLocation( implementation = query.implementation; } + const activeFlows = decodeUintArrayFromUrlComponent(query.activeFlows ?? ''); + const transforms = {}; if (selectedThreadsKey !== null) { transforms[selectedThreadsKey] = parseTransforms(query.transforms); @@ -658,6 +664,7 @@ export function stateFromLocation( transforms, sourceView, assemblyView, + activeFlows, isBottomBoxOpenPerPanel, timelineType: validateTimelineType(query.timelineType), full: { diff --git a/src/components/app/Details.js b/src/components/app/Details.js index 193a194d02..ce9b5c4564 100644 --- a/src/components/app/Details.js +++ b/src/components/app/Details.js @@ -14,7 +14,7 @@ import { LocalizedErrorBoundary } from './ErrorBoundary'; import { ProfileCallTreeView } from 'firefox-profiler/components/calltree/ProfileCallTreeView'; import { MarkerTable } from 'firefox-profiler/components/marker-table'; import { StackChart } from 'firefox-profiler/components/stack-chart/'; -import { MarkerChart } from 'firefox-profiler/components/marker-chart/'; +import { MarkerChartTab } from 'firefox-profiler/components/marker-chart-tab'; import { NetworkChart } from 'firefox-profiler/components/network-chart/'; import { FlameGraph } from 'firefox-profiler/components/flame-graph/'; import { JsTracer } from 'firefox-profiler/components/js-tracer/'; @@ -115,7 +115,7 @@ class ProfileViewerImpl extends PureComponent { calltree: , 'flame-graph': , 'stack-chart': , - 'marker-chart': , + 'marker-chart': , 'marker-table': , 'network-chart': , 'js-tracer': , diff --git a/src/components/app/DetailsContainer.css b/src/components/app/DetailsContainer.css index f46c2679eb..41b236a392 100644 --- a/src/components/app/DetailsContainer.css +++ b/src/components/app/DetailsContainer.css @@ -1,10 +1,10 @@ -.DetailsContainer .layout-pane > * { +.DetailsContainer > .layout-pane > * { width: 100%; height: 100%; box-sizing: border-box; } -.DetailsContainer .layout-pane:not(.layout-pane-primary) { +.DetailsContainer > .layout-pane:not(.layout-pane-primary) { max-width: 600px; } @@ -15,12 +15,12 @@ position: unset; } -.DetailsContainer .layout-splitter { +.DetailsContainer > .layout-splitter { border-top: 1px solid var(--grey-30); border-left: 1px solid var(--grey-30); background: var(--grey-10); /* Same background as sidebars */ } -.DetailsContainer .layout-splitter:hover { +.DetailsContainer > .layout-splitter:hover { background: var(--grey-30); /* same as the border above */ } diff --git a/src/components/flow-panel/Canvas.js b/src/components/flow-panel/Canvas.js new file mode 100644 index 0000000000..c027f3bbff --- /dev/null +++ b/src/components/flow-panel/Canvas.js @@ -0,0 +1,934 @@ +/* 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 { GREY_20, BLUE_60, BLUE_80 } from 'photon-colors'; +import * as React from 'react'; +import memoize from 'memoize-immutable'; +import { + withChartViewport, + type WithChartViewport, + type Viewport, +} from 'firefox-profiler/components/shared/chart/Viewport'; +import { ChartCanvas } from 'firefox-profiler/components/shared/chart/Canvas'; +import { TooltipMarker } from 'firefox-profiler/components/tooltip/Marker'; +import TextMeasurement from 'firefox-profiler/utils/text-measurement'; +import { bisectionRight } from 'firefox-profiler/utils/bisect'; +import { + typeof updatePreviewSelection as UpdatePreviewSelection, + typeof changeMouseTimePosition as ChangeMouseTimePosition, + typeof changeActiveFlows as ChangeActiveFlows, +} from 'firefox-profiler/actions/profile-view'; +import { TIMELINE_MARGIN_LEFT } from 'firefox-profiler/app-logic/constants'; +import type { + Milliseconds, + CssPixels, + UnitIntervalOfProfileRange, + Marker, + MarkerIndex, + FlowTiming, + ThreadIndex, + IndexIntoFlowTable, + FlowTimingRow, + FlowTimingArrow, +} from 'firefox-profiler/types'; +import { getStartEndRangeForMarker } from 'firefox-profiler/utils'; +import { ensureExists } from 'firefox-profiler/utils/flow'; +import { computeArrowsRelatedToMarker } from 'firefox-profiler/profile-logic/marker-data'; + +import type { + ChartCanvasScale, + ChartCanvasHoverInfo, +} from '../shared/chart/Canvas'; + +import type { WrapFunctionInDispatch } from 'firefox-profiler/utils/connect'; + +type HoveredFlowPanelItems = {| + rowIndex: number | null, + flowIndex: IndexIntoFlowTable | null, + indexInFlowMarkers: number | null, // index into flows[flowIndex].flowMarkers + threadIndex: ThreadIndex | null, + markerIndex: MarkerIndex | null, + flowMarkerIndex: number | null, +|}; + +type OwnProps = {| + +rangeStart: Milliseconds, + +rangeEnd: Milliseconds, + +flowTiming: FlowTiming, + +rowHeight: CssPixels, + +fullMarkerListPerThread: Marker[][], + +markerLabelGetterPerThread: Array<(MarkerIndex) => string>, + +updatePreviewSelection: WrapFunctionInDispatch, + +changeMouseTimePosition: ChangeMouseTimePosition, + +changeActiveFlows: ChangeActiveFlows, + +marginLeft: CssPixels, + +marginRight: CssPixels, + +shouldDisplayTooltips: () => boolean, +|}; + +type Props = {| + ...OwnProps, + // Bring in the viewport props from the higher order Viewport component. + +viewport: Viewport, +|}; + +const TEXT_OFFSET_TOP = 11; +const TEXT_OFFSET_START = 3; +const MARKER_DOT_RADIUS = 0.25; +const LABEL_PADDING = 5; +const MARKER_BORDER_COLOR = '#2c77d1'; + +class FlowPanelCanvasImpl extends React.PureComponent { + _textMeasurement: null | TextMeasurement; + + _memoizedGetArrows = memoize((threadIndex, flowMarkerIndex, flowTiming) => + computeArrowsRelatedToMarker(threadIndex, flowMarkerIndex, flowTiming) + ); + + drawCanvas = ( + ctx: CanvasRenderingContext2D, + scale: ChartCanvasScale, + hoverInfo: ChartCanvasHoverInfo + ) => { + const { + rowHeight, + flowTiming, + viewport: { + viewportTop, + viewportBottom, + containerWidth, + containerHeight, + }, + } = this.props; + const { cssToUserScale } = scale; + if (cssToUserScale !== 1) { + throw new Error( + 'StackChartCanvasImpl sets scaleCtxToCssPixels={true}, so canvas user space units should be equal to CSS pixels.' + ); + } + + // Convert CssPixels to Stack Depth + const rowCount = flowTiming.rows.length; + const startRow = Math.floor(viewportTop / rowHeight); + const endRow = Math.min(Math.ceil(viewportBottom / rowHeight), rowCount); + + // Common properties that won't be changed later. + ctx.lineWidth = 1; + + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, containerWidth, containerHeight); + this.drawRowHighlights(ctx, startRow, endRow); + this.drawRowContents(ctx, null, startRow, endRow); + this.drawSeparatorsAndLabels(ctx, startRow, endRow); + + const { hoveredItem } = hoverInfo; + if (hoveredItem !== null) { + const { threadIndex, flowMarkerIndex } = hoveredItem; + if (threadIndex !== null && flowMarkerIndex !== null) { + const arrows = this._memoizedGetArrows( + threadIndex, + flowMarkerIndex, + flowTiming + ); + console.log({ arrows }); + this.drawArrows(ctx, arrows, startRow, endRow); + } + } + }; + + drawRowHighlights( + ctx: CanvasRenderingContext2D, + startRow: number, + endRow: number + ) { + const { flowTiming } = this.props; + const { rows } = flowTiming; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + const rowType = rows[rowIndex].rowType; + if (rowType === 'ACTIVE') { + this.drawRowHighlight(ctx, rowIndex); + } + } + } + + drawRowHighlight(ctx: CanvasRenderingContext2D, rowIndex: number) { + const { + rowHeight, + viewport: { viewportTop, containerWidth }, + } = this.props; + + ctx.fillStyle = 'rgba(40, 122, 169, 0.2)'; + ctx.fillRect( + 0, // To include the labels also + rowIndex * rowHeight - viewportTop, + containerWidth, + rowHeight - 1 // Subtract 1 for borders. + ); + } + + drawFlowRectangle( + ctx: CanvasRenderingContext2D, + rowIndex: number, + timeAtViewportLeft: number, + timeAtViewportRightPlusMargin: number, + rangeStart: number, + rangeLength: number, + viewportLeft: number, + markerContainerWidth: number, + viewportLength: number, + marginLeft: number + ) { + const { + rowHeight, + flowTiming, + viewport: { viewportTop }, + } = this.props; + const { rows } = flowTiming; + const { devicePixelRatio } = window; + + const row = rows[rowIndex]; + const startTimestamp = row.flowStart; + const endTimestamp = row.flowEnd; + + const y: CssPixels = rowIndex * rowHeight - viewportTop; + const h: CssPixels = rowHeight - 1; + + // Only draw samples that are in bounds. + if ( + !( + endTimestamp >= timeAtViewportLeft && + startTimestamp < timeAtViewportRightPlusMargin + ) + ) { + return; + } + const startTime: UnitIntervalOfProfileRange = + (startTimestamp - rangeStart) / rangeLength; + const endTime: UnitIntervalOfProfileRange = + (endTimestamp - rangeStart) / rangeLength; + + let x: CssPixels = + ((startTime - viewportLeft) * markerContainerWidth) / viewportLength + + marginLeft; + let w: CssPixels = + ((endTime - startTime) * markerContainerWidth) / viewportLength; + + x = Math.round(x * devicePixelRatio) / devicePixelRatio; + w = Math.round(w * devicePixelRatio) / devicePixelRatio; + + ctx.strokeStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.beginPath(); + + // We want the rectangle to have a clear margin, that's why we increment y + // and decrement h (twice, for both margins). + // We also add "0.5" more so that the stroke is properly on a pixel. + // Indeed strokes are drawn on both sides equally, so half a pixel on each + // side in this case. + ctx.rect( + x + 0.5, // + 0.5 for the stroke + y + 1 + 0.5, // + 1 for the top margin, + 0.5 for the stroke + w - 1, // - 1 to account for left and right strokes. + h - 2 - 1 // + 2 accounts for top and bottom margins, + 1 accounts for top and bottom strokes + ); + ctx.stroke(); + } + + // Note: we used a long argument list instead of an object parameter on + // purpose, to reduce GC pressure while drawing. + drawOneMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + w: CssPixels, + h: CssPixels, + isInstantMarker: boolean, + markerIndex: MarkerIndex, + threadIndex: number, + isHighlighted: boolean = false + ) { + if (isInstantMarker) { + w = 1; + } + this.drawOneIntervalMarker( + ctx, + x, + y, + w, + h, + markerIndex, + threadIndex, + isHighlighted + ); + } + + drawOneIntervalMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + w: CssPixels, + h: CssPixels, + markerIndex: MarkerIndex, + threadIndex: number, + isHighlighted: boolean + ) { + const { marginLeft, markerLabelGetterPerThread } = this.props; + + if (w <= 2) { + // This is an interval marker small enough that if we drew it as a + // rectangle, we wouldn't see any inside part. With a width of 2 pixels, + // the rectangle-with-borders would only be borders. With less than 2 + // pixels, the borders would collapse. + // So let's draw it directly as a rect. + ctx.fillStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + // w is rounded in the caller, but let's make sure it's at least 1. + w = Math.max(w, 1); + ctx.fillRect(x, y + 1, w, h - 2); + } else { + // This is a bigger interval marker. + const textMeasurement = this._getTextMeasurement(ctx); + + ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff'; + ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + ctx.beginPath(); + + // We want the rectangle to have a clear margin, that's why we increment y + // and decrement h (twice, for both margins). + // We also add "0.5" more so that the stroke is properly on a pixel. + // Indeed strokes are drawn on both sides equally, so half a pixel on each + // side in this case. + ctx.rect( + x + 0.5, // + 0.5 for the stroke + y + 1 + 0.5, // + 1 for the top margin, + 0.5 for the stroke + w - 1, // - 1 to account for left and right strokes. + h - 2 - 1 // + 2 accounts for top and bottom margins, + 1 accounts for top and bottom strokes + ); + ctx.fill(); + ctx.stroke(); + + // Draw the text label + // TODO - L10N RTL. + // Constrain the x coordinate to the leftmost area. + const x2: CssPixels = + x < marginLeft ? marginLeft + TEXT_OFFSET_START : x + TEXT_OFFSET_START; + const visibleWidth = x < marginLeft ? w - marginLeft + x : w; + const w2: CssPixels = visibleWidth - 2 * TEXT_OFFSET_START; + + if (w2 > textMeasurement.minWidth) { + const fittedText = textMeasurement.getFittedText( + markerLabelGetterPerThread[threadIndex](markerIndex), + w2 + ); + if (fittedText) { + ctx.fillStyle = isHighlighted ? 'white' : 'black'; + ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); + } + } + } + } + /* + // x indicates the center of this marker + // y indicates the top of the row + // h indicates the available height in the row + drawOneInstantMarker( + ctx: CanvasRenderingContext2D, + x: CssPixels, + y: CssPixels, + h: CssPixels, + isHighlighted: boolean + ) { + ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff'; + ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; + + // We're drawing a diamond shape, whose height is h - 2, and width is h / 2. + ctx.beginPath(); + ctx.moveTo(x - h / 4, y + h / 2); + ctx.lineTo(x, y + 1.5); + ctx.lineTo(x + h / 4, y + h / 2); + ctx.lineTo(x, y + h - 1.5); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } +*/ + drawMarkersForRow( + ctx: CanvasRenderingContext2D, + rowIndex: number, + flowTimingRow: FlowTimingRow, + timeAtViewportLeft: number, + timeAtViewportRightPlusMargin: number, + rangeStart: Milliseconds, + rangeLength: Milliseconds, + viewportLeft: CssPixels, + viewportLength: CssPixels, + rowHeight: CssPixels, + viewportTop: CssPixels, + markerContainerWidth: CssPixels, + marginLeft: CssPixels + ) { + const { devicePixelRatio } = window; + + const { markers } = flowTimingRow; + + const y: CssPixels = rowIndex * rowHeight - viewportTop; + const h: CssPixels = rowHeight - 1; + + // Track the last drawn marker X position, so that we can avoid overdrawing. + let previousMarkerDrawnAtX: number | null = null; + + for (let i = 0; i < markers.length; i++) { + const startTimestamp = markers.startTime[i]; + const endTimestamp = markers.endTime[i]; + const isInstantMarker = markers.isInstant[i] === 1; + + // Only draw samples that are in bounds. + if ( + endTimestamp >= timeAtViewportLeft && + startTimestamp < timeAtViewportRightPlusMargin + ) { + const startTime: UnitIntervalOfProfileRange = + (startTimestamp - rangeStart) / rangeLength; + const endTime: UnitIntervalOfProfileRange = + (endTimestamp - rangeStart) / rangeLength; + + let x: CssPixels = + ((startTime - viewportLeft) * markerContainerWidth) / viewportLength + + marginLeft; + let w: CssPixels = + ((endTime - startTime) * markerContainerWidth) / viewportLength; + + x = Math.round(x * devicePixelRatio) / devicePixelRatio; + w = Math.round(w * devicePixelRatio) / devicePixelRatio; + + const markerIndex = markers.markerIndex[i]; + const threadIndex = markers.threadIndex[i]; + + if ( + isInstantMarker || + // Always render non-dot markers and markers that are larger than + // one pixel. + w > 1 || + // Do not render dot markers that occupy the same pixel, as this can take + // a lot of time, and not change the visual display of the chart. + x !== previousMarkerDrawnAtX + ) { + previousMarkerDrawnAtX = x; + this.drawOneMarker( + ctx, + x, + y, + w, + h, + isInstantMarker, + markerIndex, + threadIndex + ); + } + } + } + } + + drawRowContents( + ctx: CanvasRenderingContext2D, + hoveredItem: MarkerIndex | null, + startRow: number, + endRow: number + ) { + const { + rangeStart, + rangeEnd, + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { + containerWidth, + containerHeight, + viewportLeft, + viewportRight, + viewportTop, + }, + } = this.props; + + const markerContainerWidth = containerWidth - marginLeft - marginRight; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + + // Decide which samples to actually draw + const timeAtViewportLeft: Milliseconds = + rangeStart + rangeLength * viewportLeft; + const timeAtViewportRightPlusMargin: Milliseconds = + rangeStart + + rangeLength * viewportRight + + // This represents the amount of seconds in the right margin: + marginRight * ((viewportLength * rangeLength) / markerContainerWidth); + + // We'll restore the context at the end, so that the clip region will be + // removed. + ctx.save(); + // The clip operation forbids drawing in the label zone. + ctx.beginPath(); + ctx.rect(marginLeft, 0, markerContainerWidth, containerHeight); + ctx.clip(); + + // Only draw the stack frames that are vertically within view. + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // Get the timing information for a row of stack frames. + const flowTimingRow = flowTiming.rows[rowIndex]; + this.drawFlowRectangle( + ctx, + rowIndex, + timeAtViewportLeft, + timeAtViewportRightPlusMargin, + rangeStart, + rangeLength, + viewportLeft, + markerContainerWidth, + viewportLength, + marginLeft + ); + this.drawMarkersForRow( + ctx, + rowIndex, + flowTimingRow, + timeAtViewportLeft, + timeAtViewportRightPlusMargin, + rangeStart, + rangeLength, + viewportLeft, + viewportLength, + rowHeight, + viewportTop, + markerContainerWidth, + marginLeft + ); + } + + ctx.restore(); + } + + /** + * Lazily create the text measurement tool, as a valid 2d rendering context must + * exist before it is created. + */ + _getTextMeasurement(ctx: CanvasRenderingContext2D): TextMeasurement { + if (!this._textMeasurement) { + this._textMeasurement = new TextMeasurement(ctx); + } + return this._textMeasurement; + } + + drawSeparatorsAndLabels( + ctx: CanvasRenderingContext2D, + startRow: number, + endRow: number + ) { + const { + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { viewportTop, containerWidth, containerHeight }, + } = this.props; + + const usefulContainerWidth = containerWidth - marginRight; + + // Draw separators + ctx.fillStyle = GREY_20; + ctx.fillRect(marginLeft - 1, 0, 1, containerHeight); + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + // `- 1` at the end, because the top separator is not drawn in the canvas, + // it's drawn using CSS' border property. And canvas positioning is 0-based. + const y = (rowIndex + 1) * rowHeight - viewportTop - 1; + ctx.fillRect(0, y, usefulContainerWidth, 1); + } + + const textMeasurement = this._getTextMeasurement(ctx); + + // Draw the marker names in the left margin. + ctx.fillStyle = '#000000'; + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + const markerTimingRow = flowTiming.rows[rowIndex]; + // Draw the marker name. + const { label } = markerTimingRow; + + const y = rowIndex * rowHeight - viewportTop; + + // Even when it's on active tab view, have a hard cap on the text length. + const fittedText = textMeasurement.getFittedText( + label, + TIMELINE_MARGIN_LEFT - LABEL_PADDING + ); + + ctx.fillText(fittedText, LABEL_PADDING, y + TEXT_OFFSET_TOP); + } + } + + drawArrows( + ctx: CanvasRenderingContext2D, + arrows: FlowTimingArrow[], + startRow: number, + endRow: number + ) { + const { + rangeStart, + rangeEnd, + rowHeight, + marginLeft, + marginRight, + viewport: { + containerWidth, + containerHeight, + viewportLeft, + viewportRight, + viewportTop, + }, + } = this.props; + + const markerContainerWidth = containerWidth - marginLeft - marginRight; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + + // Decide which samples to actually draw + const timeAtViewportLeft: Milliseconds = + rangeStart + rangeLength * viewportLeft; + const timeAtViewportRightPlusMargin: Milliseconds = + rangeStart + + rangeLength * viewportRight + + // This represents the amount of seconds in the right margin: + marginRight * ((viewportLength * rangeLength) / markerContainerWidth); + + // We'll restore the context at the end, so that the clip region will be + // removed. + ctx.save(); + // The clip operation forbids drawing in the label zone. + ctx.beginPath(); + ctx.rect(marginLeft, 0, markerContainerWidth, containerHeight); + ctx.clip(); + + ctx.lineCap = 'round'; + ctx.strokeStyle = 'black'; + + for (const arrow of arrows) { + const { time, rowIndexesFrom, rowIndexesTo, minRowIndex, maxRowIndex } = + arrow; + if ( + maxRowIndex < startRow || + minRowIndex > endRow || + time < timeAtViewportLeft || + time > timeAtViewportRightPlusMargin + ) { + continue; + } + const minY: CssPixels = + minRowIndex * rowHeight - viewportTop + rowHeight / 2 + 2; + const maxY: CssPixels = + maxRowIndex * rowHeight - viewportTop + rowHeight / 2 - 2; + const timeAsUnit: UnitIntervalOfProfileRange = + (time - rangeStart) / rangeLength; + const x: CssPixels = + ((timeAsUnit - viewportLeft) * markerContainerWidth) / viewportLength + + marginLeft; + + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x, minY); + ctx.lineTo(x, maxY); + ctx.stroke(); + + const minFrom = Math.min(...rowIndexesFrom); + const maxFrom = Math.max(...rowIndexesFrom); + for (const rowIndex of rowIndexesFrom) { + const y = rowIndex * rowHeight - viewportTop + rowHeight / 2; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x, y); + ctx.stroke(); + } + for (const rowIndex of rowIndexesTo) { + const y = rowIndex * rowHeight - viewportTop + rowHeight / 2; + if (minFrom < rowIndex) { + // Draw arrow from top to bottom (aimed at rowIndex) + ctx.beginPath(); + ctx.moveTo(x - 3.5, y - 8); + ctx.lineTo(x + 3.5, y - 8); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.fill(); + } + if (maxFrom > rowIndex) { + // Draw arrow from bottom to top (aimed at rowIndex) + ctx.beginPath(); + ctx.moveTo(x + 3.5, y + 8); + ctx.lineTo(x - 3.5, y + 8); + ctx.lineTo(x, y); + ctx.closePath(); + ctx.fill(); + } + } + } + ctx.restore(); + } + + hitTest = (x: CssPixels, y: CssPixels): HoveredFlowPanelItems | null => { + const { + rangeStart, + rangeEnd, + flowTiming, + rowHeight, + marginLeft, + marginRight, + viewport: { viewportLeft, viewportRight, viewportTop, containerWidth }, + } = this.props; + + // Note: we may want to increase this value to hit markers that are farther. + const dotRadius: CssPixels = MARKER_DOT_RADIUS * rowHeight; + if (x < marginLeft - dotRadius) { + return null; + } + + let markerIndex = null; + let flowMarkerIndex = null; + let threadIndex = null; + let indexInFlowMarkers = null; + + const markerContainerWidth = containerWidth - marginLeft - marginRight; + + const rangeLength: Milliseconds = rangeEnd - rangeStart; + + // Reminder: this is a value between 0 and 1, and represents a percentage of + // the full time range. + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + + // This is the x position in terms of unit interval (so, between 0 and 1). + const xInUnitInterval: UnitIntervalOfProfileRange = + viewportLeft + viewportLength * ((x - marginLeft) / markerContainerWidth); + + const dotRadiusInTime = + (dotRadius / markerContainerWidth) * viewportLength * rangeLength; + + const xInTime: Milliseconds = rangeStart + xInUnitInterval * rangeLength; + const rowIndex = Math.floor((y + viewportTop) / rowHeight); + + if (rowIndex < 0 || rowIndex >= flowTiming.rows.length) { + return null; + } + + const row = flowTiming.rows[rowIndex]; + const flowIndex = row.flowIndex; + const markerTiming = row.markers; + + if ( + !markerTiming || + typeof markerTiming === 'string' || + !markerTiming.length + ) { + return null; + } + + // This is a small utility function to define if some marker timing is in + // our hit test range. + const isMarkerTimingInDotRadius = (index) => + markerTiming.startTime[index] < xInTime + dotRadiusInTime && + markerTiming.endTime[index] > xInTime - dotRadiusInTime; + + // A markerTiming line is ordered. + // 1. Let's find a marker reasonably close to our mouse cursor. + // The result of this bisection gives the first marker that starts _after_ + // our mouse cursor. Our result will be either this marker, or the previous + // one. + const nextStartIndex = bisectionRight(markerTiming.startTime, xInTime); + + if (nextStartIndex > 0 && nextStartIndex < markerTiming.length) { + // 2. This is the common case: 2 markers are candidates. Then we measure + // the distance between them and the mouse cursor and chose the smallest + // distance. + const prevStartIndex = nextStartIndex - 1; + + // Note that these values can be negative if the cursor is _inside_ a + // marker. There should be one at most in this case, and we'll want it. So + // NO Math.abs here. + const distanceToNext = markerTiming.startTime[nextStartIndex] - xInTime; + const distanceToPrev = xInTime - markerTiming.endTime[prevStartIndex]; + + const closest = + distanceToPrev < distanceToNext ? prevStartIndex : nextStartIndex; + + // 3. When we found the closest, we still have to check if it's in close + // enough! + if (isMarkerTimingInDotRadius(closest)) { + markerIndex = markerTiming.markerIndex[closest]; + flowMarkerIndex = markerTiming.flowMarkerIndex[closest]; + threadIndex = markerTiming.threadIndex[closest]; + indexInFlowMarkers = closest; + } + } else if (nextStartIndex === 0) { + // 4. Special case 1: the mouse cursor is at the left of all markers in + // this line. Then, we have only 1 candidate, we can check if it's inside + // our hit test range right away. + if (isMarkerTimingInDotRadius(nextStartIndex)) { + markerIndex = markerTiming.markerIndex[nextStartIndex]; + flowMarkerIndex = markerTiming.flowMarkerIndex[nextStartIndex]; + threadIndex = markerTiming.threadIndex[nextStartIndex]; + indexInFlowMarkers = nextStartIndex; + } + } else { + // 5. Special case 2: the mouse cursor is at the right of all markers in + // this line. Then we only have 1 candidate as well, let's check if it's + // inside our hit test range. + if (isMarkerTimingInDotRadius(nextStartIndex - 1)) { + markerIndex = markerTiming.markerIndex[nextStartIndex - 1]; + flowMarkerIndex = markerTiming.flowMarkerIndex[nextStartIndex - 1]; + threadIndex = markerTiming.threadIndex[nextStartIndex - 1]; + indexInFlowMarkers = nextStartIndex - 1; + } + } + + return { + rowIndex, + flowIndex, + indexInFlowMarkers, + markerIndex, + flowMarkerIndex, + threadIndex, + }; + }; + + onMouseMove = (event: { nativeEvent: MouseEvent }) => { + const { + changeMouseTimePosition, + rangeStart, + rangeEnd, + marginLeft, + marginRight, + viewport: { viewportLeft, viewportRight, containerWidth }, + } = this.props; + const viewportLength: UnitIntervalOfProfileRange = + viewportRight - viewportLeft; + const markerContainerWidth = containerWidth - marginLeft - marginRight; + // This is the x position in terms of unit interval (so, between 0 and 1). + const xInUnitInterval: UnitIntervalOfProfileRange = + viewportLeft + + viewportLength * + ((event.nativeEvent.offsetX - marginLeft) / markerContainerWidth); + + if (xInUnitInterval < 0 || xInUnitInterval > 1) { + changeMouseTimePosition(null); + } else { + const rangeLength: Milliseconds = rangeEnd - rangeStart; + const xInTime: Milliseconds = rangeStart + xInUnitInterval * rangeLength; + changeMouseTimePosition(xInTime); + } + }; + + onMouseLeave = () => { + this.props.changeMouseTimePosition(null); + }; + + onDoubleClickMarker = (hoveredItems: HoveredFlowPanelItems | null) => { + const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; + const threadIndex = hoveredItems === null ? null : hoveredItems.threadIndex; + if (markerIndex === null || threadIndex === null) { + return; + } + const { + fullMarkerListPerThread, + updatePreviewSelection, + rangeStart, + rangeEnd, + } = this.props; + const marker = ensureExists( + fullMarkerListPerThread[threadIndex][markerIndex] + ); + const { start, end } = getStartEndRangeForMarker( + rangeStart, + rangeEnd, + marker + ); + + updatePreviewSelection({ + hasSelection: true, + isModifying: false, + selectionStart: start, + selectionEnd: end, + }); + }; + + onSelectItem = (hoveredItems: HoveredFlowPanelItems | null) => { + const flowIndex = hoveredItems === null ? null : hoveredItems.flowIndex; + if (flowIndex === null) { + return; + } + + const { changeActiveFlows } = this.props; + changeActiveFlows([flowIndex]); + }; + + onRightClickMarker = (_hoveredItems: HoveredFlowPanelItems | null) => { + // const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; + // const { changeRightClickedMarker, threadsKey } = this.props; + // changeRightClickedMarker(threadsKey, markerIndex); + }; + + getHoveredMarkerInfo = ({ + threadIndex, + markerIndex, + }: HoveredFlowPanelItems): React.Node => { + if ( + !this.props.shouldDisplayTooltips() || + threadIndex === null || + markerIndex === null + ) { + return null; + } + + const marker = ensureExists( + this.props.fullMarkerListPerThread[threadIndex][markerIndex] + ); + return ( + + ); + }; + + render() { + const { containerWidth, containerHeight, isDragging } = this.props.viewport; + + return ( + + ); + } +} + +export const FlowPanelCanvas = (withChartViewport: WithChartViewport< + OwnProps, + Props, +>)(FlowPanelCanvasImpl); diff --git a/src/components/flow-panel/index.css b/src/components/flow-panel/index.css new file mode 100644 index 0000000000..7451d9d019 --- /dev/null +++ b/src/components/flow-panel/index.css @@ -0,0 +1,13 @@ +/* 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/. */ + +.flowPanel { + display: flex; + flex: 1; + flex-flow: column nowrap; +} + +.markerChartCanvas { + border-top: 1px solid var(--grey-30); +} diff --git a/src/components/flow-panel/index.js b/src/components/flow-panel/index.js new file mode 100644 index 0000000000..c5bc3ae852 --- /dev/null +++ b/src/components/flow-panel/index.js @@ -0,0 +1,182 @@ +/* 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 * as React from 'react'; +import { TIMELINE_MARGIN_RIGHT } from 'firefox-profiler/app-logic/constants'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { FlowPanelCanvas } from './Canvas'; + +import { + getCommittedRange, + getPreviewSelection, +} from 'firefox-profiler/selectors/profile'; +import { + getFullMarkerListPerThread, + getMarkerChartLabelGetterPerThread, + getFlowTiming, +} from 'firefox-profiler/selectors/flow'; +import { getTimelineMarginLeft } from 'firefox-profiler/selectors/app'; +import { + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, +} from 'firefox-profiler/actions/profile-view'; +import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; + +import type { + Marker, + MarkerIndex, + FlowTiming, + UnitIntervalOfProfileRange, + StartEndRange, + PreviewSelection, + CssPixels, +} from 'firefox-profiler/types'; + +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './index.css'; + +const ROW_HEIGHT = 16; + +type DispatchProps = {| + +updatePreviewSelection: typeof updatePreviewSelection, + +changeMouseTimePosition: typeof changeMouseTimePosition, + +changeActiveFlows: typeof changeActiveFlows, +|}; + +type StateProps = {| + +fullMarkerListPerThread: Marker[][], + +markerLabelGetterPerThread: Array<(MarkerIndex) => string>, + +flowTiming: FlowTiming, + +timeRange: StartEndRange, + +previewSelection: PreviewSelection, + +timelineMarginLeft: CssPixels, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +class FlowPanelImpl extends React.PureComponent { + _viewport: HTMLDivElement | null = null; + + /** + * Determine the maximum zoom of the viewport. + */ + getMaximumZoom(): UnitIntervalOfProfileRange { + const { + timeRange: { start, end }, + } = this.props; + + // This is set to a very small value, that represents 1ns. We can't set it + // to zero unless we revamp how ranges are handled in the app to prevent + // less-than-1ns ranges, otherwise we can get stuck at a "0" zoom. + const ONE_NS = 1e-6; + return ONE_NS / (end - start); + } + + _shouldDisplayTooltips = () => true; + + _takeViewportRef = (viewport: HTMLDivElement | null) => { + this._viewport = viewport; + }; + + _focusViewport = () => { + if (this._viewport) { + this._viewport.focus(); + } + }; + + componentDidMount() { + this._focusViewport(); + } + + render() { + const { + timeRange, + flowTiming, + fullMarkerListPerThread, + markerLabelGetterPerThread, + previewSelection, + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, + timelineMarginLeft, + } = this.props; + + // The viewport needs to know about the height of what it's drawing, calculate + // that here at the top level component. + const rowCount = flowTiming.rows.length; + const maxViewportHeight = rowCount * ROW_HEIGHT; + + return ( +
+ {rowCount === 0 ? null : ( + + + + )} +
+ ); + } +} + +// This function is given the FlowPanelCanvas's chartProps. +function viewportNeedsUpdate( + prevProps: { +flowTiming: FlowTiming }, + newProps: { +flowTiming: FlowTiming } +) { + return prevProps.flowTiming !== newProps.flowTiming; +} + +export const FlowPanel = explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: (state) => { + const flowTiming = getFlowTiming(state); + return { + fullMarkerListPerThread: getFullMarkerListPerThread(state), + markerLabelGetterPerThread: getMarkerChartLabelGetterPerThread(state), + flowTiming, + timeRange: getCommittedRange(state), + previewSelection: getPreviewSelection(state), + timelineMarginLeft: getTimelineMarginLeft(state), + }; + }, + mapDispatchToProps: { + updatePreviewSelection, + changeMouseTimePosition, + changeActiveFlows, + }, + component: FlowPanelImpl, +}); diff --git a/src/components/marker-chart-tab/index.css b/src/components/marker-chart-tab/index.css new file mode 100644 index 0000000000..062ddd03cb --- /dev/null +++ b/src/components/marker-chart-tab/index.css @@ -0,0 +1,9 @@ +.markerChartTabContainer { + position: relative; + min-height: 0; + flex: 1; +} + +.markerChartTabSplitter > .layout-pane { + display: flex; +} diff --git a/src/components/marker-chart-tab/index.js b/src/components/marker-chart-tab/index.js new file mode 100644 index 0000000000..f4561de97c --- /dev/null +++ b/src/components/marker-chart-tab/index.js @@ -0,0 +1,36 @@ +/* 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 * as React from 'react'; + +import SplitterLayout from 'react-splitter-layout'; +import { MarkerChart } from '../marker-chart'; +import { FlowPanel } from '../flow-panel'; + +import './index.css'; + +export function MarkerChartTab() { + return ( +
+ + + + +
+ ); +} diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index 8ecf638e07..0d10a08c68 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -20,6 +20,7 @@ import { typeof changeRightClickedMarker as ChangeRightClickedMarker, typeof changeMouseTimePosition as ChangeMouseTimePosition, typeof changeSelectedMarker as ChangeSelectedMarker, + typeof activateFlowsForMarker as ActivateFlowsForMarker, } from 'firefox-profiler/actions/profile-view'; import { TIMELINE_MARGIN_LEFT } from 'firefox-profiler/app-logic/constants'; import type { @@ -87,6 +88,7 @@ type OwnProps = {| +changeMouseTimePosition: ChangeMouseTimePosition, +changeSelectedMarker: ChangeSelectedMarker, +changeRightClickedMarker: ChangeRightClickedMarker, + +activateFlowsForMarker: WrapFunctionInDispatch, +marginLeft: CssPixels, +marginRight: CssPixels, +selectedMarkerIndex: MarkerIndex | null, @@ -295,7 +297,8 @@ class MarkerChartCanvasImpl extends React.PureComponent { isHighlighted: boolean = false ) { if (isInstantMarker) { - this.drawOneInstantMarker(ctx, x, y, h, isHighlighted); + // this.drawOneInstantMarker(ctx, x, y, h, isHighlighted); + this.drawOneIntervalMarker(ctx, x, y, 1, h, markerIndex, isHighlighted); } else { this.drawOneIntervalMarker(ctx, x, y, w, h, markerIndex, isHighlighted); } @@ -367,30 +370,6 @@ class MarkerChartCanvasImpl extends React.PureComponent { } } - // x indicates the center of this marker - // y indicates the top of the row - // h indicates the available height in the row - drawOneInstantMarker( - ctx: CanvasRenderingContext2D, - x: CssPixels, - y: CssPixels, - h: CssPixels, - isHighlighted: boolean - ) { - ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff'; - ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR; - - // We're drawing a diamond shape, whose height is h - 2, and width is h / 2. - ctx.beginPath(); - ctx.moveTo(x - h / 4, y + h / 2); - ctx.lineTo(x, y + 1.5); - ctx.lineTo(x + h / 4, y + h / 2); - ctx.lineTo(x, y + h - 1.5); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } - drawMarkers( ctx: CanvasRenderingContext2D, hoveredItem: MarkerIndex | null, @@ -893,8 +872,15 @@ class MarkerChartCanvasImpl extends React.PureComponent { onSelectItem = (hoveredItems: HoveredMarkerChartItems | null) => { const markerIndex = hoveredItems === null ? null : hoveredItems.markerIndex; - const { changeSelectedMarker, threadsKey } = this.props; + const { changeSelectedMarker, activateFlowsForMarker, threadsKey } = + this.props; changeSelectedMarker(threadsKey, markerIndex, { source: 'pointer' }); + console.log({ threadsKey, markerIndex }); + if (typeof threadsKey === 'number' && markerIndex !== null) { + console.log('hello'); + const what = activateFlowsForMarker(threadsKey, markerIndex); + console.log({ what }); + } }; onRightClickMarker = (hoveredItems: HoveredMarkerChartItems | null) => { diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 17abbb99cf..f6779774c8 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -25,6 +25,7 @@ import { changeRightClickedMarker, changeMouseTimePosition, changeSelectedMarker, + activateFlowsForMarker, } from 'firefox-profiler/actions/profile-view'; import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; @@ -51,6 +52,7 @@ type DispatchProps = {| +changeRightClickedMarker: typeof changeRightClickedMarker, +changeMouseTimePosition: typeof changeMouseTimePosition, +changeSelectedMarker: typeof changeSelectedMarker, + +activateFlowsForMarker: typeof activateFlowsForMarker, |}; type StateProps = {| @@ -117,6 +119,7 @@ class MarkerChartImpl extends React.PureComponent { updatePreviewSelection, changeMouseTimePosition, changeRightClickedMarker, + activateFlowsForMarker, rightClickedMarkerIndex, selectedMarkerIndex, changeSelectedMarker, @@ -129,12 +132,7 @@ class MarkerChartImpl extends React.PureComponent { const maxViewportHeight = maxMarkerRows * ROW_HEIGHT; return ( -
+
{maxMarkerRows === 0 ? ( @@ -166,6 +164,8 @@ class MarkerChartImpl extends React.PureComponent { updatePreviewSelection, changeMouseTimePosition, changeRightClickedMarker, + // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. + activateFlowsForMarker, rangeStart: timeRange.start, rangeEnd: timeRange.end, rowHeight: ROW_HEIGHT, @@ -220,6 +220,7 @@ export const MarkerChart = explicitConnect<{||}, StateProps, DispatchProps>({ changeMouseTimePosition, changeRightClickedMarker, changeSelectedMarker, + activateFlowsForMarker, }, component: MarkerChartImpl, }); diff --git a/src/profile-logic/marker-data.js b/src/profile-logic/marker-data.js index 4647e0b97e..ebbfc7a4b8 100644 --- a/src/profile-logic/marker-data.js +++ b/src/profile-logic/marker-data.js @@ -45,6 +45,17 @@ import type { MarkerDisplayLocation, Tid, Milliseconds, + ThreadIndex, + FlowMarker, + FlowSchemasByName, + ProfileFlowInfo, + FlowTiming, + IndexIntoFlowTable, + ConnectedFlowInfo, + FlowTimingRow, + FlowTimingRowType, + FlowTimingRowMarkerTable, + FlowTimingArrow, } from 'firefox-profiler/types'; import type { UniqueStringArray } from '../utils/unique-string-array'; @@ -1641,58 +1652,6 @@ export const stringsToMarkerRegExps = ( }; }; -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[] = []; @@ -1732,18 +1691,6 @@ class MinHeap { } } -export type FlowFieldDescriptor = {| - key: string, - isTerminating: boolean, -|}; - -export type FlowSchema = {| - flowFields: FlowFieldDescriptor[], - isStackBased: boolean, -|}; - -export type FlowSchemasByName = Map; - export function computeFlowSchemasByName( markerSchemas: MarkerSchema[] ): FlowSchemasByName { @@ -1798,11 +1745,12 @@ export function computeFlowMarkers( continue; } - const time = marker.start; + const startTime = marker.start; + const endTime = marker.end; while ( currentContextEndTimes.length !== 0 && - currentContextEndTimes[currentContextEndTimes.length - 1] < time + currentContextEndTimes[currentContextEndTimes.length - 1] < startTime ) { currentContextEndTimes.pop(); currentContextFlowMarkers.pop(); @@ -1829,7 +1777,8 @@ export function computeFlowMarkers( parentContextFlowMarker, childContextFlowMarkers: [], flowIDs, - time, + startTime, + endTime, markerIndex, }); if (flowSchema.isStackBased || marker.end === null) { @@ -1847,14 +1796,6 @@ export function computeFlowMarkers( return flowMarkers; } -export type ProfileFlowInfo = {| - flowTable: Flow[], - flowsByID: Map, - flowMarkersPerThread: FlowMarker[][], - flowMarkerFlowsPerThread: IndexIntoFlowTable[][][], - flowSchemasByName: FlowSchemasByName, -|}; - export function computeProfileFlowInfo( fullMarkerListPerThread: Marker[][], threads: Thread[], @@ -1874,7 +1815,7 @@ export function computeProfileFlowInfo( for (let threadIndex = 0; threadIndex < threadCount; threadIndex++) { const flowMarkers = flowMarkersPerThread[threadIndex]; if (flowMarkers.length !== 0) { - nextEntryHeap.insert(flowMarkers[0].time, { + nextEntryHeap.insert(flowMarkers[0].startTime, { threadIndex, nextIndex: 0, }); @@ -1897,9 +1838,10 @@ export function computeProfileFlowInfo( const { threadIndex, nextIndex } = nextEntry; const flowMarkerIndex = nextIndex; const flowMarkers = flowMarkersPerThread[threadIndex]; - const flowReference = flowMarkers[nextIndex]; + const flowMarker = flowMarkers[nextIndex]; - const { flowIDs, time } = flowReference; + const { markerIndex, flowIDs } = flowMarker; + const { start, end } = fullMarkerListPerThread[threadIndex][markerIndex]; const flowMarkerHandle = { threadIndex, flowMarkerIndex }; const flowsForThisFlowMarker = []; for (const { flowID, isTerminating } of flowIDs) { @@ -1908,8 +1850,8 @@ export function computeProfileFlowInfo( flowIndex = flowTable.length; flowTable.push({ id: flowID, - startTime: time, - endTime: time, + startTime: start, + endTime: end ?? start, flowMarkers: [flowMarkerHandle], }); if (!isTerminating) { @@ -1924,7 +1866,7 @@ export function computeProfileFlowInfo( } else { const flow = flowTable[flowIndex]; flow.flowMarkers.push(flowMarkerHandle); - flow.endTime = time; + flow.endTime = end ?? start; if (isTerminating) { currentActiveFlows.delete(flowID); } @@ -1938,7 +1880,7 @@ export function computeProfileFlowInfo( const newNextIndex = nextIndex + 1; if (newNextIndex < flowMarkers.length) { nextEntry.nextIndex = newNextIndex; - nextEntryHeap.reorder(handle, flowMarkers[newNextIndex].time); + nextEntryHeap.reorder(handle, flowMarkers[newNextIndex].startTime); } else { nextEntryHeap.delete(handle); } @@ -2029,11 +1971,11 @@ export function lookupFlow( return candidateFlows[index]; } -export function computeMarkerFlowsForConsole( +export function computeMarkerFlows( threadIndex: number, markerIndex: MarkerIndex, profileFlowInfo: ProfileFlowInfo, - threads: Thread[], + stringTablePerThread: UniqueStringArray[], fullMarkerListPerThread: Marker[][] ): IndexIntoFlowTable[] | null { const marker = fullMarkerListPerThread[threadIndex][markerIndex]; @@ -2050,7 +1992,7 @@ export function computeMarkerFlowsForConsole( return null; } - const stringTable = threads[threadIndex].stringTable; + const stringTable = stringTablePerThread[threadIndex]; const flowIndexes = []; for (const { key } of flowSchema.flowFields) { @@ -2099,13 +2041,14 @@ export function printMarkerFlows( markerIndex: MarkerIndex, profileFlowInfo: ProfileFlowInfo, threads: Thread[], + stringTablePerThread: UniqueStringArray[], fullMarkerListPerThread: Marker[][] ) { - const markerFlows = computeMarkerFlowsForConsole( + const markerFlows = computeMarkerFlows( markerThreadIndex, markerIndex, profileFlowInfo, - threads, + stringTablePerThread, fullMarkerListPerThread ); if (markerFlows === null) { @@ -2150,7 +2093,7 @@ export function printFlow( 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 ${otherMarkerIndex} (thread index: ${threadIndex}) at time ${flowMarker.startTime} on thread ${thread.name} (pid: ${thread.pid}, tid: ${thread.tid}):`, marker ); const directlyConnectedFlows = flowMarkerFlowsPerThread[threadIndex][ @@ -2207,3 +2150,236 @@ export function printFlow( // ); // } } + +export function computeFlowTiming( + profileFlowInfo: ProfileFlowInfo, + activeFlows: IndexIntoFlowTable[] +): FlowTiming { + let incomingContextFlows = []; + let directlyConnectedFlows = []; + let outgoingContextFlows = []; + + for (const flow of activeFlows) { + const connectedFlows = getConnectedFlowInfo(flow, profileFlowInfo); + incomingContextFlows.push(...connectedFlows.incomingContextFlows); + directlyConnectedFlows.push(...connectedFlows.directlyConnectedFlows); + outgoingContextFlows.push(...connectedFlows.outgoingContextFlows); + } + sortAndDedup(incomingContextFlows); + sortAndDedup(directlyConnectedFlows); + sortAndDedup(outgoingContextFlows); + + directlyConnectedFlows = directlyConnectedFlows.filter( + (icf) => activeFlows.indexOf(icf) === -1 + ); + incomingContextFlows = incomingContextFlows.filter( + (icf) => + activeFlows.indexOf(icf) === -1 && + directlyConnectedFlows.indexOf(icf) === -1 + ); + outgoingContextFlows = outgoingContextFlows.filter( + (icf) => + activeFlows.indexOf(icf) === -1 && + directlyConnectedFlows.indexOf(icf) === -1 && + incomingContextFlows.indexOf(icf) === -1 + ); + + const rawRows: Array<[FlowTimingRowType, IndexIntoFlowTable]> = [ + ...incomingContextFlows.map((flowIndex) => ['INCOMING_CONTEXT', flowIndex]), + ...activeFlows.map((flowIndex) => ['ACTIVE', flowIndex]), + ...directlyConnectedFlows.map((flowIndex) => [ + 'DIRECTLY_CONNECTED', + flowIndex, + ]), + ...outgoingContextFlows.map((flowIndex) => ['OUTGOING_CONTEXT', flowIndex]), + ]; + + const rows: FlowTimingRow[] = []; + + const { flowTable, flowMarkersPerThread } = profileFlowInfo; + + const flowIndexToRowIndex = new Map(); + + for (let rowIndex = 0; rowIndex < rawRows.length; rowIndex++) { + const [rowType, flowIndex] = rawRows[rowIndex]; + flowIndexToRowIndex.set(flowIndex, rowIndex); + + const flow = flowTable[flowIndex]; + const { flowMarkers, startTime, endTime } = flow; + const flowMarkerCount = flowMarkers.length; + + const markers: FlowTimingRowMarkerTable = { + length: flowMarkerCount, + threadIndex: new Int32Array(flowMarkerCount), + markerIndex: new Int32Array(flowMarkerCount), + flowMarkerIndex: new Int32Array(flowMarkerCount), + startTime: new Float64Array(flowMarkerCount), + endTime: new Float64Array(flowMarkerCount), + isInstant: new Uint8Array(flowMarkerCount), + }; + + for (let i = 0; i < flowMarkerCount; i++) { + const { threadIndex, flowMarkerIndex } = flowMarkers[i]; + const flowMarker = flowMarkersPerThread[threadIndex][flowMarkerIndex]; + const { markerIndex, startTime, endTime } = flowMarker; + markers.threadIndex[i] = threadIndex; + markers.markerIndex[i] = markerIndex; + markers.flowMarkerIndex[i] = flowMarkerIndex; + markers.startTime[i] = startTime; + if (endTime === null) { + markers.endTime[i] = startTime; + markers.isInstant[i] = 1; + } else { + markers.endTime[i] = endTime; + } + } + + rows.push({ + label: `Flow ${flowIndex}`, + rowType, + flowIndex, + flowStart: startTime, + flowEnd: ensureExists(endTime), + markers, + }); + } + + return { rows, flowIndexToRowIndex, profileFlowInfo }; +} + +function _arrowForMarker( + flowMarkerIndex: number, + flowIndexToRowIndex: Map, + flowMarkers: FlowMarker[], + flowMarkerFlows: IndexIntoFlowTable[][] +): FlowTimingArrow | null { + const { startTime, parentContextFlowMarker } = flowMarkers[flowMarkerIndex]; + + const rowIndexesFrom = []; + const rowIndexesTo = []; + let minRowIndex = -1; + let maxRowIndex = -1; + if (parentContextFlowMarker !== null) { + for (const flowIndex of flowMarkerFlows[parentContextFlowMarker]) { + const rowIndex = flowIndexToRowIndex.get(flowIndex); + if (rowIndex !== undefined) { + rowIndexesFrom.push(rowIndex); + if (minRowIndex === -1) { + minRowIndex = rowIndex; + maxRowIndex = rowIndex; + } else { + if (rowIndex < minRowIndex) { + minRowIndex = rowIndex; + } + if (rowIndex > maxRowIndex) { + maxRowIndex = rowIndex; + } + } + } + } + } + for (const flowIndex of flowMarkerFlows[flowMarkerIndex]) { + const rowIndex = flowIndexToRowIndex.get(flowIndex); + if (rowIndex !== undefined) { + rowIndexesTo.push(rowIndex); + if (minRowIndex === -1) { + minRowIndex = rowIndex; + maxRowIndex = rowIndex; + } else { + if (rowIndex < minRowIndex) { + minRowIndex = rowIndex; + } + if (rowIndex > maxRowIndex) { + maxRowIndex = rowIndex; + } + } + } + } + if (minRowIndex === maxRowIndex) { + return null; + } + return { + time: startTime, + rowIndexesFrom, + rowIndexesTo, + minRowIndex, + maxRowIndex, + }; +} + +export function computeArrowsRelatedToMarker( + threadIndex: ThreadIndex, + flowMarkerIndex: number, + flowTiming: FlowTiming + // flowIndexToRowIndex: Map, + // profileFlowInfo: ProfileFlowInfo +): FlowTimingArrow[] { + // We have marker-shared-between-flows "arrows" (or actually connections) + // And we have marker-has-this-parent arrows. + + // For shared-between-flows marker, as we iterate over a flow's flow markers, we find (threadIndex, flowMarkerIndex) pairs. + // From the flow marekr, we can get its "flow marker flows", which has the flow indexes for all the flows it is part of. + // In this function, we make it so that we add the arrows for the shared flows only when we encounter the marker in the first flow of the set. + // We also only do it for the activeFlows and not for the context flows. For the context flows we might not have all the directly-connected flows present in the rows, specifically we may not have the flow with the minimum index. + + const { flowIndexToRowIndex, profileFlowInfo } = flowTiming; + const { flowMarkersPerThread, flowMarkerFlowsPerThread } = profileFlowInfo; + const flowMarkers = flowMarkersPerThread[threadIndex]; + const flowMarkerFlows = flowMarkerFlowsPerThread[threadIndex]; + + const arrows: FlowTimingArrow[] = []; + + const flowMarker = flowMarkersPerThread[threadIndex][flowMarkerIndex]; + let incomingFlowMarkerIndex = flowMarkerIndex; + let incomingFlowMarker = flowMarker; + while (true) { + const { parentContextFlowMarker } = incomingFlowMarker; + const arrow = _arrowForMarker( + incomingFlowMarkerIndex, + flowIndexToRowIndex, + flowMarkers, + flowMarkerFlows + ); + if (arrow === null) { + break; + } + arrows.push(arrow); + if (parentContextFlowMarker === null) { + break; + } + incomingFlowMarkerIndex = parentContextFlowMarker; + incomingFlowMarker = flowMarkers[incomingFlowMarkerIndex]; + } + + const stack = [ + { + childMarkers: flowMarker.childContextFlowMarkers, + currentIndex: 0, + }, + ]; + while (stack.length > 0) { + const stackTop = stack[stack.length - 1]; + const { currentIndex, childMarkers } = stackTop; + if (currentIndex >= childMarkers.length) { + stack.pop(); + continue; + } + const currentFlowMarkerIndex = childMarkers[currentIndex]; + stackTop.currentIndex = currentIndex + 1; + const arrow = _arrowForMarker( + currentFlowMarkerIndex, + flowIndexToRowIndex, + flowMarkers, + flowMarkerFlows + ); + if (arrow !== null) { + arrows.push(arrow); + const children = + flowMarkers[currentFlowMarkerIndex].childContextFlowMarkers; + if (children.length !== 0) { + stack.push({ childMarkers: children, currentIndex: 0 }); + } + } + } + return arrows; +} diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 1d66a4a006..7ca8f81358 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -22,6 +22,7 @@ import type { Reducer, TimelineTrackOrganization, SourceViewState, + IndexIntoFlowTable, AssemblyViewState, IsOpenPerPanelState, TabID, @@ -658,6 +659,17 @@ const isBottomBoxOpenPerPanel: Reducer = ( } }; +const activeFlows: Reducer = (state = [], action) => { + switch (action.type) { + case 'CHANGE_ACTIVE_FLOWS': { + const { activeFlows } = action; + return activeFlows; + } + default: + return state; + } +}; + /** * Active tab specific profile url states */ @@ -723,6 +735,7 @@ const profileSpecific = combineReducers({ transforms, sourceView, assemblyView, + activeFlows, isBottomBoxOpenPerPanel, timelineType, full: fullProfileSpecific, diff --git a/src/selectors/flow.js b/src/selectors/flow.js index f72273679f..34d9a9af86 100644 --- a/src/selectors/flow.js +++ b/src/selectors/flow.js @@ -5,13 +5,24 @@ // @flow import { createSelector } from 'reselect'; -import { computeProfileFlowInfo } from '../profile-logic/marker-data'; -import type { ProfileFlowInfo } from '../profile-logic/marker-data'; +import { + computeProfileFlowInfo, + computeFlowTiming, +} from '../profile-logic/marker-data'; import { getThreadSelectors } from './per-thread'; +import { getActiveFlows } from './url-state'; import type { ThreadSelectors } from './per-thread'; import { getThreads, getMarkerSchema } from './profile'; -import type { Selector, State, Marker } from 'firefox-profiler/types'; +import type { + Selector, + State, + MarkerIndex, + Marker, + ProfileFlowInfo, + FlowTiming, +} from 'firefox-profiler/types'; +import { UniqueStringArray } from 'firefox-profiler/utils/unique-string-array'; function _arraysShallowEqual(arr1: any[], arr2: any[]): boolean { return arr1.length === arr2.length && arr1.every((val, i) => val === arr2[i]); @@ -40,9 +51,26 @@ export const getFullMarkerListPerThread: Selector = getFullMarkerList(state) ); +export const getMarkerChartLabelGetterPerThread: Selector< + Array<(MarkerIndex) => string>, +> = _createSelectorForAllThreads(({ getMarkerChartLabelGetter }, state) => + getMarkerChartLabelGetter(state) +); + +export const getStringTablePerThread: Selector = + _createSelectorForAllThreads(({ getStringTable }, state) => + getStringTable(state) + ); + export const getProfileFlowInfo: Selector = createSelector( getFullMarkerListPerThread, getThreads, getMarkerSchema, computeProfileFlowInfo ); + +export const getFlowTiming: Selector = createSelector( + getProfileFlowInfo, + getActiveFlows, + computeFlowTiming +); diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index 392b271ef3..2d805f6be1 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -35,6 +35,7 @@ import type { ActiveTabSpecificProfileUrlState, NativeSymbolInfo, TabID, + IndexIntoFlowTable, } from 'firefox-profiler/types'; import type { TabSlug } from '../app-logic/tabs-handling'; @@ -112,6 +113,8 @@ export const getNetworkSearchString: Selector = (state) => getProfileSpecificState(state).networkSearchString; export const getSelectedTab: Selector = (state) => getUrlState(state).selectedTab; +export const getActiveFlows: Selector = (state) => + getProfileSpecificState(state).activeFlows; export const getInvertCallstack: Selector = (state) => getSelectedTab(state) === 'calltree' && getProfileSpecificState(state).invertCallstack; diff --git a/src/types/actions.js b/src/types/actions.js index 99ca82a98f..3ef5c50c74 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -26,6 +26,7 @@ import type { ActiveTabTimeline, ThreadsKey, NativeSymbolInfo, + IndexIntoFlowTable, } from './profile-derived'; import type { FuncToFuncsMap } from '../profile-logic/symbolication'; import type { TemporaryError } from '../utils/errors'; @@ -248,6 +249,10 @@ type ProfileAction = +threadsKey: ThreadsKey, +markerIndex: MarkerIndex | null, |} + | {| + +type: 'CHANGE_ACTIVE_FLOWS', + +activeFlows: IndexIntoFlowTable[], + |} | {| +type: 'UPDATE_PREVIEW_SELECTION', +previewSelection: PreviewSelection, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index c06774e777..3ca022b737 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -391,6 +391,120 @@ export type ThreadWithReservedFunctions = {| >, |}; +export type GlobalFlowMarkerHandle = {| + threadIndex: number, + flowMarkerIndex: number, +|}; + +// An index into the global flow table. +export 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, + startTime: Milliseconds, + endTime: Milliseconds | null, + // 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[], +|}; + +export type FlowFieldDescriptor = {| + key: string, + isTerminating: boolean, +|}; + +export type FlowSchema = {| + flowFields: FlowFieldDescriptor[], + isStackBased: boolean, +|}; + +export type FlowSchemasByName = Map; + +export type ProfileFlowInfo = {| + flowTable: Flow[], + flowsByID: Map, + flowMarkersPerThread: FlowMarker[][], + // For each (threadIndex, flowMarkerIndex), the indexes of the flows (in + // ascending order) this marker is part of. + flowMarkerFlowsPerThread: IndexIntoFlowTable[][][], + flowSchemasByName: FlowSchemasByName, +|}; + +export type FlowTiming = {| + rows: FlowTimingRow[], + flowIndexToRowIndex: Map, + profileFlowInfo: ProfileFlowInfo, +|}; + +export type FlowTimingRowType = + | 'INCOMING_CONTEXT' + | 'ACTIVE' + | 'DIRECTLY_CONNECTED' + | 'OUTGOING_CONTEXT'; + +export type FlowTimingRow = {| + rowType: FlowTimingRowType, + label: string, + flowIndex: IndexIntoFlowTable, + flowStart: Milliseconds, + flowEnd: Milliseconds, + markers: FlowTimingRowMarkerTable, +|}; + +export type FlowTimingRowMarkerTable = {| + length: number, + threadIndex: Int32Array, // ThreadIndex[], + markerIndex: Int32Array, // MarkerIndex[], + flowMarkerIndex: Int32Array, // FlowMarkerIndex[], + startTime: Float64Array, // Milliseconds[], + endTime: Float64Array, // Milliseconds[], + isInstant: Uint8Array, // boolean[], +|}; + +export type FlowTimingArrow = {| + time: Milliseconds, + rowIndexesFrom: number[], + rowIndexesTo: number[], + minRowIndex: number, + maxRowIndex: number, +|}; + /** * The marker timing contains the necessary information to draw markers very quickly * in the marker chart. It represents a single row of markers in the chart. diff --git a/src/types/state.js b/src/types/state.js index 332a5459ac..47b0b16a36 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -33,6 +33,7 @@ import type { LocalTrack, TrackIndex, MarkerIndex, + IndexIntoFlowTable, ActiveTabTimeline, OriginsTimeline, ThreadsKey, @@ -385,6 +386,7 @@ export type ProfileSpecificUrlState = {| isBottomBoxOpenPerPanel: IsOpenPerPanelState, full: FullProfileSpecificUrlState, activeTab: ActiveTabSpecificProfileUrlState, + activeFlows: IndexIntoFlowTable[], |}; /** diff --git a/src/utils/window-console.js b/src/utils/window-console.js index 3f057d3938..cf25c416e4 100644 --- a/src/utils/window-console.js +++ b/src/utils/window-console.js @@ -80,6 +80,8 @@ export function addDataToWindowObject( const profileFlowInfo = selectorsForConsole.flow.getProfileFlowInfo(getState()); const threads = selectorsForConsole.profile.getThreads(getState()); + const stringTablePerThread = + selectorsForConsole.flow.getStringTablePerThread(getState()); const fullMarkerListPerThread = selectorsForConsole.flow.getFullMarkerListPerThread(getState()); if (markerIndex === null) { @@ -90,6 +92,7 @@ export function addDataToWindowObject( markerIndex, profileFlowInfo, threads, + stringTablePerThread, fullMarkerListPerThread ); }