Progress.cs 8.2 KB

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