Progress.cs 5.8 KB

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