Skip to content

Commit 1d36df0

Browse files
authored
Order global tracks by activity and select the most active non-parent process by default (#5491)
Previously we were keeping the original order in the profile data structure when we were initializing the global track order, and selecting the first tab visible process by default. But this was mostly incorrect for the cases where we have more than one visible tab process, because usually busier thread arrives later during the profile collection. This should help our users to see the most active process very quickly instead of having to scroll down every time.
2 parents 5c7515a + fc1a8d3 commit 1d36df0

File tree

7 files changed

+379
-94
lines changed

7 files changed

+379
-94
lines changed

src/actions/receive-profile.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,13 @@ export function finalizeFullProfileView(
260260
);
261261
const localTracksByPid = computeLocalTracksByPid(profile, globalTracks);
262262

263+
const threadActivityScores = getThreadActivityScores(getState());
263264
const legacyThreadOrder = getLegacyThreadOrder(getState());
264265
const globalTrackOrder = initializeGlobalTrackOrder(
265266
globalTracks,
266267
hasUrlInfo ? getGlobalTrackOrder(getState()) : null,
267-
legacyThreadOrder
268+
legacyThreadOrder,
269+
threadActivityScores
268270
);
269271
const localTrackOrderByPid = initializeLocalTrackOrderByPid(
270272
hasUrlInfo ? getLocalTrackOrderByPid(getState()) : null,
@@ -308,7 +310,7 @@ export function finalizeFullProfileView(
308310
hiddenTracks = computeDefaultHiddenTracks(
309311
tracksWithOrder,
310312
profile,
311-
getThreadActivityScores(getState()),
313+
threadActivityScores,
312314
// Only include the parent process if there is no tab filter applied.
313315
includeParentProcessThreads
314316
);
@@ -317,7 +319,8 @@ export function finalizeFullProfileView(
317319
const selectedThreadIndexes = initializeSelectedThreadIndex(
318320
maybeSelectedThreadIndexes,
319321
getVisibleThreads(tracksWithOrder, hiddenTracks),
320-
profile
322+
profile,
323+
threadActivityScores
321324
);
322325

323326
let timelineType = null;
@@ -1495,11 +1498,13 @@ export function changeTabFilter(tabID: TabID | null): ThunkAction<void> {
14951498
);
14961499
const localTracksByPid = computeLocalTracksByPid(profile, globalTracks);
14971500

1501+
const threadActivityScores = getThreadActivityScores(getState());
14981502
const legacyThreadOrder = getLegacyThreadOrder(getState());
14991503
const globalTrackOrder = initializeGlobalTrackOrder(
15001504
globalTracks,
15011505
null, // Passing null to urlGlobalTrackOrder to reinitilize it.
1502-
legacyThreadOrder
1506+
legacyThreadOrder,
1507+
threadActivityScores
15031508
);
15041509
const localTrackOrderByPid = initializeLocalTrackOrderByPid(
15051510
null, // Passing null to urlTrackOrderByPid to reinitilize it.
@@ -1536,7 +1541,7 @@ export function changeTabFilter(tabID: TabID | null): ThunkAction<void> {
15361541
hiddenTracks = computeDefaultHiddenTracks(
15371542
tracksWithOrder,
15381543
profile,
1539-
getThreadActivityScores(getState()),
1544+
threadActivityScores,
15401545
// Only include the parent process if there is no tab filter applied.
15411546
includeParentProcessThreads
15421547
);
@@ -1545,7 +1550,8 @@ export function changeTabFilter(tabID: TabID | null): ThunkAction<void> {
15451550
const selectedThreadIndexes = initializeSelectedThreadIndex(
15461551
null, // maybeSelectedThreadIndexes
15471552
getVisibleThreads(tracksWithOrder, hiddenTracks),
1548-
profile
1553+
profile,
1554+
threadActivityScores
15491555
);
15501556

15511557
// If the currently selected tab is only visible when the selected track

src/profile-logic/profile-data.js

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,47 +1286,6 @@ export function getTimeRangeIncludingAllThreads(
12861286
return completeRange;
12871287
}
12881288

1289-
export function defaultThreadOrder(threads: RawThread[]): ThreadIndex[] {
1290-
const threadOrder = threads.map((thread, i) => i);
1291-
1292-
// Note: to have a consistent behavior independant of the sorting algorithm,
1293-
// we need to be careful that the comparator function is consistent:
1294-
// comparator(a, b) === - comparator(b, a)
1295-
// and
1296-
// comparator(a, b) === 0 if and only if a === b
1297-
threadOrder.sort((a, b) => {
1298-
const nameA = threads[a].name;
1299-
const nameB = threads[b].name;
1300-
1301-
if (nameA === nameB) {
1302-
return a - b;
1303-
}
1304-
1305-
// Put the compositor/renderer thread last.
1306-
// Compositor will always be before Renderer, if both are present.
1307-
if (nameA === 'Compositor') {
1308-
return 1;
1309-
}
1310-
1311-
if (nameB === 'Compositor') {
1312-
return -1;
1313-
}
1314-
1315-
if (nameA === 'Renderer') {
1316-
return 1;
1317-
}
1318-
1319-
if (nameB === 'Renderer') {
1320-
return -1;
1321-
}
1322-
1323-
// Otherwise keep the existing order. We don't return 0 to guarantee that
1324-
// the sort is stable even if the sort algorithm isn't.
1325-
return a - b;
1326-
});
1327-
return threadOrder;
1328-
}
1329-
13301289
export function toValidImplementationFilter(
13311290
implementation: string
13321291
): ImplementationFilter {

src/profile-logic/tracks.js

Lines changed: 146 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import type {
2020
} from 'firefox-profiler/types';
2121

2222
import {
23-
defaultThreadOrder,
2423
getFriendlyThreadName,
2524
computeStackTableFromRawStackTable,
2625
} from './profile-data';
@@ -167,14 +166,60 @@ function _getDefaultLocalTrackOrder(tracks: LocalTrack[], profile: ?Profile) {
167166
return trackOrder;
168167
}
169168

170-
function _getDefaultGlobalTrackOrder(tracks: GlobalTrack[]) {
169+
function _getDefaultGlobalTrackOrder(
170+
tracks: GlobalTrack[],
171+
threadActivityScores: Array<ThreadActivityScore>
172+
) {
171173
const trackOrder = tracks.map((_, index) => index);
174+
172175
// In place sort!
173-
trackOrder.sort(
174-
(a, b) =>
175-
GLOBAL_TRACK_DISPLAY_ORDER[tracks[a].type] -
176-
GLOBAL_TRACK_DISPLAY_ORDER[tracks[b].type]
177-
);
176+
trackOrder.sort((a, b) => {
177+
const trackA = tracks[a];
178+
const trackB = tracks[b];
179+
180+
// First, sort by track type priority (visual progress, screenshots, then process).
181+
const typeOrderA = GLOBAL_TRACK_DISPLAY_ORDER[trackA.type];
182+
const typeOrderB = GLOBAL_TRACK_DISPLAY_ORDER[trackB.type];
183+
184+
if (typeOrderA !== typeOrderB) {
185+
return typeOrderA - typeOrderB;
186+
}
187+
188+
if (trackA.type !== 'process' || trackB.type !== 'process') {
189+
// For all the cases where both of them are not the process type, return zero.
190+
return 0;
191+
}
192+
193+
// This is the case where both of the tracks are processes. Let's sort them
194+
// by activity while keeping the parent process at the top.
195+
// mainThreadIndex might be null in case the GeckoMain thread is not
196+
// profiled in a profile.
197+
const activityA =
198+
trackA.mainThreadIndex !== null
199+
? threadActivityScores[trackA.mainThreadIndex]
200+
: null;
201+
const activityB =
202+
trackB.mainThreadIndex !== null
203+
? threadActivityScores[trackB.mainThreadIndex]
204+
: null;
205+
206+
// Keep the parent process at the top.
207+
if (activityA?.isInParentProcess && !activityB?.isInParentProcess) {
208+
return -1;
209+
}
210+
if (!activityA?.isInParentProcess && activityB?.isInParentProcess) {
211+
return 1;
212+
}
213+
214+
// For non-parent processes, sort by activity score.
215+
if (activityA && activityB) {
216+
return activityB.boostedSampleScore - activityA.boostedSampleScore;
217+
}
218+
219+
// For all other cases, maintain original order.
220+
return 0;
221+
});
222+
178223
return trackOrder;
179224
}
180225

@@ -646,7 +691,8 @@ export function initializeGlobalTrackOrder(
646691
urlGlobalTrackOrder: TrackIndex[] | null,
647692
// If viewing an old profile URL, there were not tracks, only thread indexes. Turn
648693
// the legacy ordering into track ordering.
649-
legacyThreadOrder: ThreadIndex[] | null
694+
legacyThreadOrder: ThreadIndex[] | null,
695+
threadActivityScores: Array<ThreadActivityScore>
650696
): TrackIndex[] {
651697
if (legacyThreadOrder !== null) {
652698
// Upgrade an older URL value based on the thread index to the track index based
@@ -692,18 +738,23 @@ export function initializeGlobalTrackOrder(
692738
return urlGlobalTrackOrder !== null &&
693739
_indexesAreValid(globalTracks.length, urlGlobalTrackOrder)
694740
? urlGlobalTrackOrder
695-
: _getDefaultGlobalTrackOrder(globalTracks);
741+
: _getDefaultGlobalTrackOrder(globalTracks, threadActivityScores);
696742
}
697743

698744
// Returns the selected thread (set), intersected with the set of visible threads.
699745
// Falls back to the default thread selection.
700746
export function initializeSelectedThreadIndex(
701747
selectedThreadIndexes: Set<ThreadIndex> | null,
702748
visibleThreadIndexes: ThreadIndex[],
703-
profile: Profile
749+
profile: Profile,
750+
threadActivityScores: Array<ThreadActivityScore>
704751
): Set<ThreadIndex> {
705752
if (selectedThreadIndexes === null) {
706-
return getDefaultSelectedThreadIndexes(visibleThreadIndexes, profile);
753+
return getDefaultSelectedThreadIndexes(
754+
visibleThreadIndexes,
755+
profile,
756+
threadActivityScores
757+
);
707758
}
708759

709760
// Filter out hidden threads from the set of selected threads.
@@ -713,16 +764,24 @@ export function initializeSelectedThreadIndex(
713764
);
714765
if (visibleSelectedThreadIndexes.size === 0) {
715766
// No selected threads were visible. Fall back to default selection.
716-
return getDefaultSelectedThreadIndexes(visibleThreadIndexes, profile);
767+
return getDefaultSelectedThreadIndexes(
768+
visibleThreadIndexes,
769+
profile,
770+
threadActivityScores
771+
);
717772
}
718773
return visibleSelectedThreadIndexes;
719774
}
720775

721-
// Select either the GeckoMain [tab] thread, or the first thread in the thread
722-
// order.
776+
// Select either the most active GeckoMain [tab] thread, or the most active
777+
// thread sorted by the thread activity scores.
778+
// It always selects global tracks when there is a GeckoMain [tab], but when
779+
// there is no GeckoMain [tab], it might select local tracks too depending
780+
// on the activity score.
723781
function getDefaultSelectedThreadIndexes(
724782
visibleThreadIndexes: ThreadIndex[],
725-
profile: Profile
783+
profile: Profile,
784+
threadActivityScores: Array<ThreadActivityScore>
726785
): Set<ThreadIndex> {
727786
if (profile.meta.initialSelectedThreads !== undefined) {
728787
return new Set(
@@ -740,17 +799,83 @@ function getDefaultSelectedThreadIndexes(
740799
})
741800
);
742801
}
743-
const visibleThreads = visibleThreadIndexes.map(
744-
(threadIndex) => profile.threads[threadIndex]
745-
);
746-
const defaultThread = _findDefaultThread(visibleThreads);
747-
const defaultThreadIndex = profile.threads.indexOf(defaultThread);
748-
if (defaultThreadIndex === -1) {
802+
803+
const { threads } = profile;
804+
if (threads.length === 0) {
749805
throw new Error('Expected to find a thread index to select.');
750806
}
807+
808+
const threadOrder = _defaultThreadOrder(
809+
visibleThreadIndexes,
810+
threads,
811+
threadActivityScores
812+
);
813+
814+
// Try to find a tab process with the highest activity score. If it can't
815+
// find one, select the first thread with the highest one.
816+
const defaultThreadIndex =
817+
threadOrder.find(
818+
(threadIndex) =>
819+
threads[threadIndex].name === 'GeckoMain' &&
820+
threads[threadIndex].processType === 'tab'
821+
) ?? threadOrder[0];
822+
751823
return new Set([defaultThreadIndex]);
752824
}
753825

826+
function _defaultThreadOrder(
827+
visibleThreadIndexes: ThreadIndex[],
828+
threads: RawThread[],
829+
threadActivityScores: Array<ThreadActivityScore>
830+
): ThreadIndex[] {
831+
const threadOrder = [...visibleThreadIndexes];
832+
833+
// Note: to have a consistent behavior independant of the sorting algorithm,
834+
// we need to be careful that the comparator function is consistent:
835+
// comparator(a, b) === - comparator(b, a)
836+
// and
837+
// comparator(a, b) === 0 if and only if a === b
838+
threadOrder.sort((a, b) => {
839+
const nameA = threads[a].name;
840+
const nameB = threads[b].name;
841+
842+
if (nameA === nameB) {
843+
// Sort by the activity, but keep the original order if the activity
844+
// scores are the equal.
845+
return (
846+
threadActivityScores[b].boostedSampleScore -
847+
threadActivityScores[a].boostedSampleScore || a - b
848+
);
849+
}
850+
851+
// Put the compositor/renderer thread last.
852+
// Compositor will always be before Renderer, if both are present.
853+
if (nameA === 'Compositor') {
854+
return 1;
855+
}
856+
857+
if (nameB === 'Compositor') {
858+
return -1;
859+
}
860+
861+
if (nameA === 'Renderer') {
862+
return 1;
863+
}
864+
865+
if (nameB === 'Renderer') {
866+
return -1;
867+
}
868+
869+
// Otherwise keep the existing order. We don't return 0 to guarantee that
870+
// the sort is stable even if the sort algorithm isn't.
871+
return (
872+
threadActivityScores[b].boostedSampleScore -
873+
threadActivityScores[a].boostedSampleScore || a - b
874+
);
875+
});
876+
return threadOrder;
877+
}
878+
754879
// Returns either a configuration of hidden tracks that has at least one
755880
// visible thread, or null.
756881
export function tryInitializeHiddenTracksLegacy(
@@ -1264,20 +1389,6 @@ function _computeThreadSampleScore(
12641389
return nonIdleSampleCount * referenceCPUDeltaPerInterval;
12651390
}
12661391

1267-
function _findDefaultThread(threads: RawThread[]): RawThread | null {
1268-
if (threads.length === 0) {
1269-
// Tests may have no threads.
1270-
return null;
1271-
}
1272-
const contentThreadId = threads.findIndex(
1273-
(thread) => thread.name === 'GeckoMain' && thread.processType === 'tab'
1274-
);
1275-
const defaultThreadIndex =
1276-
contentThreadId !== -1 ? contentThreadId : defaultThreadOrder(threads)[0];
1277-
1278-
return threads[defaultThreadIndex];
1279-
}
1280-
12811392
function _indexesAreValid(listLength: number, indexes: number[]) {
12821393
return (
12831394
// The item length is valid.

src/test/store/profile-view.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,10 @@ describe('actions/ProfileView', function () {
534534

535535
describe('with a comparison profile', function () {
536536
it('selects the calltree tab when selecting the diffing track', function () {
537+
const firstTrackReference = {
538+
type: 'global',
539+
trackIndex: 0,
540+
};
537541
const diffingTrackReference = {
538542
type: 'global',
539543
trackIndex: 2,
@@ -545,6 +549,7 @@ describe('actions/ProfileView', function () {
545549
]);
546550
const { getState, dispatch } = storeWithProfile(profile);
547551

552+
dispatch(ProfileView.selectTrackWithModifiers(firstTrackReference));
548553
dispatch(App.changeSelectedTab('flame-graph'));
549554
expect(UrlStateSelectors.getSelectedThreadIndexes(getState())).toEqual(
550555
new Set([0])

0 commit comments

Comments
 (0)