diff --git a/.changeset/purple-seas-visit.md b/.changeset/purple-seas-visit.md new file mode 100644 index 00000000..17cdda7c --- /dev/null +++ b/.changeset/purple-seas-visit.md @@ -0,0 +1,5 @@ +--- +"@itwin/changed-elements-react": patch +--- + +- swap stage progress with overall loading progress during version comparison diff --git a/packages/changed-elements-react/public/locales/en/VersionCompare.json b/packages/changed-elements-react/public/locales/en/VersionCompare.json index f053e0a2..f53c8453 100644 --- a/packages/changed-elements-react/public/locales/en/VersionCompare.json +++ b/packages/changed-elements-react/public/locales/en/VersionCompare.json @@ -25,6 +25,7 @@ "loadingComparison": "Loading version comparison", "loadingNamedVersions": "Loading named versions", "comparisonNotActive": "No version comparison active.", + "LoadingResults": "Loading results: {{percent}}%", "cantHideDuringContext": "Cannot show/hide unchanged while emphasizing/isolating/hiding. Clear it first.", "comparisonGetStarted": "Click on the \"+\" comparison button to get started.", "showAll": "Show All", diff --git a/packages/changed-elements-react/src/api/ChangedElementDataCache.ts b/packages/changed-elements-react/src/api/ChangedElementDataCache.ts index 2380b2ed..02701bc5 100644 --- a/packages/changed-elements-react/src/api/ChangedElementDataCache.ts +++ b/packages/changed-elements-react/src/api/ChangedElementDataCache.ts @@ -20,7 +20,7 @@ export abstract class ChangedElementDataCache { /** * Called whenever a request is done in bulk mode, useful for UI update of progress */ - public updateFunction?: () => void; + public updateFunction?: (percent: number) => void; constructor(protected _chunkSize: number = 500) { // No-op @@ -50,7 +50,7 @@ export abstract class ChangedElementDataCache { const piece = await this._request(elements.slice(i, i + this._chunkSize)); result.push(...piece); if (this.updateFunction) { - this.updateFunction(); + this.updateFunction(Math.floor(i / elements.length * 100)); } } return result; diff --git a/packages/changed-elements-react/src/api/ChangedElementEntryCache.ts b/packages/changed-elements-react/src/api/ChangedElementEntryCache.ts index 25879504..158c820f 100644 --- a/packages/changed-elements-react/src/api/ChangedElementEntryCache.ts +++ b/packages/changed-elements-react/src/api/ChangedElementEntryCache.ts @@ -14,7 +14,8 @@ import { } from "./ElementQueries.js"; import { VersionCompareUtils, VersionCompareVerboseMessages } from "./VerboseMessages.js"; import { VersionCompare } from "./VersionCompare.js"; -import { VersionCompareManager } from "./VersionCompareManager.js"; +import { VersionCompareManager, VersionCompareProgressStage } from "./VersionCompareManager.js"; +import { ProgressCoordinator } from "../widgets/ProgressCoordinator.js"; /** Changed property for a changed element */ export interface Checksums { @@ -81,6 +82,7 @@ export class ChangedElementEntryCache { } private _currentIModel: IModelConnection | undefined; private _targetIModel: IModelConnection | undefined; + private _progressCoordinator?: ProgressCoordinator; private _progressLoadingEvent?: BeEvent<(message: string) => void>; private _currentLoadingMessage = ""; private _numSteps = 0; @@ -132,8 +134,10 @@ export class ChangedElementEntryCache { currentIModel: IModelConnection, targetIModel: IModelConnection, elements: Map, + progressCoordinator?: ProgressCoordinator, progressLoadingEvent?: BeEvent<(message: string) => void>, ) { + this._progressCoordinator = progressCoordinator; this._progressLoadingEvent = progressLoadingEvent; elements.forEach((element: ChangedElement, elementId: string) => { const entry: ChangedElementEntry = { @@ -605,6 +609,7 @@ export class ChangedElementEntryCache { this._findTopParentChunkSize + 1; this._setCurrentLoadingMessage("msg_findingParents", numTopParentQueries); + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindParents); const currentTopParents = await this._findTopParents( this._currentIModel, currentEntryIds, @@ -643,14 +648,26 @@ export class ChangedElementEntryCache { (unchangedCurrentTopParents.length + unchangedTargetTopParents.length) / this._queryEntryChunkSize + 1; + + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindParents, 100); + this._setCurrentLoadingMessage("msg_obtainingElementData", numEntryQueries); + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.ObtainElementData); + + const OnObtainDataProgress = (percent: number) => { + this._progressCoordinator?.addProgress( + VersionCompareProgressStage.ObtainElementData, + percent / 2, // hardcoded to 50% cuz once called on currentIModel and once on targetIModel + ); + } + const currentParentEntries = await queryEntryDataBulk( this._currentIModel, VersionCompare.manager?.wantFastParentLoad ? unchangedCurrentTopParents : currentTopParents, this._queryEntryChunkSize, - this._updateLoadingProgress, + OnObtainDataProgress, ); const targetParentEntries = await queryEntryDataBulk( this._targetIModel, @@ -658,7 +675,7 @@ export class ChangedElementEntryCache { ? unchangedTargetTopParents : targetTopParents, this._queryEntryChunkSize, - this._updateLoadingProgress, + OnObtainDataProgress, ); // Put all data into arrays @@ -701,11 +718,15 @@ export class ChangedElementEntryCache { } } } + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.ObtainElementData, 100); + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindChildren, 0); // Load child elements of the root nodes if we are not using fast parent loading if (this._childrenCache && !VersionCompare.manager?.wantFastParentLoad) { // Set update function for UI updates - this._childrenCache.updateFunction = this._updateLoadingProgress; + this._childrenCache.updateFunction = (percent: number) => { + this._progressCoordinator?.addProgress(VersionCompareProgressStage.FindChildren, percent); + } const numQueries = this._childrenCache.calculateNumberOfRequests( parentEntries.length, ); @@ -715,6 +736,7 @@ export class ChangedElementEntryCache { // Clean-up usage of update function this._childrenCache.updateFunction = undefined; } + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.FindChildren, 100); // Put together all entries const finalEntries: ChangedElementEntry[] = []; @@ -743,11 +765,19 @@ export class ChangedElementEntryCache { // For now, use the 6 steps (3 per iModel) to get the models this._setCurrentLoadingMessage("loadingModelNodes", 6); this._updateLoadingProgress(); + + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.LoadIModelNodes); + + const model_nodes_increment = 25; // 4 steps progress uin load changed model nodes + this._uiDataProvider = new ChangesTreeDataProvider(this._manager); await this._uiDataProvider.loadChangedModelNodes( this._currentIModel, this._targetIModel, + () => this._progressCoordinator?.addProgress(VersionCompareProgressStage.LoadIModelNodes, model_nodes_increment), ); + + this._progressCoordinator?.updateProgress(VersionCompareProgressStage.LoadIModelNodes, 100); } }; } diff --git a/packages/changed-elements-react/src/api/ChangedElementsManager.ts b/packages/changed-elements-react/src/api/ChangedElementsManager.ts index 0dd106b0..0a8b1759 100644 --- a/packages/changed-elements-react/src/api/ChangedElementsManager.ts +++ b/packages/changed-elements-react/src/api/ChangedElementsManager.ts @@ -9,7 +9,8 @@ import { IModelApp, IModelConnection, ModelState } from "@itwin/core-frontend"; import { ChangedElementEntryCache, type ChangedElement, type Checksums } from "./ChangedElementEntryCache.js"; import { ChangedElementsChildrenCache } from "./ChangedElementsChildrenCache.js"; import { ChangedElementsLabelsCache } from "./ChangedElementsLabelCache.js"; -import { VersionCompareManager } from "./VersionCompareManager.js"; +import { VersionCompareManager, VersionCompareProgressStage } from "./VersionCompareManager.js"; +import { ProgressCoordinator } from "../widgets/ProgressCoordinator.js"; /** Properties that are not shown but still found by the agent */ const ignoredProperties = ["Checksum", "Version"]; @@ -449,11 +450,13 @@ export class ChangedElementsManager { public async generateEntries( currentIModel: IModelConnection, targetIModel: IModelConnection, + progressCoordinator?: ProgressCoordinator, ): Promise { this._entryCache.initialize( currentIModel, targetIModel, this._changedElements, + progressCoordinator, this._progressLoadingEvent, ); } @@ -542,6 +545,7 @@ export class ChangedElementsManager { currentIModel: IModelConnection, targetIModel: IModelConnection, forward: boolean, + progressCoordinator?: ProgressCoordinator, progressLoadingEvent?: BeEvent<(message: string) => void>, ): Promise> { // If we have model ids in the data already, simply accumulate the models from it instead of querying @@ -596,6 +600,16 @@ export class ChangedElementsManager { steps, ); + progressCoordinator?.updateProgress( + VersionCompareProgressStage.ComputeChangedModels, + Math.floor(((lastStep ?? 0) + currentStep) / (steps === 0 ? 1 : steps) * 100), + ); + + progressCoordinator?.updateProgress( + VersionCompareProgressStage.ComputeChangedModels, + Math.floor(((lastStep ?? 0) + currentStep) / (steps === 0 ? 1 : steps) * 100), + ); + for await (const row of iModel.createQueryReader(ecsql, QueryBinder.from(piece), { rowFormat: QueryRowFormat.UseJsPropertyNames, })) { @@ -1143,6 +1157,7 @@ export class ChangedElementsManager { * @param forward Whether we are comparing to a newer iModel or an older one (normally the older) * @param filterSpatial Whether to filter out non-spatial elements from the results * @param progressLoadingEvent Event raised every time the processing continues to provide UI messages to the user + * @param onOverallProgress Event raised every time the processing continues to provide UI messages to the user */ public async initialize( currentIModel: IModelConnection, @@ -1151,6 +1166,7 @@ export class ChangedElementsManager { wantedModelClasses?: string[], forward?: boolean, filterSpatial?: boolean, + progressCoordinator?: ProgressCoordinator, progressLoadingEvent?: BeEvent<(message: string) => void>, ): Promise { this._progressLoadingEvent = progressLoadingEvent; @@ -1169,12 +1185,14 @@ export class ChangedElementsManager { IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_computingChangedModels"), ); } + progressCoordinator?.updateProgress(VersionCompareProgressStage.ComputeChangedModels); // Find changed models this._changedModels = await this.findChangedModels( currentIModel, targetIModel, forward ?? false, + progressCoordinator, progressLoadingEvent, ); @@ -1183,6 +1201,7 @@ export class ChangedElementsManager { IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_computingUnchangedModels"), ); } + progressCoordinator?.updateProgress(VersionCompareProgressStage.ComputeChangedModels, 100); // Find unchanged models this._unchangedModels = await this.findUnchangedModels( @@ -1190,7 +1209,7 @@ export class ChangedElementsManager { this._changedModels, ); - await this.generateEntries(currentIModel, targetIModel); + await this.generateEntries(currentIModel, targetIModel, progressCoordinator); } } diff --git a/packages/changed-elements-react/src/api/ElementQueries.ts b/packages/changed-elements-react/src/api/ElementQueries.ts index 1daddc02..f3e7b63d 100644 --- a/packages/changed-elements-react/src/api/ElementQueries.ts +++ b/packages/changed-elements-react/src/api/ElementQueries.ts @@ -121,14 +121,14 @@ export const queryEntryData = async ( * @param iModel IModel to query * @param elementIds Ids of element to query for * @param chunkSize Chunk size for each query. Defaults to 1000 - * @param updateFunc [optional] called each time we process a chunk + * @param updateFunc [optional] called after each processed chunk with cumulative percent complete (0–100) * @returns Array of query data */ export const queryEntryDataBulk = async ( iModel: IModelConnection, elementIds: string[], chunkSize = 1000, - updateFunc?: () => void, + updateFunc?: (percent: number) => void, ): Promise => { if (elementIds.length < chunkSize) { return queryEntryData(iModel, elementIds); @@ -142,7 +142,9 @@ export const queryEntryDataBulk = async ( ); final.push(...data); if (updateFunc) { - updateFunc(); + const processed = Math.min(i + chunkSize, elementIds.length); + const percent = Math.floor((processed / elementIds.length) * 100); + updateFunc(percent); } } return final; diff --git a/packages/changed-elements-react/src/api/VersionCompareManager.ts b/packages/changed-elements-react/src/api/VersionCompareManager.ts index ecf87381..a45f8a81 100644 --- a/packages/changed-elements-react/src/api/VersionCompareManager.ts +++ b/packages/changed-elements-react/src/api/VersionCompareManager.ts @@ -18,9 +18,21 @@ import { ChangesTooltipProvider } from "./ChangesTooltipProvider.js"; import { VersionCompareUtils, VersionCompareVerboseMessages } from "./VerboseMessages.js"; import { VersionCompare, type VersionCompareFeatureTracking, type VersionCompareOptions } from "./VersionCompare.js"; import { VisualizationHandler } from "./VisualizationHandler.js"; +import { ProgressCoordinator } from "../widgets/ProgressCoordinator.js"; const LOGGER_CATEGORY = "Version-Compare"; +// different progress stages in version compare, also implies ordering of those stages +export enum VersionCompareProgressStage { + OpenTargetImodel, + InitComparison, + ComputeChangedModels, + FindParents, + ObtainElementData, + FindChildren, + LoadIModelNodes, +} + /** * Main orchestrator for version compare functionality and workflows. This class does the following: * @@ -35,11 +47,24 @@ export class VersionCompareManager { /** Changed Elements Manager responsible for maintaining the elements obtained from the service */ public changedElementsManager: ChangedElementsManager; + private progressCoordinator: ProgressCoordinator; + private _visualizationHandler: VisualizationHandler | undefined; private _hasTypeOfChange = false; private _hasPropertiesForFiltering = false; private _hasParentIds = false; + // define stage and order + private weights: Record = { + [VersionCompareProgressStage.OpenTargetImodel]: 10, + [VersionCompareProgressStage.InitComparison]: 5, + [VersionCompareProgressStage.ComputeChangedModels]: 17, + [VersionCompareProgressStage.FindParents]: 17, + [VersionCompareProgressStage.ObtainElementData]: 17, + [VersionCompareProgressStage.FindChildren]: 17, + [VersionCompareProgressStage.LoadIModelNodes]: 17, + } + /** Version Compare ITwinLocalization Namespace */ public static namespace = "VersionCompare"; @@ -59,6 +84,8 @@ export class VersionCompareManager { const tooltipProvider = new ChangesTooltipProvider(this); IModelApp.viewManager.addToolTipProvider(tooltipProvider); } + + this.progressCoordinator = new ProgressCoordinator(this.weights); } /** Create the proper visualization handler based on options */ @@ -153,6 +180,10 @@ export class VersionCompareManager { return this._targetIModel !== undefined; } + public get onOverallProgress() { + return this.progressCoordinator.onProgressChanged; + } + /** * Elements that should be ignored during initialization. Helpful for editing applications that may not want to * compare locally changed elements. @@ -316,6 +347,7 @@ export class VersionCompareManager { this.wantAllModels ? undefined : wantedModelClasses, false, this.filterSpatial, + undefined, this.loadingProgressEvent, ); const changedElementEntries = this.changedElementsManager.entryCache.getAll(); @@ -405,9 +437,7 @@ export class VersionCompareManager { throw new Error("Cannot compare with an iModel lacking iModelId or iTwinId (aka projectId)"); } - this.loadingProgressEvent.raiseEvent( - IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_openingTarget"), - ); + this.progressCoordinator.updateProgress(VersionCompareProgressStage.OpenTargetImodel); // Open the target version IModel const changesetId = targetVersion.changesetId; @@ -417,17 +447,13 @@ export class VersionCompareManager { IModelVersion.asOfChangeSet(changesetId), ); + this.progressCoordinator.updateProgress(VersionCompareProgressStage.OpenTargetImodel, 100); + // Keep metadata around for UI uses and other queries this.currentVersion = currentVersion; this.targetVersion = targetVersion; - this.loadingProgressEvent.raiseEvent( - IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_getChangedElements"), - ); - - this.loadingProgressEvent.raiseEvent( - IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_initializingComparison"), - ); + this.progressCoordinator.updateProgress(VersionCompareProgressStage.InitComparison); let wantedModelClasses = [ GeometricModel2dState.classFullName, @@ -440,6 +466,9 @@ export class VersionCompareManager { if (this.ignoredElementIds !== undefined) { filteredChangedElements = this._filterIgnoredElementsFromChangesets(changedElements); } + + this.progressCoordinator.updateProgress(VersionCompareProgressStage.InitComparison, 100); + await this.changedElementsManager.initialize( this._currentIModel, this._targetIModel, @@ -447,7 +476,7 @@ export class VersionCompareManager { this.wantAllModels ? undefined : wantedModelClasses, false, this.filterSpatial, - this.loadingProgressEvent, + this.progressCoordinator, ); const changedElementEntries = this.changedElementsManager.entryCache.getAll(); @@ -463,9 +492,6 @@ export class VersionCompareManager { ); // Get the entries - this.loadingProgressEvent.raiseEvent( - IModelApp.localization.getLocalizedString("VersionCompare:versionCompare.msg_findingAssemblies"), - ); await this.changedElementsManager.entryCache.initialLoad(changedElementEntries.map((entry) => entry.id)); // Reset the select tool to allow external iModels to be located diff --git a/packages/changed-elements-react/src/tests/ProgressCoordinator.test.ts b/packages/changed-elements-react/src/tests/ProgressCoordinator.test.ts new file mode 100644 index 00000000..ec6105aa --- /dev/null +++ b/packages/changed-elements-react/src/tests/ProgressCoordinator.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { ProgressCoordinator } from "../widgets/ProgressCoordinator.js"; + +describe("ProgressCoordinator", () => { + const enum stages { + Stage1, + Stage2, + Stage3, + } + + const weights: Record = { + [stages.Stage1]: 10, + [stages.Stage2]: 20, + [stages.Stage3]: 70, + } + + let progressCoordinator: ProgressCoordinator; + let callback: ReturnType; + + beforeEach(() => { + progressCoordinator = new ProgressCoordinator(weights); + callback = vi.fn(); + progressCoordinator.onProgressChanged.addListener(callback); + }); + + it("starts as 0% and no events", () => { + expect(progressCoordinator.getOverallPercentage()).toBe(0); + expect(callback).not.toHaveBeenCalled(); + }); + + it("updatesProgress for a specific stage", () => { + progressCoordinator.updateProgress(stages.Stage1, 50); + expect(progressCoordinator.getOverallPercentage()).toBe(5); // 50% * 10% = 5% + expect(callback).toHaveBeenCalledWith(5); + }); + + it("addProgress accumulates progress for a specific stage", () => { + progressCoordinator.updateProgress(stages.Stage1, 50); + + progressCoordinator.addProgress(stages.Stage1, 50); + expect(progressCoordinator.getOverallPercentage()).toBe(10); // 10% * 10% = 10% + expect(callback).toHaveBeenCalledWith(5); + + progressCoordinator.addProgress(stages.Stage2, 75); + expect(progressCoordinator.getOverallPercentage()).toBe(25); //10% + 20% * 75% = 25% + expect(callback).toHaveBeenCalledWith(25); + }); + + it("ignores unknown stages in updateProgress or addProgress", () => { + progressCoordinator.updateProgress(100 as unknown as stages, 50); + expect(progressCoordinator.getOverallPercentage()).toBe(0); + expect(callback).not.toHaveBeenCalled(); + + progressCoordinator.addProgress(100 as unknown as stages, 50); + expect(progressCoordinator.getOverallPercentage()).toBe(0); + expect(callback).not.toHaveBeenCalled(); + }); + + it("when updateProgress or addProgress out of bounds, should be clamped to 100", () => { + progressCoordinator.updateProgress(stages.Stage1, 150); + expect(progressCoordinator.getOverallPercentage()).toBe(10); // 10% * 100% = 10% + expect(callback).toHaveBeenCalledWith(10); + + progressCoordinator.addProgress(stages.Stage2, 999); + expect(progressCoordinator.getOverallPercentage()).toBe(30); // 10% * 100% + 20% * 100% = 30% + expect(callback).toHaveBeenCalledWith(30); + + }); + + it("race condition, when multiple updates are called, the last one should be the one that is used", () => { + progressCoordinator.updateProgress(stages.Stage1, 50); + progressCoordinator.updateProgress(stages.Stage1, 75); + progressCoordinator.updateProgress(stages.Stage1, 60); + + expect(progressCoordinator.getOverallPercentage()).toBe(6); // 10% * 60% = 6% + expect(callback).toHaveBeenCalledWith(6); + + progressCoordinator.updateProgress(stages.Stage1, 100); + progressCoordinator.updateProgress(stages.Stage2, 100); + progressCoordinator.updateProgress(stages.Stage3, 100); + progressCoordinator.updateProgress(stages.Stage1, 50); + + expect(callback).toHaveBeenCalledWith(5); + expect(progressCoordinator.getOverallPercentage()).toBe(95); // 10% * 50% + 20% * 100% + 70% * 100% = 95% + }); + +}) diff --git a/packages/changed-elements-react/src/widgets/ChangedElementsWidget.tsx b/packages/changed-elements-react/src/widgets/ChangedElementsWidget.tsx index 90d21ab6..e5b418ad 100644 --- a/packages/changed-elements-react/src/widgets/ChangedElementsWidget.tsx +++ b/packages/changed-elements-react/src/widgets/ChangedElementsWidget.tsx @@ -182,6 +182,15 @@ export class ChangedElementsWidget extends Component { + this._onProgressEvent( + IModelApp.localization.getLocalizedString( + "VersionCompare:versionCompare.LoadingResults", + { percent }, + ), + ); + } + private _refreshCheckboxesEvent = new BeEvent<() => void>(); constructor(props: ChangedElementsWidgetProps) { @@ -223,11 +232,12 @@ export class ChangedElementsWidget extends Component{ + private progress: Array<{ stage: StageType; currentProgress: number;}>; + private weights: Record; + public readonly onProgressChanged = new BeEvent<(percent: number) => void>(); + + constructor(weights: Record) { + this.weights = weights; + + this.progress = Object.keys(weights) + .map(key => Number(key) as StageType) + .sort((a, b) => a - b) + .map(stage => ({ stage, currentProgress: 0 })); + } + + /** + * Updates the progress for a specific stage with overall percentage and raises the event. + * @param stage The stage to update. + * @param progress The progress percentage for the specified stage (0-100). + */ + public updateProgress(stage: StageType, percent: number = 0): void { + this.modifyProgress(stage, () => Math.min(100, Math.max(0, percent))); + } + + /** + * Increment the progress for a specific stage with overall percentage and raises the event. + * @param stage The stage to update. + * @param progress The progress percentage for the specified stage (0-100). + */ + public addProgress(stage: StageType, percent: number): void { + this.modifyProgress(stage, curr => Math.min(100, Math.max(0, curr + percent))); + } + + /** + * Computes the overall progress as a weighted percentage across all stages. + * Each stage contributes proportionally based on its assigned weight. + * + * @returns A number (0-100) representing the global progress. + */ + public getOverallPercentage(): number { + return this.progress.reduce( + (sum, { stage, currentProgress }) => + sum + (currentProgress / 100) * this.weights[stage], + 0, + ); + } + + // helper that updates the progress and raises the event + private modifyProgress(stage: StageType, mutator: (curr: number) => number): void { + const idx = this.progress.findIndex(s => s.stage === stage); + if (idx === -1) return; + + this.progress[idx].currentProgress = mutator(this.progress[idx].currentProgress); + + const overallPct = this.getOverallPercentage(); + const cap = this.progress + .slice(0, idx + 1) // we only want to sum the weights of the stages up to and including the current one to prevent race conditions + .reduce((sum, s) => sum + this.weights[s.stage], 0); + + this.onProgressChanged.raiseEvent(Math.min(cap, overallPct)); + } +}