Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/components/stack-chart/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { changeMouseTimePosition } from '../../actions/profile-view';
type ChangeMouseTimePosition = typeof changeMouseTimePosition;
import {
mapCategoryColorNameToStackChartStyles,
getDimmedStyles,
getForegroundColor,
getBackgroundColor,
} from '../../utils/colors';
Expand All @@ -40,6 +41,8 @@ import type {
Page,
TimelineUnit,
} from 'firefox-profiler/types';
import type { BitSet } from 'firefox-profiler/utils/bitset';
import { checkBit } from 'firefox-profiler/utils/bitset';
import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info';

import type {
Expand Down Expand Up @@ -78,6 +81,7 @@ type OwnProps = {
readonly displayStackType: boolean;
readonly useStackChartSameWidths: boolean;
readonly timelineUnit: TimelineUnit;
readonly searchFilteredFuncMatchesBitSet: BitSet | null;
};

type Props = Readonly<
Expand Down Expand Up @@ -183,6 +187,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
getMarker,
marginLeft,
useStackChartSameWidths,
searchFilteredFuncMatchesBitSet,
viewport: {
containerWidth,
containerHeight,
Expand Down Expand Up @@ -480,9 +485,11 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {

// Look up information about this stack frame.
let text, category, isSelected;
let currentFuncIndex: number | null = null;
if ('callNode' in stackTiming && stackTiming.callNode) {
const callNodeIndex = stackTiming.callNode[i];
const funcIndex = callNodeTable.func[callNodeIndex];
currentFuncIndex = funcIndex;
const funcNameIndex = thread.funcTable.name[funcIndex];
text = thread.stringTable.getString(funcNameIndex);
const categoryIndex = callNodeTable.category[callNodeIndex];
Expand All @@ -507,9 +514,19 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
depth === hoveredItem.depth &&
i === hoveredItem.stackTimingIndex;

const colorStyles = mapCategoryColorNameToStackChartStyles(
category.color
);
// When a search is active, use the dimmed style for non-matching nodes
// so that matching nodes stand out with their category color.
// Hovered or selected nodes always use their real category color.
const isDimmed =
searchFilteredFuncMatchesBitSet !== null &&
currentFuncIndex !== null &&
!checkBit(searchFilteredFuncMatchesBitSet, currentFuncIndex) &&
!isHovered &&
!isSelected;
const colorStyles = isDimmed
? getDimmedStyles()
: mapCategoryColorNameToStackChartStyles(category.color);

// Draw the box.
fastFillStyle.set(
isHovered || isSelected
Expand Down Expand Up @@ -542,11 +559,11 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
if (textW > textMeasurement.minWidth) {
const fittedText = textMeasurement.getFittedText(text, textW);
if (fittedText) {
fastFillStyle.set(
isHovered || isSelected
? colorStyles.getSelectedTextColor()
: getForegroundColor()
);
if (isHovered || isSelected || isDimmed) {
fastFillStyle.set(colorStyles.getSelectedTextColor());
} else {
fastFillStyle.set(getForegroundColor());
}
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/components/stack-chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type {
Page,
TimelineUnit,
} from 'firefox-profiler/types';
import type { BitSet } from 'firefox-profiler/utils/bitset';
import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info';

import type { ConnectedProps } from '../../utils/connect';
Expand Down Expand Up @@ -87,6 +88,7 @@ type StateProps = {
readonly hasFilteredCtssSamples: boolean;
readonly useStackChartSameWidths: boolean;
readonly timelineUnit: TimelineUnit;
readonly searchFilteredFuncMatchesBitSet: BitSet | null;
};

type DispatchProps = {
Expand Down Expand Up @@ -244,6 +246,7 @@ class StackChartImpl extends React.PureComponent<Props> {
hasFilteredCtssSamples,
useStackChartSameWidths,
timelineUnit,
searchFilteredFuncMatchesBitSet,
} = this.props;

const maxViewportHeight = combinedTimingRows.length * STACK_FRAME_HEIGHT;
Expand Down Expand Up @@ -304,6 +307,7 @@ class StackChartImpl extends React.PureComponent<Props> {
displayStackType: displayStackType,
useStackChartSameWidths,
timelineUnit,
searchFilteredFuncMatchesBitSet,
}}
/>
</div>
Expand Down Expand Up @@ -347,6 +351,8 @@ export const StackChart = explicitConnect<{}, StateProps, DispatchProps>({
selectedThreadSelectors.getHasFilteredCtssSamples(state),
useStackChartSameWidths: getStackChartSameWidths(state),
timelineUnit: getProfileTimelineUnit(state),
searchFilteredFuncMatchesBitSet:
selectedThreadSelectors.getSearchFilteredFuncMatchesBitSet(state),
};
},
mapDispatchToProps: {
Expand Down
111 changes: 75 additions & 36 deletions src/profile-logic/profile-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type BitSet,
checkBit,
combineTwoBitSetsWithAnd,
combineTwoBitSetsWithOr,
makeBitSet,
setBit,
} from 'firefox-profiler/utils/bitset';
Expand Down Expand Up @@ -1720,59 +1721,90 @@ export function applyTransformOutputToThread(
});
}

export function computeTransformOutputForSearchStringFilter(
/**
* Output of the search string filter: both the filter's TransformOutput (used
* to drop non-matching stacks) and the combined func bitset (used to highlight
* matching nodes in the stack chart). Exposed together so both are computed
* once and memoized together.
*
* Stacks are AND-combined across search strings (a stack is kept only if it
* contains a match for every search string), while funcs are OR-combined (a
* func is highlighted if it matches any of the search strings).
*/
export type SearchStringFilterOutput = {
transformOutput: TransformOutput;
funcMatchesSearchStrings: BitSet | null;
};

export function computeSearchStringFilterOutput(
stackTable: StackTable,
frameTable: FrameTable,
funcTable: FuncTable,
resourceTable: ResourceTable,
sources: SourceTable,
stringTable: StringTable,
searchStrings: string[] | null
): TransformOutput {
return timeCode('computeTransformOutputForSearchStringFilter', () => {
if (!searchStrings) {
return { newStackTable: stackTable, effectOnThreadData: {} };
): SearchStringFilterOutput {
return timeCode('computeSearchStringFilterOutput', () => {
if (!searchStrings || searchStrings.length === 0) {
return {
transformOutput: { newStackTable: stackTable, effectOnThreadData: {} },
funcMatchesSearchStrings: null,
};
}

const stackMatchesAllSearchStrings = searchStrings
.filter((s) => s)
.reduce(
(
stackMatchesPreviousSearchStrings: BitSet | undefined,
searchString: string
) => {
const stackMatchesThisString = _computeStackMatchesSearchString(
stackTable,
frameTable,
funcTable,
resourceTable,
sources,
stringTable,
searchString
);
if (stackMatchesPreviousSearchStrings !== undefined) {
return combineTwoBitSetsWithAnd(
stackMatchesThisString,
stackMatchesPreviousSearchStrings
);
}
return stackMatchesThisString;
},
undefined
const computeMatchesForString = (searchString: string) => {
const funcMatches = computeFuncMatchesSearchString(
funcTable,
resourceTable,
sources,
stringTable,
searchString
);
const stackMatches = _computeStackMatchesFromFuncMatches(
stackTable,
frameTable,
funcMatches
);
return { funcMatches, stackMatches };
};

let {
funcMatches: combinedFuncMatches,
stackMatches: combinedStackMatches,
} = computeMatchesForString(searchStrings[0]);
for (let i = 1; i < searchStrings.length; i++) {
const { funcMatches, stackMatches } = computeMatchesForString(
searchStrings[i]
);
combinedFuncMatches = combineTwoBitSetsWithOr(
funcMatches,
combinedFuncMatches
);
combinedStackMatches = combineTwoBitSetsWithAnd(
stackMatches,
combinedStackMatches
);
}

return {
newStackTable: stackTable,
effectOnThreadData: {
dropIfOldStackIsNot: stackMatchesAllSearchStrings,
transformOutput: {
newStackTable: stackTable,
effectOnThreadData: {
dropIfOldStackIsNot: combinedStackMatches,
},
},
funcMatchesSearchStrings: combinedFuncMatches,
};
});
}

function _computeStackMatchesSearchString(
stackTable: StackTable,
frameTable: FrameTable,
/**
* Compute a BitSet of functions whose name, source filename, or resource name
* matches the given search string. This is used both for filtering stacks and
* for dimming non-matching nodes in the stack chart.
*/
export function computeFuncMatchesSearchString(
funcTable: FuncTable,
resourceTable: ResourceTable,
sources: SourceTable,
Expand Down Expand Up @@ -1815,7 +1847,14 @@ function _computeStackMatchesSearchString(
setBit(funcMatchesSearch, funcIndex);
}
}
return funcMatchesSearch;
}

function _computeStackMatchesFromFuncMatches(
stackTable: StackTable,
frameTable: FrameTable,
funcMatchesSearch: BitSet
): BitSet {
const stackMatchesSearch = makeBitSet(stackTable.length);
for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) {
const prefix = stackTable.prefix[stackIndex];
Expand Down
43 changes: 31 additions & 12 deletions src/selectors/per-thread/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type {
} from 'firefox-profiler/types';

import type { TransformLabeL10nIds } from 'firefox-profiler/profile-logic/transforms';
import type { BitSet } from 'firefox-profiler/utils/bitset';
import type { MarkerSelectorsPerThread } from './markers';

import { mergeThreads } from '../../profile-logic/merge-compare';
Expand All @@ -62,8 +63,8 @@ const globallyMemoizedComputeTransformOutputForImplementationFilter = memoize(
limit: 2,
}
);
const globallyMemoizedComputeTransformOutputForSearchStringFilter = memoize(
ProfileData.computeTransformOutputForSearchStringFilter,
const globallyMemoizedComputeSearchStringFilterOutput = memoize(
ProfileData.computeSearchStringFilterOutput,
{
limit: 2,
}
Expand Down Expand Up @@ -488,25 +489,42 @@ export function getThreadSelectorsWithMarkersPerThread(
}
);

const getFilteredThread: Selector<Thread> = createSelector(
_getImplementationFilteredThread,
UrlState.getSearchStrings,
(thread: Thread, searchStrings) => {
// Apply the search string filter.
const transformOutput =
globallyMemoizedComputeTransformOutputForSearchStringFilter(
/**
* Single memoized computation of the search string filter, shared by
* `getFilteredThread` (which needs the stack-drop bitset) and
* `getSearchFilteredFuncMatchesBitSet` (which needs the func-match bitset).
*/
const _getSearchStringFilterOutput: Selector<ProfileData.SearchStringFilterOutput> =
createSelector(
_getImplementationFilteredThread,
UrlState.getSearchStrings,
(thread: Thread, searchStrings) =>
globallyMemoizedComputeSearchStringFilterOutput(
thread.stackTable,
thread.frameTable,
thread.funcTable,
thread.resourceTable,
thread.sources,
thread.stringTable,
searchStrings
);
return ProfileData.applyTransformOutputToThread(transformOutput, thread);
}
)
);

const getFilteredThread: Selector<Thread> = createSelector(
_getImplementationFilteredThread,
_getSearchStringFilterOutput,
(thread: Thread, { transformOutput }) =>
ProfileData.applyTransformOutputToThread(transformOutput, thread)
);

/**
* Get a BitSet of func indices that match the current search strings.
* Returns null when there is no active search. This is used by the stack
* chart to dim non-matching nodes without recomputing on every draw call.
*/
const getSearchFilteredFuncMatchesBitSet: Selector<BitSet | null> = (state) =>
_getSearchStringFilterOutput(state).funcMatchesSearchStrings;

const getPreviewFilteredThread: Selector<Thread> = createSelector(
getFilteredThread,
ProfileSelectors.getPreviewSelection,
Expand Down Expand Up @@ -613,6 +631,7 @@ export function getThreadSelectorsWithMarkersPerThread(
getTransformStack,
getRangeAndTransformFilteredThread,
getFilteredThread,
getSearchFilteredFuncMatchesBitSet,
getPreviewFilteredThread,
getFilteredCtssSamples,
getPreviewFilteredCtssSamples,
Expand Down
Loading
Loading