Skip to content
Open
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
39 changes: 39 additions & 0 deletions src/components/stack-chart/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type OwnProps = {
readonly displayStackType: boolean;
readonly useStackChartSameWidths: boolean;
readonly timelineUnit: TimelineUnit;
readonly searchStringsRegExp: RegExp | null;
};

type Props = Readonly<
Expand Down Expand Up @@ -183,6 +184,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
getMarker,
marginLeft,
useStackChartSameWidths,
searchStringsRegExp,
viewport: {
containerWidth,
containerHeight,
Expand Down Expand Up @@ -359,6 +361,26 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {

const callNodeTable = callNodeInfo.getCallNodeTable();

// Pre-compute which call nodes match the search string so we can dim
// non-matching nodes when a search is active.
let searchMatchedCallNodes: Set<IndexIntoCallNodeTable> | null = null;
if (searchStringsRegExp) {
searchMatchedCallNodes = new Set();
for (
let callNodeIndex = 0;
callNodeIndex < callNodeTable.length;
callNodeIndex++
) {
const funcIndex = callNodeTable.func[callNodeIndex];
const funcNameIndex = thread.funcTable.name[funcIndex];
const funcName = thread.stringTable.getString(funcNameIndex);
searchStringsRegExp.lastIndex = 0;
if (searchStringsRegExp.test(funcName)) {
searchMatchedCallNodes.add(callNodeIndex);
}
}
}

// Only draw the stack frames that are vertically within view.
for (let depth = startDepth; depth < endDepth; depth++) {
// Get the timing information for a row of stack frames.
Expand Down Expand Up @@ -480,8 +502,10 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {

// Look up information about this stack frame.
let text, category, isSelected;
let currentCallNodeIndex: IndexIntoCallNodeTable | null = null;
if ('callNode' in stackTiming && stackTiming.callNode) {
const callNodeIndex = stackTiming.callNode[i];
currentCallNodeIndex = callNodeIndex;
const funcIndex = callNodeTable.func[callNodeIndex];
const funcNameIndex = thread.funcTable.name[funcIndex];
text = thread.stringTable.getString(funcNameIndex);
Expand Down Expand Up @@ -510,6 +534,16 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
const colorStyles = mapCategoryColorNameToStackChartStyles(
category.color
);

// When a search is active, dim boxes that do not match.
const isDimmed =
searchMatchedCallNodes !== null &&
currentCallNodeIndex !== null &&
!searchMatchedCallNodes.has(currentCallNodeIndex);
if (isDimmed) {
ctx.globalAlpha = 0.5;
}

// Draw the box.
fastFillStyle.set(
isHovered || isSelected
Expand Down Expand Up @@ -550,6 +584,11 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
}
}

// Reset dimming after drawing.
if (isDimmed) {
ctx.globalAlpha = 1;
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/components/stack-chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
getStackChartSameWidths,
getShowUserTimings,
getSelectedThreadsKey,
getSearchStringsAsRegExp,
} from 'firefox-profiler/selectors/url-state';
import type { SameWidthsIndexToTimestampMap } from 'firefox-profiler/profile-logic/stack-timing';
import { selectedThreadSelectors } from '../../selectors/per-thread';
Expand Down Expand Up @@ -87,6 +88,7 @@ type StateProps = {
readonly hasFilteredCtssSamples: boolean;
readonly useStackChartSameWidths: boolean;
readonly timelineUnit: TimelineUnit;
readonly searchStringsRegExp: RegExp | null;
};

type DispatchProps = {
Expand Down Expand Up @@ -244,6 +246,7 @@ class StackChartImpl extends React.PureComponent<Props> {
hasFilteredCtssSamples,
useStackChartSameWidths,
timelineUnit,
searchStringsRegExp,
} = 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,
searchStringsRegExp,
}}
/>
</div>
Expand Down Expand Up @@ -347,6 +351,7 @@ export const StackChart = explicitConnect<{}, StateProps, DispatchProps>({
selectedThreadSelectors.getHasFilteredCtssSamples(state),
useStackChartSameWidths: getStackChartSameWidths(state),
timelineUnit: getProfileTimelineUnit(state),
searchStringsRegExp: getSearchStringsAsRegExp(state),
};
},
mapDispatchToProps: {
Expand Down
51 changes: 51 additions & 0 deletions src/test/components/StackChart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
changeImplementationFilter,
changeCallTreeSummaryStrategy,
updatePreviewSelection,
changeCallTreeSearchString,
} from '../../actions/profile-view';
import { changeSelectedTab } from '../../actions/app';
import { selectedThreadSelectors } from '../../selectors/per-thread';
Expand Down Expand Up @@ -271,6 +272,56 @@ describe('StackChart', function () {
expect(drawnFrames).not.toContain('Z');
});

it('dims non-matching boxes when searching', function () {
const { dispatch, flushRafCalls } = setupSamples();
flushDrawLog();

// Dispatch a search string that matches some function names.
act(() => {
dispatch(changeCallTreeSearchString('B'));
});
flushRafCalls();

const drawCalls = flushDrawLog();

// Non-matching boxes should be drawn with reduced globalAlpha.
const dimCalls = drawCalls.filter(
([fn, value]) => fn === 'set globalAlpha' && value === 0.5
);
expect(dimCalls.length).toBeGreaterThan(0);
});

it('does not dim boxes that match the search string', function () {
// Use a single-node call stack so there is exactly one box.
const { dispatch, flushRafCalls } = setupSamples(`
A[cat:DOM]
`);
flushDrawLog();

// Search for "A" — the only node matches, so nothing should be dimmed.
act(() => {
dispatch(changeCallTreeSearchString('A'));
});
flushRafCalls();

const drawCalls = flushDrawLog();
const dimCalls = drawCalls.filter(
([fn, value]) => fn === 'set globalAlpha' && value < 1
);
expect(dimCalls).toHaveLength(0);
});

it('does not dim any boxes when there is no search string', function () {
setupSamples();
const drawCalls = flushDrawLog();

// No dimming should be applied without a search.
const dimCalls = drawCalls.filter(
([fn, value]) => fn === 'set globalAlpha' && value < 1
);
expect(dimCalls).toHaveLength(0);
});

describe('EmptyReasons', () => {
it('shows reasons when a profile has no samples', () => {
const profile = getEmptyProfile();
Expand Down
4 changes: 3 additions & 1 deletion src/test/fixtures/mocks/canvas-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type SetFillStyleOperation = ['set fillStyle', string];
export type FillRectOperation = ['fillRect', number, number, number, number];
export type ClearRectOperation = ['clearRect', number, number, number, number];
export type FillTextOperation = ['fillText', string];
export type SetGlobalAlphaOperation = ['set globalAlpha', number];

export type DrawOperation =
| BeginPathOperation
Expand All @@ -44,7 +45,8 @@ export type DrawOperation =
| SetFillStyleOperation
| FillRectOperation
| ClearRectOperation
| FillTextOperation;
| FillTextOperation
| SetGlobalAlphaOperation;

export function flushDrawLog(): DrawOperation[] {
return (window as any).__flushDrawLog();
Expand Down
Loading