Skip to content

Commit d935cc7

Browse files
committed
Recompute tabToThreadIndexesMap from the sanitized profile
When a tab filter was active during publishing, a hidden non-thread track (e.g. a screenshot or a per-process network track) could reappear after sanitization. attemptToPublish was remapping the URL state's hidden-track set against a global track list filtered using the prePublishedState's tabToThreadIndexesMap; that map's Set<ThreadIndex> values referred to old thread indexes, so when sanitization shifted surviving threads down by removing an earlier thread, filterGlobalTracksByTab picked the wrong threads and the hidden track ended up at a stale TrackIndex. attemptToPublish now builds the map from the sanitized profile's threads and pages so its ThreadIndex values are valid in the new track-index space. The innerWindowID to tabID computation is extracted into a shared computeInnerWindowIDToTabMap helper that the getInnerWindowIDToTabMap selector also uses.
1 parent a295cc1 commit d935cc7

4 files changed

Lines changed: 127 additions & 17 deletions

File tree

src/actions/publish.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
getCommittedRange,
3030
getGlobalTracks,
3131
getLocalTracksByPid,
32-
getTabToThreadIndexesMap,
3332
} from 'firefox-profiler/selectors/profile';
3433
import { viewProfile } from './receive-profile';
3534
import { ensureExists } from 'firefox-profiler/utils/types';
@@ -43,6 +42,10 @@ import {
4342
computeHiddenTracksAfterSanitization,
4443
computeTrackOrderAfterSanitization,
4544
} from 'firefox-profiler/profile-logic/tracks';
45+
import {
46+
computeInnerWindowIDToTabMap,
47+
computeTabToThreadIndexesMap,
48+
} from 'firefox-profiler/profile-logic/profile-data';
4649

4750
import type {
4851
Action,
@@ -494,13 +497,17 @@ export function attemptToPublish(
494497
oldThreadIndexToNew
495498
);
496499

497-
const tabToThreadIndexesMap =
498-
getTabToThreadIndexesMap(prePublishedState);
500+
// The selector-derived map keys ThreadIndex values from the
501+
// prePublishedState; rebuild it against the sanitized profile.
502+
const newTabToThreadIndexesMap = computeTabToThreadIndexesMap(
503+
profile.threads,
504+
computeInnerWindowIDToTabMap(profile.pages)
505+
);
499506
const tabFilter = getTabFilter(prePublishedState);
500507
const newGlobalTracks = computeGlobalTracks(
501508
profile,
502509
tabFilter,
503-
tabToThreadIndexesMap
510+
newTabToThreadIndexesMap
504511
);
505512
const newLocalTracksByPid = computeLocalTracksByPid(
506513
profile,

src/profile-logic/profile-data.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4469,6 +4469,23 @@ export function determineTimelineType(profile: Profile): TimelineType {
44694469
return 'cpu-category';
44704470
}
44714471

4472+
/**
4473+
* Compute an innerWindowID → tabID lookup from a profile's pages list.
4474+
* Returns null when the profile has no pages.
4475+
*/
4476+
export function computeInnerWindowIDToTabMap(
4477+
pages: PageList | null | undefined
4478+
): Map<InnerWindowID, TabID> | null {
4479+
if (!pages) {
4480+
return null;
4481+
}
4482+
const innerWindowIDToTabMap = new Map<InnerWindowID, TabID>();
4483+
for (const page of pages) {
4484+
innerWindowIDToTabMap.set(page.innerWindowID, page.tabID);
4485+
}
4486+
return innerWindowIDToTabMap;
4487+
}
4488+
44724489
/**
44734490
* Compute a map of tab to thread indexes map. This is useful for learning which
44744491
* threads are involved for tabs. This is mainly used for the tab selector on

src/selectors/profile.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
getFriendlyThreadName,
1717
processCounter,
1818
getInclusiveSampleIndexRangeForSelection,
19+
computeInnerWindowIDToTabMap,
1920
computeTabToThreadIndexesMap,
2021
computeStackTableFromRawStackTable,
2122
reserveFunctionsForCollapsedResources,
@@ -428,19 +429,7 @@ export const getInnerWindowIDToPageMap: Selector<Map<
428429
export const getInnerWindowIDToTabMap: Selector<Map<
429430
InnerWindowID,
430431
TabID
431-
> | null> = createSelector(getPageList, (pages) => {
432-
if (!pages) {
433-
// Return null if there are no pages.
434-
return null;
435-
}
436-
437-
const innerWindowIDToTabMap: Map<InnerWindowID, TabID> = new Map();
438-
for (const page of pages) {
439-
innerWindowIDToTabMap.set(page.innerWindowID, page.tabID);
440-
}
441-
442-
return innerWindowIDToTabMap;
443-
});
432+
> | null> = createSelector(getPageList, computeInnerWindowIDToTabMap);
444433

445434
/**
446435
* Return a map of tab to thread indexes map. This is useful for learning which

src/test/store/publish.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
hideGlobalTrack,
6060
commitRange,
6161
} from '../../actions/profile-view';
62+
import { changeTabFilter } from '../../actions/receive-profile';
6263

6364
import {
6465
retrieveUploadedProfileInformationFromDb,
@@ -629,6 +630,102 @@ describe('attemptToPublish', function () {
629630
);
630631
});
631632

633+
it('keeps a hidden screenshot track hidden after sanitization when a tab filter is active', async function () {
634+
const { profile } = getProfileFromTextSamples('A', 'B', 'C', 'D', 'E');
635+
profile.meta.updateChannel = 'release';
636+
637+
profile.threads[0].name = 'GeckoMain';
638+
profile.threads[0].isMainThread = true;
639+
profile.threads[0].processType = 'default';
640+
profile.threads[0].pid = '100';
641+
profile.threads[1].name = 'GeckoMain';
642+
profile.threads[1].isMainThread = true;
643+
profile.threads[1].processType = 'tab';
644+
profile.threads[1].pid = '200';
645+
profile.threads[2].name = 'GeckoMain';
646+
profile.threads[2].isMainThread = true;
647+
profile.threads[2].processType = 'tab';
648+
profile.threads[2].pid = '300';
649+
profile.threads[3].name = 'GeckoMain';
650+
profile.threads[3].isMainThread = true;
651+
profile.threads[3].processType = 'tab';
652+
profile.threads[3].pid = '400';
653+
profile.threads[4].name = 'GeckoMain';
654+
profile.threads[4].isMainThread = true;
655+
profile.threads[4].processType = 'tab';
656+
profile.threads[4].pid = '500';
657+
658+
const tab1ID = 1;
659+
const tab2ID = 2;
660+
const tab1InnerWindowID = 1001;
661+
const tab2InnerWindowID = 1002;
662+
profile.pages = [
663+
{
664+
tabID: tab1ID,
665+
innerWindowID: tab1InnerWindowID,
666+
url: 'https://tab1.example.com/',
667+
embedderInnerWindowID: 0,
668+
},
669+
{
670+
tabID: tab2ID,
671+
innerWindowID: tab2InnerWindowID,
672+
url: 'https://tab2.example.com/',
673+
embedderInnerWindowID: 0,
674+
},
675+
];
676+
profile.threads[0].usedInnerWindowIDs = [tab1InnerWindowID];
677+
profile.threads[1].usedInnerWindowIDs = [tab1InnerWindowID];
678+
profile.threads[2].usedInnerWindowIDs = [tab1InnerWindowID];
679+
profile.threads[3].usedInnerWindowIDs = [tab2InnerWindowID];
680+
profile.threads[4].usedInnerWindowIDs = [tab2InnerWindowID];
681+
682+
addRawMarkersToThread(profile.threads[2], profile.shared, [
683+
makeCompositorScreenshot(0.5),
684+
]);
685+
686+
const store = storeWithProfile(profile);
687+
const { dispatch, getState, resolveUpload, assertUploadSuccess } =
688+
setupFakeUploadsWithStore(store);
689+
690+
dispatch(updateSharingOption('includeScreenshots', true));
691+
dispatch(changeTabFilter(tab1ID));
692+
693+
const globalTracksBefore = getGlobalTracks(getState());
694+
const pid200TrackIndex = globalTracksBefore.findIndex(
695+
(t) => t.type === 'process' && t.pid === '200'
696+
);
697+
const screenshotTrackIndex = globalTracksBefore.findIndex(
698+
(t) => t.type === 'screenshots'
699+
);
700+
expect(pid200TrackIndex).toBeGreaterThanOrEqual(0);
701+
expect(screenshotTrackIndex).toBeGreaterThanOrEqual(0);
702+
703+
dispatch(hideGlobalTrack(pid200TrackIndex));
704+
dispatch(hideGlobalTrack(screenshotTrackIndex));
705+
706+
expect(getHiddenGlobalTracks(getState())).toContain(screenshotTrackIndex);
707+
708+
const publishAttempt = dispatch(attemptToPublish());
709+
resolveUpload(JWT_TOKEN);
710+
await assertUploadSuccess(publishAttempt);
711+
712+
const globalTracksAfter = getGlobalTracks(getState());
713+
expect(
714+
globalTracksAfter.some((t) => t.type === 'process' && t.pid === '200')
715+
).toBe(false);
716+
expect(
717+
globalTracksAfter.some((t) => t.type === 'process' && t.pid === '300')
718+
).toBe(true);
719+
const newScreenshotTrackIndex = globalTracksAfter.findIndex(
720+
(t) => t.type === 'screenshots'
721+
);
722+
expect(newScreenshotTrackIndex).toBeGreaterThanOrEqual(0);
723+
724+
expect(getHiddenGlobalTracks(getState())).toContain(
725+
newScreenshotTrackIndex
726+
);
727+
});
728+
632729
describe('with zip files', function () {
633730
const setupZipFileTests = async () => {
634731
const { store } = await storeWithZipFile([

0 commit comments

Comments
 (0)