A simple solution here if applicable (doesn't consolidate progress for asynchronous operations running independently of each other, though you can still use this method for those to compute percentages within them) and you can anticipate the number of phases in advance in an outer context (even if you can't anticipate how much work is required in each phase prior to invoking them) which leads to a fairly smooth progress bar* is like this:
- It might not increment in progress in a perfectly consistent way across phases, but should still keep moving forward without going past 100%.
if stack_size < n:
return (steps[n] / total[n]) +
(1 / total[n]) * progress(n+1);
Hopefully the diagram and code already gives you an idea of the implementation using a "progress stack" and some simple arithmetic to compute the progress so far, and you can nest these as deep as you want (phases which break down into sub-phases which break down into sub-sub phases) without worrying about overflowing the bar and going past 100% or anything like that.
All you need to do is in an outer context, anticipate how many phases you have (3 in this case), in which case you increment the progress in 33.3% increments per phase. But when we call the function for
phase 1, we might discover that this first phase needs 100 sub-steps to complete after calling the function, at which point that 33.3% chunk of progress, to speak, then gets incremented in 100 steps to advance from 0% to 33%, and so forth. The client code might take on this shape:
// begin performing a total of 3 units/steps of work
// begin performing a total of 100 units/steps of work
for j in range(1, 100):
... something of this sort and recursive calls to
begin "subdivide" the outer chunk of progress to be incremented. When the number of steps reaches the total for a phase or sub-phase or sub-sub-phase or whatever, then you can pop off the stack. This does still require specifying numbers but doesn't require you to necessarily have that much information upfront about all the steps involved in all these nested function calls, only the number of steps/phases involving in a particular function and not the "sub-functions".
Oddly as simple as this solution is, I point it out because I've seen very convoluted "flat" solutions (without this kind of stack for nested operations) where the developers had to anticipate the total number of steps for everything upfront, and that sometimes requires x-raying the implementation of the functions being called and the function calls of the functions being called and so on. So this avoids that extensive amount of knowledge required upfront and only requires that a function needs to know how many steps it performs, not all the functions deeper into the call stack.
Of course it might not be perfectly consistent in the way it increments (ex: it might take 1 second to go from 0 to 33% and 3 seconds to go from 33% to 66%), but it can stay very smooth and responsive and constantly moving forward in progress provided the steps are granular enough in your "leaf" calls (your phase or sub-phase or sub-sub-sub-sub-phase or whatever).
I even started thinking about recording the time taken, and using these numbers to make dynamic estimations during the next run - but that's overkill, I think.
If it comes to that I'd just go with the advice to just show something going on without a progress bar, per se. That's just getting so fancy for so little in exchange if you can't anticipate even the number of phases involved in advance (ex: a heuristic which requires
N passes over data where
N cannot be anticipated in advance prior to the algorithm actually completing).
Chunky progress bars which don't increment very smoothly also don't feel so bad if you have like some animating thing next to it and can cancel the operation instantly, for example. It sufficiently communicates that the application hasn't just stalled and become completely unresponsive. So you might not even need to bother to "subdivide" the progress bar for sub-steps of the phases and just instead make sure the UI continues to update and shows something going on at a reasonable frame rate and responds quickly enough if the user can abort even if the bar isn't moving at such a smooth and rapid pace.