Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,18 @@ ProfileLoaderAnimation--loading-view-not-found = View not found

ProfileRootMessage--title = { -profiler-brand-name }
ProfileRootMessage--additional = Back to home
# This string is used as the accessible label for the download progress bar.
ProfileRootMessage--download-progress-label =
.aria-label = Download progress
# This string is displayed when the total download size is known.
# Variables:
# $receivedSize (String) - Amount of data received so far, e.g. "3.2 MB"
# $totalSize (String) - Total download size, e.g. "14.5 MB"
ProfileRootMessage--download-progress-known = { $receivedSize } / { $totalSize }
# This string is displayed when the total download size is unknown.
# Variables:
# $receivedSize (String) - Amount of data received so far, e.g. "3.2 MB"
ProfileRootMessage--download-progress-unknown = { $receivedSize } downloaded

## Root

Expand Down
45 changes: 45 additions & 0 deletions src/actions/receive-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fetchProfile,
getProfileUrlForHash,
type ProfileOrZip,
type DownloadProgress,
deduceContentType,
extractJsonFromArrayBuffer,
} from 'firefox-profiler/utils/profile-fetch';
Expand Down Expand Up @@ -994,6 +995,30 @@ export function retrieveProfileFromStore(
return retrieveProfileOrZipFromUrl(getProfileUrlForHash(hash), initialLoad);
}

/**
* Create a callback that aggregates download progress across multiple parallel
* fetches and dispatches the combined totals. Used for compare-mode downloads.
*/
function _makeAggregatedProgressDispatcher(
dispatch: (receivedBytes: number, totalBytes: number | null) => void
): (key: string, progress: DownloadProgress) => void {
const progressByKey = new Map<string, DownloadProgress>();
return (key: string, progress: DownloadProgress) => {
progressByKey.set(key, progress);
let receivedBytes = 0;
let totalBytes: number | null = 0;
for (const p of progressByKey.values()) {
receivedBytes += p.receivedBytes;
if (totalBytes !== null && p.totalBytes !== null) {
totalBytes += p.totalBytes;
} else {
totalBytes = null;
}
}
dispatch(receivedBytes, totalBytes);
};
}

/**
* Runs a fetch on a URL, and downloads the file. If it's JSON, then it attempts
* to process the profile. If it's a zip file, it tries to unzip it, and save it
Expand All @@ -1012,6 +1037,13 @@ export function retrieveProfileOrZipFromUrl(
onTemporaryError: (e: TemporaryError) => {
dispatch(temporaryError(e));
},
onDownloadProgress: ({ receivedBytes, totalBytes }) => {
dispatch({
type: 'PROFILE_DOWNLOAD_PROGRESS',
receivedBytes,
totalBytes,
});
},
});

switch (response.responseType) {
Expand Down Expand Up @@ -1178,6 +1210,16 @@ export function retrieveProfilesToCompare(
return async (dispatch) => {
dispatch(waitingForProfileFromUrl());

const reportAggregatedProgress = _makeAggregatedProgressDispatcher(
(receivedBytes, totalBytes) => {
dispatch({
type: 'PROFILE_DOWNLOAD_PROGRESS',
receivedBytes,
totalBytes,
});
}
);

try {
const profilesAndStates = await Promise.all(
profileViewUrls.map(async (url) => {
Expand All @@ -1196,6 +1238,9 @@ export function retrieveProfilesToCompare(
onTemporaryError: (e: TemporaryError) => {
dispatch(temporaryError(e));
},
onDownloadProgress: (progress: DownloadProgress) => {
reportAggregatedProgress(profileUrl, progress);
},
});
if (response.responseType !== 'PROFILE') {
throw new Error('Expected to receive a profile from fetchProfile');
Expand Down
4 changes: 4 additions & 0 deletions src/components/app/ProfileLoaderAnimation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ class ProfileLoaderAnimationImpl extends PureComponent<ProfileLoaderAnimationPro
dataSource === 'from-file'
);

const downloadProgress =
'downloadProgress' in view ? view.downloadProgress : null;

return (
<Localized id={message} attrs={{ title: true }} elems={{ a: <span /> }}>
<ProfileRootMessage
additionalMessage={this._renderAdditionalMessage()}
showLoader={showLoader}
showBackHomeLink={showBackHomeLink}
downloadProgress={downloadProgress}
>{`Untranslated ${message}`}</ProfileRootMessage>
</Localized>
);
Expand Down
50 changes: 50 additions & 0 deletions src/components/app/ProfileRootMessage.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,56 @@
font-size: 12px;
}

.downloadProgress {
margin-top: 16px;
}

.downloadProgressBarTrack {
overflow: hidden;
height: 8px;
border-radius: 4px;
background-color: var(--grey-30);
}

.downloadProgressBarFill {
height: 100%;
border-radius: 4px;
background-color: var(--blue-60);
transition: width 150ms ease-out;
}

.downloadProgressBarFillIndeterminate {
width: 30%;
height: 100%;
border-radius: 4px;
animation: indeterminateProgress 1.5s ease-in-out infinite;
background-color: var(--blue-60);
}

@keyframes indeterminateProgress {
0% {
transform: translateX(-100%);
}

100% {
transform: translateX(433%);
}
}

@media (prefers-reduced-motion: reduce) {
.downloadProgressBarFillIndeterminate {
width: 100%;
animation: none;
opacity: 0.5;
}
}

.downloadProgressText {
margin-top: 4px;
color: var(--grey-50);
font-size: 12px;
}

.loading {
position: relative;
height: 40px;
Expand Down
90 changes: 87 additions & 3 deletions src/components/app/ProfileRootMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,40 @@ import * as React from 'react';

import './ProfileRootMessage.css';

type DownloadProgressInfo = {
readonly receivedBytes: number;
readonly totalBytes: number | null;
};

type Props = {
readonly title?: string;
readonly additionalMessage: React.ReactNode;
readonly showLoader: boolean;
readonly showBackHomeLink: boolean;
readonly downloadProgress?: DownloadProgressInfo | null;
readonly children: React.ReactNode;
};

function _formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

export class ProfileRootMessage extends React.PureComponent<Props> {
override render() {
const { children, additionalMessage, showLoader, showBackHomeLink, title } =
this.props;
const {
children,
additionalMessage,
showLoader,
showBackHomeLink,
downloadProgress,
title,
} = this.props;
return (
<div className="rootMessageContainer">
<div className="rootMessage">
Expand All @@ -29,6 +51,9 @@ export class ProfileRootMessage extends React.PureComponent<Props> {
<div className="rootMessageText">
<p>{children}</p>
</div>
{downloadProgress
? this._renderDownloadProgress(downloadProgress)
: null}
{additionalMessage ? (
<div className="rootMessageAdditional">{additionalMessage}</div>
) : null}
Expand All @@ -41,7 +66,7 @@ export class ProfileRootMessage extends React.PureComponent<Props> {
</a>
</div>
) : null}
{showLoader ? (
{showLoader && !downloadProgress ? (
<div className="loading">
<div className="loading-div loading-div-1 loading-row-1" />
<div className="loading-div loading-div-2 loading-row-2" />
Expand All @@ -59,4 +84,63 @@ export class ProfileRootMessage extends React.PureComponent<Props> {
</div>
);
}

_renderDownloadProgress(
downloadProgress: DownloadProgressInfo
): React.ReactNode {
const receivedStr = _formatBytes(downloadProgress.receivedBytes);
const progressText = downloadProgress.totalBytes
? `${receivedStr} / ${_formatBytes(downloadProgress.totalBytes)}`
: `${receivedStr} downloaded`;

return (
<div className="downloadProgress">
<Localized
id="ProfileRootMessage--download-progress-label"
attrs={{ 'aria-label': true }}
>
<div
className="downloadProgressBarTrack"
role="progressbar"
aria-valuenow={downloadProgress.receivedBytes}
aria-valuemin={0}
aria-valuemax={downloadProgress.totalBytes ?? undefined}
aria-valuetext={progressText}
aria-label="Download progress"
>
{downloadProgress.totalBytes ? (
<div
className="downloadProgressBarFill"
style={{
width: `${Math.min(100, (downloadProgress.receivedBytes / downloadProgress.totalBytes) * 100)}%`,
}}
/>
) : (
<div className="downloadProgressBarFillIndeterminate" />
)}
</div>
</Localized>
<div className="downloadProgressText">
{downloadProgress.totalBytes ? (
<Localized
id="ProfileRootMessage--download-progress-known"
vars={{
receivedSize: receivedStr,
totalSize: _formatBytes(downloadProgress.totalBytes),
}}
>
{progressText}
</Localized>
) : (
<Localized
id="ProfileRootMessage--download-progress-unknown"
vars={{ receivedSize: receivedStr }}
>
{progressText}
</Localized>
)}
</div>
</div>
);
}
}
8 changes: 8 additions & 0 deletions src/reducers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const view: Reducer<AppViewState> = (
};
case 'FATAL_ERROR':
return { phase: 'FATAL_ERROR', error: action.error };
case 'PROFILE_DOWNLOAD_PROGRESS':
return {
phase: 'INITIALIZING',
downloadProgress: {
receivedBytes: action.receivedBytes,
totalBytes: action.totalBytes,
},
};
case 'WAITING_FOR_PROFILE_FROM_BROWSER':
case 'WAITING_FOR_PROFILE_FROM_URL':
case 'WAITING_FOR_PROFILE_FROM_FILE':
Expand Down
12 changes: 12 additions & 0 deletions src/test/store/receive-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,12 @@ describe('actions/receive-profile', function () {
message: errorMessage,
},
},
expect.objectContaining({
phase: 'INITIALIZING',
downloadProgress: expect.objectContaining({
receivedBytes: expect.any(Number),
}),
}),
{ phase: 'PROFILE_LOADED' },
{ phase: 'DATA_LOADED' },
]);
Expand Down Expand Up @@ -1082,6 +1088,12 @@ describe('actions/receive-profile', function () {
message: errorMessage,
},
},
expect.objectContaining({
phase: 'INITIALIZING',
downloadProgress: expect.objectContaining({
receivedBytes: expect.any(Number),
}),
}),
{ phase: 'PROFILE_LOADED' },
{ phase: 'DATA_LOADED' },
]);
Expand Down
5 changes: 5 additions & 0 deletions src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,11 @@ type ReceiveProfileAction =
readonly profileUrl: string | null;
}
| { readonly type: 'TRIGGER_LOADING_FROM_URL'; readonly profileUrl: string }
| {
readonly type: 'PROFILE_DOWNLOAD_PROGRESS';
readonly receivedBytes: number;
readonly totalBytes: number | null;
}
| {
readonly type: 'UPDATE_PAGES';
readonly newPages: PageList;
Expand Down
4 changes: 4 additions & 0 deletions src/types/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ export type AppViewState =
readonly attempt: Attempt | null;
readonly message: string;
};
readonly downloadProgress?: {
readonly receivedBytes: number;
readonly totalBytes: number | null;
};
};

export type Phase = AppViewState['phase'];
Expand Down
Loading