JobScheduler.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import defined from "../Core/defined.js";
  2. import DeveloperError from "../Core/DeveloperError.js";
  3. import getTimestamp from "../Core/getTimestamp.js";
  4. import JobType from "./JobType.js";
  5. /**
  6. *
  7. * @private
  8. * @constructor
  9. */
  10. function JobTypeBudget(total) {
  11. /**
  12. * Total budget, in milliseconds, allowed for one frame
  13. */
  14. this._total = total;
  15. /**
  16. * Time, in milliseconds, used so far during this frame
  17. */
  18. this.usedThisFrame = 0.0;
  19. /**
  20. * Time, in milliseconds, that other job types stole this frame
  21. */
  22. this.stolenFromMeThisFrame = 0.0;
  23. /**
  24. * Indicates if this job type was starved this frame, i.e., a job
  25. * tried to run but didn't have budget
  26. */
  27. this.starvedThisFrame = false;
  28. /**
  29. * Indicates if this job was starved last frame. This prevents it
  30. * from being stolen from this frame.
  31. */
  32. this.starvedLastFrame = false;
  33. }
  34. Object.defineProperties(JobTypeBudget.prototype, {
  35. total: {
  36. get: function () {
  37. return this._total;
  38. },
  39. },
  40. });
  41. /**
  42. * Engine for time slicing jobs during a frame to amortize work over multiple frames. This supports:
  43. * <ul>
  44. * <li>
  45. * Separate budgets for different job types, e.g., texture, shader program, and buffer creation. This
  46. * allows all job types to make progress each frame.
  47. * </li>
  48. * <li>
  49. * Stealing from other jobs type budgets if they were not exhausted in the previous frame. This allows
  50. * using the entire budget for all job types each frame even if, for example, all the jobs are the same type.
  51. * </li>
  52. * <li>
  53. * Guaranteed progress on all job types each frame, even if it means exceeding the total budget for the frame.
  54. * This prevents, for example, several expensive texture uploads over many frames from prevent a shader compile.
  55. * </li>
  56. * </ul>
  57. *
  58. * @private
  59. */
  60. function JobScheduler(budgets) {
  61. //>>includeStart('debug', pragmas.debug);
  62. if (defined(budgets) && budgets.length !== JobType.NUMBER_OF_JOB_TYPES) {
  63. throw new DeveloperError(
  64. "A budget must be specified for each job type; budgets.length should equal JobType.NUMBER_OF_JOB_TYPES."
  65. );
  66. }
  67. //>>includeEnd('debug');
  68. // Total for defaults is half of of one frame at 10 fps
  69. const jobBudgets = new Array(JobType.NUMBER_OF_JOB_TYPES);
  70. jobBudgets[JobType.TEXTURE] = new JobTypeBudget(
  71. defined(budgets) ? budgets[JobType.TEXTURE] : 10.0
  72. );
  73. // On cache miss, this most likely only allows one shader compile per frame
  74. jobBudgets[JobType.PROGRAM] = new JobTypeBudget(
  75. defined(budgets) ? budgets[JobType.PROGRAM] : 10.0
  76. );
  77. jobBudgets[JobType.BUFFER] = new JobTypeBudget(
  78. defined(budgets) ? budgets[JobType.BUFFER] : 30.0
  79. );
  80. const length = jobBudgets.length;
  81. let i;
  82. let totalBudget = 0.0;
  83. for (i = 0; i < length; ++i) {
  84. totalBudget += jobBudgets[i].total;
  85. }
  86. const executedThisFrame = new Array(length);
  87. for (i = 0; i < length; ++i) {
  88. executedThisFrame[i] = false;
  89. }
  90. this._totalBudget = totalBudget;
  91. this._totalUsedThisFrame = 0.0;
  92. this._budgets = jobBudgets;
  93. this._executedThisFrame = executedThisFrame;
  94. }
  95. // For unit testing
  96. JobScheduler.getTimestamp = getTimestamp;
  97. Object.defineProperties(JobScheduler.prototype, {
  98. totalBudget: {
  99. get: function () {
  100. return this._totalBudget;
  101. },
  102. },
  103. });
  104. JobScheduler.prototype.disableThisFrame = function () {
  105. // Prevent jobs from running this frame
  106. this._totalUsedThisFrame = this._totalBudget;
  107. };
  108. JobScheduler.prototype.resetBudgets = function () {
  109. const budgets = this._budgets;
  110. const length = budgets.length;
  111. for (let i = 0; i < length; ++i) {
  112. const budget = budgets[i];
  113. budget.starvedLastFrame = budget.starvedThisFrame;
  114. budget.starvedThisFrame = false;
  115. budget.usedThisFrame = 0.0;
  116. budget.stolenFromMeThisFrame = 0.0;
  117. }
  118. this._totalUsedThisFrame = 0.0;
  119. };
  120. JobScheduler.prototype.execute = function (job, jobType) {
  121. const budgets = this._budgets;
  122. const budget = budgets[jobType];
  123. // This ensures each job type makes progress each frame by executing at least once
  124. const progressThisFrame = this._executedThisFrame[jobType];
  125. if (this._totalUsedThisFrame >= this._totalBudget && progressThisFrame) {
  126. // No budget left this frame for jobs of any type
  127. budget.starvedThisFrame = true;
  128. return false;
  129. }
  130. let stolenBudget;
  131. if (budget.usedThisFrame + budget.stolenFromMeThisFrame >= budget.total) {
  132. // No budget remaining for jobs of this type. Try to steal from other job types.
  133. const length = budgets.length;
  134. let i;
  135. for (i = 0; i < length; ++i) {
  136. stolenBudget = budgets[i];
  137. // Steal from this budget if it has time left and it wasn't starved last fame
  138. if (
  139. stolenBudget.usedThisFrame + stolenBudget.stolenFromMeThisFrame <
  140. stolenBudget.total &&
  141. !stolenBudget.starvedLastFrame
  142. ) {
  143. break;
  144. }
  145. }
  146. if (i === length && progressThisFrame) {
  147. // No other job types can give up their budget this frame, and
  148. // this job type already progressed this frame
  149. return false;
  150. }
  151. if (progressThisFrame) {
  152. // It is considered "starved" even if it executes using stolen time so that
  153. // next frame, no other job types can steal time from it.
  154. budget.starvedThisFrame = true;
  155. }
  156. }
  157. const startTime = JobScheduler.getTimestamp();
  158. job.execute();
  159. const duration = JobScheduler.getTimestamp() - startTime;
  160. // Track both time remaining for this job type and all jobs
  161. // so budget stealing does send us way over the total budget.
  162. this._totalUsedThisFrame += duration;
  163. if (stolenBudget) {
  164. stolenBudget.stolenFromMeThisFrame += duration;
  165. } else {
  166. budget.usedThisFrame += duration;
  167. }
  168. this._executedThisFrame[jobType] = true;
  169. return true;
  170. };
  171. export default JobScheduler;