In a web app some task takes multiple sequential ajax call steps to complete. Each takes 12–18 seconds.
I want to present user a progress indicator that is making frequent little steps instead.
My first bet was to assume a linear progress = k * time
function, self-adjusting the k
on each new response. Fiddle emulated the response times with random values in a +- 4s range.
This approach appears wrong: in cases with a relatively long response after a serie of fast ones, progress takes a negative step to catch up with real pace.
Feels like a function should go in "waves": faster at beginning, slowing down closer to the end to extent in case of a delayed checkpoint.
What is the best practice for such a task?
I'm going to focus on the mathematical part of your request:
a function should go in "waves": faster at beginning, slowing down closer to the end to extent in case of a delayed checkpoint
Translation: a monotonic function (i.e. no backward steps) with a positive initial gradient and asymptotic behaviour for large x.
Consider atan(theta): it has a gradient of 1 for small x, and asymptotically approaches π/2 for large x. We can scale it so that the expected end happens when the chunk is at some fraction of the available length - that is, if you expect it to take 4s, and it takes 4s, it can jump the remainder. If it takes longer, this remainder is the bit we will asymptotically eat up. Thus:
function chunkProgressFraction(expectedEndT, currentT, expectationFraction) {
// validate
if(!expectedEndT) { return 0; }
// defaults
if(!expectationFraction) { expectationFraction = 0.85 }
// y = k atan(mx)
// to reach 1.0 at large x:
// 1.0 = k . atan(+lots) = k . pi/2
var k = 2.0 / Math.PI;
// scale the function so that the expectationFraction result happens
// at expectedEndT, i.e.
// expectationFraction = k * atan(expectedEndT * m)
// expectedEndT * m = tan(expectationFraction / k)
var m = Math.tan(expectationFraction / k) / expectedEndT;
return k * Math.atan(m * currentT);
}
So if we want to get to 100 pixels, and we expect it to take 4s, and we want 20% slack:
progressPixelsThisChunk = 100.0 *
chunkProgressValue(4000.0, thisChunkTimeInMilliseconds, 0.8);
By all means scale the expectedEndT using the time taken by the previous chunk.