From 3e287e550e45834a2a42ebd0ef42e0dd23a906a1 Mon Sep 17 00:00:00 2001 From: cnuart Date: Sat, 10 Oct 2026 09:41:35 +0200 Subject: [PATCH] add progress bar and status for preparing datasets --- .../SimulationCard/SimulationCardContent.tsx | 89 +++++--- .../Geant4LocalWorkerSimulationService.ts | 202 ++++++++++++++---- src/types/ResponseTypes.ts | 12 ++ 3 files changed, 231 insertions(+), 72 deletions(-) diff --git a/src/WrapperApp/components/Simulation/SimulationCard/SimulationCardContent.tsx b/src/WrapperApp/components/Simulation/SimulationCard/SimulationCardContent.tsx index 0b453a672..5ad322bd9 100644 --- a/src/WrapperApp/components/Simulation/SimulationCard/SimulationCardContent.tsx +++ b/src/WrapperApp/components/Simulation/SimulationCard/SimulationCardContent.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { currentJobStatusData, currentTaskStatusData, + DatasetPreparationInfo, JobUnknownStatus, SimulationInfo, StatusState, @@ -102,6 +103,41 @@ export function SimulationProgress(props: { ); } +export function SimulationPreparation(props: { preparation: DatasetPreparationInfo }) { + const { stage, fromCache, completedCount, totalCount, activeDataset, overallProgress } = + props.preparation; + + const showCounter = totalCount > 0 && completedCount > 0; + const counter = showCounter ? ` (${completedCount}/${totalCount})` : ''; + const suffix = activeDataset ? ` — ${activeDataset}` : ''; + + const label = + stage === 'initializing' + ? 'Initializing simulation engine' + : stage === 'preparing' + ? 'Preparing simulation' + : fromCache + ? `Loading datasets from cache${counter}${suffix}` + : `Preparing datasets${counter}${suffix}`; + + const determinate = stage === 'loading' && overallProgress !== undefined; + + return ( + <> + + {label} + + + + ); +} + export const SimulationCardContent = ({ duration, endTime, @@ -166,32 +202,33 @@ export const SimulationCardContent = ({ ))} - {needsProgressBar(simulationStatus.jobState) - ? currentJobStatusData['hasSpecificProperty']( - simulationStatus, - 'jobTasksStatus' - ) && ( - - ) - : simulationStatus.jobState === StatusState.COMPLETED && ( - <> - - Start: {formatedStartDate} - - {endTime && ( - - End: {formatedEndDate} - - )} - - Duration: {millisecondsToTimeString(duration)} - - - )} + {simulationStatus.jobState === StatusState.PENDING && + simulationStatus.datasetPreparation ? ( + + ) : needsProgressBar(simulationStatus.jobState) ? ( + currentJobStatusData['hasSpecificProperty']( + simulationStatus, + 'jobTasksStatus' + ) && ( + + ) + ) : ( + simulationStatus.jobState === StatusState.COMPLETED && ( + <> + Start: {formatedStartDate} + {endTime && ( + End: {formatedEndDate} + )} + + Duration: {millisecondsToTimeString(duration)} + + + ) + )} ); diff --git a/src/services/Geant4LocalWorkerSimulationService.ts b/src/services/Geant4LocalWorkerSimulationService.ts index 2d177d8a0..850a77f28 100644 --- a/src/services/Geant4LocalWorkerSimulationService.ts +++ b/src/services/Geant4LocalWorkerSimulationService.ts @@ -1,3 +1,4 @@ +import { GEANT4_DATASETS, TOTAL_DATASET_SIZE_MB } from '../Geant4Worker/Geant4DatasetCacheService'; import Geant4Worker from '../Geant4Worker/Geant4Worker'; import { Estimator, Page1D, Page2D } from '../JsRoot/GraphData'; import { PythonConverterContext } from '../PythonConverter/PythonConverterService'; @@ -19,6 +20,7 @@ import { SimulatorType } from '../types/RequestTypes'; import { + DatasetPreparationInfo, EstimatorItem, Geant4InputFilesNames, InputFilesRecord, @@ -38,6 +40,8 @@ type JobId = string; interface JobMetadata { title: string; inputType: SimulationSourceType; + phase: 'initializing' | 'loading' | 'preparing'; + datasetType?: Geant4DatasetsType; } export default class Geant4LocalWorkerSimulationService implements SimulationService { @@ -105,7 +109,12 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer const worker = new Geant4Worker(); this.workers[jobId] = worker; - this.jobsMetadata[jobId] = { title: title ?? '', inputType }; + this.jobsMetadata[jobId] = { + title: title ?? '', + inputType, + phase: 'initializing', + datasetType: geant4DatasetType + }; this.inputFiles[jobId] = { // @ts-ignore @@ -124,12 +133,16 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer this.numPrimaries = rawNumPrimaries ? parseInt(rawNumPrimaries) : 0; worker.init().then(async () => { - if (geant4DatasetType && geant4DatasetType === Geant4DatasetsType.FULL) { + this.jobsMetadata[jobId].phase = 'loading'; + + if (geant4DatasetType === Geant4DatasetsType.FULL) { await worker.loadDeps(); } else { await worker.loadDepsLazy(); } + this.jobsMetadata[jobId].phase = 'preparing'; + // @ts-ignore await worker.includeFile('geometry.gdml', simData['geometry.gdml']); // @ts-ignore @@ -179,12 +192,97 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer ]; } + private async getDatasetPreparation( + jobId: string + ): Promise { + const worker = this.workers[jobId]; + const metadata = this.jobsMetadata[jobId]; + + if (!worker || !metadata || worker.getState() !== StatusState.PENDING) { + return undefined; + } + + const fromCache = metadata.datasetType === Geant4DatasetsType.FULL; + const isFullDataset = metadata.datasetType === Geant4DatasetsType.FULL; + const totalCount = isFullDataset ? GEANT4_DATASETS.length : 0; + + if (metadata.phase === 'initializing') { + return { + stage: 'initializing', + fromCache, + completedCount: 0, + totalCount: 0 + }; + } + + if (metadata.phase === 'preparing') { + return { + stage: 'preparing', + fromCache, + completedCount: 0, + totalCount + }; + } + + const base: DatasetPreparationInfo = { + stage: 'loading', + fromCache, + completedCount: 0, + totalCount + }; + + if (!isFullDataset || !worker.getIsInitialized()) { + return base; + } + + try { + const progress = await worker.pollDatasetProgress(); + + if (!progress) { + return base; + } + + let completedCount = 0; + let activeDataset: string | undefined; + let weightedProgress = 0; + + for (const dataset of GEANT4_DATASETS) { + const entry = progress[dataset.name]; + + if (!entry) { + continue; + } + + if (entry.stage === 'done') { + completedCount += 1; + } else if (!activeDataset) { + activeDataset = dataset.name; + } + + weightedProgress += entry.progress * dataset.approximateSizeMB; + } + + return { + ...base, + completedCount, + activeDataset, + overallProgress: + TOTAL_DATASET_SIZE_MB > 0 ? weightedProgress / TOTAL_DATASET_SIZE_MB : undefined + }; + } catch { + return base; + } + } + async getJobStatus(...args: RequestGetJobStatus): Promise { const [info, cache = true, beforeCacheWrite, signal] = args; const { jobId } = info; if (this.responseCache.jobStatusData.hasOwnProperty(info.jobId)) { - return this.responseCache.jobStatusData[info.jobId]; + const cached = this.responseCache.jobStatusData[info.jobId]; + cached.datasetPreparation = await this.getDatasetPreparation(jobId); + + return cached; } if (!this.workers.hasOwnProperty(info.jobId)) { @@ -203,7 +301,8 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer platform: 'DIRECT' }, jobState: this.workers[jobId].getState(), - jobTasksStatus: this.getJobTasksStatus(jobId) + jobTasksStatus: this.getJobTasksStatus(jobId), + datasetPreparation: await this.getDatasetPreparation(jobId) } as JobStatusData; if (cache) beforeCacheWrite?.(jobId, this.responseCache.jobStatusData[info.jobId]); @@ -417,6 +516,7 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer platform: 'DIRECT' }, jobTasksStatus: this.getJobTasksStatus(jobId), + datasetPreparation: await this.getDatasetPreparation(jobId), input: { inputType: this.jobsMetadata[jobId].inputType, inputFiles: this.inputFiles[jobId] as InputFilesRecord, @@ -472,23 +572,30 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer pageSize * pageIdx ); + const simulations = await Promise.all( + paginatedWorkersEntries.map( + async ([jobId, worker]): Promise => ({ + jobId, + title: this.jobsMetadata[jobId]?.title, + startTime: worker.getStartTime().toString(), + endTime: worker.getEndTime()?.toString(), + metadata: { + inputType: this.jobsMetadata[jobId].inputType, + simType: 'Geant4', + server: '', + platform: 'DIRECT' + }, + jobState: worker.getState(), + jobTasksStatus: this.getJobTasksStatus(jobId), + datasetPreparation: await this.getDatasetPreparation(jobId) + }) + ) + ); + return { pageCount: Math.ceil(filteredWorkersEntries.length / pageSize), simulationsCount: filteredWorkersEntries.length, - simulations: paginatedWorkersEntries.map(([jobId, worker]) => ({ - jobId, - title: this.jobsMetadata[jobId]?.title, - startTime: worker.getStartTime().toString(), - endTime: worker.getEndTime()?.toString(), - metadata: { - inputType: this.jobsMetadata[jobId].inputType, - simType: 'Geant4', - server: '', - platform: 'DIRECT' - }, - jobState: worker.getState(), - jobTasksStatus: this.getJobTasksStatus(jobId) - })), + simulations, message: '' }; } @@ -505,34 +612,37 @@ export default class Geant4LocalWorkerSimulationService implements SimulationSer ) .filter(Boolean); - return workersEntries.map(([jobId, worker]) => { - if (this.responseCache.pageStatus.hasOwnProperty(jobId)) { - return this.responseCache.pageStatus[jobId]; - } - - const response = { - jobId, - title: this.jobsMetadata[jobId]?.title, - startTime: worker.getStartTime().toString(), - endTime: worker.getEndTime()?.toString(), - metadata: { - inputType: this.jobsMetadata[jobId].inputType, - simType: 'Geant4', - server: '', - platform: 'DIRECT' - }, - jobState: worker.getState(), - jobTasksStatus: this.getJobTasksStatus(jobId) - } as JobStatusData; - - if (worker.getState() === StatusState.COMPLETED) { - this.responseCache.pageStatus[jobId] = response; - - if (cache) beforeCacheWrite?.(jobId, response); - } - - return response; - }); + return Promise.all( + workersEntries.map(async ([jobId, worker]) => { + if (this.responseCache.pageStatus.hasOwnProperty(jobId)) { + return this.responseCache.pageStatus[jobId]; + } + + const response = { + jobId, + title: this.jobsMetadata[jobId]?.title, + startTime: worker.getStartTime().toString(), + endTime: worker.getEndTime()?.toString(), + metadata: { + inputType: this.jobsMetadata[jobId].inputType, + simType: 'Geant4', + server: '', + platform: 'DIRECT' + }, + jobState: worker.getState(), + jobTasksStatus: this.getJobTasksStatus(jobId), + datasetPreparation: await this.getDatasetPreparation(jobId) + } as JobStatusData; + + if (worker.getState() === StatusState.COMPLETED) { + this.responseCache.pageStatus[jobId] = response; + + if (cache) beforeCacheWrite?.(jobId, response); + } + + return response; + }) + ); } cancelJob(info: SimulationInfo, signal?: AbortSignal | undefined): Promise { diff --git a/src/types/ResponseTypes.ts b/src/types/ResponseTypes.ts index 996f510e1..1f2bd600b 100644 --- a/src/types/ResponseTypes.ts +++ b/src/types/ResponseTypes.ts @@ -254,6 +254,17 @@ type JobAllStatuses = | JobStatusFailed | JobStatusCanceled; +export type DatasetPreparationStage = 'initializing' | 'loading' | 'preparing'; + +export type DatasetPreparationInfo = { + stage: DatasetPreparationStage; + fromCache: boolean; + completedCount: number; + totalCount: number; + activeDataset?: string; + overallProgress?: number; +}; + /** * This type is used to represent the status of a job of unknown type. * To make it easier to read, it is a shorthand for `Partial>` @@ -270,6 +281,7 @@ export type JobUnknownStatus = { | StatusState.CANCELED; message?: string; jobTasksStatus?: Array; + datasetPreparation?: DatasetPreparationInfo; }; type ResponseJobRequestFailure = Omit<