diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 62977a680a..ae24cd91d3 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -383,6 +383,12 @@ MarkerContextMenu--copy-url = Copy URL MarkerContextMenu--copy-page-url = Copy page URL MarkerContextMenu--copy-as-json = Copy as JSON +# This string is used on the marker context menu item when right clicked on any marker. +# It drops the samples outside of given markers. +# Variables: +# $markerName (String) - Name of the marker that is selected. +MarkerContextMenu--drop-samples-outside-of-marker = Drop samples outside of “{ $markerName }” markers + # This string is used on the marker context menu item when right clicked on an # IPC marker. # Variables: @@ -950,6 +956,11 @@ TransformNavigator--collapse-direct-recursion-only = Collapse direct recursion o # $item (String) - Name of the function that transform applied to. TransformNavigator--collapse-function-subtree = Collapse subtree: { $item } +# "Drop samples outside of markers" transform. +# Variables: +# $item (String) - Name of the markers that transform will filter to. +TransformNavigator--drop-samples-outside-of-markers = Drop samples outside of markers: { $item } + ## "Bottom box" - a view which contains the source view and the assembly view, ## at the bottom of the profiler UI ## diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 8bfba53344..2b747b40d0 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -363,6 +363,10 @@ class CallNodeContextMenuImpl extends React.PureComponent { }); break; } + case 'filter-samples': + throw new Error( + "Filter samples transform can't be applied from the call node context menu." + ); default: assertExhaustiveCheck(type); } diff --git a/src/components/shared/MarkerContextMenu.css b/src/components/shared/MarkerContextMenu.css index 22d06d6859..8c04b4b46e 100644 --- a/src/components/shared/MarkerContextMenu.css +++ b/src/components/shared/MarkerContextMenu.css @@ -51,6 +51,10 @@ background-image: url(firefox-profiler-res/img/svg/select-thread.svg); } +.markerContextMenuIconFilterSamples { + background-image: url(firefox-profiler-res/img/svg/drop-icon.svg); +} + /* These icons don't look nice when selected, unless if we invert them. However * the other icons look better without the filter, so we don't change them in * this state. */ @@ -63,6 +67,7 @@ .react-contextmenu-item--selected .markerContextMenuIconCopyPayload, .react-contextmenu-item:hover .markerContextMenuIconCopyPayload, .react-contextmenu-item--selected .markerContextMenuSelectThread, -.react-contextmenu-item:hover .markerContextMenuSelectThread { +.react-contextmenu-item:hover .markerContextMenuSelectThread, +.react-contextmenu-item:hover .markerContextMenuIconFilterSamples { filter: invert(1); } diff --git a/src/components/shared/MarkerContextMenu.js b/src/components/shared/MarkerContextMenu.js index a6318580c2..54f654fcfe 100644 --- a/src/components/shared/MarkerContextMenu.js +++ b/src/components/shared/MarkerContextMenu.js @@ -15,7 +15,9 @@ import { setContextMenuVisibility, updatePreviewSelection, selectTrackFromTid, + addTransformToStack, } from 'firefox-profiler/actions/profile-view'; +import { changeSelectedTab } from 'firefox-profiler/actions/app'; import { getPreviewSelection, getCommittedRange, @@ -60,7 +62,7 @@ type StateProps = {| +markerIndex: MarkerIndex, +previewSelection: PreviewSelection, +committedRange: StartEndRange, - +thread: Thread | null, + +thread: Thread, +implementationFilter: ImplementationFilter, +getMarkerLabelToCopy: (MarkerIndex) => string, +profiledThreadIds: Set, @@ -71,6 +73,8 @@ type DispatchProps = {| +updatePreviewSelection: typeof updatePreviewSelection, +setContextMenuVisibility: typeof setContextMenuVisibility, +selectTrackFromTid: typeof selectTrackFromTid, + +addTransformToStack: typeof addTransformToStack, + +changeSelectedTab: typeof changeSelectedTab, |}; type Props = ConnectedProps; @@ -182,6 +186,25 @@ class MarkerContextMenuImpl extends PureComponent { copy(getMarkerLabelToCopy(markerIndex)); }; + filterByMarkerName = () => { + const { + rightClickedMarkerInfo, + marker, + addTransformToStack, + changeSelectedTab, + thread, + } = this.props; + const { threadsKey } = rightClickedMarkerInfo; + addTransformToStack(threadsKey, { + type: 'filter-samples', + filterType: 'marker', + filter: thread.stringTable.indexForString(marker.name), + }); + + // Then change the selected tab to call tree. + changeSelectedTab('calltree'); + }; + copyMarkerCause = () => { const { marker } = this.props; @@ -479,7 +502,32 @@ class MarkerContextMenuImpl extends PureComponent { )} + {/* Only show this context menu item for interval markers */} + {marker.start !== null && marker.end !== null ? ( + <> +
+ + + + }} + > + {/* Using a fragment here so we can have a strong tag inside. */} + <> + Drop samples outside of “{marker.name}” + markers + + + + + ) : null} +
+ @@ -536,6 +584,8 @@ const MarkerContextMenu = explicitConnect({ updatePreviewSelection, setContextMenuVisibility, selectTrackFromTid, + addTransformToStack, + changeSelectedTab, }, component: MarkerContextMenuImpl, }); diff --git a/src/profile-logic/transforms.js b/src/profile-logic/transforms.js index 62bd2f7a16..37724573ea 100644 --- a/src/profile-logic/transforms.js +++ b/src/profile-logic/transforms.js @@ -40,6 +40,11 @@ import type { TransformType, TransformStack, ProfileMeta, + StartEndRange, + FilterSamplesType, + Marker, + Milliseconds, + IndexIntoStringTable, } from 'firefox-profiler/types'; /** @@ -61,6 +66,7 @@ const SHORT_KEY_TO_TRANSFORM: { [string]: TransformType } = {}; 'collapse-direct-recursion', 'collapse-recursion', 'collapse-function-subtree', + 'filter-samples', ].forEach((transform: TransformType) => { // This is kind of an awkward switch, but it ensures we've exhaustively checked that // we have a mapping for every transform. @@ -96,6 +102,9 @@ const SHORT_KEY_TO_TRANSFORM: { [string]: TransformType } = {}; case 'collapse-function-subtree': shortKey = 'cfs'; break; + case 'filter-samples': + shortKey = 'fs'; + break; default: { throw assertExhaustiveCheck(transform); } @@ -268,6 +277,19 @@ export function parseTransforms(transformString: string): TransformStack { break; } + case 'filter-samples': { + // e.g. "fs-m-BackboneJS-TodoMVC.Adding100Items-async" + const [, shortFilterType, filter] = tuple; + const filterType = convertToFullFilterType(shortFilterType); + const filterIndex = parseInt(filter, 10); + + transforms.push({ + type: 'filter-samples', + filterType, + filter: filterIndex, + }); + break; + } default: throw assertExhaustiveCheck(type); } @@ -275,6 +297,30 @@ export function parseTransforms(transformString: string): TransformStack { return transforms; } +/** + * Convert the shortened filter type into the full filter type. + */ +function convertToFullFilterType(shortFilterType: string): FilterSamplesType { + switch (shortFilterType) { + case 'm': + return 'marker'; + default: + throw new Error('Unknown filter type.'); + } +} + +/** + * Convert the full filter type into the shortened filter type. + */ +function convertToShortFilterType(filterType: FilterSamplesType): string { + switch (filterType) { + case 'marker': + return 'm'; + default: + throw assertExhaustiveCheck(filterType); + } +} + /** * Each transform in the stack is separated by a "~". */ @@ -317,6 +363,10 @@ export function stringifyTransforms(transformStack: TransformStack): string { } return string; } + case 'filter-samples': + return `${shortKey}-${convertToShortFilterType( + transform.filterType + )}-${transform.filter}`; default: throw assertExhaustiveCheck(transform); } @@ -364,6 +414,18 @@ export function getTransformLabelL10nIds( }; } + if (transform.type === 'filter-samples') { + switch (transform.filterType) { + case 'marker': + return { + l10nId: 'TransformNavigator--drop-samples-outside-of-markers', + item: stringTable.getString(transform.filter), + }; + default: + throw assertExhaustiveCheck(transform.filterType); + } + } + // Lookup function name. let funcIndex; switch (transform.type) { @@ -474,6 +536,15 @@ export function applyTransformToCallNodePath( transform.funcIndex, callNodePath ); + case 'filter-samples': + // There's nothing to update in the call node path. But this call node path + // could disappear if we filtered out all the samples with this path. + // This is also the case for drop-function transform. We need to have a + // generic mechanism for: if the selected call node (after the transformation + // has been applied to the call path) is not present in the call tree, run + // some generic code that finds a close-by call node which is present. + // See: https://github.com/firefox-devtools/profiler/issues/4618 + return callNodePath; default: throw assertExhaustiveCheck(transform); } @@ -1674,10 +1745,107 @@ export function funcHasRecursiveCall( return false; } +function _findRangesByMarkerFilter( + markers: Marker[], + filter: string +): StartEndRange[] { + const ranges = []; + + for (let markerIndex = 0; markerIndex < markers.length; markerIndex++) { + const { name, start, end } = markers[markerIndex]; + + if (start === null || end === null) { + // This is not an interval marker, so we can't use it as a range. + continue; + } + + if (name === filter) { + ranges.push({ start: start, end: end }); + } + } + return ranges; +} + +/** + * Find the sample ranges to filter depending on the filter type, then go + * through all the samples and remove the ones that are outside of the ranges. + */ +export function filterSamples( + thread: Thread, + markers: Marker[], + filterType: FilterSamplesType, + filter: IndexIntoStringTable +): Thread { + return timeCode('filterSamples', () => { + // Find the ranges to filter. + const filterString = thread.stringTable.getString(filter); + let ranges: StartEndRange[]; + switch (filterType) { + case 'marker': + ranges = _findRangesByMarkerFilter(markers, filterString); + 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, + time: Milliseconds[], + length: number, + } + >(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; + break; + } + } + + if (!sampleInRange) { + newTable.stack[tableIndex] = null; + } + } + + return newTable; + } + + const newThread = { + ...thread, + samples: filterTable(samples), + }; + + 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 newThread; + }); +} + export function applyTransform( thread: Thread, transform: Transform, - defaultCategory: IndexIntoCategoryList + defaultCategory: IndexIntoCategoryList, + markers: Marker[] ): Thread { switch (transform.type) { case 'focus-subtree': @@ -1727,6 +1895,13 @@ export function applyTransform( transform.funcIndex, defaultCategory ); + case 'filter-samples': + return filterSamples( + thread, + markers, + transform.filterType, + transform.filter + ); default: throw assertExhaustiveCheck(transform); } diff --git a/src/selectors/per-thread/index.js b/src/selectors/per-thread/index.js index 4881cc481c..5554e0f494 100644 --- a/src/selectors/per-thread/index.js +++ b/src/selectors/per-thread/index.js @@ -7,7 +7,8 @@ import memoize from 'memoize-immutable'; import * as UrlState from '../url-state'; import * as ProfileData from '../../profile-logic/profile-data'; import { - getThreadSelectorsPerThread, + getThreadSelectorsWithMarkersPerThread, + getBasicThreadSelectorsPerThread, type ThreadSelectorsPerThread, } from './thread'; import { @@ -152,12 +153,26 @@ function _buildThreadSelectors( threadIndexes: Set, threadsKey: ThreadsKey = ProfileData.getThreadsKey(threadIndexes) ) { - // We define the thread selectors in 3 steps to ensure clarity in the + // We define the thread selectors in 5 steps to ensure clarity in the // separate files. - // 1. The basic selectors. - let selectors = getThreadSelectorsPerThread(threadIndexes, threadsKey); - // 2. Stack, sample and marker selectors that need the previous basic - // selectors for their own definition. + // 1. The basic thread selectors. + let selectors = getBasicThreadSelectorsPerThread(threadIndexes, threadsKey); + // 2. The marker selectors. + selectors = { + ...selectors, + ...getMarkerSelectorsPerThread(selectors, threadIndexes, threadsKey), + }; + // 3. The thread selectors that need marker selectors. + selectors = { + ...selectors, + ...getThreadSelectorsWithMarkersPerThread( + selectors, + threadIndexes, + threadsKey + ), + }; + // 4. Stack, sample selectors that need the previous selectors for their + // own definition. selectors = { ...selectors, ...getStackAndSampleSelectorsPerThread( @@ -165,9 +180,8 @@ function _buildThreadSelectors( threadIndexes, threadsKey ), - ...getMarkerSelectorsPerThread(selectors, threadIndexes, threadsKey), }; - // 3. Other selectors that need selectors from different files to be defined. + // 5. Other selectors that need selectors from different files to be defined. selectors = { ...selectors, ...getComposedSelectorsPerThread(selectors), diff --git a/src/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 96b5760e20..3b0228b6e9 100644 --- a/src/selectors/per-thread/markers.js +++ b/src/selectors/per-thread/markers.js @@ -13,7 +13,7 @@ import * as ProfileSelectors from '../profile'; import { getRightClickedMarkerInfo } from '../right-clicked-marker'; import { getLabelGetter } from '../../profile-logic/marker-schema'; -import type { ThreadSelectorsPerThread } from './thread'; +import type { BasicThreadSelectorsPerThread } from './thread'; import type { RawMarkerTable, ThreadIndex, @@ -43,7 +43,7 @@ export type MarkerSelectorsPerThread = $ReturnType< * Create the selectors for a thread that have to do with either markers. */ export function getMarkerSelectorsPerThread( - threadSelectors: ThreadSelectorsPerThread, + threadSelectors: BasicThreadSelectorsPerThread, threadIndexes: Set, threadsKey: ThreadsKey ) { @@ -610,6 +610,7 @@ export function getMarkerSelectorsPerThread( getTimelineJankMarkerIndexes, getDerivedMarkerInfo, getMarkerIndexToRawMarkerIndexes, + getFullMarkerList, getFullMarkerListIndexes, getNetworkMarkerIndexes, getSearchFilteredNetworkMarkerIndexes, diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 41554ee576..674eefd6cd 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -44,6 +44,7 @@ import type { } from 'firefox-profiler/types'; import type { ThreadSelectorsPerThread } from './thread'; +import type { MarkerSelectorsPerThread } from './markers'; /** * Infer the return type from the getStackAndSampleSelectorsPerThread function. This @@ -54,11 +55,16 @@ export type StackAndSampleSelectorsPerThread = $ReturnType< typeof getStackAndSampleSelectorsPerThread >; +type ThreadAndMarkerSelectorsPerThread = {| + ...ThreadSelectorsPerThread, + ...MarkerSelectorsPerThread, +|}; + /** * Create the selectors for a thread that have to do with either stacks or samples. */ export function getStackAndSampleSelectorsPerThread( - threadSelectors: ThreadSelectorsPerThread, + threadSelectors: ThreadAndMarkerSelectorsPerThread, threadIndexes: Set, threadsKey: ThreadsKey ) { diff --git a/src/selectors/per-thread/thread.js b/src/selectors/per-thread/thread.js index 0d4da59e77..76bae8b963 100644 --- a/src/selectors/per-thread/thread.js +++ b/src/selectors/per-thread/thread.js @@ -44,24 +44,30 @@ import type { import type { UniqueStringArray } from '../../utils/unique-string-array'; import type { TransformLabeL10nIds } from 'firefox-profiler/profile-logic/transforms'; +import type { MarkerSelectorsPerThread } from './markers'; import { mergeThreads } from '../../profile-logic/merge-compare'; import { defaultThreadViewOptions } from '../../reducers/profile-view'; /** - * Infer the return type from the getThreadSelectorsPerThread function. This - * is done that so that the local type definition with `Selector` is the canonical - * definition for the type of the selector. + * Infer the return type from the getBasicThreadSelectorsPerThread and + * getThreadSelectorsWithMarkersPerThread functions. This is done that so that + * the local type definition with `Selector` is the canonical definition for + * the type of the selector. */ -export type ThreadSelectorsPerThread = $ReturnType< - typeof getThreadSelectorsPerThread +export type BasicThreadSelectorsPerThread = $ReturnType< + typeof getBasicThreadSelectorsPerThread >; +export type ThreadSelectorsPerThread = {| + ...BasicThreadSelectorsPerThread, + ...$ReturnType, +|}; /** * Create the selectors for a thread that have to do with an entire thread. This includes * the general filtering pipeline for threads. */ -export function getThreadSelectorsPerThread( +export function getBasicThreadSelectorsPerThread( threadIndexes: Set, threadsKey: ThreadsKey ) { @@ -172,74 +178,6 @@ export function getThreadSelectorsPerThread( } ); - // It becomes very expensive to apply each transform over and over again as they - // typically take around 100ms to run per transform on a fast machine. Memoize - // memoize each step individually so that they transform stack can be pushed and - // popped frequently and easily. - const _applyTransformMemoized = memoize(Transforms.applyTransform, { - cache: new MixedTupleMap(), - }); - - const getTransformStack: Selector = (state) => - UrlState.getTransformStack(state, threadsKey); - - const getRangeAndTransformFilteredThread: Selector = createSelector( - getRangeFilteredThread, - getTransformStack, - ProfileSelectors.getDefaultCategory, - (startingThread, transforms, defaultCategory) => { - return transforms.reduce( - // Apply the reducer using an arrow function to ensure correct memoization. - (thread, transform) => - _applyTransformMemoized(thread, transform, defaultCategory), - startingThread - ); - } - ); - - const _getImplementationFilteredThread: Selector = createSelector( - getRangeAndTransformFilteredThread, - UrlState.getImplementationFilter, - ProfileSelectors.getDefaultCategory, - ProfileData.filterThreadByImplementation - ); - - const _getImplementationAndSearchFilteredThread: Selector = - createSelector( - _getImplementationFilteredThread, - UrlState.getSearchStrings, - (thread, searchStrings) => { - return ProfileData.filterThreadToSearchStrings(thread, searchStrings); - } - ); - - const getFilteredThread: Selector = createSelector( - _getImplementationAndSearchFilteredThread, - UrlState.getInvertCallstack, - ProfileSelectors.getDefaultCategory, - (thread, shouldInvertCallstack, defaultCategory) => { - return shouldInvertCallstack - ? ProfileData.invertCallstack(thread, defaultCategory) - : thread; - } - ); - - const getPreviewFilteredThread: Selector = createSelector( - getFilteredThread, - ProfileSelectors.getPreviewSelection, - (thread, previewSelection): Thread => { - if (!previewSelection.hasSelection) { - return thread; - } - const { selectionStart, selectionEnd } = previewSelection; - return ProfileData.filterThreadSamplesToRange( - thread, - selectionStart, - selectionEnd - ); - } - ); - /** * The CallTreeSummaryStrategy determines how the call tree summarizes the * the current thread. By default, this is done by timing, but other @@ -295,20 +233,6 @@ export function getThreadSelectorsPerThread( CallTree.extractSamplesLikeTable ); - const getFilteredSamplesForCallTree: Selector = - createSelector( - getFilteredThread, - getCallTreeSummaryStrategy, - CallTree.extractSamplesLikeTable - ); - - const getPreviewFilteredSamplesForCallTree: Selector = - createSelector( - getPreviewFilteredThread, - getCallTreeSummaryStrategy, - CallTree.extractSamplesLikeTable - ); - /** * This selector returns the offset to add to a sampleIndex when accessing the * base thread, if your thread is a range filtered thread (all but the base @@ -328,29 +252,6 @@ export function getThreadSelectorsPerThread( } ); - /** - * This selector returns the offset to add to a sampleIndex when accessing the - * base thread, if your thread is the preview filtered thread. - */ - const getSampleIndexOffsetFromPreviewRange: Selector = createSelector( - getFilteredSamplesForCallTree, - ProfileSelectors.getPreviewSelection, - getSampleIndexOffsetFromCommittedRange, - (samples, previewSelection, sampleIndexFromCommittedRange) => { - if (!previewSelection.hasSelection) { - return sampleIndexFromCommittedRange; - } - - const [beginSampleIndex] = ProfileData.getSampleIndexRangeForSelection( - samples, - previewSelection.selectionStart, - previewSelection.selectionEnd - ); - - return sampleIndexFromCommittedRange + beginSampleIndex; - } - ); - const getFriendlyThreadName: Selector = createSelector( ProfileSelectors.getThreads, getThread, @@ -363,27 +264,6 @@ export function getThreadSelectorsPerThread( ProfileData.getThreadProcessDetails ); - const getTransformLabelL10nIds: Selector = - createSelector( - ProfileSelectors.getMeta, - getRangeAndTransformFilteredThread, - getFriendlyThreadName, - getTransformStack, - Transforms.getTransformLabelL10nIds - ); - - const getLocalizedTransformLabels: Selector = createSelector( - getTransformLabelL10nIds, - (transformL10nIds) => - transformL10nIds.map((transform) => ( - - )) - ); - const getViewOptions: Selector = (state) => ProfileSelectors.getProfileViewOptions(state).perThread[threadsKey] || defaultThreadViewOptions; @@ -478,20 +358,11 @@ export function getThreadSelectorsPerThread( getNativeAllocations, getJsAllocations, getThreadRange, - getFilteredThread, getRangeFilteredThread, - getRangeAndTransformFilteredThread, - getPreviewFilteredThread, getUnfilteredSamplesForCallTree, - getFilteredSamplesForCallTree, - getPreviewFilteredSamplesForCallTree, getSampleIndexOffsetFromCommittedRange, - getSampleIndexOffsetFromPreviewRange, getFriendlyThreadName, getThreadProcessDetails, - getTransformLabelL10nIds, - getLocalizedTransformLabels, - getTransformStack, getViewOptions, getJsTracerTable, getExpensiveJsTracerTiming, @@ -507,3 +378,153 @@ export function getThreadSelectorsPerThread( getCallTreeSummaryStrategy, }; } + +type BasicThreadAndMarkerSelectorsPerThread = {| + ...BasicThreadSelectorsPerThread, + ...MarkerSelectorsPerThread, +|}; + +export function getThreadSelectorsWithMarkersPerThread( + threadSelectors: BasicThreadAndMarkerSelectorsPerThread, + threadIndexes: Set, + threadsKey: ThreadsKey +) { + // It becomes very expensive to apply each transform over and over again as they + // typically take around 100ms to run per transform on a fast machine. Memoize + // memoize each step individually so that they transform stack can be pushed and + // popped frequently and easily. + const _applyTransformMemoized = memoize(Transforms.applyTransform, { + cache: new MixedTupleMap(), + }); + + const getTransformStack: Selector = (state) => + UrlState.getTransformStack(state, threadsKey); + + const getRangeAndTransformFilteredThread: Selector = createSelector( + threadSelectors.getRangeFilteredThread, + getTransformStack, + ProfileSelectors.getDefaultCategory, + threadSelectors.getFullMarkerList, + (startingThread, transforms, defaultCategory, markers) => { + return transforms.reduce( + // Apply the reducer using an arrow function to ensure correct memoization. + (thread, transform) => + _applyTransformMemoized(thread, transform, defaultCategory, markers), + startingThread + ); + } + ); + + const _getImplementationFilteredThread: Selector = createSelector( + getRangeAndTransformFilteredThread, + UrlState.getImplementationFilter, + ProfileSelectors.getDefaultCategory, + ProfileData.filterThreadByImplementation + ); + + const _getImplementationAndSearchFilteredThread: Selector = + createSelector( + _getImplementationFilteredThread, + UrlState.getSearchStrings, + (thread, searchStrings) => { + return ProfileData.filterThreadToSearchStrings(thread, searchStrings); + } + ); + + const getFilteredThread: Selector = createSelector( + _getImplementationAndSearchFilteredThread, + UrlState.getInvertCallstack, + ProfileSelectors.getDefaultCategory, + (thread, shouldInvertCallstack, defaultCategory) => { + return shouldInvertCallstack + ? ProfileData.invertCallstack(thread, defaultCategory) + : thread; + } + ); + + const getPreviewFilteredThread: Selector = createSelector( + getFilteredThread, + ProfileSelectors.getPreviewSelection, + (thread, previewSelection): Thread => { + if (!previewSelection.hasSelection) { + return thread; + } + const { selectionStart, selectionEnd } = previewSelection; + return ProfileData.filterThreadSamplesToRange( + thread, + selectionStart, + selectionEnd + ); + } + ); + + const getFilteredSamplesForCallTree: Selector = + createSelector( + getFilteredThread, + threadSelectors.getCallTreeSummaryStrategy, + CallTree.extractSamplesLikeTable + ); + + const getPreviewFilteredSamplesForCallTree: Selector = + createSelector( + getPreviewFilteredThread, + threadSelectors.getCallTreeSummaryStrategy, + CallTree.extractSamplesLikeTable + ); + + /** + * This selector returns the offset to add to a sampleIndex when accessing the + * base thread, if your thread is the preview filtered thread. + */ + const getSampleIndexOffsetFromPreviewRange: Selector = createSelector( + getFilteredSamplesForCallTree, + ProfileSelectors.getPreviewSelection, + threadSelectors.getSampleIndexOffsetFromCommittedRange, + (samples, previewSelection, sampleIndexFromCommittedRange) => { + if (!previewSelection.hasSelection) { + return sampleIndexFromCommittedRange; + } + + const [beginSampleIndex] = ProfileData.getSampleIndexRangeForSelection( + samples, + previewSelection.selectionStart, + previewSelection.selectionEnd + ); + + return sampleIndexFromCommittedRange + beginSampleIndex; + } + ); + + const getTransformLabelL10nIds: Selector = + createSelector( + ProfileSelectors.getMeta, + getRangeAndTransformFilteredThread, + threadSelectors.getFriendlyThreadName, + getTransformStack, + Transforms.getTransformLabelL10nIds + ); + + const getLocalizedTransformLabels: Selector = createSelector( + getTransformLabelL10nIds, + (transformL10nIds) => + transformL10nIds.map((transform) => ( + + )) + ); + + return { + getTransformStack, + getRangeAndTransformFilteredThread, + getFilteredThread, + getPreviewFilteredThread, + getFilteredSamplesForCallTree, + getPreviewFilteredSamplesForCallTree, + getSampleIndexOffsetFromPreviewRange, + getTransformLabelL10nIds, + getLocalizedTransformLabels, + }; +} diff --git a/src/test/components/MarkerTable.test.js b/src/test/components/MarkerTable.test.js index e1ef7be42e..41ea969795 100644 --- a/src/test/components/MarkerTable.test.js +++ b/src/test/components/MarkerTable.test.js @@ -141,7 +141,8 @@ describe('MarkerTable', function () { fireFullContextMenu(getByText(/setTimeout/)); checkMenuIsDisplayedForNode(/setTimeout/); - expect(getRowElement(/setTimeout/)).toHaveClass('isRightClicked'); + // There is another text with setTimeout inside the context menu. Pick the right one. + expect(getRowElement(/setTimeout\s/)).toHaveClass('isRightClicked'); // Wait that all timers are done before trying again. jest.runAllTimers(); diff --git a/src/test/components/__snapshots__/MarkerChart.test.js.snap b/src/test/components/__snapshots__/MarkerChart.test.js.snap index 199cb5b4af..b44be478a5 100644 --- a/src/test/components/__snapshots__/MarkerChart.test.js.snap +++ b/src/test/components/__snapshots__/MarkerChart.test.js.snap @@ -133,6 +133,24 @@ exports[`MarkerChart context menus displays when right clicking on a marker 1`]
+ +