diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index 4027aa479..c88c7974d 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -37,9 +37,95 @@ export enum Geant4DatasetsType { FULL } +function formatTime(seconds: number): string { + if (seconds < 1) { + return '<1s'; + } + + const totalSeconds = Math.ceil(seconds); + + if (totalSeconds < 60) { + return `${totalSeconds}s`; + } + + const minutes = Math.floor(totalSeconds / 60); + const remainingSeconds = totalSeconds % 60; + + return `${minutes}m ${remainingSeconds}s`; +} + +interface SpeedHistory { + lastDone: number; + lastTime: number; + currentSpeed: number; +} + +const SMOOTHING = 0.1; + function DatasetCurrentStatus(props: { status: DatasetStatus }) { const { status } = props; + const [speedHistory, setSpeedHistory] = useState({ + lastDone: status.done ?? 0, + lastTime: Date.now(), + currentSpeed: 0 + }); + + useEffect(() => { + const currentDone = status.done ?? 0; + const currentTime = Date.now(); + + if (status.status === DatasetDownloadStatus.DOWNLOADING) { + setSpeedHistory(prev => { + const timeDelta = (currentTime - prev.lastTime) / 1000; + const bytesDelta = currentDone - prev.lastDone; + + if (bytesDelta > 0 && timeDelta > 0) { + const instSpeed = bytesDelta / timeDelta; + + const newSpeed = + prev.currentSpeed === 0 + ? instSpeed + : prev.currentSpeed * (1 - SMOOTHING) + instSpeed * SMOOTHING; + + return { + lastDone: currentDone, + lastTime: currentTime, + currentSpeed: newSpeed + }; + } + + return { + ...prev, + lastTime: currentTime + }; + }); + } + + if ( + status.status === DatasetDownloadStatus.DONE || + status.status === DatasetDownloadStatus.IDLE + ) { + setSpeedHistory({ + lastDone: status.done ?? 0, + lastTime: Date.now(), + currentSpeed: 0 + }); + } + }, [status.done, status.status]); + + const remainingBytes = (status.total ?? 0) - (status.done ?? 0); + let estimatedTimeRemaining = ''; + + if ( + status.status === DatasetDownloadStatus.DOWNLOADING && + speedHistory.currentSpeed > 0 && + remainingBytes > 0 + ) { + const remainingSeconds = remainingBytes / speedHistory.currentSpeed; + estimatedTimeRemaining = ` (est. ${formatTime(remainingSeconds)} remaining)`; + } + const idleIcon = status.cached ? ( ) : ( @@ -68,6 +154,13 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { variant='indeterminate' color='warning' /> + ], + [ + DatasetDownloadStatus.IDLE, + ] ]); @@ -75,6 +168,14 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { {status.name} + + {status.status === DatasetDownloadStatus.DOWNLOADING && ( + + {estimatedTimeRemaining} + + )} {datasetStatusIcon.get(status.status)} @@ -324,7 +425,10 @@ export function Geant4Datasets() { }}> } - onClick={() => setOpen(!open)}> + onClick={e => { + e.stopPropagation(); + setOpen(!open); + }}>