From c0148a6e8752348bc68a370138889c78782c2a20 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Wed, 29 Apr 2020 13:14:42 -0500 Subject: [PATCH 1/3] Change showTabOnly to use TimelineTrackOrganization --- src/actions/receive-profile.js | 129 +++++++--- src/app-logic/url-handling.js | 182 ++++++++++---- src/components/timeline/FullTimeline.js | 57 +++-- src/components/timeline/TrackContextMenu.js | 23 +- src/components/timeline/index.js | 29 ++- src/reducers/profile-view.js | 5 + src/reducers/url-state.js | 26 +- src/selectors/app.js | 236 ++++++++++-------- src/selectors/per-thread/composed.js | 16 +- src/selectors/profile.js | 59 +++-- src/selectors/url-state.js | 8 +- src/test/components/ActiveTabTimeline.test.js | 12 +- .../components/FilterNavigatorBar.test.js | 10 +- src/test/components/Timeline.test.js | 15 +- src/test/fixtures/profiles/tracks.js | 8 + src/test/store/active-tab.test.js | 7 +- src/test/store/markers.test.js | 9 +- src/test/store/profile-view.test.js | 13 +- src/test/store/receive-profile.test.js | 48 +++- src/test/url-handling.test.js | 29 ++- src/types/actions.js | 2 +- src/types/state.js | 13 +- 22 files changed, 609 insertions(+), 327 deletions(-) diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index f426c6f725..22fbc91fd1 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -25,8 +25,12 @@ import { getLocalTrackOrderByPid, getLegacyThreadOrder, getLegacyHiddenThreads, - getShowTabOnly, -} from '../selectors/url-state'; + getTimelineTrackOrganization, + getProfileOrNull, + getProfile, + getView, + getRelevantPagesForActiveTab, +} from 'firefox-profiler/selectors'; import { stateFromLocation, getDataSourceFromPathParts, @@ -41,14 +45,8 @@ import { initializeHiddenGlobalTracks, getVisibleThreads, } from '../profile-logic/tracks'; -import { - getProfileOrNull, - getProfile, - getRelevantPagesForActiveTab, -} from '../selectors/profile'; -import { getView } from '../selectors/app'; -import { setDataSource } from './profile-view'; import { computeActiveTabTracks } from '../profile-logic/active-tab'; +import { setDataSource } from './profile-view'; import type { FunctionsUpdatePerThread, @@ -58,6 +56,7 @@ import type { } from '../types/actions'; import type { TransformStacksPerThread } from '../types/transforms'; import type { Action, ThunkAction, Dispatch } from '../types/store'; +import type { TimelineTrackOrganization } from '../types/state'; import type { Profile, ThreadIndex, @@ -91,6 +90,7 @@ export function waitingForProfileFromAddon(): Action { export function loadProfile( profile: Profile, config: $Shape<{| + timelineTrackOrganization: TimelineTrackOrganization, pathInZipFile: string, implementationFilter: ImplementationFilter, transformStacks: TransformStacksPerThread, @@ -130,7 +130,12 @@ export function loadProfile( // before finalizing profile view. That's why we are dispatching this action // after completing those steps inside `setupInitialUrlState`. if (initialLoad === false) { - await dispatch(finalizeProfileView(config.geckoProfiler)); + await dispatch( + finalizeProfileView( + config.geckoProfiler, + config.timelineTrackOrganization + ) + ); } }; } @@ -143,7 +148,8 @@ export function loadProfile( * functions in the src/profile-logic/tracks.js file. */ export function finalizeProfileView( - geckoProfiler?: $GeckoProfiler + geckoProfiler?: $GeckoProfiler, + timelineTrackOrganization?: TimelineTrackOrganization ): ThunkAction> { return async (dispatch, getState) => { const profile = getProfileOrNull(getState()); @@ -156,15 +162,50 @@ export function finalizeProfileView( // been seen before. If it's non-null, then there is profile view information // encoded into the URL. const selectedThreadIndex = getSelectedThreadIndexOrNull(getState()); + const pages = profile.pages; + if (!timelineTrackOrganization) { + // Most likely we'll need to load the timeline track organization, as requested + // by the URL, but tests can pass in a value. + timelineTrackOrganization = getTimelineTrackOrganization(getState()); + } - if (selectedThreadIndex !== null && getShowTabOnly(getState()) !== null) { - // The url state says this is an active tab view. We should compute and - // initialize the state relevant to that state. - dispatch(finalizeActiveTabProfileView(profile, selectedThreadIndex)); - } else { - // The url state says this is a full view. We should compute and initialize - // the state relevant to that state. - dispatch(finalizeFullProfileView(profile, selectedThreadIndex)); + switch (timelineTrackOrganization.type) { + case 'full': + // The url state says this is a full view. We should compute and initialize + // the state relevant to that state. + dispatch(finalizeFullProfileView(profile, selectedThreadIndex)); + break; + case 'active-tab': + if (selectedThreadIndex === null) { + // Switch back over to the full view. + timelineTrackOrganization = { type: 'full' }; + } else { + // The url state says this is an active tab view. We should compute and + // initialize the state relevant to that state. + dispatch( + finalizeActiveTabProfileView( + profile, + selectedThreadIndex, + timelineTrackOrganization.browsingContextID + ) + ); + } + break; + case 'origins': { + if (pages) { + throw new Error("This isn't handled yet."); + } else { + // Don't fully trust the URL, this view doesn't support the origins based + // view. Switch to fulll view. + dispatch(finalizeFullProfileView(profile, selectedThreadIndex)); + } + break; + } + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled TimelineTrackOrganization type.` + ); } // Note we kick off symbolication only for the profiles we know for sure @@ -188,8 +229,7 @@ export function finalizeProfileView( */ export function finalizeFullProfileView( profile: Profile, - selectedThreadIndex: ThreadIndex | null, - showTabOnly?: BrowsingContextID | null + selectedThreadIndex: ThreadIndex | null ): ThunkAction { return (dispatch, getState) => { const hasUrlInfo = selectedThreadIndex !== null; @@ -278,7 +318,6 @@ export function finalizeFullProfileView( localTracksByPid, hiddenLocalTracksByPid, localTrackOrderByPid, - showTabOnly, }); }; } @@ -292,7 +331,7 @@ export function finalizeFullProfileView( export function finalizeActiveTabProfileView( profile: Profile, selectedThreadIndex: ThreadIndex, - showTabOnly?: BrowsingContextID | null + browsingContextID: BrowsingContextID ): ThunkAction { return (dispatch, getState) => { const relevantPages = getRelevantPagesForActiveTab(getState()); @@ -309,7 +348,7 @@ export function finalizeActiveTabProfileView( globalTracks, resourceTracks, selectedThreadIndex, - showTabOnly, + browsingContextID, }); }; } @@ -318,8 +357,8 @@ export function finalizeActiveTabProfileView( * Re-compute the profile view data. That's used to be able to switch between * full and active tab view. */ -export function changeViewAndRecomputeProfileData( - showTabOnly: BrowsingContextID | null +export function changeTimelineTrackOrganization( + timelineTrackOrganization: TimelineTrackOrganization ): ThunkAction { return (dispatch, getState) => { const profile = getProfile(getState()); @@ -330,18 +369,31 @@ export function changeViewAndRecomputeProfileData( type: 'DATA_RELOAD', }); - if (showTabOnly === null) { - // The url state says this is a full view. We should compute and initialize - // the state relevant to that state. - dispatch( - finalizeFullProfileView(profile, selectedThreadIndex, showTabOnly) - ); - } else { - // The url state says this is an active tab view. We should compute and - // initialize the state relevant to that state. - dispatch( - finalizeActiveTabProfileView(profile, selectedThreadIndex, showTabOnly) - ); + switch (timelineTrackOrganization.type) { + case 'full': + // The url state says this is a full view. We should compute and initialize + // the state relevant to that state. + dispatch(finalizeFullProfileView(profile, selectedThreadIndex)); + break; + case 'active-tab': + // The url state says this is an active tab view. We should compute and + // initialize the state relevant to that state. + dispatch( + finalizeActiveTabProfileView( + profile, + selectedThreadIndex, + timelineTrackOrganization.browsingContextID + ) + ); + break; + case 'origins': { + throw new Error("This isn't handled yet."); + } + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled TimelineTrackOrganization type.` + ); } }; } @@ -373,6 +425,7 @@ export function resymbolicateProfile(): ThunkAction> { export function viewProfile( profile: Profile, config: $Shape<{| + timelineTrackOrganization: TimelineTrackOrganization, pathInZipFile: string, implementationFilter: ImplementationFilter, transformStacks: TransformStacksPerThread, diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index d3221ae189..4cac35538f 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -19,7 +19,7 @@ import { } from '../utils/flow'; import { toValidCallTreeSummaryStrategy } from '../profile-logic/profile-data'; import { oneLine } from 'common-tags'; -import type { UrlState } from '../types/state'; +import type { UrlState, TimelineTrackOrganization } from '../types/state'; import type { DataSource } from '../types/actions'; import type { Pid, @@ -97,6 +97,9 @@ type FullProfileSpecificBaseQuery = {| // Base query that only applies to active tab profile view. type ActiveTabProfileSpecificBaseQuery = {||}; +// Base query that only applies to origins profile view. +type OriginsProfileSpecificBaseQuery = {||}; + // "null | void" in the query objects are flags which map to true for null, and false // for void. False flags do not show up the URL. type BaseQuery = {| @@ -108,8 +111,10 @@ type BaseQuery = {| profiles: string[], profileName: string, showTabOnly1: BrowsingContextID, + view: string, ...FullProfileSpecificBaseQuery, ...ActiveTabProfileSpecificBaseQuery, + ...OriginsProfileSpecificBaseQuery, |}; type CallTreeQuery = {| @@ -162,6 +167,10 @@ type FullProfileSpecificBaseQueryShape = $Shape< type ActiveTabProfileSpecificBaseQueryShape = $Shape< $ObjMap >; +type OriginsProfileSpecificBaseQueryShape = $Shape< + $ObjMap +>; + // Query shapes for individual query paths. These are needed for QueryShape union type. type CallTreeQueryShape = $Shape<$ObjMap>; type MarkersQueryShape = $Shape<$ObjMap>; @@ -209,49 +218,83 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { // Start with the query parameters that are shown regardless of the active panel. let baseQuery; - if (urlState.showTabOnly === null) { - // Add the full profile specific state query here. - baseQuery = ({}: FullProfileSpecificBaseQueryShape); - baseQuery.globalTrackOrder = - urlState.profileSpecific.full.globalTrackOrder.join('-') || undefined; - - // Add the parameter hiddenGlobalTracks only when needed. - if (urlState.profileSpecific.full.hiddenGlobalTracks.size > 0) { - baseQuery.hiddenGlobalTracks = [ - ...urlState.profileSpecific.full.hiddenGlobalTracks, - ].join('-'); - } + const { timelineTrackOrganization } = urlState; + switch (timelineTrackOrganization.type) { + case 'full': { + // Add the full profile specific state query here. + baseQuery = ({}: FullProfileSpecificBaseQueryShape); + baseQuery.globalTrackOrder = + urlState.profileSpecific.full.globalTrackOrder.join('-') || undefined; + + // Add the parameter hiddenGlobalTracks only when needed. + if (urlState.profileSpecific.full.hiddenGlobalTracks.size > 0) { + baseQuery.hiddenGlobalTracks = [ + ...urlState.profileSpecific.full.hiddenGlobalTracks, + ].join('-'); + } - let hiddenLocalTracksByPid = ''; - for (const [pid, tracks] of urlState.profileSpecific.full - .hiddenLocalTracksByPid) { - if (tracks.size > 0) { - hiddenLocalTracksByPid += [pid, ...tracks].join('-') + '~'; + let hiddenLocalTracksByPid = ''; + for (const [pid, tracks] of urlState.profileSpecific.full + .hiddenLocalTracksByPid) { + if (tracks.size > 0) { + hiddenLocalTracksByPid += [pid, ...tracks].join('-') + '~'; + } + } + if (hiddenLocalTracksByPid.length > 0) { + // Only add to the query string if something was actually hidden. + // Also, slice off the last '~'. + baseQuery.hiddenLocalTracksByPid = hiddenLocalTracksByPid.slice(0, -1); } - } - if (hiddenLocalTracksByPid.length > 0) { - // Only add to the query string if something was actually hidden. - // Also, slice off the last '~'. - baseQuery.hiddenLocalTracksByPid = hiddenLocalTracksByPid.slice(0, -1); - } - if (urlState.profileSpecific.full.timelineType === 'stack') { - // The default is the category view, so only add it to the URL if it's the - // stack view. - baseQuery.timelineType = 'stack'; - } + if (urlState.profileSpecific.full.timelineType === 'stack') { + // The default is the category view, so only add it to the URL if it's the + // stack view. + baseQuery.timelineType = 'stack'; + } - let localTrackOrderByPid = ''; - for (const [pid, trackOrder] of urlState.profileSpecific.full - .localTrackOrderByPid) { - if (trackOrder.length > 0) { - localTrackOrderByPid += `${String(pid)}-` + trackOrder.join('-') + '~'; + let localTrackOrderByPid = ''; + for (const [pid, trackOrder] of urlState.profileSpecific.full + .localTrackOrderByPid) { + if (trackOrder.length > 0) { + localTrackOrderByPid += + `${String(pid)}-` + trackOrder.join('-') + '~'; + } } + baseQuery.localTrackOrderByPid = localTrackOrderByPid || undefined; + break; + } + case 'active-tab': { + baseQuery = ({}: ActiveTabProfileSpecificBaseQueryShape); + break; } - baseQuery.localTrackOrderByPid = localTrackOrderByPid || undefined; - } else { - // Add the active tab profile specific state query here. - baseQuery = ({}: ActiveTabProfileSpecificBaseQueryShape); + case 'origins': + baseQuery = ({}: OriginsProfileSpecificBaseQueryShape); + break; + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled GlobalTrack type.` + ); + } + + let showTabOnly1; + let view; + switch (timelineTrackOrganization.type) { + case 'full': + // Dont URL-encode anything. + break; + case 'active-tab': + view = timelineTrackOrganization.type; + showTabOnly1 = timelineTrackOrganization.browsingContextID; + break; + case 'origins': + view = timelineTrackOrganization.type; + break; + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + 'Unhandled TimelineTrackOrganization case' + ); } baseQuery = ({ @@ -262,9 +305,10 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { thread: selectedThread === null ? undefined : selectedThread.toString(), file: urlState.pathInZipFile || undefined, profiles: urlState.profilesToCompare || undefined, + showTabOnly1, + view, v: CURRENT_URL_VERSION, profileName: urlState.profileName || undefined, - showTabOnly1: urlState.showTabOnly || undefined, }: BaseQueryShape); // Depending on which panel is active, also show tab-specific query parameters. @@ -317,15 +361,28 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { query.networkSearch = urlState.profileSpecific.networkSearchString || undefined; break; - case 'js-tracer': + case 'js-tracer': { query = (baseQuery: JsTracerQueryShape); - if (urlState.showTabOnly === null) { - // `null` adds the parameter to the query, while `undefined` doesn't. - query.summary = urlState.profileSpecific.full.showJsTracerSummary - ? null - : undefined; + const { timelineTrackOrganization } = urlState; + switch (timelineTrackOrganization.type) { + case 'full': + case 'origins': + // `null` adds the parameter to the query, while `undefined` doesn't. + query.summary = urlState.profileSpecific.full.showJsTracerSummary + ? null + : undefined; + break; + case 'active-tab': + // JS Tracer isn't helpful for web developers. + break; + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + 'Unhandled timelineTrackOrganization case' + ); } break; + } default: throw assertExhaustiveCheck(selectedTab); } @@ -417,9 +474,9 @@ export function stateFromLocation( : []; } - let showTabOnly = null; + let browsingContextId = null; if (query.showTabOnly1 && Number.isInteger(Number(query.showTabOnly1))) { - showTabOnly = Number(query.showTabOnly1); + browsingContextId = Number(query.showTabOnly1); } return { @@ -430,7 +487,10 @@ export function stateFromLocation( selectedTab: toValidTabSlug(pathParts[selectedTabPathPart]) || 'calltree', pathInZipFile: query.file || null, profileName: query.profileName, - showTabOnly: showTabOnly, + timelineTrackOrganization: validateTimelineTrackOrganization( + query.view, + browsingContextId + ), profileSpecific: { implementation, lastSelectedCallTreeSummaryStrategy: toValidCallTreeSummaryStrategy( @@ -771,3 +831,29 @@ function getVersion4JSCallNodePathFromStackIndex( } return callNodePath; } + +function validateTimelineTrackOrganization( + type: ?string, + browsingContextID: number | null +): TimelineTrackOrganization { + // Pretend this is a TimelineTrackOrganization so that we can exhaustively + // go through each option. + const timelineTrackOrganization: TimelineTrackOrganization = ({ type }: any); + switch (timelineTrackOrganization.type) { + case 'full': + return { type: 'full' }; + case 'active-tab': + if (browsingContextID) { + return { type: 'active-tab', browsingContextID }; + } + return { type: 'full' }; + + case 'origins': + return { type: 'origins' }; + default: + // Type assert we've checked everythign: + (timelineTrackOrganization: empty); + + return { type: 'full' }; + } +} diff --git a/src/components/timeline/FullTimeline.js b/src/components/timeline/FullTimeline.js index 018a3a2ef1..eedb219815 100644 --- a/src/components/timeline/FullTimeline.js +++ b/src/components/timeline/FullTimeline.js @@ -13,7 +13,6 @@ import OverflowEdgeIndicator from './OverflowEdgeIndicator'; import Reorderable from '../shared/Reorderable'; import { withSize } from '../shared/WithSize'; import explicitConnect from '../../utils/connect'; -import { getPanelLayoutGeneration } from '../../selectors/app'; import { getCommittedRange, getZeroAt, @@ -21,12 +20,11 @@ import { getGlobalTrackReferences, getHiddenTrackCount, getActiveBrowsingContextID, -} from '../../selectors/profile'; -import { + getTimelineTrackOrganization, getGlobalTrackOrder, getTimelineType, - getShowTabOnly, -} from '../../selectors/url-state'; + getPanelLayoutGeneration, +} from 'firefox-profiler/selectors'; import { TIMELINE_MARGIN_LEFT, TIMELINE_MARGIN_RIGHT, @@ -43,7 +41,7 @@ import { changeTimelineType, changeRightClickedTrack, } from '../../actions/profile-view'; -import { changeViewAndRecomputeProfileData } from '../../actions/receive-profile'; +import { changeTimelineTrackOrganization } from '../../actions/receive-profile'; import type { BrowsingContextID } from '../../types/profile'; import type { @@ -51,6 +49,7 @@ import type { GlobalTrack, InitialSelectedTrackReference, } from '../../types/profile-derived'; +import type { TimelineTrackOrganization } from '../../types/state'; import type { GlobalTrackReference, TimelineType, @@ -69,14 +68,14 @@ type StateProps = {| +timelineType: TimelineType, +hiddenTrackCount: HiddenTrackCount, +activeBrowsingContextID: BrowsingContextID | null, - +showTabOnly: BrowsingContextID | null, + +timelineTrackOrganization: TimelineTrackOrganization, |}; type DispatchProps = {| +changeGlobalTrackOrder: typeof changeGlobalTrackOrder, +changeTimelineType: typeof changeTimelineType, +changeRightClickedTrack: typeof changeRightClickedTrack, - +changeViewAndRecomputeProfileData: typeof changeViewAndRecomputeProfileData, + +changeTimelineTrackOrganization: typeof changeTimelineTrackOrganization, |}; type Props = {| @@ -167,24 +166,30 @@ class TimelineSettingsHiddenTracks extends React.PureComponent<{| class TimelineSettingsActiveTabView extends React.PureComponent<{| +activeBrowsingContextID: BrowsingContextID | null, - +showTabOnly: BrowsingContextID | null, - +changeViewAndRecomputeProfileData: typeof changeViewAndRecomputeProfileData, + +timelineTrackOrganization: TimelineTrackOrganization, + +changeTimelineTrackOrganization: typeof changeTimelineTrackOrganization, |}> { - _toggleShowTabOnly = () => { + _toggleActiveTabView = () => { const { - showTabOnly, - changeViewAndRecomputeProfileData, + timelineTrackOrganization, + changeTimelineTrackOrganization, activeBrowsingContextID, } = this.props; - if (showTabOnly === null) { - changeViewAndRecomputeProfileData(activeBrowsingContextID); + if ( + timelineTrackOrganization.type === 'full' && + activeBrowsingContextID !== null + ) { + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID: activeBrowsingContextID, + }); } else { - changeViewAndRecomputeProfileData(null); + changeTimelineTrackOrganization({ type: 'full' }); } }; render() { - const { activeBrowsingContextID, showTabOnly } = this.props; + const { activeBrowsingContextID, timelineTrackOrganization } = this.props; if (activeBrowsingContextID === null) { return null; } @@ -196,8 +201,8 @@ class TimelineSettingsActiveTabView extends React.PureComponent<{| type="checkbox" name="timelineSettingsActiveTabToggle" className="photon-checkbox photon-checkbox-micro" - onChange={this._toggleShowTabOnly} - checked={showTabOnly !== null} + onChange={this._toggleActiveTabView} + checked={timelineTrackOrganization.type === 'active-tab'} /> Show active tab only @@ -234,8 +239,8 @@ class FullTimeline extends React.PureComponent { changeTimelineType, changeRightClickedTrack, activeBrowsingContextID, - showTabOnly, - changeViewAndRecomputeProfileData, + timelineTrackOrganization, + changeTimelineTrackOrganization, } = this.props; // Do not include the left and right margins when computing the timeline width. @@ -265,10 +270,8 @@ class FullTimeline extends React.PureComponent { {true ? null : ( )} @@ -320,13 +323,13 @@ export default explicitConnect<{||}, StateProps, DispatchProps>({ timelineType: getTimelineType(state), hiddenTrackCount: getHiddenTrackCount(state), activeBrowsingContextID: getActiveBrowsingContextID(state), - showTabOnly: getShowTabOnly(state), + timelineTrackOrganization: getTimelineTrackOrganization(state), }), mapDispatchToProps: { changeGlobalTrackOrder, changeTimelineType, changeRightClickedTrack, - changeViewAndRecomputeProfileData, + changeTimelineTrackOrganization, }, component: withSize(FullTimeline), }); diff --git a/src/components/timeline/TrackContextMenu.js b/src/components/timeline/TrackContextMenu.js index 0692c5aa95..a2584ec084 100644 --- a/src/components/timeline/TrackContextMenu.js +++ b/src/components/timeline/TrackContextMenu.js @@ -34,22 +34,17 @@ import { getHiddenGlobalTracks, getHiddenLocalTracksByPid, getLocalTrackOrderByPid, - getShowTabOnly, + getTimelineTrackOrganization, } from '../../selectors/url-state'; import classNames from 'classnames'; -import type { - Thread, - ThreadIndex, - Pid, - BrowsingContextID, -} from '../../types/profile'; +import type { Thread, ThreadIndex, Pid } from '../../types/profile'; import type { TrackIndex, GlobalTrack, LocalTrack, } from '../../types/profile-derived'; -import type { State } from '../../types/state'; +import type { State, TimelineTrackOrganization } from '../../types/state'; import type { TrackReference } from '../../types/actions'; import type { ConnectedProps } from '../../utils/connect'; @@ -66,7 +61,7 @@ type StateProps = {| +globalTrackNames: string[], +localTracksByPid: Map, +localTrackNamesByPid: Map, - +showTabOnly: BrowsingContextID | null, + +timelineTrackOrganization: TimelineTrackOrganization, +activeTabHiddenGlobalTracksGetter: () => Set, +activeTabHiddenLocalTracksByPidGetter: () => Map>, |}; @@ -208,7 +203,7 @@ class TimelineTrackContextMenu extends PureComponent { renderGlobalTrack(trackIndex: TrackIndex) { const { - showTabOnly, + timelineTrackOrganization, activeTabHiddenGlobalTracksGetter, hiddenGlobalTracks, globalTrackNames, @@ -218,7 +213,7 @@ class TimelineTrackContextMenu extends PureComponent { const track = globalTracks[trackIndex]; if ( - showTabOnly !== null && + timelineTrackOrganization.type === 'active-tab' && activeTabHiddenGlobalTracksGetter().has(trackIndex) ) { // Hide the global track if it's hidden by active tab view. @@ -261,7 +256,7 @@ class TimelineTrackContextMenu extends PureComponent { activeTabHiddenLocalTracksByPidGetter, hiddenGlobalTracks, localTracksByPid, - showTabOnly, + timelineTrackOrganization, } = this.props; const isGlobalTrackHidden = hiddenGlobalTracks.has(globalTrackIndex); @@ -287,7 +282,7 @@ class TimelineTrackContextMenu extends PureComponent { for (const trackIndex of localTrackOrder) { // We hide some of the local tracks by default for single tab view. If // showTabOnly is not null, do not include that track if it's not allowed. - if (showTabOnly !== null) { + if (timelineTrackOrganization.type === 'active-tab') { // We need to defer the call of this as much as possible. const activeTabHiddenLocalTracks = activeTabHiddenLocalTracksByPidGetter().get( pid @@ -584,7 +579,7 @@ export default explicitConnect<{||}, StateProps, DispatchProps>({ globalTrackNames: getGlobalTrackNames(state), localTracksByPid: getLocalTracksByPid(state), localTrackNamesByPid: getLocalTrackNamesByPid(state), - showTabOnly: getShowTabOnly(state), + timelineTrackOrganization: getTimelineTrackOrganization(state), activeTabHiddenGlobalTracksGetter: getActiveTabHiddenGlobalTracksGetter( state ), diff --git a/src/components/timeline/index.js b/src/components/timeline/index.js index 3702003047..0a81e28a85 100644 --- a/src/components/timeline/index.js +++ b/src/components/timeline/index.js @@ -6,32 +6,43 @@ import * as React from 'react'; import explicitConnect from '../../utils/connect'; -import { getShowTabOnly } from '../../selectors/url-state'; +import { getTimelineTrackOrganization } from 'firefox-profiler/selectors'; import FullTimeline from '../timeline/FullTimeline'; import ActiveTabTimeline from '../timeline/ActiveTabTimeline'; +import { assertExhaustiveCheck } from '../../utils/flow'; -import type { BrowsingContextID } from '../../types/profile'; import type { ConnectedProps } from '../../utils/connect'; +import type { TimelineTrackOrganization } from '../../types/state'; type StateProps = {| - +showTabOnly: BrowsingContextID | null, + +timelineTrackOrganization: TimelineTrackOrganization, |}; type Props = ConnectedProps<{||}, StateProps, {||}>; class Timeline extends React.PureComponent { render() { - const { showTabOnly } = this.props; - // Show different timeline components depending on the view we are in. - // If showTabOnly state is non-null, then show the active tab timeline. - // Otherwise, show the full timeline. - return showTabOnly === null ? : ; + const { timelineTrackOrganization } = this.props; + switch (timelineTrackOrganization.type) { + case 'full': + return ; + case 'active-tab': + return ; + case 'origins': + // This doesn't exist yet. + return null; + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled ViewType` + ); + } } } export default explicitConnect<{||}, StateProps, {||}>({ mapStateToProps: state => ({ - showTabOnly: getShowTabOnly(state), + timelineTrackOrganization: getTimelineTrackOrganization(state), }), component: Timeline, }); diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index a150051263..e7790175d8 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -607,6 +607,10 @@ const rightClickedMarker: Reducer = ( } }; +const origins: Reducer = (state = null, _action) => { + return state; +}; + /** * Provide a mechanism to wrap the reducer in a special function that can reset * the state to the default values. This is useful when viewing multiple profiles @@ -655,6 +659,7 @@ const profileViewReducer: Reducer = wrapReducerInResetter( hiddenGlobalTracksGetter: activeTabHiddenGlobalTracksGetter, hiddenLocalTracksByPidGetter: activeTabHiddenLocalTracksByPidGetter, }), + origins, }) ); diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index d20e586b98..0de276870b 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -7,7 +7,7 @@ import { combineReducers } from 'redux'; import { oneLine } from 'common-tags'; import { objectEntries } from '../utils/flow'; -import type { ThreadIndex, Pid, BrowsingContextID } from '../types/profile'; +import type { ThreadIndex, Pid } from '../types/profile'; import type { TrackIndex } from '../types/profile-derived'; import type { StartEndRange } from '../types/units'; import type { TransformStacksPerThread } from '../types/transforms'; @@ -17,7 +17,11 @@ import type { CallTreeSummaryStrategy, TimelineType, } from '../types/actions'; -import type { UrlState, Reducer } from '../types/state'; +import type { + UrlState, + Reducer, + TimelineTrackOrganization, +} from '../types/state'; import type { TabSlug } from '../app-logic/tabs-handling'; /* @@ -405,18 +409,20 @@ const profileName: Reducer = (state = '', action) => { } }; -const showTabOnly: Reducer = ( - state = null, +const timelineTrackOrganization: Reducer = ( + state = { type: 'full' }, action ) => { switch (action.type) { case 'VIEW_FULL_PROFILE': + return { type: 'full' }; case 'VIEW_ACTIVE_TAB_PROFILE': - if (action.showTabOnly !== undefined) { - // Do not change the state if it's undefined. - return action.showTabOnly; - } - return state; + return { + type: 'active-tab', + browsingContextID: action.browsingContextID, + }; + case 'VIEW_ORIGINS_PROFILE': + return { type: 'origins' }; default: return state; } @@ -506,7 +512,7 @@ const urlStateReducer: Reducer = wrapReducerInResetter( pathInZipFile, profileSpecific, profileName, - showTabOnly, + timelineTrackOrganization, }) ); diff --git a/src/selectors/app.js b/src/selectors/app.js index e202fdf632..0adea85eb3 100644 --- a/src/selectors/app.js +++ b/src/selectors/app.js @@ -5,7 +5,11 @@ // @flow import { createSelector } from 'reselect'; -import { getDataSource, getSelectedTab, getShowTabOnly } from './url-state'; +import { + getDataSource, + getSelectedTab, + getTimelineTrackOrganization, +} from './url-state'; import { getGlobalTracks, getLocalTracksByPid, @@ -64,11 +68,20 @@ export const getIsDragAndDropOverlayRegistered: Selector = state => * Height of screenshot track is different depending on the view. */ export const getScreenshotTrackHeight: Selector = createSelector( - getShowTabOnly, - showTabOnly => { - return showTabOnly === null - ? FULL_TRACK_SCREENSHOT_HEIGHT - : ACTIVE_TAB_TRACK_SCREENSHOT_HEIGHT; + getTimelineTrackOrganization, + timelineTrackOrganization => { + switch (timelineTrackOrganization.type) { + case 'active-tab': + return ACTIVE_TAB_TRACK_SCREENSHOT_HEIGHT; + case 'full': + case 'origins': + return FULL_TRACK_SCREENSHOT_HEIGHT; + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled TimelineTrackOrganization` + ); + } } ); @@ -83,153 +96,158 @@ export const getScreenshotTrackHeight: Selector = createSelector( * and get added in here. */ export const getTimelineHeight: Selector = createSelector( + getTimelineTrackOrganization, getGlobalTracks, getLocalTracksByPid, getComputedHiddenGlobalTracks, getComputedHiddenLocalTracksByPid, getTrackThreadHeights, getActiveTabGlobalTracks, - getShowTabOnly, getScreenshotTrackHeight, ( + timelineTrackOrganization, globalTracks, localTracksByPid, hiddenGlobalTracks, hiddenLocalTracksByPid, trackThreadHeights, activeTabGlobalTracks, - showTabOnly, screenshotTrackHeight ) => { let height = TIMELINE_RULER_HEIGHT; const border = 1; - - if (showTabOnly === null) { - // Full profile view - // Only the full view has the timeline settings panel. - height += TIMELINE_SETTINGS_HEIGHT; - - for (const [trackIndex, globalTrack] of globalTracks.entries()) { - if (!hiddenGlobalTracks.has(trackIndex)) { - switch (globalTrack.type) { - case 'screenshots': - height += screenshotTrackHeight + border; - break; - case 'visual-progress': - case 'perceptual-visual-progress': - case 'contentful-visual-progress': - height += TRACK_VISUAL_PROGRESS_HEIGHT; - break; - case 'process': { - // The thread tracks have enough complexity that it warrants measuring - // them rather than statically using a value like the other tracks. - const { mainThreadIndex } = globalTrack; - if (mainThreadIndex === null) { - height += TRACK_PROCESS_BLANK_HEIGHT + border; - } else { - const trackThreadHeight = trackThreadHeights[mainThreadIndex]; - if (trackThreadHeight === undefined) { - // The height isn't computed yet, return. - return null; - } - } - break; - } - default: - throw assertExhaustiveCheck(globalTrack); - } - } - } - - // Figure out which PIDs are hidden. - const hiddenPids = new Set(); - for (const trackIndex of hiddenGlobalTracks) { - const globalTrack = globalTracks[trackIndex]; - if (globalTrack.type === 'process') { - hiddenPids.add(globalTrack.pid); - } + switch (timelineTrackOrganization.type) { + case 'origins': { + return height + 500; } - - for (const [pid, localTracks] of localTracksByPid) { - if (hiddenPids.has(pid)) { - // This track is hidden already. - continue; - } - for (const [trackIndex, localTrack] of localTracks.entries()) { - const hiddenLocalTracks = ensureExists( - hiddenLocalTracksByPid.get(pid), - 'Could not look up the hidden local tracks from the given PID' - ); - if (!hiddenLocalTracks.has(trackIndex)) { - switch (localTrack.type) { - case 'thread': + case 'active-tab': { + for (const [ + trackIndex, + globalTrack, + ] of activeTabGlobalTracks.entries()) { + if (!hiddenGlobalTracks.has(trackIndex)) { + switch (globalTrack.type) { + case 'screenshots': + height += screenshotTrackHeight + border; + break; + case 'tab': { // The thread tracks have enough complexity that it warrants measuring // them rather than statically using a value like the other tracks. - const trackThreadHeight = - trackThreadHeights[localTrack.threadIndex]; - if (trackThreadHeight === undefined) { - // The height isn't computed yet, return. - return null; + const { threadIndex } = globalTrack; + if (threadIndex === null) { + height += TRACK_PROCESS_BLANK_HEIGHT + border; + } else { + const trackThreadHeight = trackThreadHeights[threadIndex]; + if (trackThreadHeight === undefined) { + // The height isn't computed yet, return. + return null; + } + height += trackThreadHeight + border; } - height += trackThreadHeight + border; - } - - break; - case 'network': - if (!showTabOnly) { - height += TRACK_NETWORK_HEIGHT + border; - } - break; - case 'memory': - if (!showTabOnly) { - height += TRACK_MEMORY_HEIGHT + border; - } - break; - case 'ipc': - if (!showTabOnly) { - height += TRACK_IPC_HEIGHT + border; } break; default: - throw assertExhaustiveCheck(localTrack); + throw assertExhaustiveCheck(globalTrack); } } } + return height; } - } else { - // Active tab view - for (const [trackIndex, globalTrack] of activeTabGlobalTracks.entries()) { - if (!hiddenGlobalTracks.has(trackIndex)) { - switch (globalTrack.type) { - case 'screenshots': - height += screenshotTrackHeight + border; - break; - case 'tab': - { + case 'full': { + // Only the full view has the timeline settings panel. + height += TIMELINE_SETTINGS_HEIGHT; + + for (const [trackIndex, globalTrack] of globalTracks.entries()) { + if (!hiddenGlobalTracks.has(trackIndex)) { + switch (globalTrack.type) { + case 'screenshots': + height += screenshotTrackHeight + border; + break; + case 'visual-progress': + case 'perceptual-visual-progress': + case 'contentful-visual-progress': + height += TRACK_VISUAL_PROGRESS_HEIGHT; + break; + case 'process': { // The thread tracks have enough complexity that it warrants measuring // them rather than statically using a value like the other tracks. - const { threadIndex } = globalTrack; - if (threadIndex === null) { + const { mainThreadIndex } = globalTrack; + if (mainThreadIndex === null) { height += TRACK_PROCESS_BLANK_HEIGHT + border; } else { - const trackThreadHeight = trackThreadHeights[threadIndex]; + const trackThreadHeight = trackThreadHeights[mainThreadIndex]; if (trackThreadHeight === undefined) { // The height isn't computed yet, return. return null; } - height += trackThreadHeight + border; } + break; } - break; - default: - throw assertExhaustiveCheck(globalTrack); + default: + throw assertExhaustiveCheck(globalTrack); + } + } + } + + // Figure out which PIDs are hidden. + const hiddenPids = new Set(); + for (const trackIndex of hiddenGlobalTracks) { + const globalTrack = globalTracks[trackIndex]; + if (globalTrack.type === 'process') { + hiddenPids.add(globalTrack.pid); } } + + for (const [pid, localTracks] of localTracksByPid) { + if (hiddenPids.has(pid)) { + // This track is hidden already. + continue; + } + for (const [trackIndex, localTrack] of localTracks.entries()) { + const hiddenLocalTracks = ensureExists( + hiddenLocalTracksByPid.get(pid), + 'Could not look up the hidden local tracks from the given PID' + ); + if (!hiddenLocalTracks.has(trackIndex)) { + switch (localTrack.type) { + case 'thread': + { + // The thread tracks have enough complexity that it warrants measuring + // them rather than statically using a value like the other tracks. + const trackThreadHeight = + trackThreadHeights[localTrack.threadIndex]; + if (trackThreadHeight === undefined) { + // The height isn't computed yet, return. + return null; + } + height += trackThreadHeight + border; + } + + break; + case 'network': + height += TRACK_NETWORK_HEIGHT + border; + break; + case 'memory': + height += TRACK_MEMORY_HEIGHT + border; + break; + case 'ipc': + height += TRACK_IPC_HEIGHT + border; + break; + default: + throw assertExhaustiveCheck(localTrack); + } + } + } + } + return height; } + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + `Unhandled TimelineTrackOrganization` + ); } - - return height; } ); diff --git a/src/selectors/per-thread/composed.js b/src/selectors/per-thread/composed.js index b72c28ba9e..9bc8eefb97 100644 --- a/src/selectors/per-thread/composed.js +++ b/src/selectors/per-thread/composed.js @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import { tabSlugs, type TabSlug } from '../../app-logic/tabs-handling'; -import { getShowTabOnly } from '../url-state'; +import { getTimelineTrackOrganization } from '../url-state'; import type { Selector } from '../../types/store'; import type { $ReturnType } from '../../types/utils'; @@ -58,8 +58,13 @@ export function getComposedSelectorsPerThread( threadSelectors.getThread, threadSelectors.getIsNetworkChartEmptyInFullRange, threadSelectors.getJsTracerTable, - getShowTabOnly, - ({ processType }, isNetworkChartEmpty, jsTracerTable, showTabOnly) => { + getTimelineTrackOrganization, + ( + { processType }, + isNetworkChartEmpty, + jsTracerTable, + timelineTrackOrganization + ) => { if (processType === 'comparison') { // For a diffing tracks, we display only the calltree tab for now, because // other views make no or not much sense. @@ -67,7 +72,10 @@ export function getComposedSelectorsPerThread( } let visibleTabs = tabSlugs; - if (isNetworkChartEmpty || showTabOnly !== null) { + if ( + isNetworkChartEmpty || + timelineTrackOrganization.type === 'active-tab' + ) { // We either don't show the network chart if it's empty or we don't show it // for now when we are in single tab mode. This is because we don't know // which network request belongs to which page currently. diff --git a/src/selectors/profile.js b/src/selectors/profile.js index 357bbc269d..3c4bc54dee 100644 --- a/src/selectors/profile.js +++ b/src/selectors/profile.js @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import * as Tracks from '../profile-logic/tracks'; import * as UrlState from './url-state'; -import { ensureExists } from '../utils/flow'; +import { ensureExists, assertExhaustiveCheck } from '../utils/flow'; import { filterCounterToRange, accumulateCounterSamples, @@ -58,6 +58,7 @@ import type { SymbolicationStatus, FullProfileViewState, ActiveTabProfileViewState, + OriginsViewState, } from '../types/state'; import type { $ReturnType } from '../types/utils'; @@ -67,6 +68,8 @@ export const getFullProfileView: Selector = state => getProfileView(state).full; export const getActiveTabProfileView: Selector = state => getProfileView(state).activeTab; +export const getOriginsProfileView: Selector = state => + getProfileView(state).origins; /** * Profile View Options @@ -479,7 +482,7 @@ export const getHiddenTrackCount: Selector = createSelector( getActiveTabHiddenLocalTracksByPidGetter, UrlState.getHiddenGlobalTracks, getActiveTabHiddenGlobalTracksGetter, - UrlState.getShowTabOnly, + UrlState.getTimelineTrackOrganization, ( globalTracks, localTracksByPid, @@ -487,7 +490,7 @@ export const getHiddenTrackCount: Selector = createSelector( activeTabHiddenLocalTracksByPidGetter, hiddenGlobalTracks, activeTabHiddenGlobalTracksGetter, - showTabOnly + timelineTrackOrganization ) => { let hidden = 0; let total = 0; @@ -498,7 +501,7 @@ export const getHiddenTrackCount: Selector = createSelector( const hiddenLocalTracks = hiddenLocalTracksByPid.get(pid) || new Set(); const activeTabHiddenLocalTracks = // Do not call the getter if we are not in the single tab view. - showTabOnly !== null + timelineTrackOrganization.type === 'active-tab' ? activeTabHiddenLocalTracksByPidGetter().get(pid) || new Set() : new Set(); const globalTrackIndex = globalTracks.findIndex( @@ -515,7 +518,7 @@ export const getHiddenTrackCount: Selector = createSelector( if (hiddenGlobalTracks.has(globalTrackIndex)) { // The entire process group is hidden, count all of the tracks. - if (showTabOnly !== null) { + if (timelineTrackOrganization.type === 'active-tab') { if (!activeTabHiddenGlobalTracksGetter().has(globalTrackIndex)) { // If we are in active tab view and the current hidden track is not // hidden by that, count its local tracks but also make sure that we @@ -528,7 +531,7 @@ export const getHiddenTrackCount: Selector = createSelector( } else { // Only count the hidden local tracks. hidden += hiddenLocalTracks.size; - if (showTabOnly !== null) { + if (timelineTrackOrganization.type === 'active-tab') { // If we are in active tab view, we should remove the count of active // tab hidden tracks since they won't be visible at all. hidden -= [...activeTabHiddenLocalTracks].filter(t => @@ -537,14 +540,14 @@ export const getHiddenTrackCount: Selector = createSelector( } } total += localTracks.length; - if (showTabOnly !== null) { + if (timelineTrackOrganization.type === 'active-tab') { // Again, if we are in active tab view, do not count the tracks that are hidden by active tab view. total -= activeTabHiddenLocalTracks.size; } } // Count up the global tracks - if (showTabOnly) { + if (timelineTrackOrganization.type === 'active-tab') { const activeTabHiddenGlobalTracks = activeTabHiddenGlobalTracksGetter(); total += globalTracks.length - activeTabHiddenGlobalTracks.size; hidden += [...hiddenGlobalTracks].filter( @@ -720,15 +723,21 @@ export const getRelevantInnerWindowIDsForActiveTab: Selector< export const getRelevantInnerWindowIDsForCurrentTab: Selector< Set > = createSelector( - UrlState.getShowTabOnly, + UrlState.getTimelineTrackOrganization, getRelevantInnerWindowIDsForActiveTab, - (showTabOnly, relevantPages) => { - if (showTabOnly === null) { - // Return an empty set if we want to see everything or that data is not there. - return new Set(); + (timelineTrackOrganization, relevantInnerWindowIDs) => { + switch (timelineTrackOrganization.type) { + case 'active-tab': + return relevantInnerWindowIDs; + case 'full': + case 'origins': + return new Set(); + default: + throw assertExhaustiveCheck( + timelineTrackOrganization, + 'Unhandled timelineTrackOrganization case' + ); } - - return relevantPages; } ); @@ -740,14 +749,18 @@ export const getComputedHiddenGlobalTracks: Selector< Set > = createSelector( UrlState.getHiddenGlobalTracks, - UrlState.getShowTabOnly, + UrlState.getTimelineTrackOrganization, getActiveTabHiddenGlobalTracksGetter, - (hiddenGlobalTracks, showTabOnly, activeTabHiddenGlobalTracksGetter) => { - if (showTabOnly === null) { + ( + hiddenGlobalTracks, + timelineTrackOrganization, + activeTabHiddenGlobalTracksGetter + ) => { + if (timelineTrackOrganization !== 'active-tab') { return hiddenGlobalTracks; } - // We are in the showTabOnly mode and we need to hide the tracks that don't + // We are in the active tab mode and we need to hide the tracks that don't // belong to the active tab as well. return new Set([ ...hiddenGlobalTracks, @@ -764,18 +777,18 @@ export const getComputedHiddenLocalTracksByPid: Selector< Map> > = createSelector( UrlState.getHiddenLocalTracksByPid, - UrlState.getShowTabOnly, + UrlState.getTimelineTrackOrganization, getActiveTabHiddenLocalTracksByPidGetter, ( hiddenLocalTracksByPid, - showTabOnly, + timelineTrackOrganization, activeTabHiddenLocalTracksByPidGetter ) => { - if (showTabOnly === null) { + if (timelineTrackOrganization !== 'active-tab') { return hiddenLocalTracksByPid; } - // We are in the showTabOnly mode and we need to hide the tracks that don't + // We are in the active tab mode and we need to hide the tracks that don't // belong to the active tab as well. // We need to deep copy those Maps and Sets here, just in case. diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index fa55fb37a8..ede4b56550 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -9,7 +9,7 @@ import { ensureExists } from '../utils/flow'; import { urlFromState } from '../app-logic/url-handling'; import * as CommittedRanges from '../profile-logic/committed-ranges'; -import type { ThreadIndex, Pid, BrowsingContextID } from '../types/profile'; +import type { ThreadIndex, Pid } from '../types/profile'; import type { TransformStack } from '../types/transforms'; import type { Action, @@ -19,7 +19,7 @@ import type { CallTreeSummaryStrategy, } from '../types/actions'; import type { TabSlug } from '../app-logic/tabs-handling'; -import type { UrlState } from '../types/state'; +import type { UrlState, TimelineTrackOrganization } from '../types/state'; import type { Selector, DangerousSelectorWithArguments } from '../types/store'; import type { StartEndRange } from '../types/units'; import type { TrackIndex } from '../types/profile-derived'; @@ -44,8 +44,6 @@ export const getProfilesToCompare: Selector = state => getUrlState(state).profilesToCompare; export const getProfileNameFromUrl: Selector = state => getUrlState(state).profileName; -export const getShowTabOnly: Selector = state => - getUrlState(state).showTabOnly; export const getAllCommittedRanges: Selector = state => getProfileSpecificState(state).committedRanges; export const getImplementationFilter: Selector = state => @@ -58,6 +56,8 @@ export const getShowUserTimings: Selector = state => getProfileSpecificState(state).showUserTimings; export const getShowJsTracerSummary: Selector = state => getFullProfileSpecificState(state).showJsTracerSummary; +export const getTimelineTrackOrganization: Selector = state => + getUrlState(state).timelineTrackOrganization; /** * Raw search strings, before any splitting has been performed. diff --git a/src/test/components/ActiveTabTimeline.test.js b/src/test/components/ActiveTabTimeline.test.js index 967da14de7..b28072ef16 100644 --- a/src/test/components/ActiveTabTimeline.test.js +++ b/src/test/components/ActiveTabTimeline.test.js @@ -12,7 +12,7 @@ import { render, fireEvent } from 'react-testing-library'; import { Provider } from 'react-redux'; import { storeWithProfile } from '../fixtures/stores'; import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; -import { changeViewAndRecomputeProfileData } from '../../actions/receive-profile'; +import { changeTimelineTrackOrganization } from '../../actions/receive-profile'; import { getBoundingBox } from '../fixtures/utils'; import { addActiveTabInformationToProfile } from '../fixtures/profiles/processed-profile'; import mockCanvasContext from '../fixtures/mocks/canvas-context'; @@ -51,7 +51,10 @@ describe('ActiveTabTimeline', function() { profile.threads[0].frameTable.innerWindowID[0] = parentInnerWindowIDsWithChildren; const store = storeWithProfile(profile); store.dispatch( - changeViewAndRecomputeProfileData(firstTabBrowsingContextID) + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID: firstTabBrowsingContextID, + }) ); const { container } = render( @@ -71,7 +74,10 @@ describe('ActiveTabTimeline', function() { pageInfo.parentInnerWindowIDsWithChildren; const store = storeWithProfile(profile); store.dispatch( - changeViewAndRecomputeProfileData(pageInfo.firstTabBrowsingContextID) + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID: pageInfo.firstTabBrowsingContextID, + }) ); const trackIndex = 0; const { getState, dispatch } = store; diff --git a/src/test/components/FilterNavigatorBar.test.js b/src/test/components/FilterNavigatorBar.test.js index c7e2f6edf7..954337581e 100644 --- a/src/test/components/FilterNavigatorBar.test.js +++ b/src/test/components/FilterNavigatorBar.test.js @@ -85,7 +85,10 @@ describe('app/ProfileFilterNavigator', () => { it('renders the site hostname as its first element in the single tab view', () => { const { dispatch, container } = setup(); dispatch( - ReceiveProfile.changeViewAndRecomputeProfileData(browsingContextID) + ReceiveProfile.changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID, + }) ); expect(container.firstChild).toMatchSnapshot(); }); @@ -93,7 +96,10 @@ describe('app/ProfileFilterNavigator', () => { it('displays the site hostname as its first element in the single tab view', () => { const { dispatch, queryByText } = setup(); dispatch( - ReceiveProfile.changeViewAndRecomputeProfileData(browsingContextID) + ReceiveProfile.changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID, + }) ); expect(queryByText('Full Range')).toBeFalsy(); // Using regexp because searching for a partial text. diff --git a/src/test/components/Timeline.test.js b/src/test/components/Timeline.test.js index 21a165a3b7..4b89b24b7c 100644 --- a/src/test/components/Timeline.test.js +++ b/src/test/components/Timeline.test.js @@ -13,7 +13,7 @@ import mockCanvasContext from '../fixtures/mocks/canvas-context'; import mockRaf from '../fixtures/mocks/request-animation-frame'; import { getBoundingBox } from '../fixtures/utils'; import ReactDOM from 'react-dom'; -import { getShowTabOnly } from '../../selectors/url-state'; +import { getTimelineTrackOrganization } from '../../selectors/url-state'; import { getRightClickedTrack } from '../../selectors/profile'; import type { Profile } from '../../types/profile'; @@ -153,13 +153,20 @@ describe('Timeline', function() { ); - expect(getShowTabOnly(store.getState())).toEqual(null); + expect(getTimelineTrackOrganization(store.getState())).toEqual({ + type: 'full', + }); fireEvent.click(getByText('Show active tab only')); - expect(getShowTabOnly(store.getState())).toEqual(123); + expect(getTimelineTrackOrganization(store.getState())).toEqual({ + type: 'active-tab', + browsingContextID: 123, + }); fireEvent.click(getByText('Show active tab only')); - expect(getShowTabOnly(store.getState())).toEqual(null); + expect(getTimelineTrackOrganization(store.getState())).toEqual({ + type: 'full', + }); }); }); diff --git a/src/test/fixtures/profiles/tracks.js b/src/test/fixtures/profiles/tracks.js index 071abffb78..9d691669ab 100644 --- a/src/test/fixtures/profiles/tracks.js +++ b/src/test/fixtures/profiles/tracks.js @@ -44,7 +44,15 @@ export function getHumanReadableTracks(state: State): string[] { const globalTracks = profileViewSelectors.getGlobalTracks(state); const hiddenGlobalTracks = urlStateSelectors.getHiddenGlobalTracks(state); const selectedThreadIndex = urlStateSelectors.getSelectedThreadIndex(state); + const timelineTrackOrganization = urlStateSelectors.getTimelineTrackOrganization( + state + ); const text: string[] = []; + + if (timelineTrackOrganization.type !== 'full') { + throw new Error('Expected to have the full timeline track organization.'); + } + for (const globalTrackIndex of urlStateSelectors.getGlobalTrackOrder(state)) { const globalTrack = globalTracks[globalTrackIndex]; const globalHiddenText = hiddenGlobalTracks.has(globalTrackIndex) diff --git a/src/test/store/active-tab.test.js b/src/test/store/active-tab.test.js index 72b020f9dd..b6a2cd3b6a 100644 --- a/src/test/store/active-tab.test.js +++ b/src/test/store/active-tab.test.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow -import { changeViewAndRecomputeProfileData } from '../../actions/receive-profile'; +import { changeTimelineTrackOrganization } from '../../actions/receive-profile'; import { getHumanReadableActiveTabTracks, getProfileWithNiceTracks, @@ -29,7 +29,10 @@ describe('ActiveTab', function() { const { dispatch, getState } = storeWithProfile(profile); dispatch( - changeViewAndRecomputeProfileData(pageInfo.activeBrowsingContextID) + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID: pageInfo.activeBrowsingContextID, + }) ); return { diff --git a/src/test/store/markers.test.js b/src/test/store/markers.test.js index 293cedd6ea..5149ea9f5a 100644 --- a/src/test/store/markers.test.js +++ b/src/test/store/markers.test.js @@ -9,7 +9,7 @@ import { getProfileWithMarkers, getNetworkTrackProfile, } from '../fixtures/profiles/processed-profile'; -import { changeViewAndRecomputeProfileData } from '../../actions/receive-profile'; +import { changeTimelineTrackOrganization } from '../../actions/receive-profile'; describe('selectors/getMarkerChartTimingAndBuckets', function() { function getMarkerChartTimingAndBuckets(testMarkers) { @@ -427,7 +427,12 @@ describe('selectors/getCommittedRangeAndTabFilteredMarkerIndexes', function() { const { getState, dispatch } = storeWithProfile(profile); if (showTabOnly) { - dispatch(changeViewAndRecomputeProfileData(browsingContextID)); + dispatch( + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID, + }) + ); } const markerIndexes = selectedThreadSelectors.getCommittedRangeAndTabFilteredMarkerIndexes( getState() diff --git a/src/test/store/profile-view.test.js b/src/test/store/profile-view.test.js index 09f85a53be..1a677604ab 100644 --- a/src/test/store/profile-view.test.js +++ b/src/test/store/profile-view.test.js @@ -32,7 +32,7 @@ import * as App from '../../actions/app'; import * as ProfileView from '../../actions/profile-view'; import { viewProfile, - changeViewAndRecomputeProfileData, + changeTimelineTrackOrganization, } from '../../actions/receive-profile'; import * as ProfileViewSelectors from '../../selectors/profile'; import * as UrlStateSelectors from '../../selectors/url-state'; @@ -1662,7 +1662,9 @@ describe('snapshots of selectors/profile', function() { it('matches the last stored run of selectedThreadSelector.getTabFilteredThread', function() { const { getState, dispatch } = setupStore(); - dispatch(changeViewAndRecomputeProfileData(browsingContextID)); + dispatch( + changeTimelineTrackOrganization({ type: 'active-tab', browsingContextID }) + ); expect( selectedThreadSelectors.getTabFilteredThread(getState()) ).toMatchSnapshot(); @@ -2874,7 +2876,12 @@ describe('pages and active tab selectors', function() { profile.threads.push(getEmptyThread()); const { dispatch, getState } = storeWithProfile(profile); - dispatch(changeViewAndRecomputeProfileData(activeBrowsingContextID)); + dispatch( + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID: activeBrowsingContextID, + }) + ); return { profile, dispatch, getState, ...pageInfo }; } diff --git a/src/test/store/receive-profile.test.js b/src/test/store/receive-profile.test.js index f9a2f05b39..2302b73be7 100644 --- a/src/test/store/receive-profile.test.js +++ b/src/test/store/receive-profile.test.js @@ -30,7 +30,7 @@ import { retrieveProfilesToCompare, _fetchProfile, getProfilesFromRawUrl, - changeViewAndRecomputeProfileData, + changeTimelineTrackOrganization, } from '../../actions/receive-profile'; import { SymbolsNotFoundError } from '../../profile-logic/errors'; import fakeIndexedDB from 'fake-indexeddb'; @@ -434,7 +434,7 @@ describe('actions/receive-profile', function() { }); }); - describe('changeViewAndRecomputeProfileData', function() { + describe('changeTimelineTrackOrganization', function() { const browsingContextID = 123; const innerWindowID = 111111; function setup(initializeShowTabOnly: boolean = false) { @@ -462,7 +462,12 @@ describe('actions/receive-profile', function() { store.dispatch(viewProfile(profile)); if (initializeShowTabOnly) { - store.dispatch(changeViewAndRecomputeProfileData(browsingContextID)); + store.dispatch( + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID, + }) + ); } return { ...store, profile }; @@ -470,20 +475,39 @@ describe('actions/receive-profile', function() { it('should be able to switch to active tab view from the full view', function() { const { dispatch, getState } = setup(); - expect(UrlStateSelectors.getShowTabOnly(getState())).toBe(null); - dispatch(changeViewAndRecomputeProfileData(browsingContextID)); - expect(UrlStateSelectors.getShowTabOnly(getState())).toBe( - browsingContextID + expect( + UrlStateSelectors.getTimelineTrackOrganization(getState()) + ).toEqual({ + type: 'full', + }); + dispatch( + changeTimelineTrackOrganization({ + type: 'active-tab', + browsingContextID, + }) ); + expect( + UrlStateSelectors.getTimelineTrackOrganization(getState()) + ).toEqual({ + type: 'active-tab', + browsingContextID, + }); }); it('should be able to switch to full view from the active tab', function() { const { dispatch, getState } = setup(true); - expect(UrlStateSelectors.getShowTabOnly(getState())).toBe( - browsingContextID - ); - dispatch(changeViewAndRecomputeProfileData(null)); - expect(UrlStateSelectors.getShowTabOnly(getState())).toBe(null); + expect( + UrlStateSelectors.getTimelineTrackOrganization(getState()) + ).toEqual({ + type: 'active-tab', + browsingContextID, + }); + dispatch(changeTimelineTrackOrganization({ type: 'full' })); + expect( + UrlStateSelectors.getTimelineTrackOrganization(getState()) + ).toEqual({ + type: 'full', + }); }); }); diff --git a/src/test/url-handling.test.js b/src/test/url-handling.test.js index fcbbe8e818..c9e8505b4f 100644 --- a/src/test/url-handling.test.js +++ b/src/test/url-handling.test.js @@ -23,7 +23,7 @@ import { import { blankStore } from './fixtures/stores'; import { viewProfile, - changeViewAndRecomputeProfileData, + changeTimelineTrackOrganization, } from '../actions/receive-profile'; import type { Profile } from '../types/profile'; import getProfile from './fixtures/profiles/call-nodes'; @@ -401,24 +401,31 @@ describe('profileName', function() { describe('showTabOnly', function() { it('serializes the showTabOnly in the URL ', function() { const { getState, dispatch } = _getStoreWithURL(); - const showTabOnly = 123; + const browsingContextID = 123; - dispatch(changeViewAndRecomputeProfileData(showTabOnly)); + dispatch( + changeTimelineTrackOrganization({ type: 'active-tab', browsingContextID }) + ); const urlState = urlStateReducers.getUrlState(getState()); const { query } = urlStateToUrlObject(urlState); - expect(query.showTabOnly1).toBe(showTabOnly); + expect(query.showTabOnly1).toBe(browsingContextID); }); it('reflects in the state from URL', function() { const { getState } = _getStoreWithURL({ - search: '?showTabOnly1=123', + search: '?showTabOnly1=123&view=active-tab', + }); + expect(urlStateReducers.getTimelineTrackOrganization(getState())).toEqual({ + type: 'active-tab', + browsingContextID: 123, }); - expect(urlStateReducers.getShowTabOnly(getState())).toBe(123); }); - it('returns null when showTabOnly is not specified', function() { + it('returns the full view when showTabOnly is not specified', function() { const { getState } = _getStoreWithURL(); - expect(urlStateReducers.getShowTabOnly(getState())).toBe(null); + expect(urlStateReducers.getTimelineTrackOrganization(getState())).toEqual({ + type: 'full', + }); }); it('should use the finalizeActiveTabProfileView path and initialize active tab profile view state', function() { @@ -431,7 +438,7 @@ describe('showTabOnly', function() { profile.threads[1].frameTable.innerWindowID[0] = iframeInnerWindowIDsWithChild; const { getState } = _getStoreWithURL( { - search: '?showTabOnly1=123', + search: '?view=active-tab&showTabOnly1=123', }, profile ); @@ -456,7 +463,7 @@ describe('showTabOnly', function() { it('should remove other full view url states if present', function() { const { getState } = _getStoreWithURL({ search: - '?showTabOnly1=123&globalTrackOrder=3-2-1-0&hiddenGlobalTracks=4-5&hiddenLocalTracksByPid=111-1&thread=0', + '?showTabOnly1=123&view=active-tab&globalTrackOrder=3-2-1-0&hiddenGlobalTracks=4-5&hiddenLocalTracksByPid=111-1&thread=0', }); const newUrl = new URL( @@ -465,7 +472,7 @@ describe('showTabOnly', function() { ); // The url states that are relevant to full view should be stripped out. expect(newUrl.search).toEqual( - `?showTabOnly1=123&thread=0&v=${CURRENT_URL_VERSION}` + `?showTabOnly1=123&thread=0&v=${CURRENT_URL_VERSION}&view=active-tab` ); }); }); diff --git a/src/types/actions.js b/src/types/actions.js index 7734157c55..d9e4fb4e45 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -269,7 +269,7 @@ type ReceiveProfileAction = +selectedThreadIndex: ThreadIndex, +globalTracks: ActiveTabGlobalTrack[], +resourceTracks: LocalTrack[], - +showTabOnly?: BrowsingContextID | null, + +browsingContextID: BrowsingContextID, |} | {| +type: 'DATA_RELOAD', diff --git a/src/types/state.js b/src/types/state.js index d5cf6d270b..9a9c1145c0 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -63,6 +63,8 @@ export type FullProfileViewState = {| localTracksByPid: Map, |}; +export type OriginsViewState = null; + /** * Active tab profile view state * They should not be used from the full view. @@ -94,6 +96,7 @@ export type ProfileViewState = { +profile: Profile | null, +full: FullProfileViewState, +activeTab: ActiveTabProfileViewState, + +origins: OriginsViewState, }; export type AppViewState = @@ -234,6 +237,14 @@ export type ProfileSpecificUrlState = {| // activeTab: ActiveTabSpecificProfileUrlState, |}; +/** + * Determines how the timeline's tracks are organized. + */ +export type TimelineTrackOrganization = + | {| +type: 'full' |} + | {| +type: 'active-tab', +browsingContextID: BrowsingContextID |} + | {| +type: 'origins' |}; + export type UrlState = {| +dataSource: DataSource, // This is used for the "public" dataSource". @@ -245,7 +256,7 @@ export type UrlState = {| +selectedTab: TabSlug, +pathInZipFile: string | null, +profileName: string, - +showTabOnly: BrowsingContextID | null, + +timelineTrackOrganization: TimelineTrackOrganization, +profileSpecific: ProfileSpecificUrlState, |}; From 3e3faa9b5458ced59e8b12f0786eb6668709ae41 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Wed, 29 Apr 2020 13:16:42 -0500 Subject: [PATCH 2/3] Add an experimental origins-based view --- src/actions/receive-profile.js | 200 +++++++++++++++++++- src/components/timeline/OriginsTimeline.css | 10 + src/components/timeline/OriginsTimeline.js | 181 ++++++++++++++++++ src/components/timeline/index.js | 4 +- src/reducers/app.js | 1 + src/reducers/profile-view.js | 18 +- src/reducers/publish.js | 1 + src/reducers/url-state.js | 1 + src/reducers/zipped-profiles.js | 1 + src/selectors/profile.js | 8 + src/test/fixtures/profiles/tracks.js | 61 ++++++ src/test/store/origins.test.js | 119 ++++++++++++ src/types/actions.js | 6 + src/types/profile-derived.js | 49 +++++ src/types/state.js | 5 +- 15 files changed, 656 insertions(+), 9 deletions(-) create mode 100644 src/components/timeline/OriginsTimeline.css create mode 100644 src/components/timeline/OriginsTimeline.js create mode 100644 src/test/store/origins.test.js diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index 22fbc91fd1..6debd09991 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -62,8 +62,12 @@ import type { ThreadIndex, IndexIntoFuncTable, BrowsingContextID, + Page, + InnerWindowID, + Pid, } from '../types/profile'; -import { assertExhaustiveCheck } from '../utils/flow'; +import type { OriginsTimelineRoot } from '../types/profile-derived'; +import { assertExhaustiveCheck, ensureExists } from '../utils/flow'; /** * This file collects all the actions that are used for receiving the profile in the @@ -193,7 +197,9 @@ export function finalizeProfileView( break; case 'origins': { if (pages) { - throw new Error("This isn't handled yet."); + dispatch( + finalizeOriginProfileView(profile, pages, selectedThreadIndex) + ); } else { // Don't fully trust the URL, this view doesn't support the origins based // view. Switch to fulll view. @@ -322,6 +328,187 @@ export function finalizeFullProfileView( }; } +/** + * This is a small utility to extract the origin from a URL, to build the origins-based + * profile view. + */ +function getOrigin(urlString: string): string { + if (urlString.startsWith('chrome://')) { + return urlString; + } + try { + const url = new URL(urlString); + if (url.origin === 'null') { + // This can happen for "about:newtab" + return urlString; + } + return url.origin; + } catch { + // This failed, maybe it's an internal URL. + return urlString; + } +} + +/** + * Finalize the profile state for the origin-based view. This is an experimental + * view for fission. It's not turned on by default. Note, that this function + * probably needs a lot of work to become more correct to handle everything, + * so it shouldn't be trusted too much at this time. + */ +export function finalizeOriginProfileView( + profile: Profile, + pages: Page[], + selectedThreadIndex: ThreadIndex | null +): ThunkAction { + return dispatch => { + const idToPage: Map = new Map(); + for (const page of pages) { + idToPage.set(page.innerWindowID, page); + } + + // TODO - A thread can have multiple pages. Ignore this for now. + const pageOfThread: Array = []; + // These maps essentially serve as a tuple of the InnerWindowID and ThreadIndex + // that can be iterated through on a "for of" loop. + const rootOrigins: Map = new Map(); + const subOrigins: Map = new Map(); + // The set of all thread indexes that do not have an origin associated with them. + const noOrigins: Set = new Set(); + + // Populate the collections above by iterating through all of the threads. + for ( + let threadIndex = 0; + threadIndex < profile.threads.length; + threadIndex++ + ) { + const { frameTable } = profile.threads[threadIndex]; + + let originFound = false; + for (let frameIndex = 0; frameIndex < frameTable.length; frameIndex++) { + const innerWindowID = frameTable.innerWindowID[frameIndex]; + if (innerWindowID === null || innerWindowID === 0) { + continue; + } + + const page = idToPage.get(innerWindowID); + if (!page) { + // This should only happen if there is an error in the Gecko implementation. + console.error('Could not find the page for an innerWindowID', { + innerWindowID, + pages, + }); + break; + } + + if (page.embedderInnerWindowID === 0) { + rootOrigins.set(innerWindowID, threadIndex); + } else { + subOrigins.set(innerWindowID, threadIndex); + } + + originFound = true; + pageOfThread[threadIndex] = page; + break; + } + + if (!originFound) { + pageOfThread[threadIndex] = null; + noOrigins.add(threadIndex); + } + } + + // Build up the `originsTimelineRoots` variable and any relationships needed + // for determining the structure of the threads in terms of their origins. + const originsTimelineRoots: OriginsTimelineRoot[] = []; + // This map can be used to take a thread with no origin information, and assign + // it to some origin based on a shared PID. + const pidToRootInnerWindowID: Map = new Map(); + // The root is a root domain only. + const innerWindowIDToRoot: Map = new Map(); + for (const [innerWindowID, threadIndex] of rootOrigins) { + const thread = profile.threads[threadIndex]; + const page = ensureExists(pageOfThread[threadIndex]); + pidToRootInnerWindowID.set(thread.pid, innerWindowID); + // These are all roots. + innerWindowIDToRoot.set(innerWindowID, innerWindowID); + originsTimelineRoots.push({ + type: 'origin', + innerWindowID, + threadIndex, + page, + origin: getOrigin(page.url), + children: [], + }); + } + + // Iterate and drain the sub origins from this set, and attempt to assign them + // to a root origin. This needs to loop to handle arbitrary sub-iframe depths. + const remainingSubOrigins = new Set([...subOrigins]); + let lastRemaining = Infinity; + while (lastRemaining !== remainingSubOrigins.size) { + lastRemaining = remainingSubOrigins.size; + for (const suborigin of remainingSubOrigins) { + const [innerWindowID, threadIndex] = suborigin; + const page = ensureExists(pageOfThread[threadIndex]); + const rootInnerWindowID = innerWindowIDToRoot.get( + page.embedderInnerWindowID + ); + if (rootInnerWindowID === undefined) { + // This root has not been found yet. + continue; + } + const thread = profile.threads[threadIndex]; + pidToRootInnerWindowID.set(thread.pid, rootInnerWindowID); + + remainingSubOrigins.delete(suborigin); + innerWindowIDToRoot.set(innerWindowID, rootInnerWindowID); + const root = ensureExists( + originsTimelineRoots.find( + root => root.innerWindowID === rootInnerWindowID + ) + ); + root.children.push({ + type: 'sub-origin', + innerWindowID, + threadIndex, + origin: getOrigin(page.url), + page, + }); + } + } + + // Try to blame a thread on another thread with an origin. If this doesn't work, + // then add it to this originsTimelineNoOrigin array. + const originsTimelineNoOrigin = []; + for (const threadIndex of noOrigins) { + const thread = profile.threads[threadIndex]; + const rootInnerWindowID = pidToRootInnerWindowID.get(thread.pid); + const noOriginEntry = { + type: 'no-origin', + threadIndex, + }; + if (rootInnerWindowID) { + const root = ensureExists( + originsTimelineRoots.find( + root => root.innerWindowID === rootInnerWindowID + ) + ); + root.children.push(noOriginEntry); + } else { + originsTimelineNoOrigin.push(noOriginEntry); + } + } + + dispatch({ + type: 'VIEW_ORIGINS_PROFILE', + // TODO - We should pick the best selected thread. + selectedThreadIndex: + selectedThreadIndex === null ? 0 : selectedThreadIndex, + originsTimeline: [...originsTimelineNoOrigin, ...originsTimelineRoots], + }); + }; +} + /** * Finalize the profile state for active tab view. * This function will take the view information from the URL, such as hiding and sorting @@ -387,7 +574,14 @@ export function changeTimelineTrackOrganization( ); break; case 'origins': { - throw new Error("This isn't handled yet."); + const pages = ensureExists( + profile.pages, + 'There was no page information in the profile.' + ); + dispatch( + finalizeOriginProfileView(profile, pages, selectedThreadIndex) + ); + break; } default: throw assertExhaustiveCheck( diff --git a/src/components/timeline/OriginsTimeline.css b/src/components/timeline/OriginsTimeline.css new file mode 100644 index 0000000000..2aa4910e14 --- /dev/null +++ b/src/components/timeline/OriginsTimeline.css @@ -0,0 +1,10 @@ +/* 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/. */ + +/** + * This file is just a stub for the WIP origins-based timeline. + */ +.originsTimelineTrack { + margin: 5px; +} diff --git a/src/components/timeline/OriginsTimeline.js b/src/components/timeline/OriginsTimeline.js new file mode 100644 index 0000000000..c4459f37c4 --- /dev/null +++ b/src/components/timeline/OriginsTimeline.js @@ -0,0 +1,181 @@ +/* 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 memoize from 'memoize-immutable'; +import * as React from 'react'; +import TimelineRuler from './Ruler'; +import TimelineSelection from './Selection'; +import OverflowEdgeIndicator from './OverflowEdgeIndicator'; +import { withSize } from '../shared/WithSize'; +import explicitConnect from '../../utils/connect'; +import { assertExhaustiveCheck } from '../../utils/flow'; +import { + getPanelLayoutGeneration, + getCommittedRange, + getZeroAt, + getOriginsTimeline, + getThreads, +} from 'firefox-profiler/selectors'; +import { getFriendlyThreadName } from '../../profile-logic/profile-data'; +import { changeSelectedThread } from '../../actions/profile-view'; + +import type { SizeProps } from '../shared/WithSize'; +import type { Thread, ThreadIndex } from '../../types/profile'; +import type { + InitialSelectedTrackReference, + OriginsTimeline, + OriginsTimelineTrack, +} from '../../types/profile-derived'; +import type { Milliseconds, StartEndRange } from '../../types/units'; +import type { ConnectedProps } from '../../utils/connect'; + +import './OriginsTimeline.css'; + +type StateProps = {| + +committedRange: StartEndRange, + +panelLayoutGeneration: number, + +originsTimeline: OriginsTimeline, + +zeroAt: Milliseconds, + +threads: Thread[], +|}; + +type DispatchProps = {| + +changeSelectedThread: typeof changeSelectedThread, +|}; + +type Props = {| + ...SizeProps, + ...ConnectedProps<{||}, StateProps, DispatchProps>, +|}; + +type State = {| + initialSelected: InitialSelectedTrackReference | null, +|}; + +/** + * This view is an experimental view, not meant for real usage at this time. It + * implements the absolute minimum structure to show what real data looks like when + * attempting to view threads organized according to their origin. The origin is + * the `https://example.com` part of a full URL. + */ +class OriginsTimelineView extends React.PureComponent { + state = { + initialSelected: null, + }; + + /** + * This method collects the initially selected track's HTMLElement. This allows the timeline + * to scroll the initially selected track into view once the page is loaded. + */ + setInitialSelected = (el: InitialSelectedTrackReference) => { + this.setState({ initialSelected: el }); + }; + + /** + * This makes it handy to view a track that's been organized in the view. + * Memoizing it is probably over-kill, but there you go. + */ + clickTrack = memoize((threadIndex: ThreadIndex) => { + return (event: Event) => { + event.preventDefault(); + this.props.changeSelectedThread(threadIndex); + }; + }); + + /** + * TODO - These might be better to use the other track data structures. + */ + renderTrack = (track: OriginsTimelineTrack) => { + const { threads } = this.props; + switch (track.type) { + case 'origin': + return ( +
  • + + {track.origin} + +
      {track.children.map(this.renderTrack)}
    +
  • + ); + case 'no-origin': { + const thread = threads[track.threadIndex]; + return ( +
  • + + {getFriendlyThreadName(threads, thread)} + +
  • + ); + } + case 'sub-origin': { + return ( +
  • + + {track.origin} + +
  • + ); + } + default: + throw assertExhaustiveCheck(track, 'Unhandled OriginsTimelineTrack.'); + } + }; + + render() { + const { + committedRange, + zeroAt, + width, + panelLayoutGeneration, + originsTimeline, + } = this.props; + + return ( + <> + + + +
      + {originsTimeline.map(this.renderTrack)} +
    +
    +
    + + ); + } +} + +export default explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: state => ({ + threads: getThreads(state), + committedRange: getCommittedRange(state), + zeroAt: getZeroAt(state), + panelLayoutGeneration: getPanelLayoutGeneration(state), + originsTimeline: getOriginsTimeline(state), + }), + mapDispatchToProps: { + changeSelectedThread, + }, + component: withSize(OriginsTimelineView), +}); diff --git a/src/components/timeline/index.js b/src/components/timeline/index.js index 0a81e28a85..758d6933fd 100644 --- a/src/components/timeline/index.js +++ b/src/components/timeline/index.js @@ -9,6 +9,7 @@ import explicitConnect from '../../utils/connect'; import { getTimelineTrackOrganization } from 'firefox-profiler/selectors'; import FullTimeline from '../timeline/FullTimeline'; import ActiveTabTimeline from '../timeline/ActiveTabTimeline'; +import OriginsTimelineView from '../timeline/OriginsTimeline'; import { assertExhaustiveCheck } from '../../utils/flow'; import type { ConnectedProps } from '../../utils/connect'; @@ -29,8 +30,7 @@ class Timeline extends React.PureComponent { case 'active-tab': return ; case 'origins': - // This doesn't exist yet. - return null; + return ; default: throw assertExhaustiveCheck( timelineTrackOrganization, diff --git a/src/reducers/app.js b/src/reducers/app.js index 078ca02e19..6563a85355 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -46,6 +46,7 @@ const view: Reducer = ( return { phase: 'DATA_RELOAD' }; case 'RECEIVE_ZIP_FILE': case 'VIEW_FULL_PROFILE': + case 'VIEW_ORIGINS_PROFILE': case 'VIEW_ACTIVE_TAB_PROFILE': return { phase: 'DATA_LOADED' }; default: diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index e7790175d8..372e86032e 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -18,6 +18,7 @@ import type { GlobalTrack, TrackIndex, ActiveTabGlobalTrack, + OriginsTimeline, } from '../types/profile-derived'; import type { StartEndRange } from '../types/units'; import type { @@ -607,8 +608,17 @@ const rightClickedMarker: Reducer = ( } }; -const origins: Reducer = (state = null, _action) => { - return state; +/** + * The origins timeline is experimental. See the OriginsTimeline component + * for more information. + */ +const originsTimeline: Reducer = (state = [], action) => { + switch (action.type) { + case 'VIEW_ORIGINS_PROFILE': + return action.originsTimeline; + default: + return state; + } }; /** @@ -659,7 +669,9 @@ const profileViewReducer: Reducer = wrapReducerInResetter( hiddenGlobalTracksGetter: activeTabHiddenGlobalTracksGetter, hiddenLocalTracksByPidGetter: activeTabHiddenLocalTracksByPidGetter, }), - origins, + origins: combineReducers({ + originsTimeline, + }), }) ); diff --git a/src/reducers/publish.js b/src/reducers/publish.js index 3dffa60f6a..42376c9517 100644 --- a/src/reducers/publish.js +++ b/src/reducers/publish.js @@ -181,6 +181,7 @@ const isHidingStaleProfile: Reducer = (state = false, action) => { case 'HIDE_STALE_PROFILE': return true; case 'VIEW_FULL_PROFILE': + case 'VIEW_ORIGINS_PROFILE': case 'VIEW_ACTIVE_TAB_PROFILE': return false; default: diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 0de276870b..019c4c256d 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -114,6 +114,7 @@ const selectedThread: Reducer = (state = null, action) => { case 'CHANGE_SELECTED_THREAD': case 'SELECT_TRACK': case 'VIEW_FULL_PROFILE': + case 'VIEW_ORIGINS_PROFILE': case 'VIEW_ACTIVE_TAB_PROFILE': case 'ISOLATE_PROCESS': case 'ISOLATE_PROCESS_MAIN_THREAD': diff --git a/src/reducers/zipped-profiles.js b/src/reducers/zipped-profiles.js index e404ebd92b..1e96b482ea 100644 --- a/src/reducers/zipped-profiles.js +++ b/src/reducers/zipped-profiles.js @@ -86,6 +86,7 @@ const zipFile: Reducer = ( zip: ensureExists(state.zip), pathInZipFile: ensureExists(state.pathInZipFile), }); + case 'VIEW_ORIGINS_PROFILE': case 'VIEW_FULL_PROFILE': case 'VIEW_ACTIVE_TAB_PROFILE': // Only process this as a change if a zip file is actually loaded. diff --git a/src/selectors/profile.js b/src/selectors/profile.js index 3c4bc54dee..cf074be66c 100644 --- a/src/selectors/profile.js +++ b/src/selectors/profile.js @@ -42,6 +42,7 @@ import type { AccumulatedCounterSamples, ProfileFilterPageData, ActiveTabGlobalTrack, + OriginsTimeline, } from '../types/profile-derived'; import type { Milliseconds, StartEndRange } from '../types/units'; import type { @@ -467,6 +468,13 @@ export const getActiveTabHiddenLocalTracksByPidGetter: Selector< () => Map> > = state => getActiveTabProfileView(state).hiddenLocalTracksByPidGetter; +/** + * Origins profile view selectors. + */ + +export const getOriginsTimeline: Selector = state => + getOriginsProfileView(state).originsTimeline; + /** * It's a bit hard to deduce the total amount of hidden tracks, as there are both * global and local tracks, and they are stored by PID. If a global track is hidden, diff --git a/src/test/fixtures/profiles/tracks.js b/src/test/fixtures/profiles/tracks.js index 9d691669ab..30b679e7f4 100644 --- a/src/test/fixtures/profiles/tracks.js +++ b/src/test/fixtures/profiles/tracks.js @@ -12,9 +12,11 @@ import { import { storeWithProfile } from '../stores'; import { oneLine } from 'common-tags'; +import type { OriginsTimelineTrack } from '../../../types/profile-derived'; import type { Profile } from '../../../types/profile'; import type { State } from '../../../types/state'; import { assertExhaustiveCheck } from '../../../utils/flow'; +import { getFriendlyThreadName } from '../../../profile-logic/profile-data'; /** * This function takes the current timeline tracks, and generates a human readable result @@ -232,3 +234,62 @@ export function getHumanReadableActiveTabTracks(state: State): string[] { return text; } + +/** + * This function takes the current origins timeline tracks, and generates a + * human readable result that makes it easy to assert the shape and structure + * of the tracks in tests. + * + * Usage: + * + * expect(getHumanReadableOriginTracks(getState())).toEqual([ + * 'Parent Process', + * 'Compositor', + * 'GeckoMain pid:(2)', + * 'GeckoMain pid:(3)', + * 'https://aaaa.example.com', + * ' - https://bbbb.example.com', + * ' - https://cccc.example.com', + * 'https://dddd.example.com', + * ' - https://eeee.example.com', + * ' - https://ffff.example.com', + * ]); + */ +export function getHumanReadableOriginTracks(state: State): string[] { + const threads = profileViewSelectors.getThreads(state); + const originsTimeline = profileViewSelectors.getOriginsTimeline(state); + + const results: string[] = []; + + function addHumanFriendlyTrack( + track: OriginsTimelineTrack, + nested: boolean = false + ) { + const prefix = nested ? ' - ' : ''; + switch (track.type) { + case 'origin': + results.push(track.origin); + for (const child of track.children) { + addHumanFriendlyTrack(child, true); + } + break; + case 'no-origin': { + const thread = threads[track.threadIndex]; + results.push(prefix + getFriendlyThreadName(threads, thread)); + break; + } + case 'sub-origin': { + results.push(prefix + track.origin); + break; + } + default: + throw assertExhaustiveCheck(track, 'Unhandled OriginsTimelineTrack.'); + } + } + + for (const track of originsTimeline) { + addHumanFriendlyTrack(track); + } + + return results; +} diff --git a/src/test/store/origins.test.js b/src/test/store/origins.test.js new file mode 100644 index 0000000000..a965f5bfaa --- /dev/null +++ b/src/test/store/origins.test.js @@ -0,0 +1,119 @@ +/* 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 { getHumanReadableOriginTracks } from '../fixtures/profiles/tracks'; +import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; +import { viewProfile } from '../../actions/receive-profile'; +import { ensureExists } from '../../utils/flow'; +import createStore from '../../app-logic/create-store'; + +type TestDefinedOriginThread = {| + name?: string, + origin?: string, + parentOrigin?: string, + pid?: number, +|}; + +function getProfileWithOrigins(...originThreads: TestDefinedOriginThread[]) { + let uniqueId = 1; + const sampleNames = originThreads.map(({ name, origin }) => { + if (name) { + return name; + } + if (origin) { + return origin; + } + throw new Error('Expected a name or origin.'); + }); + + const { profile } = getProfileFromTextSamples(...sampleNames); + const pages = ensureExists(profile.pages, 'Expected to find profile pages.'); + + for ( + let threadIndex = 0; + threadIndex < profile.threads.length; + threadIndex++ + ) { + const thread = profile.threads[threadIndex]; + const { name, origin, parentOrigin, pid } = originThreads[threadIndex]; + if (origin) { + // The arbitrary innerWindowID is set up to be the same as the thread index. + const innerWindowID = threadIndex; + let embedderInnerWindowID = 0; + if (parentOrigin) { + embedderInnerWindowID = originThreads.findIndex( + other => other.origin === parentOrigin + ); + if (embedderInnerWindowID === -1) { + throw new Error('Could not find'); + } + } + pages.push({ + browsingContextID: uniqueId++, + // The arbitrary innerWindowID is set up to be the same as the thread index. + innerWindowID, + url: origin, + embedderInnerWindowID, + }); + thread.frameTable.innerWindowID[0] = innerWindowID; + } + + if (name) { + thread.name = name; + } + thread.pid = pid === undefined ? threadIndex : pid; + } + + return profile; +} + +describe('origins timeline', function() { + function setup(...originThreads: TestDefinedOriginThread[]): * { + const store = createStore(); + const profile = getProfileWithOrigins(...originThreads); + const { dispatch } = store; + const timelineTrackOrganization = { type: 'origins' }; + dispatch(viewProfile(profile, { timelineTrackOrganization })); + return store; + } + + it('can compute an origins based view', function() { + const { getState } = setup( + { name: `GeckoMain`, pid: 1 }, + { name: `Compositor`, pid: 1 }, + { origin: `https://AAAA.example.com` }, + { + origin: `https://BBBB.example.com`, + parentOrigin: `https://AAAA.example.com`, + }, + { + origin: `https://CCCC.example.com`, + parentOrigin: `https://AAAA.example.com`, + }, + { origin: `https://DDDD.example.com` }, + { + origin: `https://EEEE.example.com`, + parentOrigin: `https://DDDD.example.com`, + }, + { + origin: `https://FFFF.example.com`, + parentOrigin: `https://DDDD.example.com`, + }, + { name: `GeckoMain pid:(2)` }, + { name: `GeckoMain pid:(3)` } + ); + expect(getHumanReadableOriginTracks(getState())).toEqual([ + 'Parent Process', + 'Compositor', + 'GeckoMain pid:(2)', + 'GeckoMain pid:(3)', + 'https://aaaa.example.com', + ' - https://bbbb.example.com', + ' - https://cccc.example.com', + 'https://dddd.example.com', + ' - https://eeee.example.com', + ' - https://ffff.example.com', + ]); + }); +}); diff --git a/src/types/actions.js b/src/types/actions.js index d9e4fb4e45..8f664b0857 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -21,6 +21,7 @@ import type { TrackIndex, MarkerIndex, ActiveTabGlobalTrack, + OriginsTimeline, } from './profile-derived'; import type { TemporaryError } from '../utils/errors'; import type { Transform, TransformStacksPerThread } from './transforms'; @@ -264,6 +265,11 @@ type ReceiveProfileAction = +localTrackOrderByPid: Map, +showTabOnly?: BrowsingContextID | null, |} + | {| + +type: 'VIEW_ORIGINS_PROFILE', + +selectedThreadIndex: ThreadIndex, + +originsTimeline: OriginsTimeline, + |} | {| +type: 'VIEW_ACTIVE_TAB_PROFILE', +selectedThreadIndex: ThreadIndex, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 8dddbd6f43..38948d6485 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -12,6 +12,8 @@ import type { IndexIntoJsTracerEvents, IndexIntoCategoryList, CounterIndex, + InnerWindowID, + Page, } from './profile'; import type { StackTiming } from '../profile-logic/stack-timing'; export type IndexIntoCallNodeTable = number; @@ -202,6 +204,53 @@ export type LocalTrack = export type Track = GlobalTrack | LocalTrack; export type TrackIndex = number; +/** + * The origins timeline view is experimental. These data structures may need to be + * adjusted to fit closer to the other track types, but they were easy to do for now. + */ + +/** + * This origin was loaded as a sub-frame to another one. It will be nested in the view. + */ +export type OriginsTimelineEntry = {| + type: 'sub-origin', + innerWindowID: InnerWindowID, + threadIndex: ThreadIndex, + page: Page, + origin: string, +|}; + +/** + * This is a "root" origin, which is viewed at the top level in a tab. + */ +export type OriginsTimelineRoot = {| + type: 'origin', + innerWindowID: InnerWindowID, + threadIndex: ThreadIndex, + page: Page, + origin: string, + children: Array, +|}; + +/** + * This thread does not have any origin information associated with it. However + * it may be listed as a child of another "root" timeline origin if it is in the + * same process as that thread. + */ +export type OriginsTimelineNoOrigin = {| + type: 'no-origin', + threadIndex: ThreadIndex, +|}; + +export type OriginsTimelineTrack = + | OriginsTimelineEntry + | OriginsTimelineRoot + | OriginsTimelineNoOrigin; + +export type OriginsTimeline = Array< + OriginsTimelineNoOrigin | OriginsTimelineRoot +>; + /** * Active tab view tracks */ diff --git a/src/types/state.js b/src/types/state.js index 9a9c1145c0..2f3084b98d 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -26,6 +26,7 @@ import type { TrackIndex, MarkerIndex, ActiveTabGlobalTrack, + OriginsTimeline, } from './profile-derived'; import type { Attempt } from '../utils/errors'; import type { TransformStacksPerThread } from './transforms'; @@ -63,7 +64,9 @@ export type FullProfileViewState = {| localTracksByPid: Map, |}; -export type OriginsViewState = null; +export type OriginsViewState = {| + originsTimeline: OriginsTimeline, +|}; /** * Active tab profile view state From f71e075b3a53951048a36f9cf04b1a50616731cc Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Wed, 29 Apr 2020 13:56:23 -0500 Subject: [PATCH 3/3] Rename showTabOnly1 to ctxId --- src/app-logic/url-handling.js | 12 ++++++------ src/test/url-handling.test.js | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index 4cac35538f..4d5f7d5e80 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -110,7 +110,7 @@ type BaseQuery = {| transforms: string, profiles: string[], profileName: string, - showTabOnly1: BrowsingContextID, + ctxId: BrowsingContextID, view: string, ...FullProfileSpecificBaseQuery, ...ActiveTabProfileSpecificBaseQuery, @@ -277,7 +277,7 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { ); } - let showTabOnly1; + let ctxId; let view; switch (timelineTrackOrganization.type) { case 'full': @@ -285,7 +285,7 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { break; case 'active-tab': view = timelineTrackOrganization.type; - showTabOnly1 = timelineTrackOrganization.browsingContextID; + ctxId = timelineTrackOrganization.browsingContextID; break; case 'origins': view = timelineTrackOrganization.type; @@ -305,7 +305,7 @@ export function urlStateToUrlObject(urlState: UrlState): UrlObject { thread: selectedThread === null ? undefined : selectedThread.toString(), file: urlState.pathInZipFile || undefined, profiles: urlState.profilesToCompare || undefined, - showTabOnly1, + ctxId, view, v: CURRENT_URL_VERSION, profileName: urlState.profileName || undefined, @@ -475,8 +475,8 @@ export function stateFromLocation( } let browsingContextId = null; - if (query.showTabOnly1 && Number.isInteger(Number(query.showTabOnly1))) { - browsingContextId = Number(query.showTabOnly1); + if (query.ctxId && Number.isInteger(Number(query.ctxId))) { + browsingContextId = Number(query.ctxId); } return { diff --git a/src/test/url-handling.test.js b/src/test/url-handling.test.js index c9e8505b4f..2834a38ee7 100644 --- a/src/test/url-handling.test.js +++ b/src/test/url-handling.test.js @@ -408,12 +408,12 @@ describe('showTabOnly', function() { ); const urlState = urlStateReducers.getUrlState(getState()); const { query } = urlStateToUrlObject(urlState); - expect(query.showTabOnly1).toBe(browsingContextID); + expect(query.ctxId).toBe(browsingContextID); }); it('reflects in the state from URL', function() { const { getState } = _getStoreWithURL({ - search: '?showTabOnly1=123&view=active-tab', + search: '?ctxId=123&view=active-tab', }); expect(urlStateReducers.getTimelineTrackOrganization(getState())).toEqual({ type: 'active-tab', @@ -438,7 +438,7 @@ describe('showTabOnly', function() { profile.threads[1].frameTable.innerWindowID[0] = iframeInnerWindowIDsWithChild; const { getState } = _getStoreWithURL( { - search: '?view=active-tab&showTabOnly1=123', + search: '?view=active-tab&ctxId=123', }, profile ); @@ -463,7 +463,7 @@ describe('showTabOnly', function() { it('should remove other full view url states if present', function() { const { getState } = _getStoreWithURL({ search: - '?showTabOnly1=123&view=active-tab&globalTrackOrder=3-2-1-0&hiddenGlobalTracks=4-5&hiddenLocalTracksByPid=111-1&thread=0', + '?ctxId=123&view=active-tab&globalTrackOrder=3-2-1-0&hiddenGlobalTracks=4-5&hiddenLocalTracksByPid=111-1&thread=0', }); const newUrl = new URL( @@ -472,7 +472,7 @@ describe('showTabOnly', function() { ); // The url states that are relevant to full view should be stripped out. expect(newUrl.search).toEqual( - `?showTabOnly1=123&thread=0&v=${CURRENT_URL_VERSION}&view=active-tab` + `?ctxId=123&thread=0&v=${CURRENT_URL_VERSION}&view=active-tab` ); }); });