diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index a7bf7ddc39..2ffba4e896 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -156,9 +156,7 @@ export function selectSelfCallNode( return (dispatch, getState) => { const threadSelectors = getThreadSelectorsFromThreadsKey(threadsKey); const sampleCallNodes = - threadSelectors.getSampleIndexToNonInvertedCallNodeIndexForFilteredThread( - getState() - ); + threadSelectors.getSampleCallNodesForFilteredThread(getState()); if ( sampleIndex === null || diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx index 1c163512b2..badc12addd 100644 --- a/src/components/flame-graph/Canvas.tsx +++ b/src/components/flame-graph/Canvas.tsx @@ -17,8 +17,11 @@ import { formatPercent, } from 'firefox-profiler/utils/format-numbers'; import { TooltipCallNode } from 'firefox-profiler/components/tooltip/CallNode'; -import { getTimingsForCallNodeIndex } from 'firefox-profiler/profile-logic/profile-data'; import MixedTupleMap from 'mixedtuplemap'; +import { + getCallNodeTimings, + getSampleSelectedStates, +} from 'firefox-profiler/profile-logic/profile-data'; import type { Thread, @@ -74,6 +77,7 @@ export type OwnProps = { readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; + readonly ctssSampleCallNodes: Array; readonly tracedTiming: CallTreeTimingsNonInverted | null; readonly displayStackType: boolean; }; @@ -344,7 +348,10 @@ class FlameGraphCanvasImpl extends React.PureComponent { }; // Properly memoize this derived information for the Tooltip component. - _getTimingsForCallNodeIndex = memoize(getTimingsForCallNodeIndex, { + _getSampleSelectedStates = memoize(getSampleSelectedStates, { + cache: new MixedTupleMap(), + }); + _getCallNodeTimings = memoize(getCallNodeTimings, { cache: new MixedTupleMap(), }); @@ -365,6 +372,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { weightType, ctssSamples, ctssSampleCategoriesAndSubcategories, + ctssSampleCallNodes, tracedTiming, displayStackType, } = this.props; @@ -404,6 +412,11 @@ class FlameGraphCanvasImpl extends React.PureComponent { // strategies, but it may not be worth implementing it. callTreeSummaryStrategy === 'timing'; + const callNodeInfoInverted = callNodeInfo.asInverted(); + const isInvertedRoot = + callNodeInfoInverted !== null && + callNodeInfoInverted.isRoot(callNodeIndex); + return ( // Important! Only pass in props that have been properly memoized so this component // doesn't over-render. @@ -420,12 +433,16 @@ class FlameGraphCanvasImpl extends React.PureComponent { callTreeSummaryStrategy={callTreeSummaryStrategy} timings={ shouldComputeTimings - ? this._getTimingsForCallNodeIndex( - callNodeIndex, - callNodeInfo, + ? this._getCallNodeTimings( categories, ctssSamples, - ctssSampleCategoriesAndSubcategories + ctssSampleCategoriesAndSubcategories, + this._getSampleSelectedStates( + callNodeInfo, + ctssSampleCallNodes, + callNodeIndex + ), + isInvertedRoot ) : undefined } diff --git a/src/components/flame-graph/FlameGraph.tsx b/src/components/flame-graph/FlameGraph.tsx index 376d0f3025..3a2d8eab5c 100644 --- a/src/components/flame-graph/FlameGraph.tsx +++ b/src/components/flame-graph/FlameGraph.tsx @@ -87,6 +87,7 @@ type StateProps = { readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; + readonly ctssSampleCallNodes: Array; readonly tracedTiming: CallTreeTimings | null; readonly displayStackType: boolean; }; @@ -341,6 +342,7 @@ class FlameGraphImpl weightType, ctssSamples, ctssSampleCategoriesAndSubcategories, + ctssSampleCallNodes, tracedTiming, displayStackType, } = this.props; @@ -406,6 +408,7 @@ class FlameGraphImpl isInverted, ctssSamples, ctssSampleCategoriesAndSubcategories, + ctssSampleCallNodes, tracedTiming: tracedTimingNonInverted, displayStackType, }} @@ -459,6 +462,8 @@ export const FlameGraph = explicitConnectWithForwardRef< selectedThreadSelectors.getPreviewFilteredCtssSampleCategoriesAndSubcategories( state ), + ctssSampleCallNodes: + selectedThreadSelectors.getPreviewFilteredCtssSampleCallNodes(state), tracedTiming: selectedThreadSelectors.getTracedTiming(state), displayStackType: getProfileUsesMultipleStackTypes(state), }), diff --git a/src/components/shared/thread/ActivityGraphCanvas.tsx b/src/components/shared/thread/ActivityGraphCanvas.tsx index 36356a762b..d58a1af75e 100644 --- a/src/components/shared/thread/ActivityGraphCanvas.tsx +++ b/src/components/shared/thread/ActivityGraphCanvas.tsx @@ -4,11 +4,15 @@ * */ import * as React from 'react'; import { InView } from 'react-intersection-observer'; +import memoizeOne from 'memoize-one'; import type { ActivityFillGraphQuerier, CategoryDrawStyles, } from './ActivityGraphFills'; -import { computeActivityGraphFills } from './ActivityGraphFills'; +import { + computeActivityGraphFills, + precomputePositions, +} from './ActivityGraphFills'; import { timeCode } from 'firefox-profiler/utils/time-code'; import { mapCategoryColorNameToStyles } from 'firefox-profiler/utils/colors'; @@ -46,6 +50,7 @@ export class ActivityGraphCanvas extends React.PureComponent { renderScheduled: false, inView: false, }; + _memoizedPrecomputePositions = memoizeOne(precomputePositions); _renderCanvas() { if (!this._canvasState.inView) { @@ -141,6 +146,17 @@ export class ActivityGraphCanvas extends React.PureComponent { canvas.width = canvasPixelWidth; canvas.height = canvasPixelHeight; + const xPixelsPerMs = canvasPixelWidth / (rangeEnd - rangeStart); + const precomputedPositions = this._memoizedPrecomputePositions( + fullThread.samples.time, + sampleIndexOffset, + rangeFilteredThread.samples.length, + rangeStart, + xPixelsPerMs, + interval, + canvasPixelWidth + ); + const { fills, fillsQuerier } = computeActivityGraphFills({ canvasPixelWidth, canvasPixelHeight, @@ -154,6 +170,7 @@ export class ActivityGraphCanvas extends React.PureComponent { xPixelsPerMs: canvasPixelWidth / (rangeEnd - rangeStart), treeOrderSampleComparator, categoryDrawStyles: this._getCategoryDrawStyles(ctx!), + precomputedPositions, }); // The value in fillsQuerier is needed in ActivityGraph but is computed in this method diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index 0b01e803dd..1bd518ea0b 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -13,7 +13,10 @@ import type { DevicePixels, CssPixels, } from 'firefox-profiler/types'; -import { SelectedState } from 'firefox-profiler/types'; +import { + SAMPLE_RELATION_TO_SELECTED_STATE_MASK, + SelectedState, +} from 'firefox-profiler/types'; import type { HoveredPixelState } from './ActivityGraph'; /** @@ -41,6 +44,18 @@ type RenderedComponentSettings = { | null; readonly sampleSelectedStates: Uint8Array; readonly categoryDrawStyles: CategoryDrawStyles; + readonly precomputedPositions: PrecomputedPositions; +}; + +export type PrecomputedPositions = { + // The fractional device pixel position per sample in the range-filtered thread. + // Each position is clamped such that 0 <= pos < canvasPixelWidth. + samplePositions: Int32Array; // (DevicePixel * (1 << FIXED_POINT_BITS))[] + // The fractional device pixel position of the half-way point *before* the sample, + // per sample in the range-filtered thread. Has one extra element at the end for + // the half-way position after the last sample. + // Each position is clamped such that 0 <= pos < canvasPixelWidth. + halfwayPositions: Int32Array; // (DevicePixel * (1 << FIXED_POINT_BITS))[] }; type SampleContributionToPixel = { @@ -58,7 +73,7 @@ type CategoryFill = { readonly fillStyle: string | CanvasPattern; // Mutated in place during the computation step. // Contains values between 0 and 100. - readonly perPixelContribution: Float32Array; + readonly perPixelContribution: Int32Array; // Mutated in place during the computation step. // Contains values between 0 and 1. readonly accumulatedUpperEdge: Float32Array; @@ -76,10 +91,6 @@ export type CategoryDrawStyles = ReadonlyArray<{ readonly filteredOutByTransformFillStyle: CanvasPattern | string; }>; -// These Float32Arrays are mutated in place during the computation step. -// buffers[selectedState] is the buffer for the given SelectedState enum value. -type SelectedPercentageAtPixelBuffers = Float32Array[]; - export type CpuRatioInTimeRange = { readonly cpuRatio: number; readonly timeRange: Milliseconds; @@ -94,19 +105,88 @@ const SMOOTHING_KERNEL: Float32Array = _getSmoothingKernel( BOX_BLUR_RADII ); +const FIXED_POINT_BITS = 16; +const FIXED_POINT_MASK = (1 << FIXED_POINT_BITS) - 1; + +export function precomputePositions( + fullThreadSampleTimes: Milliseconds[], + sampleIndexOffset: number, + sampleCount: number, + rangeStart: Milliseconds, + xPixelsPerMs: number, + interval: Milliseconds, + canvasPixelWidth: DevicePixels +): PrecomputedPositions { + const canvasPixelsFp = canvasPixelWidth << FIXED_POINT_BITS; + const xPixelsFpPerMs = xPixelsPerMs * (1 << FIXED_POINT_BITS); + + function convertTimeToClampedPositionFixedPointPrecision( + time: Milliseconds + ): DevicePixels { + const posFp = ((time - rangeStart) * xPixelsFpPerMs) | 0; + if (posFp < 0) { + return 0; + } + if (posFp >= canvasPixelsFp) { + return canvasPixelsFp - 1; + } + return posFp; + } + + // The fractional device pixel position per sample in the range-filtered thread. + const samplePositions = new Int32Array(sampleCount); // DevicePixel[] + + // The fractional device pixel position of the half-way point *before* the sample, + // per sample in the range-filtered thread. Has one extra element at the end for + // the half-way position after the last sample. + const halfwayPositions = new Int32Array(sampleCount + 1); // DevicePixel[] + + let previousSampleTime = + sampleIndexOffset > 0 + ? fullThreadSampleTimes[sampleIndexOffset - 1] + : fullThreadSampleTimes[0] - interval; + // Go through the samples and accumulate the category into the percentageBuffer. + for (let i = 0; i < sampleCount; i++) { + const sampleTime = fullThreadSampleTimes[sampleIndexOffset + i]; + samplePositions[i] = + convertTimeToClampedPositionFixedPointPrecision(sampleTime); + + const halfwayPointTimeBefore = (previousSampleTime + sampleTime) / 2; + halfwayPositions[i] = convertTimeToClampedPositionFixedPointPrecision( + halfwayPointTimeBefore + ); + + previousSampleTime = sampleTime; + } + + // Add another half-way point for after the last sample. + const afterLastSampleTime = + sampleIndexOffset + sampleCount < fullThreadSampleTimes.length + ? fullThreadSampleTimes[sampleIndexOffset + sampleCount] + : previousSampleTime + interval; + const halfwayPointTime = (previousSampleTime + afterLastSampleTime) / 2; + halfwayPositions[sampleCount] = + convertTimeToClampedPositionFixedPointPrecision(halfwayPointTime); + + return { + samplePositions, + halfwayPositions, + }; +} + export function computeActivityGraphFills( renderedComponentSettings: RenderedComponentSettings ) { - const mutablePercentageBuffers = _createSelectedPercentageAtPixelBuffers( + const mutablePercentageBuffer = _createSelectedPercentageAtPixelBuffers( renderedComponentSettings ); const mutableFills = _getCategoryFills( renderedComponentSettings.categoryDrawStyles, - mutablePercentageBuffers + mutablePercentageBuffer ); const activityGraphFills = new ActivityGraphFillComputer( renderedComponentSettings, - mutablePercentageBuffers, + mutablePercentageBuffer, mutableFills ); @@ -132,16 +212,16 @@ export function computeActivityGraphFills( export class ActivityGraphFillComputer { readonly renderedComponentSettings: RenderedComponentSettings; // The fills and percentages are mutated in place. - readonly mutablePercentageBuffers: SelectedPercentageAtPixelBuffers[]; + readonly mutablePercentageBuffer: Int32Array; readonly mutableFills: CategoryFill[]; constructor( renderedComponentSettings: RenderedComponentSettings, - mutablePercentageBuffers: SelectedPercentageAtPixelBuffers[], + mutablePercentageBuffer: Int32Array, mutableFills: CategoryFill[] ) { this.renderedComponentSettings = renderedComponentSettings; - this.mutablePercentageBuffers = mutablePercentageBuffers; + this.mutablePercentageBuffer = mutablePercentageBuffer; this.mutableFills = mutableFills; } @@ -183,7 +263,8 @@ export class ActivityGraphFillComputer { // Only copy the first array, as there is no accumulation. const { accumulatedUpperEdge, perPixelContribution } = mutableFills[0]; for (let i = 0; i < perPixelContribution.length; i++) { - accumulatedUpperEdge[i] = perPixelContribution[i] / 100; + accumulatedUpperEdge[i] = + perPixelContribution[i] / (100 << FIXED_POINT_BITS); } } @@ -195,7 +276,8 @@ export class ActivityGraphFillComputer { } of mutableFills.slice(1)) { for (let i = 0; i < perPixelContribution.length; i++) { accumulatedUpperEdge[i] = - previousUpperEdge[i] + perPixelContribution[i] / 100; + previousUpperEdge[i] + + perPixelContribution[i] / (100 << FIXED_POINT_BITS); } previousUpperEdge = accumulatedUpperEdge; } @@ -210,82 +292,132 @@ export class ActivityGraphFillComputer { * with these methods. */ _accumulateSampleCategories() { + const { mutablePercentageBuffer: buffer, renderedComponentSettings } = this; const { - fullThread, + canvasPixelWidth, rangeFilteredThread: { samples }, - interval, - sampleIndexOffset, - rangeStart, sampleSelectedStates, - } = this.renderedComponentSettings; + precomputedPositions, + } = renderedComponentSettings; if (samples.length === 0) { // If we have no samples, there's nothing to do. return; } - let prevSampleTime = samples.time[0] - interval; - let sampleTime = samples.time[0]; - - if (sampleIndexOffset > 0) { - // If sampleIndexOffset is greater than zero, it means that we are zoomed - // in the timeline and we are seeing a portion of it. In that case, - // rangeFilteredThread will not have the information of the first previous - // sample. So we need to get that information from the full thread. - prevSampleTime = fullThread.samples.time[sampleIndexOffset - 1]; - } - - // Go through the samples and accumulate the category into the percentageBuffers. - const { threadCPUPercent } = samples; - for (let i = 0; i < samples.length - 1; i++) { - const nextSampleTime = samples.time[i + 1]; - const category = samples.category[i]; - - const beforeSampleCpuPercent = threadCPUPercent[i]; + // Go through the samples and accumulate the category into the buffer. + const { samplePositions, halfwayPositions } = precomputedPositions; + const { threadCPUPercent, category: sampleCategories } = samples; + let beforeSampleCpuPercent = threadCPUPercent[0]; + let halfwayPositionBefore = halfwayPositions[0]; + for (let i = 0; i < samples.length; i++) { + // Note that both halfwayPositions and threadCPUPercent have an extra element + // at the end, so accessing [i + 1] is valid even for the last sample. + const halfwayPositionAfter = halfwayPositions[i + 1]; const afterSampleCpuPercent = threadCPUPercent[i + 1]; - const percentageBuffers = this.mutablePercentageBuffers[category]; - const selectedState = sampleSelectedStates[i]; - const percentageBuffer = percentageBuffers[selectedState]; - - _accumulateInBuffer( - percentageBuffer, - this.renderedComponentSettings, - prevSampleTime, - sampleTime, - nextSampleTime, - beforeSampleCpuPercent, - afterSampleCpuPercent, - rangeStart - ); - - prevSampleTime = sampleTime; - sampleTime = nextSampleTime; - } - - // Handle the last sample, which was not covered by the for loop above. - const lastIdx = samples.length - 1; - const lastSampleCategory = samples.category[lastIdx]; - - const beforeSampleCpuPercent = threadCPUPercent[lastIdx]; - const afterSampleCpuPercent = threadCPUPercent[lastIdx + 1]; // guaranteed to exist + if (beforeSampleCpuPercent === 0 && afterSampleCpuPercent === 0) { + halfwayPositionBefore = halfwayPositionAfter; + beforeSampleCpuPercent = afterSampleCpuPercent; + continue; + } - const nextSampleTime = sampleTime + interval; - const percentageBuffers = this.mutablePercentageBuffers[lastSampleCategory]; + const category = sampleCategories[i]; + + const bufferIndex = + sampleSelectedStates[i] & SAMPLE_RELATION_TO_SELECTED_STATE_MASK; + const bufferRow = category * SELECTED_STATE_BUFFER_COUNT + bufferIndex; + const baseIndex = bufferRow * canvasPixelWidth; + const samplePosition = samplePositions[i]; + + // Samples have two parts to be able to present the different CPU usages properly. + // This is because CPU usage number of a sample represents the CPU usage + // starting starting from the previous sample time to this sample time. + // These parts will be: + // - Between `halfwayPositionBefore` and `samplePosition` with beforeSampleCpuPercent. + // - Between `samplePosition` and `halfwayPositionAfter` with afterSampleCpuPercent. + + // Every sample has two parts because of different CPU usage values. + // For every sample part, we have a fractional interval of this sample part's + // contribution to the graph's pixels. + // + // v v v v v v v v v + // +-------+-------+-----+-+-------+-------+-----+-+-------+-------+ + // | | | |///////////////////////| | | | + // | | | |///////////////////////| | | | + // | | | |///////////////////////| | | | + // +-------+-------+-----+///////////////////////+-+-------+-------+ + // + // We have a device-pixel array of contributions. We map the fractional + // interval to this array of device pixels: Fully overlapping pixels are + // 1, and the partial overlapping pixels are the degree of overlap. + + // | + // v + // + // +-------+-------+-------+-------+-------+-------+-------+-------+ + // | | | |///////////////+-------+ | | + // | | | |///////////////////////| | | + // | | +-------+///////////////////////| | | + // +-------+-------+///////////////////////////////+-------+-------+ + + if (beforeSampleCpuPercent !== 0) { + const startPos = halfwayPositionBefore; + const endPos = samplePosition; + const cpuPercent = beforeSampleCpuPercent; + + const intStartPos = startPos >> FIXED_POINT_BITS; + const intEndPos = endPos >> FIXED_POINT_BITS; + + if (intStartPos === intEndPos) { + buffer[baseIndex + intStartPos] += cpuPercent * (endPos - startPos); + } else { + if (intStartPos + 1 < intEndPos) { + buffer.fill( + cpuPercent << FIXED_POINT_BITS, + baseIndex + intStartPos + 1, + baseIndex + intEndPos + ); + } + + const startPosFrac = startPos & FIXED_POINT_MASK; + buffer[baseIndex + intStartPos] += + cpuPercent * ((1 << FIXED_POINT_BITS) - startPosFrac); + const endPosFrac = endPos & FIXED_POINT_MASK; + buffer[baseIndex + intEndPos] += cpuPercent * endPosFrac; + } + } - const selectedState = sampleSelectedStates[lastIdx]; - const percentageBuffer = percentageBuffers[selectedState]; + if (afterSampleCpuPercent !== 0) { + const startPos = samplePosition; + const endPos = halfwayPositionAfter; + const cpuPercent = afterSampleCpuPercent; + + const intStartPos = startPos >> FIXED_POINT_BITS; + const intEndPos = endPos >> FIXED_POINT_BITS; + + if (intStartPos === intEndPos) { + buffer[baseIndex + intStartPos] += cpuPercent * (endPos - startPos); + } else { + if (intStartPos + 1 < intEndPos) { + buffer.fill( + cpuPercent << FIXED_POINT_BITS, + baseIndex + intStartPos + 1, + baseIndex + intEndPos + ); + } + + const startPosFrac = startPos & FIXED_POINT_MASK; + buffer[baseIndex + intStartPos] += + cpuPercent * ((1 << FIXED_POINT_BITS) - startPosFrac); + const endPosFrac = endPos & FIXED_POINT_MASK; + buffer[baseIndex + intEndPos] += cpuPercent * endPosFrac; + } + } - _accumulateInBuffer( - percentageBuffer, - this.renderedComponentSettings, - prevSampleTime, - sampleTime, - nextSampleTime, - beforeSampleCpuPercent, - afterSampleCpuPercent, - rangeStart - ); + halfwayPositionBefore = halfwayPositionAfter; + beforeSampleCpuPercent = afterSampleCpuPercent; + } } } @@ -586,47 +718,32 @@ export class ActivityFillGraphQuerier { ): number { const { rangeFilteredThread: { samples }, - interval, - sampleIndexOffset, - fullThread, - xPixelsPerMs, - rangeStart, + precomputedPositions: { samplePositions, halfwayPositions }, } = this.renderedComponentSettings; - const kernelPos = xPixel - SMOOTHING_RADIUS; - const pixelsAroundX = new Float32Array(SMOOTHING_KERNEL.length); - const sampleTime = samples.time[sample]; - // Use the fullThread here to properly get the next and previous in case zoomed in. - const fullThreadSample = sample + sampleIndexOffset; - const prevSampleTime = - fullThreadSample > 0 - ? fullThread.samples.time[fullThreadSample - 1] - : sampleTime - interval; - const nextSampleTime = - fullThreadSample + 1 < fullThread.samples.length - ? fullThread.samples.time[fullThreadSample + 1] - : sampleTime + interval; + + const halfwayPositionBefore = halfwayPositions[sample]; + const halfwayPositionAfter = halfwayPositions[sample + 1]; + const samplePosition = samplePositions[sample]; const { threadCPUPercent } = samples; const beforeSampleCpuPercent = threadCPUPercent[sample]; const afterSampleCpuPercent = threadCPUPercent[sample + 1]; // guaranteed to exist - const kernelRangeStartTime = rangeStart + kernelPos / xPixelsPerMs; - - _accumulateInBuffer( - pixelsAroundX, - this.renderedComponentSettings, - prevSampleTime, - sampleTime, - nextSampleTime, - beforeSampleCpuPercent, - afterSampleCpuPercent, - kernelRangeStartTime - ); + const kernelCenterPosition = Math.round(xPixel); let sum = 0; - for (let i = 0; i < SMOOTHING_KERNEL.length; i++) { - sum += SMOOTHING_KERNEL[i] * pixelsAroundX[i]; - } + sum += _accumulateHalfSampleToKernelSum( + kernelCenterPosition, + halfwayPositionBefore, + samplePosition, + beforeSampleCpuPercent + ); + sum += _accumulateHalfSampleToKernelSum( + kernelCenterPosition, + samplePosition, + halfwayPositionAfter, + afterSampleCpuPercent + ); return sum / 100; } @@ -658,14 +775,9 @@ function _createSelectedPercentageAtPixelBuffers({ }: { categoryDrawStyles: CategoryDrawStyles; canvasPixelWidth: number; -}): SelectedPercentageAtPixelBuffers[] { - return categoryDrawStyles.map(() => { - const percentageBuffers = []; - for (let i = 0; i < SELECTED_STATE_BUFFER_COUNT; i++) { - percentageBuffers[i] = new Float32Array(canvasPixelWidth); - } - return percentageBuffers; - }); +}): Int32Array { + const rowCount = categoryDrawStyles.length * SELECTED_STATE_BUFFER_COUNT; + return new Int32Array(canvasPixelWidth * rowCount); } /** @@ -679,8 +791,13 @@ function _createSelectedPercentageAtPixelBuffers({ */ function _getCategoryFills( categoryDrawStyles: CategoryDrawStyles, - percentageBuffers: SelectedPercentageAtPixelBuffers[] + percentageBuffer: Int32Array ): CategoryFill[] { + const canvasPixelWidth = + percentageBuffer.length / + categoryDrawStyles.length / + SELECTED_STATE_BUFFER_COUNT; + // Sort all of the categories by their gravity. const categoryIndexesByGravity = categoryDrawStyles .map((_, i) => i) @@ -688,38 +805,55 @@ function _getCategoryFills( (a, b) => categoryDrawStyles[b].gravity - categoryDrawStyles[a].gravity ); + function bufferForCategoryAndSelectedState( + category: IndexIntoCategoryList, + selectedState: SelectedState + ): Int32Array { + const rowIndex = category * SELECTED_STATE_BUFFER_COUNT + selectedState; + const sliceStart = rowIndex * canvasPixelWidth; + const sliceEnd = sliceStart + canvasPixelWidth; + return percentageBuffer.subarray(sliceStart, sliceEnd); + } + const nestedFills: CategoryFill[][] = categoryIndexesByGravity.map( (categoryIndex) => { const categoryDrawStyle = categoryDrawStyles[categoryIndex]; - const buffer = percentageBuffers[categoryIndex]; - const canvasPixelWidth = - buffer[SelectedState.UnselectedOrderedBeforeSelected].length; // For every category we draw four fills, for the four selection kinds: return [ { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getUnselectedFillStyle(), - perPixelContribution: - buffer[SelectedState.UnselectedOrderedBeforeSelected], + perPixelContribution: bufferForCategoryAndSelectedState( + categoryIndex, + SelectedState.UnselectedOrderedBeforeSelected + ), accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getSelectedFillStyle(), - perPixelContribution: buffer[SelectedState.Selected], + perPixelContribution: bufferForCategoryAndSelectedState( + categoryIndex, + SelectedState.Selected + ), accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.getUnselectedFillStyle(), - perPixelContribution: - buffer[SelectedState.UnselectedOrderedAfterSelected], + perPixelContribution: bufferForCategoryAndSelectedState( + categoryIndex, + SelectedState.UnselectedOrderedAfterSelected + ), accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, { category: categoryDrawStyle.category, fillStyle: categoryDrawStyle.filteredOutByTransformFillStyle, - perPixelContribution: buffer[SelectedState.FilteredOutByTransform], + perPixelContribution: bufferForCategoryAndSelectedState( + categoryIndex, + SelectedState.FilteredOutByTransform + ), accumulatedUpperEdge: new Float32Array(canvasPixelWidth), }, ]; @@ -730,106 +864,62 @@ function _getCategoryFills( return ([] as CategoryFill[]).concat(...nestedFills); } -/** - * Mutates `percentageBuffer` by adding contributions from a single sample to - * the pixels that the sample overlaps with. The buffer covers the following - * time range: It starts at `rangeStart` and ends at - * `rangeStart + percentageBuffer.length / renderedComponentSettings.xPixelsPerMs`. - */ -function _accumulateInBuffer( - percentageBuffer: Float32Array, - renderedComponentSettings: RenderedComponentSettings, - prevSampleTime: Milliseconds, - sampleTime: Milliseconds, - nextSampleTime: Milliseconds, - beforeSampleCpuPercent: number, - afterSampleCpuPercent: number, - bufferTimeRangeStart: Milliseconds -) { - const { xPixelsPerMs } = renderedComponentSettings; - const sampleCategoryStartTime = (prevSampleTime + sampleTime) / 2; - const sampleCategoryEndTime = (sampleTime + nextSampleTime) / 2; - let sampleCategoryStartPixel = - (sampleCategoryStartTime - bufferTimeRangeStart) * xPixelsPerMs; - let sampleCategoryEndPixel = - (sampleCategoryEndTime - bufferTimeRangeStart) * xPixelsPerMs; - if (sampleCategoryStartPixel < 0) { - sampleCategoryStartPixel = 0; - } - if (sampleCategoryEndPixel > percentageBuffer.length - 1) { - sampleCategoryEndPixel = percentageBuffer.length - 1; - } - const samplePixel = (sampleTime - bufferTimeRangeStart) * xPixelsPerMs; - const intCategoryStartPixel = sampleCategoryStartPixel | 0; - const intCategoryEndPixel = sampleCategoryEndPixel | 0; - const intSamplePixel = samplePixel | 0; - - // Every sample has two parts because of different CPU usage values. - // For every sample part, we have a fractional interval of this sample part's - // contribution to the graph's pixels. - // - // v v v v v v v v v - // +-------+-------+-----+-+-------+-------+-----+-+-------+-------+ - // | | | |///////////////////////| | | | - // | | | |///////////////////////| | | | - // | | | |///////////////////////| | | | - // +-------+-------+-----+///////////////////////+-+-------+-------+ - // - // We have a device-pixel array of contributions. We map the fractional - // interval to this array of device pixels: Fully overlapping pixels are - // 1, and the partial overlapping pixels are the degree of overlap. - - // | - // v - // - // +-------+-------+-------+-------+-------+-------+-------+-------+ - // | | | |///////////////+-------+ | | - // | | | |///////////////////////| | | - // | | +-------+///////////////////////| | | - // +-------+-------+///////////////////////////////+-------+-------+ - - // Samples have two parts to be able to present the different CPU usages properly. - // This is because CPU usage number of a sample represents the CPU usage - // starting starting from the previous sample time to this sample time. - // These parts will be: - // - Between `sampleCategoryStartPixel` and `samplePixel` with beforeSampleCpuPercent. - // - Between `samplePixel` and `sampleCategoryEndPixel` with afterSampleCpuPercent. - - // Here we are accumulating the first part of the sample. It will use the - // CPU delta number that belongs to this sample. - // This part starts from the "sample start time" to "sample time" and uses - // beforeSampleCpuPercent. - for (let i = intCategoryStartPixel; i <= intSamplePixel; i++) { - percentageBuffer[i] += beforeSampleCpuPercent; +function _accumulateHalfSampleToKernelSum( + kernelCenterPosition: DevicePixels, + startPos: DevicePixels, + endPos: DevicePixels, + cpuPercent: number +): number { + function kernelVal(intPos: number): number { + const indexInSmoothingKernel = + intPos - kernelCenterPosition + SMOOTHING_RADIUS; + if ( + indexInSmoothingKernel < 0 || + indexInSmoothingKernel >= SMOOTHING_RADIUS * 2 + 1 + ) { + return 0; + } + return SMOOTHING_KERNEL[indexInSmoothingKernel]; } - // Subtract the partial pixels from start and end of the first part. - percentageBuffer[intCategoryStartPixel] -= - beforeSampleCpuPercent * (sampleCategoryStartPixel - intCategoryStartPixel); - percentageBuffer[intSamplePixel] -= - beforeSampleCpuPercent * (1 - (samplePixel - intSamplePixel)); - - // Here we are accumulating the second part of the sample. It will use the - // CPU delta number that belongs to the next sample. - // This part starts from "sample time" to "sample end time" and uses - // afterSampleCpuPercent. - for (let i = intSamplePixel; i <= intCategoryEndPixel; i++) { - percentageBuffer[i] += afterSampleCpuPercent; - } + const intStartPos = startPos >> FIXED_POINT_BITS; + const intEndPos = endPos >> FIXED_POINT_BITS; + + let sum = 0; - // Subtract the partial pixels from start and end of the second part. - percentageBuffer[intSamplePixel] -= - afterSampleCpuPercent * (samplePixel - intSamplePixel); - percentageBuffer[intCategoryEndPixel] -= - afterSampleCpuPercent * - (1 - (sampleCategoryEndPixel - intCategoryEndPixel)); + if (intStartPos === intEndPos) { + const frac = (endPos - startPos) / (1 << FIXED_POINT_BITS); + sum += kernelVal(intStartPos) * cpuPercent * frac; + } else { + if (intStartPos + 1 < intEndPos) { + for (let i = intStartPos + 1; i < intEndPos; i++) { + sum += kernelVal(i) * cpuPercent; + } + } + + const startPosFrac = + (startPos & FIXED_POINT_MASK) / (1 << FIXED_POINT_BITS); + sum += kernelVal(intStartPos) * cpuPercent * (1 - startPosFrac); + const endPosFrac = (endPos & FIXED_POINT_MASK) / (1 << FIXED_POINT_BITS); + sum += kernelVal(intEndPos) * cpuPercent * endPosFrac; + } + return sum; } + +type TypedArray = { + readonly length: number; + [n: number]: number; + BYTES_PER_ELEMENT: number; + set(array: T, offset?: number): void; + slice(start?: number, end?: number): T; +}; + /** * Apply a 1d box blur to a destination array. */ -function _boxBlur1D( - srcArray: Float32Array, - destArray: Float32Array, +function _boxBlur1D>( + srcArray: T, + destArray: T, radius: number ): void { if (srcArray.length < radius) { @@ -862,12 +952,12 @@ function _boxBlur1D( /** * Apply a blur with a gaussian distribution to a destination array. */ -function _applyGaussianBlur1D( - srcArray: Float32Array, +function _applyGaussianBlur1D>( + srcArray: T, boxBlurRadii: number[] ): void { let a = srcArray; - let b = new Float32Array(srcArray.length); + let b = srcArray.slice(); for (const radius of boxBlurRadii) { _boxBlur1D(a, b, radius); [b, a] = [a, b]; @@ -877,8 +967,6 @@ function _applyGaussianBlur1D( // The last blur was applied to the temporary array, blit the final values back // to the srcArray. This ensures that we are always mutating the values of the // src array, and not returning the newly created array. - for (let i = 0; i < srcArray.length; i++) { - srcArray[i] = a[i]; - } + srcArray.set(a); } } diff --git a/src/components/shared/thread/HeightGraph.tsx b/src/components/shared/thread/HeightGraph.tsx index 25b75dc4ac..d117ad19fb 100644 --- a/src/components/shared/thread/HeightGraph.tsx +++ b/src/components/shared/thread/HeightGraph.tsx @@ -4,7 +4,10 @@ import { PureComponent } from 'react'; import classNames from 'classnames'; import { timeCode } from 'firefox-profiler/utils/time-code'; -import { getSampleIndexClosestToStartTime } from 'firefox-profiler/profile-logic/profile-data'; +import { + getSampleIndexClosestToStartTime, + toSelectedState, +} from 'firefox-profiler/profile-logic/profile-data'; import { bisectionRight } from 'firefox-profiler/utils/bisect'; import { BLUE_70, BLUE_40 } from 'photon-colors'; import './HeightGraph.css'; @@ -14,6 +17,7 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, + SampleRelationToNode, } from 'firefox-profiler/types'; import { SelectedState } from 'firefox-profiler/types'; @@ -129,7 +133,9 @@ export class ThreadHeightGraph extends PureComponent { continue; } - const state = sampleSelectedStates[i] as SelectedState; + const state = toSelectedState( + sampleSelectedStates[i] as SampleRelationToNode + ); if (state === SelectedState.FilteredOutByTransform) { continue; } diff --git a/src/components/shared/thread/SampleGraph.tsx b/src/components/shared/thread/SampleGraph.tsx index 7790794440..b74ecaf628 100644 --- a/src/components/shared/thread/SampleGraph.tsx +++ b/src/components/shared/thread/SampleGraph.tsx @@ -6,7 +6,10 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { InView } from 'react-intersection-observer'; import { timeCode } from 'firefox-profiler/utils/time-code'; -import { getSampleIndexClosestToCenteredTime } from 'firefox-profiler/profile-logic/profile-data'; +import { + getSampleIndexClosestToCenteredTime, + toSelectedState, +} from 'firefox-profiler/profile-logic/profile-data'; import { bisectionRight } from 'firefox-profiler/utils/bisect'; import { withSize } from 'firefox-profiler/components/shared/WithSize'; import { BLUE_40, BLUE_50, BLUE_70 } from 'photon-colors'; @@ -26,6 +29,7 @@ import type { Milliseconds, CssPixels, ImplementationFilter, + SampleRelationToNode, } from 'firefox-profiler/types'; import { SelectedState } from 'firefox-profiler/types'; import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; @@ -186,7 +190,9 @@ class ThreadSampleGraphCanvas extends React.PureComponent { if (sampleTime < nextMinTime) { continue; } - const state = sampleSelectedStates[i] as SelectedState; + const state = toSelectedState( + sampleSelectedStates[i] as SampleRelationToNode + ); if (state === SelectedState.FilteredOutByTransform) { continue; } diff --git a/src/components/sidebar/CallTreeSidebar.tsx b/src/components/sidebar/CallTreeSidebar.tsx index 8e0fac7817..9b0bd19dd9 100644 --- a/src/components/sidebar/CallTreeSidebar.tsx +++ b/src/components/sidebar/CallTreeSidebar.tsx @@ -319,7 +319,11 @@ class CallTreeSidebarImpl extends React.PureComponent { const totalTimePercent = Math.round((totalTime.value / rootTime) * 100); const selfTimePercent = Math.round((selfTime.value / rootTime) * 100); const totalTimeBreakdownByCategory = totalTime.breakdownByCategory; - const selfTimeBreakdownByCategory = selfTime.breakdownByCategory; + // For inverted root nodes, self === total, so showing a separate self + // breakdown would be redundant. + const selfTimeBreakdownByCategory = timings.isInvertedRoot + ? null + : selfTime.breakdownByCategory; return (