Progress.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. namespace MeloongCore;
  2. /// <summary>
  3. /// 提供一个追踪任务进度的对象,它所提供的供用户查看的进度必定单调递增。
  4. /// 支持重试、拆分子项进度、记录部分进度被跳过。
  5. /// </summary>
  6. public class ProgressProvider {
  7. /// <summary>
  8. /// 当原始进度可能被改变时触发。
  9. /// </summary>
  10. public event Action<(double actual, double skiped)>? ProgressChanged;
  11. private void InvokeProgressChanged((double actual, double skiped) raw) => ProgressChanged?.Invoke(raw);
  12. private void InvokeProgressChanged() => InvokeProgressChanged(GetRaw());
  13. // ===================================== 主项进度 =====================================
  14. private (double actual, double skiped, double splited) progressParts = (0, 0, 0);
  15. private double progressSum => progressParts.actual + progressParts.skiped + progressParts.splited;
  16. public bool Finished => progressParts.actual + progressParts.skiped > 0.9999999;
  17. private bool _Set(double value, bool skiped) {
  18. value = value.Clamp(0, 1);
  19. double delta = value - progressSum;
  20. if (delta < 0) { // 等比减少
  21. progressParts = (progressParts.actual * value / progressSum,
  22. progressParts.skiped * value / progressSum,
  23. progressParts.splited * value / progressSum);
  24. } else if (skiped) {
  25. progressParts.skiped += delta;
  26. } else {
  27. progressParts.actual += delta;
  28. }
  29. return delta != 0;
  30. }
  31. /// <summary>
  32. /// 将当前进度设置为指定值。
  33. /// 若指定了 <paramref name="skiped"/>,这段进度的增量值将被计为跳过:当前观测进度不会增加,后续观测进度将增加地更快。
  34. /// </summary>
  35. public void Set(double value, bool skiped = false, ChildrenAction action = ChildrenAction.Finished) {
  36. bool changed;
  37. lock (this) {
  38. changed = _Set(value, skiped);
  39. changed |= AssumeChildrenAs(action);
  40. }
  41. if (changed) InvokeProgressChanged();
  42. }
  43. /// <summary>
  44. /// 将当前进度增加指定值。
  45. /// 若指定了 <paramref name="skiped"/>,这段进度的增量值将被计为跳过:当前观测进度不会增加,后续观测进度将增加地更快。
  46. /// 若指定了 <paramref name="markChildrenAsFinished"/>,当前所有未完成的子项将被视为已完成。
  47. /// </summary>
  48. public void Add(double value, bool skiped = false, ChildrenAction action = ChildrenAction.Finished) {
  49. bool changed;
  50. lock (this) {
  51. changed = _Set(value + progressSum, skiped);
  52. changed |= AssumeChildrenAs(action);
  53. }
  54. if (changed) InvokeProgressChanged();
  55. }
  56. /// <summary>完成当前项的剩余进度。</summary>
  57. public void Finish(ChildrenAction action = ChildrenAction.Finished) => Set(1, skiped: false, action: action);
  58. /// <summary>跳过当前项的剩余进度。</summary>
  59. public void Skip(ChildrenAction action = ChildrenAction.Skiped) => Set(1, skiped: true, action: action);
  60. /// <summary>
  61. /// 重置当前进度,并清除所有子项。
  62. /// </summary>
  63. public void Reset() {
  64. bool changed;
  65. lock (this) {
  66. changed = progressSum > 0;
  67. progressParts = (0, 0, 0);
  68. childrens.Clear();
  69. }
  70. if (changed) InvokeProgressChanged();
  71. }
  72. // ===================================== 子项进度 =====================================
  73. private readonly List<(double percentage, ProgressProvider child)> childrens = [];
  74. /// <summary>
  75. /// 拆分一个子项:当该子项完成时,进度将从当前值增加至 <paramref name="value"/>。
  76. /// </summary>
  77. public ProgressProvider SplitTo(double value) {
  78. lock (this) return SplitBy(value - progressSum).First();
  79. }
  80. /// <summary>
  81. /// 拆分多个子项,每个子项都占据对应指定的进度量。
  82. /// <para/>例如,调用 <c>SplitBy(0.2, 0.3)</c> 将返回两个子项,分别占据总进度的 20%、30%。
  83. /// <para/>若总进度量 > 1,子项的进度量将相对缩小。
  84. /// </summary>
  85. public List<ProgressProvider> SplitBy(params double[] percentages) {
  86. lock (this) {
  87. if (percentages.Any(p => p <= 0)) throw new ArgumentException("子项进度必须为正数且");
  88. progressParts.splited += (percentages.Sum() + progressSum).Clamp(0, 1) - progressSum;
  89. return percentages.Select(percentage => {
  90. var sub = new ProgressProvider();
  91. sub.ProgressChanged += InvokeProgressChanged;
  92. childrens.Add((percentage, sub));
  93. return sub;
  94. }).ToList();
  95. }
  96. }
  97. public enum ChildrenAction {
  98. /// <summary>不做任何处理。</summary>
  99. None,
  100. /// <summary>将未完成的子项的剩余进度视为已完成。</summary>
  101. Finished,
  102. /// <summary>将未完成的子项的剩余进度视为已跳过。</summary>
  103. Skiped
  104. }
  105. /// <summary>
  106. /// 修改所有未完成的子项。
  107. /// </summary>
  108. /// <returns>是否修改了任意进度不到 1 的子项。</returns>
  109. private bool AssumeChildrenAs(ChildrenAction action) {
  110. if (action == ChildrenAction.None) return false;
  111. lock (this) {
  112. bool changed = false;
  113. if (childrens.Any(c => c.percentage > 0)) { // 将子项的进度加入主项
  114. var mult = progressParts.splited / childrens.Sum(c => c.percentage);
  115. childrens.ForEach(c => {
  116. var (subActual, subSkiped) = c.child.GetRaw();
  117. progressParts.actual += mult * c.percentage * (action == ChildrenAction.Finished ? (1 - subSkiped) : subActual);
  118. progressParts.skiped += mult * c.percentage * (action == ChildrenAction.Skiped ? (1 - subActual) : subSkiped);
  119. c.child.ProgressChanged -= InvokeProgressChanged;
  120. if (subActual + subSkiped < 0.9999999) changed = true;
  121. });
  122. }
  123. progressParts.splited = 0;
  124. childrens.Clear();
  125. return changed;
  126. }
  127. }
  128. // ===================================== 观测 =====================================
  129. /// <summary>
  130. /// 获取包含子项进度的实际总进度值。
  131. /// </summary>
  132. public (double actual, double skiped) GetRaw() {
  133. lock (this) {
  134. (double actual, double skiped) current = (progressParts.actual, progressParts.skiped);
  135. if (childrens.Any(c => c.percentage > 0)) { // 将子项的进度加入主项
  136. var mult = progressParts.splited / childrens.Sum(c => c.percentage);
  137. childrens.ForEach(c => {
  138. var (subActual, subSkiped) = c.child.GetRaw();
  139. current.actual += mult * c.percentage * subActual;
  140. current.skiped += mult * c.percentage * subSkiped;
  141. });
  142. }
  143. return current;
  144. }
  145. }
  146. private (double actual, double skiped) observedProgress = (0, 0);
  147. private double incrementProgress = 0;
  148. /// <summary>
  149. /// 获取一个在多次观测之间必定单调递增的进度值,范围为 [0, 1]。
  150. /// </summary>
  151. public double GetIncrement() {
  152. lock (this) {
  153. (double actual, double skiped) current = GetRaw();
  154. if (current == observedProgress) return incrementProgress; // 未改变
  155. if (current.actual + current.skiped > 0.9999999) { // 已完成
  156. incrementProgress = 1;
  157. } else if (current.actual >= observedProgress.actual) { // 进度增加
  158. incrementProgress += (1 - incrementProgress) *
  159. (current.actual - observedProgress.actual) / (1 - observedProgress.actual - current.skiped);
  160. }
  161. observedProgress = current;
  162. return incrementProgress;
  163. }
  164. }
  165. }
  166. /// <summary>
  167. /// 监听进度变化,并将观测到的进度节流后传给更新方法。
  168. /// </summary>
  169. public class ProgressObserver : RateLimitedWorker {
  170. private readonly ProgressProvider provider;
  171. public ProgressObserver(ProgressProvider provider, Action<double> updateAction, double minimalIntervalMs = 70)
  172. : base(() => Observe(provider, updateAction), minimalIntervalMs, RateLimitMode.ImmediateThenMerge) {
  173. this.provider = provider;
  174. provider.ProgressChanged += StartObserver;
  175. }
  176. public override void Dispose() {
  177. provider.ProgressChanged -= StartObserver;
  178. base.Dispose();
  179. }
  180. private void StartObserver((double actual, double skiped) raw) => Start();
  181. private static void Observe(ProgressProvider provider, Action<double> updateAction) {
  182. double progress = provider.GetIncrement();
  183. // TODO: 添加平缓过渡
  184. updateAction(progress);
  185. }
  186. }