Skip to content
Merged
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: 34 additions & 5 deletions src/profile-logic/profile-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2101,16 +2101,19 @@ export function invertCallstack(
* out the manipulation of the data structures so that we can properly update
* the stack table and any possible allocation information.
*/
export function updateThreadStacks(
export function updateThreadStacksByGeneratingNewStackColumns(
thread: Thread,
newStackTable: StackTable,
convertStack: (IndexIntoStackTable | null) => IndexIntoStackTable | null
computeFilteredStackColumn: (
Array<IndexIntoStackTable | null>,
Array<Milliseconds>
) => Array<IndexIntoStackTable | null>
): Thread {
const { jsAllocations, nativeAllocations, samples } = thread;

const newSamples = {
...samples,
stack: samples.stack.map((oldStack) => convertStack(oldStack)),
stack: computeFilteredStackColumn(samples.stack, samples.time),
};

const newThread = {
Expand All @@ -2120,21 +2123,47 @@ export function updateThreadStacks(
};

if (jsAllocations) {
// Filter the JS allocations if there are any.
newThread.jsAllocations = {
...jsAllocations,
stack: jsAllocations.stack.map((oldStack) => convertStack(oldStack)),
stack: computeFilteredStackColumn(
jsAllocations.stack,
jsAllocations.time
),
};
}
if (nativeAllocations) {
// Filter the native allocations if there are any.
newThread.nativeAllocations = {
...nativeAllocations,
stack: nativeAllocations.stack.map((oldStack) => convertStack(oldStack)),
stack: computeFilteredStackColumn(
nativeAllocations.stack,
nativeAllocations.time
),
};
}

return newThread;
}

/**
* A simpler variant of updateThreadStacksByGeneratingNewStackColumns which just
* accepts a convertStack function. Use this when you don't need to filter by
* sample timestamp.
*/
export function updateThreadStacks(
thread: Thread,
newStackTable: StackTable,
convertStack: (IndexIntoStackTable | null) => IndexIntoStackTable | null
): Thread {
return updateThreadStacksByGeneratingNewStackColumns(
thread,
newStackTable,
(stackColumn, _timeColumn) =>
stackColumn.map((oldStack) => convertStack(oldStack))
);
}

/**
* When manipulating stack tables, the most common operation is to map from one
* stack to a new stack using a Map. This function returns another function that
Expand Down
103 changes: 50 additions & 53 deletions src/profile-logic/transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
toValidImplementationFilter,
getCallNodeIndexFromPath,
updateThreadStacks,
updateThreadStacksByGeneratingNewStackColumns,
getMapStackUpdater,
getCallNodeIndexFromParentAndFunc,
} from './profile-data';
import { timeCode } from '../utils/time-code';
import { assertExhaustiveCheck, convertToTransformType } from '../utils/flow';
import { canonicalizeRangeSet } from '../utils/range-set';
import { CallTree } from '../profile-logic/call-tree';
import { getSearchFilteredMarkerIndexes } from '../profile-logic/marker-data';
import {
Expand Down Expand Up @@ -1799,71 +1801,66 @@ export function filterSamples(
): Thread {
return timeCode('filterSamples', () => {
// Find the ranges to filter.
let ranges: StartEndRange[];
switch (filterType) {
case 'marker-search':
ranges = _findRangesByMarkerFilter(
getMarker,
markerIndexes,
markerSchemaByName,
categoryList,
filter
);
break;
default:
throw assertExhaustiveCheck(filterType);
}

// Now let's go through all the samples and remove the ones that are outside
// of the ranges.
const { samples, jsAllocations, nativeAllocations } = thread;

function filterTable<
Table: {
stack: Array<IndexIntoStackTable | null>,
time: Milliseconds[],
length: number,
function getFilterRanges(): StartEndRange[] {
switch (filterType) {
case 'marker-search':
return _findRangesByMarkerFilter(
getMarker,
markerIndexes,
markerSchemaByName,
categoryList,
filter
);
default:
throw assertExhaustiveCheck(filterType);
}
>(table: Table): Table {
const newTable = {
...table,
stack: table.stack.slice(),
};

for (let tableIndex = 0; tableIndex < newTable.length; tableIndex++) {
const sampleTime = newTable.time[tableIndex];
}

let sampleInRange = false;
for (const { start, end } of ranges) {
if (sampleTime >= start && sampleTime <= end) {
sampleInRange = true;
const ranges = canonicalizeRangeSet(getFilterRanges());

function computeFilteredStackColumn(
originalStackColumn: Array<IndexIntoStackTable | null>,
timeColumn: Milliseconds[]
): Array<IndexIntoStackTable | null> {
const newStackColumn = originalStackColumn.slice();

// Walk the ranges and samples in order. Both are sorted by time.
// For each range, drop the samples before the range and skip the samples
// inside the range.
let sampleIndex = 0;
const sampleCount = timeColumn.length;
for (const range of ranges) {
const { start: rangeStart, end: rangeEnd } = range;
// Drop samples before the range.
for (; sampleIndex < sampleCount; sampleIndex++) {
if (timeColumn[sampleIndex] >= rangeStart) {
break;
}
newStackColumn[sampleIndex] = null;
}

if (!sampleInRange) {
newTable.stack[tableIndex] = null;
// Skip over samples inside the range.
for (; sampleIndex < sampleCount; sampleIndex++) {
if (timeColumn[sampleIndex] >= rangeEnd) {
break;
}
}
}

return newTable;
}

const newThread = {
...thread,
samples: filterTable(samples),
};
// Drop the remaining samples, i.e. the samples after the last range.
while (sampleIndex < sampleCount) {
newStackColumn[sampleIndex] = null;
sampleIndex++;
}

if (jsAllocations) {
// Filter the JS allocations if there are any.
newThread.jsAllocations = filterTable(jsAllocations);
}
if (nativeAllocations) {
// Filter the native allocations if there are any.
newThread.nativeAllocations = filterTable(nativeAllocations);
return newStackColumn;
}

return newThread;
return updateThreadStacksByGeneratingNewStackColumns(
thread,
thread.stackTable,
computeFilteredStackColumn
);
});
}

Expand Down
76 changes: 76 additions & 0 deletions src/test/unit/range-set.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow

import { canonicalizeRangeSet } from '../../utils/range-set';

describe('canonicalizeRangeSet', function () {
Comment thread
mstange marked this conversation as resolved.
it('sorts unsorted ranges', function () {
expect(
canonicalizeRangeSet([
{ start: 5, end: 6 },
{ start: 1, end: 2 },
])
).toEqual([
{ start: 1, end: 2 },
{ start: 5, end: 6 },
]);
});

it('absorbs nested ranges', function () {
expect(
canonicalizeRangeSet([
{ start: 1, end: 6 },
{ start: 3, end: 6 },
])
).toEqual([{ start: 1, end: 6 }]);
});

it('unifies overlapping ranges', function () {
expect(
canonicalizeRangeSet([
{ start: 1, end: 4 },
{ start: 3, end: 6 },
])
).toEqual([{ start: 1, end: 6 }]);
});

it('unifies adjacent ranges', function () {
expect(
canonicalizeRangeSet([
{ start: 1, end: 3 },
{ start: 3, end: 6 },
])
).toEqual([{ start: 1, end: 6 }]);
});

it('removes empty ranges', function () {
expect(
canonicalizeRangeSet([
{ start: 1, end: 3 },
{ start: 6, end: 6 },
])
).toEqual([{ start: 1, end: 3 }]);
});

it('handles this complicated example', function () {
expect(
canonicalizeRangeSet([
{ start: 1, end: 2.5 },
{ start: 1.5, end: 2 },
{ start: 1, end: 2 },
{ start: 1.7, end: 2.6 },
{ start: -4, end: -4 },
{ start: -4, end: -3.8 },
{ start: -3.5, end: 2.5 },
{ start: -3.5, end: -3 },
{ start: -6, end: -4 },
])
).toEqual([
{ start: -6, end: -3.8 },
{ start: -3.5, end: 2.6 },
]);
});
});
50 changes: 50 additions & 0 deletions src/utils/range-set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// @flow

import type { StartEndRange } from 'firefox-profiler/types';

/**
* Canonicalize the list of ranges, by OR'ing nested and overlapping ranges
* together so that the resulting list of ranges is a flat list of ranges which
* covers the same time values. The resulting list has the following properties:
*
* - Sorted by range.start
* - No empty ranges
* - No overlap
* - Adjacent ranges are collapsed into one
*/
export function canonicalizeRangeSet(ranges: StartEndRange[]): StartEndRange[] {
if (ranges.length === 0) {
return [];
}

const sortedRanges = ranges.slice().sort((a, b) => a.start - b.start);
let lastCanonRange = { ...sortedRanges[0] };
const canonRanges = [lastCanonRange];

for (let i = 1; i < sortedRanges.length; i++) {
const range = sortedRanges[i];
if (range.start >= range.end) {
// Empty or invalid range, skip.
continue;
}
Comment thread
mstange marked this conversation as resolved.

if (range.end <= lastCanonRange.end) {
// lastCanonRange already covers this range completely.
continue;
}

if (range.start <= lastCanonRange.end) {
// range's beginning overlaps lastCanonRange's end. Merge the two ranges.
lastCanonRange.end = range.end;
continue;
}

lastCanonRange = { ...range };
canonRanges.push(lastCanonRange);
}

return canonRanges;
}