Smart progress bar ETA computation

后端 未结 11 457
谎友^
谎友^ 2020-12-22 16:10

In many applications, we have some progress bar for a file download, for a compression task, for a search, etc. We all often use progress bars to let users know something is

11条回答
  •  礼貌的吻别
    2020-12-22 16:15

    First off, it helps to generate a running moving average. This weights more recent events more heavily.

    To do this, keep a bunch of samples around (circular buffer or list), each a pair of progress and time. Keep the most recent N seconds of samples. Then generate a weighted average of the samples:

    totalProgress += (curSample.progress - prevSample.progress) * scaleFactor
    totalTime += (curSample.time - prevSample.time) * scaleFactor
    

    where scaleFactor goes linearly from 0...1 as an inverse function of time in the past (thus weighing more recent samples more heavily). You can play around with this weighting, of course.

    At the end, you can get the average rate of change:

     averageProgressRate = (totalProgress / totalTime);
    

    You can use this to figure out the ETA by dividing the remaining progress by this number.

    However, while this gives you a good trending number, you have one other issue - jitter. If, due to natural variations, your rate of progress moves around a bit (it's noisy) - e.g. maybe you're using this to estimate file downloads - you'll notice that the noise can easily cause your ETA to jump around, especially if it's pretty far in the future (several minutes or more).

    To avoid jitter from affecting your ETA too much, you want this average rate of change number to respond slowly to updates. One way to approach this is to keep around a cached value of averageProgressRate, and instead of instantly updating it to the trending number you've just calculated, you simulate it as a heavy physical object with mass, applying a simulated 'force' to slowly move it towards the trending number. With mass, it has a bit of inertia and is less likely to be affected by jitter.

    Here's a rough sample:

    // desiredAverageProgressRate is computed from the weighted average above
    // m_averageProgressRate is a member variable also in progress units/sec
    // lastTimeElapsed = the time delta in seconds (since last simulation) 
    // m_averageSpeed is a member variable in units/sec, used to hold the 
    // the velocity of m_averageProgressRate
    
    
    const float frictionCoeff = 0.75f;
    const float mass = 4.0f;
    const float maxSpeedCoeff = 0.25f;
    
    // lose 25% of our speed per sec, simulating friction
    m_averageSeekSpeed *= pow(frictionCoeff, lastTimeElapsed); 
    
    float delta = desiredAvgProgressRate - m_averageProgressRate;
    
    // update the velocity
    float oldSpeed = m_averageSeekSpeed;
    float accel = delta / mass;    
    m_averageSeekSpeed += accel * lastTimeElapsed;  // v += at
    
    // clamp the top speed to 25% of our current value
    float sign = (m_averageSeekSpeed > 0.0f ? 1.0f : -1.0f);
    float maxVal = m_averageProgressRate * maxSpeedCoeff;
    if (fabs(m_averageSeekSpeed) > maxVal)
    {
     m_averageSeekSpeed = sign * maxVal;
    }
    
    // make sure they have the same sign
    if ((m_averageSeekSpeed > 0.0f) == (delta > 0.0f))
    {
     float adjust = (oldSpeed + m_averageSeekSpeed) * 0.5f * lastTimeElapsed;
    
     // don't overshoot.
     if (fabs(adjust) > fabs(delta))
     {
        adjust = delta;
                // apply damping
        m_averageSeekSpeed *= 0.25f;
     }
    
     m_averageProgressRate += adjust;
    }    
    

提交回复
热议问题