Skip to content

Commit c9c8f24

Browse files
committed
Dim non-matching nodes in the stack chart when searching
When a search string is active, reduce the opacity of stack chart nodes whose function name does not match. This makes matched nodes visually stand out while non-matching nodes fade into the background.
1 parent b41e166 commit c9c8f24

4 files changed

Lines changed: 98 additions & 1 deletion

File tree

src/components/stack-chart/Canvas.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type OwnProps = {
7878
readonly displayStackType: boolean;
7979
readonly useStackChartSameWidths: boolean;
8080
readonly timelineUnit: TimelineUnit;
81+
readonly searchStringsRegExp: RegExp | null;
8182
};
8283

8384
type Props = Readonly<
@@ -183,6 +184,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
183184
getMarker,
184185
marginLeft,
185186
useStackChartSameWidths,
187+
searchStringsRegExp,
186188
viewport: {
187189
containerWidth,
188190
containerHeight,
@@ -359,6 +361,26 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
359361

360362
const callNodeTable = callNodeInfo.getCallNodeTable();
361363

364+
// Pre-compute which call nodes match the search string so we can dim
365+
// non-matching nodes when a search is active.
366+
let searchMatchedCallNodes: Set<IndexIntoCallNodeTable> | null = null;
367+
if (searchStringsRegExp) {
368+
searchMatchedCallNodes = new Set();
369+
for (
370+
let callNodeIndex = 0;
371+
callNodeIndex < callNodeTable.length;
372+
callNodeIndex++
373+
) {
374+
const funcIndex = callNodeTable.func[callNodeIndex];
375+
const funcNameIndex = thread.funcTable.name[funcIndex];
376+
const funcName = thread.stringTable.getString(funcNameIndex);
377+
searchStringsRegExp.lastIndex = 0;
378+
if (searchStringsRegExp.test(funcName)) {
379+
searchMatchedCallNodes.add(callNodeIndex);
380+
}
381+
}
382+
}
383+
362384
// Only draw the stack frames that are vertically within view.
363385
for (let depth = startDepth; depth < endDepth; depth++) {
364386
// Get the timing information for a row of stack frames.
@@ -480,8 +502,10 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
480502

481503
// Look up information about this stack frame.
482504
let text, category, isSelected;
505+
let currentCallNodeIndex: IndexIntoCallNodeTable | null = null;
483506
if ('callNode' in stackTiming && stackTiming.callNode) {
484507
const callNodeIndex = stackTiming.callNode[i];
508+
currentCallNodeIndex = callNodeIndex;
485509
const funcIndex = callNodeTable.func[callNodeIndex];
486510
const funcNameIndex = thread.funcTable.name[funcIndex];
487511
text = thread.stringTable.getString(funcNameIndex);
@@ -510,6 +534,16 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
510534
const colorStyles = mapCategoryColorNameToStackChartStyles(
511535
category.color
512536
);
537+
538+
// When a search is active, dim boxes that do not match.
539+
const isDimmed =
540+
searchMatchedCallNodes !== null &&
541+
currentCallNodeIndex !== null &&
542+
!searchMatchedCallNodes.has(currentCallNodeIndex);
543+
if (isDimmed) {
544+
ctx.globalAlpha = 0.5;
545+
}
546+
513547
// Draw the box.
514548
fastFillStyle.set(
515549
isHovered || isSelected
@@ -550,6 +584,11 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
550584
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
551585
}
552586
}
587+
588+
// Reset dimming after drawing.
589+
if (isDimmed) {
590+
ctx.globalAlpha = 1;
591+
}
553592
}
554593
}
555594

src/components/stack-chart/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getStackChartSameWidths,
2424
getShowUserTimings,
2525
getSelectedThreadsKey,
26+
getSearchStringsAsRegExp,
2627
} from 'firefox-profiler/selectors/url-state';
2728
import type { SameWidthsIndexToTimestampMap } from 'firefox-profiler/profile-logic/stack-timing';
2829
import { selectedThreadSelectors } from '../../selectors/per-thread';
@@ -87,6 +88,7 @@ type StateProps = {
8788
readonly hasFilteredCtssSamples: boolean;
8889
readonly useStackChartSameWidths: boolean;
8990
readonly timelineUnit: TimelineUnit;
91+
readonly searchStringsRegExp: RegExp | null;
9092
};
9193

9294
type DispatchProps = {
@@ -244,6 +246,7 @@ class StackChartImpl extends React.PureComponent<Props> {
244246
hasFilteredCtssSamples,
245247
useStackChartSameWidths,
246248
timelineUnit,
249+
searchStringsRegExp,
247250
} = this.props;
248251

249252
const maxViewportHeight = combinedTimingRows.length * STACK_FRAME_HEIGHT;
@@ -304,6 +307,7 @@ class StackChartImpl extends React.PureComponent<Props> {
304307
displayStackType: displayStackType,
305308
useStackChartSameWidths,
306309
timelineUnit,
310+
searchStringsRegExp,
307311
}}
308312
/>
309313
</div>
@@ -347,6 +351,7 @@ export const StackChart = explicitConnect<{}, StateProps, DispatchProps>({
347351
selectedThreadSelectors.getHasFilteredCtssSamples(state),
348352
useStackChartSameWidths: getStackChartSameWidths(state),
349353
timelineUnit: getProfileTimelineUnit(state),
354+
searchStringsRegExp: getSearchStringsAsRegExp(state),
350355
};
351356
},
352357
mapDispatchToProps: {

src/test/components/StackChart.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
changeImplementationFilter,
3434
changeCallTreeSummaryStrategy,
3535
updatePreviewSelection,
36+
changeCallTreeSearchString,
3637
} from '../../actions/profile-view';
3738
import { changeSelectedTab } from '../../actions/app';
3839
import { selectedThreadSelectors } from '../../selectors/per-thread';
@@ -271,6 +272,56 @@ describe('StackChart', function () {
271272
expect(drawnFrames).not.toContain('Z');
272273
});
273274

275+
it('dims non-matching boxes when searching', function () {
276+
const { dispatch, flushRafCalls } = setupSamples();
277+
flushDrawLog();
278+
279+
// Dispatch a search string that matches some function names.
280+
act(() => {
281+
dispatch(changeCallTreeSearchString('B'));
282+
});
283+
flushRafCalls();
284+
285+
const drawCalls = flushDrawLog();
286+
287+
// Non-matching boxes should be drawn with reduced globalAlpha.
288+
const dimCalls = drawCalls.filter(
289+
([fn, value]) => fn === 'set globalAlpha' && value === 0.5
290+
);
291+
expect(dimCalls.length).toBeGreaterThan(0);
292+
});
293+
294+
it('does not dim boxes that match the search string', function () {
295+
// Use a single-node call stack so there is exactly one box.
296+
const { dispatch, flushRafCalls } = setupSamples(`
297+
A[cat:DOM]
298+
`);
299+
flushDrawLog();
300+
301+
// Search for "A" — the only node matches, so nothing should be dimmed.
302+
act(() => {
303+
dispatch(changeCallTreeSearchString('A'));
304+
});
305+
flushRafCalls();
306+
307+
const drawCalls = flushDrawLog();
308+
const dimCalls = drawCalls.filter(
309+
([fn, value]) => fn === 'set globalAlpha' && value < 1
310+
);
311+
expect(dimCalls).toHaveLength(0);
312+
});
313+
314+
it('does not dim any boxes when there is no search string', function () {
315+
setupSamples();
316+
const drawCalls = flushDrawLog();
317+
318+
// No dimming should be applied without a search.
319+
const dimCalls = drawCalls.filter(
320+
([fn, value]) => fn === 'set globalAlpha' && value < 1
321+
);
322+
expect(dimCalls).toHaveLength(0);
323+
});
324+
274325
describe('EmptyReasons', () => {
275326
it('shows reasons when a profile has no samples', () => {
276327
const profile = getEmptyProfile();

src/test/fixtures/mocks/canvas-context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type SetFillStyleOperation = ['set fillStyle', string];
3535
export type FillRectOperation = ['fillRect', number, number, number, number];
3636
export type ClearRectOperation = ['clearRect', number, number, number, number];
3737
export type FillTextOperation = ['fillText', string];
38+
export type SetGlobalAlphaOperation = ['set globalAlpha', number];
3839

3940
export type DrawOperation =
4041
| BeginPathOperation
@@ -44,7 +45,8 @@ export type DrawOperation =
4445
| SetFillStyleOperation
4546
| FillRectOperation
4647
| ClearRectOperation
47-
| FillTextOperation;
48+
| FillTextOperation
49+
| SetGlobalAlphaOperation;
4850

4951
export function flushDrawLog(): DrawOperation[] {
5052
return (window as any).__flushDrawLog();

0 commit comments

Comments
 (0)