Skip to content

Commit 6d0f011

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 6d0f011

4 files changed

Lines changed: 99 additions & 2 deletions

File tree

src/components/stack-chart/Canvas.tsx

Lines changed: 40 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.2;
545+
}
546+
513547
// Draw the box.
514548
fastFillStyle.set(
515549
isHovered || isSelected
@@ -526,6 +560,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
526560
intW + BORDER_OPACITY,
527561
intH
528562
);
563+
529564
lastDrawnPixelX =
530565
intX +
531566
intW +
@@ -550,6 +585,11 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
550585
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
551586
}
552587
}
588+
589+
// Reset dimming after drawing.
590+
if (isDimmed) {
591+
ctx.globalAlpha = 1;
592+
}
553593
}
554594
}
555595

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.2
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +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-
38+
export type SetGlobalAlphaOperation = ['set globalAlpha', number];
3939
export type DrawOperation =
4040
| BeginPathOperation
4141
| MoveToOperation
@@ -44,7 +44,8 @@ export type DrawOperation =
4444
| SetFillStyleOperation
4545
| FillRectOperation
4646
| ClearRectOperation
47-
| FillTextOperation;
47+
| FillTextOperation
48+
| SetGlobalAlphaOperation;
4849

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

0 commit comments

Comments
 (0)