All files / engine/Source/Scene JobScheduler.js

100% Statements 66/66
100% Branches 28/28
100% Functions 7/7
100% Lines 65/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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