Skip to content

Commit b345b45

Browse files
committed
Make flame graphs in benchmark comparison viewer only show measured samples of that subtest
1 parent 4bd0212 commit b345b45

4 files changed

Lines changed: 193 additions & 56 deletions

File tree

src/components/app/BenchmarkCompareViewer.tsx

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@ import type {
2727
} from 'firefox-profiler/profile-logic/benchmark/perf-compare-stats';
2828
import type { Profile } from 'firefox-profiler/types';
2929
import { BucketFlameGraphPair } from './BucketFlameGraphPair';
30-
import type { BucketProfileBundle } from './BucketFlameGraphPair';
3130
import {
32-
buildDerivedThread,
33-
getCategoriesForProfile,
34-
getDefaultCategoryIndex,
31+
makeBucketProfileBundle,
32+
makeSuiteFilteredThread,
3533
} from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data';
36-
import { getBenchmarkInfo } from 'firefox-profiler/profile-logic/benchmark/benchmark-stuff';
34+
import type { BucketProfileBundle } from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data';
3735
import './BenchmarkCompareViewer.css';
3836

3937
type ComparisonData = {
@@ -286,17 +284,10 @@ function ScoreTable({
286284
</thead>
287285
<tbody>
288286
<tr className="benchmarkRow--overall">
289-
<td
290-
className="benchmarkCell--scoreLabel"
291-
title={overallScore.label}
292-
>
287+
<td className="benchmarkCell--scoreLabel" title={overallScore.label}>
293288
{overallScore.label}
294289
</td>
295-
<ScoreRow
296-
row={overallScore}
297-
isOverall={true}
298-
numSuites={numSuites}
299-
/>
290+
<ScoreRow row={overallScore} isOverall={true} numSuites={numSuites} />
300291
</tr>
301292
{suiteScores.map((row) => {
302293
const isExpanded = expanded.has(row.label);
@@ -315,10 +306,7 @@ function ScoreTable({
315306
title={row.label}
316307
>
317308
{expandable && (
318-
<span
319-
className="benchmarkDisclosure"
320-
aria-hidden="true"
321-
>
309+
<span className="benchmarkDisclosure" aria-hidden="true">
322310
{isExpanded ? '▼' : '▶'}
323311
</span>
324312
)}
@@ -369,6 +357,18 @@ function BucketTable({
369357
baseSubtestMean !== undefined && numSuites !== undefined;
370358
const columnCount = showSubtestColumns ? 6 : 5;
371359

360+
// Build per-suite bundles whose `thread.samples.weight` is zeroed outside
361+
// this suite's iteration markers, so flame graphs reflect only the samples
362+
// that contribute to this suite's score.
363+
const baseSuiteBundle = useMemo(
364+
() => withSuiteFilteredThread(baseBundle, label),
365+
[baseBundle, label]
366+
);
367+
const newSuiteBundle = useMemo(
368+
() => withSuiteFilteredThread(newBundle, label),
369+
[newBundle, label]
370+
);
371+
372372
const [expanded, setExpanded] = useState<Set<string>>(new Set());
373373
const toggle = (bucketName: string) => {
374374
setExpanded((prev) => {
@@ -380,7 +380,7 @@ function BucketTable({
380380
};
381381

382382
const significant = comparisons
383-
.filter((c) => c.confidence !== 'LOW' && c.effectSize !== 'Negligible')
383+
// .filter((c) => c.confidence !== 'LOW' && c.effectSize !== 'Negligible')
384384
.sort(
385385
(a, b) =>
386386
Math.abs(b.newMean - b.baseMean) - Math.abs(a.newMean - a.baseMean)
@@ -465,16 +465,18 @@ function BucketTable({
465465
<td className="benchmarkCell--number">
466466
{c.baseMean.toFixed(2)}
467467
</td>
468-
<td className="benchmarkCell--number">{c.newMean.toFixed(2)}</td>
468+
<td className="benchmarkCell--number">
469+
{c.newMean.toFixed(2)}
470+
</td>
469471
<td className="benchmarkCell--number">{absDiffStr}</td>
470472
{pctCells}
471473
</tr>
472474
{expandable && isExpanded && (
473475
<tr className="benchmarkRow--bucket-expansion">
474476
<td colSpan={columnCount}>
475477
<BucketFlameGraphPair
476-
baseBundle={baseBundle}
477-
newBundle={newBundle}
478+
baseBundle={baseSuiteBundle}
479+
newBundle={newSuiteBundle}
478480
baseFunc={c.baseFunc}
479481
newFunc={c.newFunc}
480482
/>
@@ -489,20 +491,14 @@ function BucketTable({
489491
);
490492
}
491493

492-
/** Build the (profile, derivedThread, categories) bundle once per profile.
493-
* Computing the derived thread is expensive, so we memoize on profile identity
494-
* and reuse the same bundle across every bucket the user expands. */
495-
function makeBucketProfileBundle(profile: Profile): BucketProfileBundle {
496-
const categories = getCategoriesForProfile(profile);
497-
const defaultCategory = getDefaultCategoryIndex(categories);
498-
const benchmarkInfo = getBenchmarkInfo(profile, 'speedometer');
499-
const thread = buildDerivedThread(
500-
profile,
501-
benchmarkInfo.threadIndex,
502-
categories,
503-
defaultCategory
504-
);
505-
return { profile, thread, categories, defaultCategory };
494+
/** Return a copy of `bundle` whose `thread` has sample weights zeroed outside
495+
* this suite's iteration markers (matching the filtering applied to the suite
496+
* count). All other bundle fields are shared with the input. */
497+
function withSuiteFilteredThread(
498+
bundle: BucketProfileBundle,
499+
suiteName: string
500+
): BucketProfileBundle {
501+
return { ...bundle, thread: makeSuiteFilteredThread(bundle, suiteName) };
506502
}
507503

508504
function ComparisonResults({ data }: { data: ComparisonData }) {
@@ -514,11 +510,11 @@ function ComparisonResults({ data }: { data: ComparisonData }) {
514510
);
515511

516512
const baseBundle = useMemo(
517-
() => makeBucketProfileBundle(data.baseProfile),
513+
() => makeBucketProfileBundle(data.baseProfile, 'speedometer'),
518514
[data.baseProfile]
519515
);
520516
const newBundle = useMemo(
521-
() => makeBucketProfileBundle(data.newProfile),
517+
() => makeBucketProfileBundle(data.newProfile, 'speedometer'),
522518
[data.newProfile]
523519
);
524520

src/components/app/BucketFlameGraphPair.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,16 @@ import { useMemo, useState } from 'react';
77
import { FlameGraph } from 'firefox-profiler/components/flame-graph/FlameGraph';
88
import { computeBucketFlameGraphData } from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data';
99

10-
import type { BucketFlameGraphData } from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data';
1110
import type {
12-
Profile,
13-
Thread,
14-
CategoryList,
15-
IndexIntoCategoryList,
11+
BucketFlameGraphData,
12+
BucketProfileBundle,
13+
} from 'firefox-profiler/profile-logic/benchmark/bucket-flame-graph-data';
14+
import type {
1615
IndexIntoFuncTable,
1716
IndexIntoCallNodeTable,
1817
} from 'firefox-profiler/types';
1918

20-
/** Per-profile prep data passed in from the viewer. The derived `thread` is
21-
* expensive to build, so it's computed once at the viewer level and reused
22-
* across every bucket the user expands. */
23-
export type BucketProfileBundle = {
24-
profile: Profile;
25-
thread: Thread;
26-
categories: CategoryList;
27-
defaultCategory: IndexIntoCategoryList;
28-
};
19+
export type { BucketProfileBundle };
2920

3021
type SideProps = {
3122
label: string;

src/profile-logic/benchmark/benchmark-stuff.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ensureExists } from 'firefox-profiler/utils/types';
1515

1616
export type BenchmarkHarness = 'speedometer' | 'jetstream';
1717

18-
type BenchmarkInfo = {
18+
export type BenchmarkInfo = {
1919
suiteNameIfSingleSuite: string | null;
2020
threadIndex: number;
2121
getMeasuredTimeRanges: (
@@ -307,6 +307,33 @@ export type IterationMarkersAndMeasuredSamples = {
307307
measuredSamples: SamplesTableForThisStuff;
308308
};
309309

310+
/**
311+
* Compute per-suite sample weights, filtered to (already-applied measured time
312+
* ranges) ∩ (this suite's iteration marker ranges). The input weights are
313+
* `measuredSamples.weight` (i.e. weights with -async/-sync filtering and
314+
* ignored-bucket zeroing already applied). The output zeroes out any weight
315+
* outside this suite's iteration markers, so the flame graph for this suite
316+
* reflects exactly the same samples that the suite's score counts.
317+
*
318+
* Iteration markers are assumed to be sorted by start time and non-overlapping
319+
* (matching the assumption in `computeSuiteScores`).
320+
*/
321+
export function computeSuiteFilteredSampleWeights(
322+
measuredSampleWeights: Float64Array,
323+
sampleTimes: Float64Array,
324+
iterationMarkers: Marker[]
325+
): Float64Array {
326+
const filtered = measuredSampleWeights.slice();
327+
const ranges: StartEndRange[] = [];
328+
for (const m of iterationMarkers) {
329+
if (m.end !== null) {
330+
ranges.push({ start: m.start, end: m.end });
331+
}
332+
}
333+
zeroWeightsOutsideRanges(filtered, sampleTimes, ranges);
334+
return filtered;
335+
}
336+
310337
export function computeIterationMarkersAndMeasuredSamples(
311338
benchmarkInfo: BenchmarkInfo,
312339
filteredMarkers: Marker[],
@@ -356,7 +383,7 @@ function computeGeomean(values: number[]): number {
356383
return Math.pow(product, 1 / values.length);
357384
}
358385

359-
function zeroWeightsOutsideRanges(
386+
export function zeroWeightsOutsideRanges(
360387
sampleWeights: Float64Array,
361388
sampleTimes: Float64Array,
362389
nonZeroRanges: StartEndRange[]

src/profile-logic/benchmark/bucket-flame-graph-data.ts

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
createThreadFromDerivedTables,
2222
getCallNodeInfo,
2323
getSampleIndexToCallNodeIndex,
24+
getTimeRangeForThread,
2425
} from '../profile-data';
2526
import * as Transforms from '../transforms';
2627
import * as CallTree from '../call-tree';
@@ -29,8 +30,19 @@ import { computeReferenceCPUDeltaPerMs } from '../cpu';
2930
import { getDefaultCategories } from '../data-structures';
3031
import { StringTable } from '../../utils/string-table';
3132
import { base64StringToBytes } from '../../utils/base64';
33+
import {
34+
correlateIPCMarkers,
35+
deriveMarkersFromRawMarkerTable,
36+
} from '../marker-data';
37+
import {
38+
computeSuiteFilteredSampleWeights,
39+
getBenchmarkInfo,
40+
zeroWeightsOutsideRanges,
41+
} from './benchmark-stuff';
42+
import type { BenchmarkHarness, BenchmarkInfo } from './benchmark-stuff';
3243

3344
import type {
45+
Marker,
3446
Thread,
3547
Profile,
3648
IndexIntoFuncTable,
@@ -70,7 +82,9 @@ export function getCategoriesForProfile(profile: Profile): CategoryList {
7082
}
7183

7284
/** Default category index — the "Other" / grey category. */
73-
export function getDefaultCategoryIndex(categories: CategoryList): IndexIntoCategoryList {
85+
export function getDefaultCategoryIndex(
86+
categories: CategoryList
87+
): IndexIntoCategoryList {
7488
return categories.findIndex((c) => c.color === 'grey');
7589
}
7690

@@ -151,7 +165,10 @@ export function computeBucketFlameGraphData(
151165
);
152166

153167
// 3. CTSS samples (timing strategy → just thread.samples).
154-
const ctssSamples = CallTree.extractSamplesLikeTable(selfWingThread, 'timing');
168+
const ctssSamples = CallTree.extractSamplesLikeTable(
169+
selfWingThread,
170+
'timing'
171+
);
155172

156173
// 4. Map samples → call nodes.
157174
const sampleIndexToCallNodeIndex = getSampleIndexToCallNodeIndex(
@@ -233,3 +250,109 @@ export function computeBucketFlameGraphData(
233250
rootTotalSummary: callNodeSelfAndSummary.rootTotalSummary,
234251
};
235252
}
253+
254+
/** Per-profile prep data passed in from the viewer. The derived `thread` is
255+
* expensive to build, so it's computed once at the viewer level and reused
256+
* across every bucket the user expands. Also carries the benchmark marker
257+
* info needed to lazily build per-suite filtered threads. */
258+
export type BucketProfileBundle = {
259+
profile: Profile;
260+
thread: Thread;
261+
categories: CategoryList;
262+
defaultCategory: IndexIntoCategoryList;
263+
benchmarkInfo: BenchmarkInfo;
264+
/** `thread.samples.time` as a Float64Array, for fast range filtering. */
265+
sampleTimes: Float64Array;
266+
/** Sample weights with the global -async/-sync measured-time filter applied,
267+
* matching the `measuredSamples.weight` used by score computation. */
268+
measuredSampleWeights: Float64Array;
269+
/** Iteration markers per suite name. Sorted by start time, non-overlapping. */
270+
markersPerSuite: Map<string, Marker[]>;
271+
};
272+
273+
export function makeBucketProfileBundle(
274+
profile: Profile,
275+
benchmarkHarness: BenchmarkHarness
276+
): BucketProfileBundle {
277+
const categories = getCategoriesForProfile(profile);
278+
const defaultCategory = getDefaultCategoryIndex(categories);
279+
const benchmarkInfo = getBenchmarkInfo(profile, benchmarkHarness);
280+
const thread = buildDerivedThread(
281+
profile,
282+
benchmarkInfo.threadIndex,
283+
categories,
284+
defaultCategory
285+
);
286+
287+
const { shared } = profile;
288+
const rawThread = profile.threads[benchmarkInfo.threadIndex];
289+
const stringTable = StringTable.withBackingArray(shared.stringArray);
290+
const { markers: derivedMarkers } = deriveMarkersFromRawMarkerTable(
291+
rawThread.markers,
292+
shared.stringArray,
293+
rawThread.tid,
294+
getTimeRangeForThread(rawThread, profile.meta.interval),
295+
correlateIPCMarkers(profile.threads, shared)
296+
);
297+
298+
const sampleCount = thread.samples.length;
299+
const sampleTimes = new Float64Array(thread.samples.time);
300+
const measuredSampleWeights = thread.samples.weight
301+
? new Float64Array(thread.samples.weight)
302+
: new Float64Array(sampleCount).fill(1);
303+
const measuredTimeRanges = benchmarkInfo.getMeasuredTimeRanges(
304+
derivedMarkers,
305+
stringTable
306+
);
307+
if (measuredTimeRanges !== null) {
308+
zeroWeightsOutsideRanges(
309+
measuredSampleWeights,
310+
sampleTimes,
311+
measuredTimeRanges
312+
);
313+
}
314+
315+
const markersPerSuite = benchmarkInfo.getMarkersPerSuite(
316+
derivedMarkers,
317+
stringTable
318+
);
319+
320+
return {
321+
profile,
322+
thread,
323+
categories,
324+
defaultCategory,
325+
benchmarkInfo,
326+
sampleTimes,
327+
measuredSampleWeights,
328+
markersPerSuite,
329+
};
330+
}
331+
332+
/**
333+
* Return a Thread that shares all tables with `bundle.thread` but has sample
334+
* weights zeroed outside this suite's iteration marker ranges. The flame graph
335+
* built from this thread then reflects only the samples that contribute to
336+
* this suite's score (matching `computeSuiteScores`).
337+
*/
338+
export function makeSuiteFilteredThread(
339+
bundle: BucketProfileBundle,
340+
suiteName: string
341+
): Thread {
342+
const { thread, sampleTimes, measuredSampleWeights, markersPerSuite } =
343+
bundle;
344+
const iterationMarkers = markersPerSuite.get(suiteName) ?? [];
345+
const filteredWeights = computeSuiteFilteredSampleWeights(
346+
measuredSampleWeights,
347+
sampleTimes,
348+
iterationMarkers
349+
);
350+
return {
351+
...thread,
352+
samples: {
353+
...thread.samples,
354+
weight: Array.from(filteredWeights),
355+
weightType: thread.samples.weightType ?? 'samples',
356+
},
357+
};
358+
}

0 commit comments

Comments
 (0)