智能进度条ETA计算


73

在许多应用程序中,我们都有一些进度条,用于文件下载,压缩任务,搜索等。我们都经常使用进度条让用户知道正在发生的事情。而且,如果我们知道一些细节,例如已完成的工作量和剩余的工作量,我们甚至可以给出时间估计,通常可以通过推断达到当前进度所需的时间来推断。

压缩ETA屏幕截图
(来源:jameslao.com

但是,我们还看到了这个“ ETA”时间显示的程序,可笑的是糟糕的。它声称将在20秒内完成文件复制,然后一秒钟后它说将需要4天,然后又闪烁了20分钟。这不仅无益,而且令人困惑!ETA变化很大的原因是,进度本身可能变化,并且程序员的数学可能过于敏感。

苹果通过避免任何准确的预测并仅给出模糊的估计来避开这种情况! (来源:autodesk.com苹果的模糊回避

这也很烦人,我是否有时间快速休息,还是我的任务将在2秒内完成?如果预测过于模糊,则根本无法进行任何预测。

简单但错误的方法

作为首轮ETA计算,大概我们都可以做一个函数,例如如果p是已经完成的小数百分比,而t是到目前为止所花费的时间,我们输出t *(1-p)/ p作为对p的估计。需要多长时间才能完成。这个简单的比率可以“正常”运行,但也很糟糕,尤其是在计算结束时。如果您的缓慢下载速度使副本在一整夜里缓慢前进,最后在早晨,则出现了一些情况,并且副本开始以全速以100倍的速度全速运行,您的ETA完成了90%时可能会说“ 1小时”和10秒后来您达到95%,ETA会说“ 30分钟”,这显然是一个令人尴尬的猜测。.在这种情况下,“ 10秒”是一个更好得多的估计。

发生这种情况时,您可能会考虑将计算更改为使用最新速度而非平均速度来估计ETA。您可以获取最近10秒钟的平均下载速率或完成率,并使用该速率来预测完成时间。在之前的过夜下载(最终速度加快)示例中,该方法的效果很好,因为最终它会给出非常好的最终完成估算值。但这仍然有很大的问题..当您的速率在短时间内快速变化时,它会导致您的ETA剧烈反弹,并且您得到“在20秒内完成,在2小时内完成,在2秒内完成,在30秒内完成分钟”迅速显示出编程的耻辱。

实际问题:

给定计算的时间历史,计算任务完成时间的最佳方法是什么?我不是在寻找指向GUI工具包或Qt库的链接。我问的算法来生成最明智的和准确的完成时间估计。

数学公式是否成功?某种平均,可能是通过使用10秒以上的速率的平均值,1分钟以上的速率和1小时以上的速率的平均值?某种人工过滤,例如“如果我的新估计值与先前的估计值相差太大,将其调低,不要让其反弹太多”?某种花式历史分析,您可以在其中整合进度与时间进度,以找到费率的标准偏差,从而在完成时提供统计误差指标?

您尝试过什么,什么最有效?


Answers:


32

原始答案

创建此站点的公司显然提供了一个调度系统,该系统可以在员工编写代码的情况下回答此问题。它的工作方式是基于过去的蒙特卡洛模拟未来。

附录:蒙特卡洛的解释

这是此算法在您的情况下的工作方式:

您将任务建模为一系列微任务,例如其中的1000个。假设一个小时后您完成了其中的100个。现在,通过随机选择90个已完成的微任务,将它们的时间相加并乘以10,可以对其余900个步骤进行仿真。重复N次,剩下的时间就有N个估算值。请注意,这些估算值之间的平均时间约为9小时-在此不感到意外。但是,通过向用户呈现结果分布,您将诚实地向用户传达赔率,例如“概率为90%,这将再花费3-15个小时”

根据定义,如果可以将所讨论的任务建模为一堆独立的随机微任务,则此算法将产生完整的结果。仅当您知道任务如何偏离此模型时,您才能获得更好的答案:例如,安装程序通常具有下载/解包/安装任务列表,并且一个任务的速度无法预测另一个任务。

附录:简化蒙特卡洛

我不是统计学专家,但我认为,如果您仔细研究此方法中的模拟,它将始终返回正态分布,作为大量独立随机变量的总和。因此,您根本不需要执行它。实际上,您甚至不需要存储所有完成的时间,因为您只需要它们的和和平方的和。

用也许不是很标准的符号

sigma = sqrt ( sum_of_times_squared-sum_of_times^2 )
scaling = 900/100          // that is (totalSteps - elapsedSteps) / elapsedSteps
lowerBound = sum_of_times*scaling - 3*sigma*sqrt(scaling)
upperBound = sum_of_times*scaling + 3*sigma*sqrt(scaling)

有了这个,您可以输出一条消息,说事情将从现在开始在[lowerBound,upperBound]之间以一定的固定概率结束(应该是95%,但我可能错过了一些恒定因子)。


1
这是一个引导估计。
piccolbo 2010年

1
你能否澄清sum_of_times_squaredsum_of_times?是sum_of_times_squared += time^2吗 如果是这样,那肯定sum_of_times_squared - sum_of_times^2是负面的吗?
RazerM 2015年

1
这实际上是SPWorley采用Sigma估计的“简单但错误”的方法中的第一个。通过假定该过程由许多随机且独立的微任务组成,它将速度的所有变化归因于噪声,均值为零,并估计实际速度,就好像它是恒定的。它可以工作,但是在性能变化的情况下效果很差。我承认自己亲自使用了这种原始方法(请参阅我的回答),可以说我们的客户经常会嘲笑我的进度条,这可能首先是报告ETA不断增长,然后以每秒2秒的两倍速率下降到零。
Ant_222 '17

15

这是我发现的效果很好!对于任务的前50%,您假设速率是恒定且外推的。时间预测非常稳定,不会反弹太多。

一旦超过50%,就可以切换计算策略。您将剩下的工作要做的一小部分(1-p),然后回顾自己的进度历史,并找到(通过二进制搜索和线性插值)最后一次花了多长时间(1 -p)百分比,并将用作您的时间估算完成次数。

因此,如果您现在完成71%,那么您还有29%的剩余时间。您回顾历史,发现自己完成(71-29 = 42%)多久了。将该时间报告为您的预计到达时间。

这自然是自适应的。如果您有X项工作要做,它只会在执行X项工作时才显示。最后,当您完成99%的工作时,它仅使用非常新的,最新的数据进行估算。

当然,它并不是完美的,但它会平滑地变化,并且在最有用的最后尤其准确。


9

尽管所有示例都是有效的,但对于“剩下的下载时间”的特定情况,我认为最好查看现有的开源项目以了解它们的作用。

据我所知,Mozilla Firefox最能估计剩余时间。

火狐浏览器

Firefox会跟踪剩余时间的最后一个估计值,并通过使用该估计值和当前剩余时间的估计值来对时间进行平滑处理。请在此处查看ETA​​代码。这使用先前计算的“速度”在此处并且是最后10个读数的平滑平均值。

这有点复杂,因此请解释一下:

  • 根据先前速度的90%和新速度的10%取平滑的速度平均值。
  • 通过这种平滑的平均速度,可以算出估计的剩余时间。
  • 使用此估计剩余时间,以及先前的估计剩余时间来创建新的估计剩余时间(以避免跳跃)

谷歌浏览器

Chrome似乎无处不在,代码显示了这一点

我喜欢Chrome的一件事是它们如何格式化剩余时间。对于> 1小时,它说'还剩1个小时'对于<1小时,它说'还剩59分钟'对于<1分钟,它说'还剩52秒”

您可以在此处查看其格式

DownThemAll!经理

它没有使用任何聪明的东西,这意味着ETA到处都是。

在这里查看代码

pySmartDL(python下载器)

取得最近30次ETA计算的平均ETA。听起来像是一种合理的方法。

此处查看代码/blob/916f2592db326241a2bf4d8f2e0719c58b71e385/pySmartDL/pySmartDL.py#L651)

传播

在大多数情况下提供不错的ETA(可能会出现这种情况,但开始时除外)。

在过去5个读数中使用平滑因子,类似于Firefox,但不太复杂。从根本上类似于Gooli的答案。

这里查看代码


8

我通常使用指数移动平均值来计算平滑度为0.1的运算速度,然后使用该指数计算剩余时间。这样,所有测得的速度都会对当前速度产生影响,但是最近的测量比远处的影响要大得多。

在代码中,它看起来像这样:

alpha = 0.1 # smoothing factor
...
speed = (speed * (1 - alpha)) + (currentSpeed * alpha)

如果您的任务大小相同,则currentSpeed仅是执行上一个任务所花费的时间。如果任务的大小不同,并且您知道一个任务的长度应该是另一个任务的两倍,则可以将执行任务所需的时间除以其相对大小,以得出当前速度。使用speed您可以通过将剩余时间乘以剩余任务的总大小(如果任务是统一的,则乘以其数量)来计算剩余时间。

希望我的解释很清楚,这有点迟了。


1
遵循这些思路是一个好主意。但是,如果更新刻度的间距不规则,则可能会导致大问题。也许使“ alpha”平滑因子成为自上次更新以来时间的函数,例如alpha = exp(-C * TimeSinceLastUpdate))?也许C应该根据竞争百分比改变自己?
SPWorley

@Enerccio:速度从0开始
马亭皮特斯

3

在某些情况下,当您需要定期执行同一任务时,最好使用过去的完成时间进行平均。

例如,我有一个应用程序通过其COM接口加载iTunes库。就项目数量而言,给定iTunes库的大小从启动到启动通常不会显着增加,因此在此示例中,可以跟踪最后三个加载时间和加载速率,然后根据该平均值和计算您的当前预计到达时间。

这将比瞬时测量准确得多,并且也可能更加一致。

但是,此方法取决于任务的大小,该任务的大小与之前的任务相对相似,因此,这对于解压缩方法或其他任何给定的字节流是要处理的数据的方法都不起作用。

我的$ 0.02


3

首先,它有助于生成移动均线。这对最近事件的加权更大。

为此,请保留一堆样本(循环缓冲区或列表),每个样本都有一对进度和时间。保留最近的N秒样本。然后生成样本的加权平均值:

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

其中scaleFactor从0 ... 1线性地变为过去的时间的反函数(因此,对最近采样的权重更大)。您当然可以使用此权重。

最后,您可以获得平均变化率:

 averageProgressRate = (totalProgress / totalTime);

您可以通过将剩余进度除以该数字来计算出预计到达时间。

但是,尽管这为您提供了一个很好的趋势数字,但您还有另一个问题-抖动。如果由于自然变化,您的进度有所波动(很吵)-例如,也许您正在使用它来估计文件下载量-您会注意到噪音很容易导致您的ETA跳动,特别是在不久以后(数分钟或更长时间)。

为了避免抖动对您的ETA造成太大影响,您希望此平均更改率数字对更新做出缓慢响应。解决此问题的一种方法是保持averageProgressRate的缓存值不变,而不是立即将其更新为您刚刚计算的趋势数,而是将其模拟为具有质量的重物,然后将模拟“力”缓慢施加将其移向趋势数。对于质量,它具有一点惯性,并且不太可能受到抖动的影响。

这是一个粗略的示例:

// 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;
}    

2

您的问题是一个好问题。如果可以将问题分解为离散的单元,则进行准确的计算通常效果最佳。不幸的是,即使您要安装50个组件,每个组件可能占2%,但事实并非如此,但其中一个组件可能很大。我取得了一定程度的成功的一件事是为CPU和磁盘计时,并根据观测数据给出了不错的估计。知道某些检查点确实是x点,这使您有机会纠正环境因素(网络,磁盘活动,CPU负载)。但是,由于其依赖于观测数据,因此该解决方案本质上并不通用。使用诸如rpm文件大小之类的辅助数据有助于我提高进度条的准确性,但是它们从来都不是防弹的。


1

均匀平均

最简单的方法是线性预测剩余时间:

t_rem := t_spent ( n - prog ) / prog

哪里t_rem是预计的预计t_spent到达时间,是从开始操作以来经过的时间prog,即完成的微任务数量n。解释n一下-可能是表中要处理的行数或要复制的文件数。

这种没有参数的方法,不必担心衰减指数的微调。由于所有样本对估计的贡献均相等,因此折衷不能很好地适应变化的进度,而仅满足于最近的样本应具有比旧样本更大的权重,这导致我们得出以下结论:

速率的指数平滑

其中标准技术是通过平均先前的点测量来估计进度:

rate := 1 / (n * dt); { rate equals normalized progress per unit time }
if prog = 1 then      { if first microtask just completed }
    rate_est := rate; { initialize the estimate }
else
begin
    weight   := Exp( - dt / DECAY_T );
    rate_est := rate_est * weight + rate * (1.0 - weight);
    t_rem    := (1.0 - prog / n) / rate_est;
end;

其中dt表示最后完成的微任务的持续时间,并且等于自上次进度更新以来经过的时间。请注意,这weight不是一个常数,必须根据rate观察到某个物体的时间长度进行调整,因为我们观察到特定速度的时间越长,先前测量的指数衰减就越高。该常数DECAY_T表示样品重量减少e的时间长度。SPWorley本人建议对gooli的提案进行类似的修改,尽管他将其应用于错误的术语。等距测量的指数平均值为:

Avg_e(n) = Avg_e(n-1) * alpha + m_n * (1 - alpha)

但是,如果样本不是等距的(与典型进度条中的时间相同),该怎么办?考虑到alpha以上只是一个经验商,其真值为:

alpha = Exp( - lambda * dt ),

哪里lambda是指数窗口的参数以及dt自上一个样本以来的变化量(不一定是时间,而是任何线性和累加参数)。alpha对于等距测量,常数是恒定的,但是随着的变化而变化dt

标记此方法依赖于预定义的时间常数,并且在时间上不可伸缩。换句话说,如果将完全相同的过程统一减慢一个常数因子,则该基于速率的滤波器将对信号变化成比例地更敏感,因为每个步骤weight都会减少。但是,如果我们希望独立于时间范围进行平滑处理,则应考虑

慢度的指数平滑

这本质上是将比率上下颠倒的平滑与常数weight的简化简化了,因为prog它以等距的增量增长:

slowness := n * dt;   { slowness is the amount of time per unity progress }
if prog = 1 then      { if first microtask just completed }
    slowness_est := slowness; { initialize the estimate }
else
begin
    weight       := Exp( - 1 / (n * DECAY_P ) );
    slowness_est := slowness_est * weight + slowness * (1.0 - weight);
    t_rem        := (1.0 - prog / n) * slowness_est;
end;

无量纲常数DECAY_P表示权重为1与e的两个样本之间的归一化进度差异。换句话说,此常数确定进度域中而不是时域中的平滑窗口的宽度。因此,该技术与时间范围无关,并且具有恒定的空间分辨率。

进一步的研究:自适应指数平滑

现在,您已准备好尝试各种自适应指数平滑算法。只记得将它应用于缓慢而不是给评分


这就是问题中提到的“简单” /“错误” /“确定”方法。我简化了公式,最适合我。反对者对什么是“智能” /“理智” /“准确”构成分歧。如果我的链接随机变化,我更喜欢简单的方法。
Cees Timmerman

@SPWorley,答案扩大了。
Ant_222

0

我一直希望这些事情能告诉我一个范围。如果它说“此任务很可能在8分钟到30分钟之间完成”,那么我对应该采取哪种休息时间有所了解。如果它在各处弹跳,我很想观看它直到它安定下来,这是在浪费大量时间。


通过估算进度的标准偏差,这是可能的
Ant_222 '17

0

我已经尝试并简化了您的“简单” /“错误” /“确定”公式,它最适合我:

t / p - t

在Python中:

>>> done=0.3; duration=10; "time left: %i" % (duration / done - duration)
'time left: 23'

与(dur *(1-done)/ done)相比,节省了一个运算。而且,在您所描述的最极端的情况下,在整夜等待之后,可能会忽略30分钟的对话框几乎没有关系。

这种简单方法与Transmission所使用的方法进行比较,我发现它的准确率提高了72%。


不是很有帮助。该算法将导致“剩余时间”值反弹所有的地方
帕特里克

@Patrick它比建议的公式具有“确定”结果更好,并且随着时间的推移会变得更加准确。如果您更喜欢最近的汇率,估计会反弹更多,而平滑处理则隐藏了事实。
Cees Timmerman'7

-4

我不费吹灰之力,它只是应用程序的一小部分。我告诉他们发生了什么,然后让他们去做其他事情。


2
您必须要点人,除非这是客户为您支付的费用:)
Shimmy Weitzhandler 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.