From dbda56da0726febab1f9b5a712414c03614299bc Mon Sep 17 00:00:00 2001 From: martastn Date: Mon, 30 Mar 2026 14:18:10 +0200 Subject: [PATCH 1/7] dataset download eta --- .../Simulation/Geant4DatasetDownload.tsx | 114 +++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index 4027aa479..3125cce36 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -37,9 +37,103 @@ 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`; +} + +export interface Geant4DatasetsProps { + geant4DownloadManagerState: DownloadManagerStatus; + geant4DatasetStates: DatasetStatus[]; + geant4DatasetDownloadStart: () => void; + geant4DatasetType: Geant4DatasetsType; + setGeant4DatasetType: (type: Geant4DatasetsType) => void; +} + +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 = ` (${formatTime(remainingSeconds)})`; + } + const idleIcon = status.cached ? ( ) : ( @@ -68,6 +162,13 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { variant='indeterminate' color='warning' /> + ], + [ + DatasetDownloadStatus.IDLE, + ] ]); @@ -75,6 +176,14 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { {status.name} + + {status.status === DatasetDownloadStatus.DOWNLOADING && ( + + {estimatedTimeRemaining} + + )} {datasetStatusIcon.get(status.status)} @@ -324,7 +433,10 @@ export function Geant4Datasets() { }}> } - onClick={() => setOpen(!open)}> + onClick={e => { + e.stopPropagation(); + setOpen(!open); + }}> From 4307cf12b44484700c220bfe221c7cff48bf787b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marta=20Stanis=C5=82awska?= Date: Mon, 30 Mar 2026 14:59:33 +0200 Subject: [PATCH 2/7] removed unnecessary export Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/Simulation/Geant4DatasetDownload.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index 3125cce36..cb3d2934e 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -54,14 +54,6 @@ function formatTime(seconds: number): string { return `${minutes}m ${remainingSeconds}s`; } -export interface Geant4DatasetsProps { - geant4DownloadManagerState: DownloadManagerStatus; - geant4DatasetStates: DatasetStatus[]; - geant4DatasetDownloadStart: () => void; - geant4DatasetType: Geant4DatasetsType; - setGeant4DatasetType: (type: Geant4DatasetsType) => void; -} - interface SpeedHistory { lastDone: number; lastTime: number; From 418dfd1ee760393e3d055ca1ada90e0d6b99a4b3 Mon Sep 17 00:00:00 2001 From: martastn Date: Mon, 30 Mar 2026 15:08:32 +0200 Subject: [PATCH 3/7] text explaining displayed time --- src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index cb3d2934e..c88c7974d 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -123,7 +123,7 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { remainingBytes > 0 ) { const remainingSeconds = remainingBytes / speedHistory.currentSpeed; - estimatedTimeRemaining = ` (${formatTime(remainingSeconds)})`; + estimatedTimeRemaining = ` (est. ${formatTime(remainingSeconds)} remaining)`; } const idleIcon = status.cached ? ( From 563800078577832bf49d711f0a4d5d94c9c47a53 Mon Sep 17 00:00:00 2001 From: martastn Date: Mon, 14 Sep 2026 18:39:48 +0200 Subject: [PATCH 4/7] fix bar animation after page refresh --- .../Simulation/Geant4DatasetDownload.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index c88c7974d..a687a3cf0 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -78,10 +78,10 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { if (status.status === DatasetDownloadStatus.DOWNLOADING) { setSpeedHistory(prev => { const timeDelta = (currentTime - prev.lastTime) / 1000; - const bytesDelta = currentDone - prev.lastDone; + const progressDelta = currentDone - prev.lastDone; - if (bytesDelta > 0 && timeDelta > 0) { - const instSpeed = bytesDelta / timeDelta; + if (progressDelta > 0 && timeDelta > 0) { + const instSpeed = progressDelta / timeDelta; const newSpeed = prev.currentSpeed === 0 @@ -114,15 +114,15 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { } }, [status.done, status.status]); - const remainingBytes = (status.total ?? 0) - (status.done ?? 0); + const remainingProgress = (status.total ?? 0) - (status.done ?? 0); let estimatedTimeRemaining = ''; if ( status.status === DatasetDownloadStatus.DOWNLOADING && speedHistory.currentSpeed > 0 && - remainingBytes > 0 + remainingProgress > 0 ) { - const remainingSeconds = remainingBytes / speedHistory.currentSpeed; + const remainingSeconds = remainingProgress / speedHistory.currentSpeed; estimatedTimeRemaining = ` (est. ${formatTime(remainingSeconds)} remaining)`; } @@ -154,13 +154,6 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { variant='indeterminate' color='warning' /> - ], - [ - DatasetDownloadStatus.IDLE, - ] ]); @@ -169,7 +162,7 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { {status.name} - {status.status === DatasetDownloadStatus.DOWNLOADING && ( + {status.status === DatasetDownloadStatus.DOWNLOADING && estimatedTimeRemaining && ( @@ -427,7 +420,7 @@ export function Geant4Datasets() { expandIcon={} onClick={e => { e.stopPropagation(); - setOpen(!open); + setOpen(prevOpen => !prevOpen); }}> Date: Sun, 20 Sep 2026 19:23:11 +0200 Subject: [PATCH 5/7] extract ETA formatting into time util --- .../Simulation/Geant4DatasetDownload.tsx | 30 ++++--------------- src/util/time.ts | 17 +++++++++++ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index a687a3cf0..4cd9480b8 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -31,29 +31,13 @@ import { useDialog } from '../../../services/DialogService'; import { useSharedDatasetManager } from '../../../services/Geant4DatasetContextProvider'; import StyledAccordion from '../../../shared/components/StyledAccordion'; import { StyledExclusiveToggleButtonGroup } from '../../../shared/components/StyledExclusiveToggleButtonGroup'; +import { secondsToShortDurationString } from '../../../util/time'; export enum Geant4DatasetsType { PARTIAL, 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; @@ -115,16 +99,12 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { }, [status.done, status.status]); const remainingProgress = (status.total ?? 0) - (status.done ?? 0); - let estimatedTimeRemaining = ''; - - if ( + const estimatedTimeRemaining = status.status === DatasetDownloadStatus.DOWNLOADING && speedHistory.currentSpeed > 0 && remainingProgress > 0 - ) { - const remainingSeconds = remainingProgress / speedHistory.currentSpeed; - estimatedTimeRemaining = ` (est. ${formatTime(remainingSeconds)} remaining)`; - } + ? ` (est. ${secondsToShortDurationString(remainingProgress / speedHistory.currentSpeed)} remaining)` + : ''; const idleIcon = status.cached ? ( @@ -162,7 +142,7 @@ function DatasetCurrentStatus(props: { status: DatasetStatus }) { {status.name} - {status.status === DatasetDownloadStatus.DOWNLOADING && estimatedTimeRemaining && ( + {estimatedTimeRemaining && ( diff --git a/src/util/time.ts b/src/util/time.ts index 6f0d8133e..9fcd30971 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -5,3 +5,20 @@ export const millisecondsToTimeString = (milliseconds: number): string => { return [hours, minutes % 60, seconds % 60].map(v => v.toString().padStart(2, '0')).join(':'); // hh:mm:ss }; + +export const secondsToShortDurationString = (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`; +}; From 873bcca7f2a99a3561a9702e62b9197bcb5c8ffe Mon Sep 17 00:00:00 2001 From: martastn Date: Sun, 20 Sep 2026 20:09:11 +0200 Subject: [PATCH 6/7] move dataset download ETA computation to dataset download manager + fix stale ETA when dataset download fails --- src/Geant4Worker/DatasetDownloadSpeed.ts | 34 ++++++++ src/Geant4Worker/Geant4DatasetManager.ts | 85 ++++++++++++++++--- .../Simulation/Geant4DatasetDownload.tsx | 64 +------------- 3 files changed, 111 insertions(+), 72 deletions(-) create mode 100644 src/Geant4Worker/DatasetDownloadSpeed.ts diff --git a/src/Geant4Worker/DatasetDownloadSpeed.ts b/src/Geant4Worker/DatasetDownloadSpeed.ts new file mode 100644 index 000000000..ffd2a0de0 --- /dev/null +++ b/src/Geant4Worker/DatasetDownloadSpeed.ts @@ -0,0 +1,34 @@ +export interface SpeedHistoryEntry { + lastDone: number; + lastTime: number; + currentSpeed: number; +} + +export type SpeedHistory = Record; + +export const SPEED_SMOOTHING = 0.1; + +export function nextSpeedHistoryEntry( + prev: SpeedHistoryEntry | undefined, + done: number, + currentTime: number +): SpeedHistoryEntry { + if (!prev) { + return { lastDone: done, lastTime: currentTime, currentSpeed: 0 }; + } + + const timeDelta = (currentTime - prev.lastTime) / 1000; + const progressDelta = done - prev.lastDone; + + if (progressDelta <= 0 || timeDelta <= 0) { + return { ...prev, lastTime: currentTime }; + } + + const instantSpeed = progressDelta / timeDelta; + const currentSpeed = + prev.currentSpeed === 0 + ? instantSpeed + : prev.currentSpeed * (1 - SPEED_SMOOTHING) + instantSpeed * SPEED_SMOOTHING; + + return { lastDone: done, lastTime: currentTime, currentSpeed }; +} diff --git a/src/Geant4Worker/Geant4DatasetManager.ts b/src/Geant4Worker/Geant4DatasetManager.ts index d44a42362..e4ffb6ae7 100644 --- a/src/Geant4Worker/Geant4DatasetManager.ts +++ b/src/Geant4Worker/Geant4DatasetManager.ts @@ -1,8 +1,17 @@ // Additional credits: // - @kmichalik -import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; - +import { + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useEffect, + useRef, + useState +} from 'react'; + +import { nextSpeedHistoryEntry, SpeedHistory } from './DatasetDownloadSpeed'; import { checkAllDatasetsCacheStatus, clearDatasetCache, @@ -40,29 +49,56 @@ export interface DatasetStatus { total?: number; totalSizeMB?: number; cached?: boolean; + estimatedSecondsRemaining?: number; } async function fetchProgress( worker: Geant4Worker, - setDatasetStates: Dispatch>> + setDatasetStates: Dispatch>>, + speedHistoryRef: MutableRefObject ) { if (!worker.getIsInitialized()) return; const progress = await worker.pollDatasetProgress(); if (progress) { + const currentTime = Date.now(); + setDatasetStates(prev => { const newStates: Record = { ...prev }; for (const [datasetName, datasetProgress] of Object.entries(progress)) { - let status = statusTypeMap[datasetProgress.stage] ?? DatasetDownloadStatus.IDLE; + const status = statusTypeMap[datasetProgress.stage] ?? DatasetDownloadStatus.IDLE; + const done = Math.floor(datasetProgress.progress * 100); + const total = 100; + + let estimatedSecondsRemaining: number | undefined; + + if (status === DatasetDownloadStatus.DOWNLOADING) { + const speedEntry = nextSpeedHistoryEntry( + speedHistoryRef.current[datasetName], + done, + currentTime + ); + + speedHistoryRef.current[datasetName] = speedEntry; + + const remaining = total - done; + + if (speedEntry.currentSpeed > 0 && remaining > 0) { + estimatedSecondsRemaining = remaining / speedEntry.currentSpeed; + } + } else { + delete speedHistoryRef.current[datasetName]; + } newStates[datasetName] = { ...newStates[datasetName], name: datasetName, status, - done: Math.floor(datasetProgress.progress * 100), - total: 100 + done, + total, + estimatedSecondsRemaining }; } @@ -77,6 +113,7 @@ type StartDownloadArgs = { setManagerState: Dispatch>; setDatasetStates: Dispatch>>; setIdle: Dispatch>; + speedHistoryRef: MutableRefObject; }; function startDownload({ @@ -84,23 +121,26 @@ function startDownload({ managerState, setManagerState, setDatasetStates, - setIdle + setIdle, + speedHistoryRef }: StartDownloadArgs) { if (managerState !== DownloadManagerStatus.IDLE || !worker.getIsInitialized()) { return; } + speedHistoryRef.current = {}; + const loadDepsPromise = worker.loadDeps(); const interval = setInterval(async () => { - await fetchProgress(worker, setDatasetStates); + await fetchProgress(worker, setDatasetStates, speedHistoryRef); }, 500); loadDepsPromise .then(async () => { clearInterval(interval); - await fetchProgress(worker, setDatasetStates); + await fetchProgress(worker, setDatasetStates, speedHistoryRef); setManagerState(DownloadManagerStatus.FINISHED); worker.markSafeForTermination(); @@ -109,6 +149,22 @@ function startDownload({ console.error('Dataset download error:', error); setManagerState(DownloadManagerStatus.ERROR); clearInterval(interval); + + speedHistoryRef.current = {}; + setDatasetStates(prev => { + const newStates: Record = { ...prev }; + + for (const [datasetName, datasetState] of Object.entries(newStates)) { + if (datasetState.estimatedSecondsRemaining !== undefined) { + newStates[datasetName] = { + ...datasetState, + estimatedSecondsRemaining: undefined + }; + } + } + + return newStates; + }); }); setManagerState(DownloadManagerStatus.WORKING); setIdle(false); @@ -135,6 +191,7 @@ export function useDatasetManager(): UseDatasetManagerResult { const [idle, setIdle] = useState(false); const [worker] = useState(new Geant4Worker()); const initCalledRef = useRef(false); + const speedHistoryRef = useRef({}); const [storageEstimate, setStorageEstimate] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -196,6 +253,7 @@ export function useDatasetManager(): UseDatasetManagerResult { setIdle(true); setManagerState(DownloadManagerStatus.IDLE); setDatasetStates({}); + speedHistoryRef.current = {}; await refresh(); } @@ -225,7 +283,14 @@ export function useDatasetManager(): UseDatasetManagerResult { if (!idle) return; if (!worker.getIsInitialized()) return; - startDownload({ worker, managerState, setManagerState, setDatasetStates, setIdle }); + startDownload({ + worker, + managerState, + setManagerState, + setDatasetStates, + setIdle, + speedHistoryRef + }); }, [worker, idle, managerState]); return { diff --git a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx index 4cd9480b8..ef27b349c 100644 --- a/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx +++ b/src/WrapperApp/components/Simulation/Geant4DatasetDownload.tsx @@ -38,72 +38,12 @@ export enum Geant4DatasetsType { FULL } -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 progressDelta = currentDone - prev.lastDone; - - if (progressDelta > 0 && timeDelta > 0) { - const instSpeed = progressDelta / 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 remainingProgress = (status.total ?? 0) - (status.done ?? 0); const estimatedTimeRemaining = - status.status === DatasetDownloadStatus.DOWNLOADING && - speedHistory.currentSpeed > 0 && - remainingProgress > 0 - ? ` (est. ${secondsToShortDurationString(remainingProgress / speedHistory.currentSpeed)} remaining)` + status.status === DatasetDownloadStatus.DOWNLOADING && status.estimatedSecondsRemaining + ? ` (est. ${secondsToShortDurationString(status.estimatedSecondsRemaining)} remaining)` : ''; const idleIcon = status.cached ? ( From 2a1ac89f59145b4a48ac43407323b68300f83c71 Mon Sep 17 00:00:00 2001 From: martastn Date: Mon, 21 Sep 2026 20:08:55 +0200 Subject: [PATCH 7/7] fix download speed measured over a single poll interval + grow ETA while progress stalls --- src/Geant4Worker/DatasetDownloadSpeed.ts | 65 ++++++++++++++++++------ src/Geant4Worker/Geant4DatasetManager.ts | 25 +++++---- src/util/time.ts | 11 +++- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/Geant4Worker/DatasetDownloadSpeed.ts b/src/Geant4Worker/DatasetDownloadSpeed.ts index ffd2a0de0..13f130450 100644 --- a/src/Geant4Worker/DatasetDownloadSpeed.ts +++ b/src/Geant4Worker/DatasetDownloadSpeed.ts @@ -1,34 +1,67 @@ export interface SpeedHistoryEntry { - lastDone: number; - lastTime: number; - currentSpeed: number; + /** Progress at the last recorded change, as a fraction in the 0..1 range. */ + lastProgressFraction: number; + /** Epoch timestamp in milliseconds of the last recorded progress change. */ + lastTimestampMs: number; + /** Smoothed download speed, expressed as progress fraction per second. */ + speedFractionPerSecond: number; } export type SpeedHistory = Record; +/** Weight of the newest speed sample- the higher, the faster but jumpier the reaction. */ export const SPEED_SMOOTHING = 0.1; export function nextSpeedHistoryEntry( prev: SpeedHistoryEntry | undefined, - done: number, - currentTime: number + progressFraction: number, + timestampMs: number ): SpeedHistoryEntry { if (!prev) { - return { lastDone: done, lastTime: currentTime, currentSpeed: 0 }; + return { + lastProgressFraction: progressFraction, + lastTimestampMs: timestampMs, + speedFractionPerSecond: 0 + }; } - const timeDelta = (currentTime - prev.lastTime) / 1000; - const progressDelta = done - prev.lastDone; + const elapsedSeconds = (timestampMs - prev.lastTimestampMs) / 1000; + const progressFractionDelta = progressFraction - prev.lastProgressFraction; - if (progressDelta <= 0 || timeDelta <= 0) { - return { ...prev, lastTime: currentTime }; + if (progressFractionDelta < 0) { + return { ...prev, lastProgressFraction: progressFraction, lastTimestampMs: timestampMs }; } - const instantSpeed = progressDelta / timeDelta; - const currentSpeed = - prev.currentSpeed === 0 - ? instantSpeed - : prev.currentSpeed * (1 - SPEED_SMOOTHING) + instantSpeed * SPEED_SMOOTHING; + if (progressFractionDelta === 0 || elapsedSeconds <= 0) { + return prev; + } + + const instantSpeedFractionPerSecond = progressFractionDelta / elapsedSeconds; + const speedFractionPerSecond = + prev.speedFractionPerSecond === 0 + ? instantSpeedFractionPerSecond + : prev.speedFractionPerSecond * (1 - SPEED_SMOOTHING) + + instantSpeedFractionPerSecond * SPEED_SMOOTHING; + + return { + lastProgressFraction: progressFraction, + lastTimestampMs: timestampMs, + speedFractionPerSecond + }; +} + +export function estimateSecondsRemaining( + entry: SpeedHistoryEntry, + progressFraction: number, + timestampMs: number +): number | undefined { + const remainingFraction = 1 - progressFraction; + + if (entry.speedFractionPerSecond <= 0 || remainingFraction <= 0) { + return undefined; + } + + const stalledSeconds = Math.max(0, (timestampMs - entry.lastTimestampMs) / 1000); - return { lastDone: done, lastTime: currentTime, currentSpeed }; + return remainingFraction / entry.speedFractionPerSecond + stalledSeconds; } diff --git a/src/Geant4Worker/Geant4DatasetManager.ts b/src/Geant4Worker/Geant4DatasetManager.ts index e4ffb6ae7..c76e8146c 100644 --- a/src/Geant4Worker/Geant4DatasetManager.ts +++ b/src/Geant4Worker/Geant4DatasetManager.ts @@ -11,7 +11,11 @@ import { useState } from 'react'; -import { nextSpeedHistoryEntry, SpeedHistory } from './DatasetDownloadSpeed'; +import { + estimateSecondsRemaining, + nextSpeedHistoryEntry, + SpeedHistory +} from './DatasetDownloadSpeed'; import { checkAllDatasetsCacheStatus, clearDatasetCache, @@ -62,14 +66,15 @@ async function fetchProgress( const progress = await worker.pollDatasetProgress(); if (progress) { - const currentTime = Date.now(); + const currentTimeMs = Date.now(); setDatasetStates(prev => { const newStates: Record = { ...prev }; for (const [datasetName, datasetProgress] of Object.entries(progress)) { const status = statusTypeMap[datasetProgress.stage] ?? DatasetDownloadStatus.IDLE; - const done = Math.floor(datasetProgress.progress * 100); + const progressFraction = datasetProgress.progress; + const done = Math.floor(progressFraction * 100); const total = 100; let estimatedSecondsRemaining: number | undefined; @@ -77,17 +82,17 @@ async function fetchProgress( if (status === DatasetDownloadStatus.DOWNLOADING) { const speedEntry = nextSpeedHistoryEntry( speedHistoryRef.current[datasetName], - done, - currentTime + progressFraction, + currentTimeMs ); speedHistoryRef.current[datasetName] = speedEntry; - const remaining = total - done; - - if (speedEntry.currentSpeed > 0 && remaining > 0) { - estimatedSecondsRemaining = remaining / speedEntry.currentSpeed; - } + estimatedSecondsRemaining = estimateSecondsRemaining( + speedEntry, + progressFraction, + currentTimeMs + ); } else { delete speedHistoryRef.current[datasetName]; } diff --git a/src/util/time.ts b/src/util/time.ts index 9fcd30971..4afe6c7cd 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -17,8 +17,15 @@ export const secondsToShortDurationString = (seconds: number): string => { return `${totalSeconds}s`; } - const minutes = Math.floor(totalSeconds / 60); + const totalMinutes = Math.floor(totalSeconds / 60); const remainingSeconds = totalSeconds % 60; - return `${minutes}m ${remainingSeconds}s`; + if (totalMinutes < 60) { + return `${totalMinutes}m ${remainingSeconds}s`; + } + + const hours = Math.floor(totalMinutes / 60); + const remainingMinutes = totalMinutes % 60; + + return `${hours}h ${remainingMinutes}m`; };