在软件开发中,经常会遇到需要用户等待的情况,例如数据库升级、大文件解析、文件压缩编码等。这些操作可能需要几分钟到几天不等的时间。为了提高用户体验,希望提供一个准确的剩余时间估计。然而,估计时间的准确性往往受到多种因素的影响,如性能变化、多线程处理、不同类型的进度显示等。本文介绍的算法旨在通过有限的历史数据和线性回归来提供一个更准确的时间估计。
开发者在设计软件时,经常需要处理用户等待的情况。例如,长时间的系统升级、磁盘扫描、索引重建、网络扫描等。有些用户可能会在看到“这可能需要一段时间”的提示时去泡杯咖啡,而有些用户可能会因为等待时间过长而感到烦恼。因此,需要一个简单且资源消耗不大的时间估计器,它能够根据最近的历史数据来估算剩余时间,并且能够适应性能变化和不同类型的进度显示。
算法的核心思想是在进程运行过程中,开发者通过添加一个带有特定值的标记来更新计时器,并将这个标记存储在一个链表中(从旧到新)。标记的时间戳是从计时器初始化开始的内部时钟获取的。链表被限制在一个特定的时间范围内,这意味着:
最近一个标记的时间 - 最早一个标记的时间 <= 时间范围
这样做的目的是为了根据最近的历史数据进行估算。时间范围可以根据用户对整个过程所需时间的估计来定义。如果一个过程大约需要15分钟,那么一分钟的时间范围就足够了。如果一个过程需要2天,那么10-15分钟的时间范围可能会产生更好的结果。
更新计时器非常重要,并且应该尽可能地进行,而不一定是在均匀的时间间隔内。例如,如果进程正在运行,并且进程有某种完成值,那么每次它变化时都应该添加它。
一旦收集到足够的数据,用户可以通过调用正确的方法来获取剩余时间估计。使用了线性回归来确定进程的估计经过时间,然后通过从经过时间中减去当前时钟,找到估计的剩余时间。
可以在这里了解更多关于线性回归的信息。
代码易于理解,没有添加任何优化,以便使代码易于理解。'Mark'用于向列表中添加一个值。只有当列表中添加或移除新数据时,才进行估算。
public void Mark(double Value) {
TimedValue TV = new TimedValue(_SW.ElapsedMilliseconds, Value);
lock(_SyncLock) {
_Data.AddFirst(TV);
_NeedToRecomputeEstimation = true;
ClearOutOfWindowData(); // 移除窗口外的数据
}
}
获取剩余估算是通过重新计算线性系数(使用线性回归),然后计算剩余时间(想象Y轴包含值,X轴包含时间)。
public TimeSpan GetRemainingEstimation() {
if (_NeedToRecomputeEstimation) {
lock (_SyncLock) {
ClearOutOfWindowData();
if (_Data.Count != 0) {
// 使用线性回归计算线性系数
ComputeLinearCoefficients();
double RemainingMilliseconds = (_TargetValue - _LastYint) / _LastSlope;
// y = m*x+b --> (y-b)/m = x
if (double.IsNaN(RemainingMilliseconds) || double.IsInfinity(RemainingMilliseconds)) {
_LastElapsed = TimeSpan.MaxValue; // 没有数据或数据无效
} else {
_LastElapsed = TimeSpan.FromMilliseconds(RemainingMilliseconds);
}
_NeedToRecomputeEstimation = false; // 直到数据列表再次更改
}
}
}
if (_LastElapsed != TimeSpan.MaxValue) {
return _LastElapsed - _SW.Elapsed;
} else {
return TimeSpan.MaxValue;
}
}
在代码中,附加了一个简短的测试程序。点击任何键都会增加值(0-100)。通过快速/慢速点击进行实验。数据的相关性(括号内)可以表示“估算的准确性”,其中0.0表示不好,1.0表示非常好。