private void OnCancelInternal(uint id, bool timedOut, Exception exception = null) { var onCancel = RegisteredJobs[id].JobCancellationHandler; RegisteredJobs.Remove(id); if (onCancel != null) { onCancel(id, timedOut, exception); // Call the caller's 'cleanup' code if it was provided } }
// This is the WRAPPER. This is what is actually executing as a Unity coroutine. // It is structured to use a given timeslice share (expressed as number of stopwatch ticks) private IEnumerator RunJob(uint id, IEnumerator job, bool startImmediately) { bool jobDone = false; var jobName = RegisteredJobs[id].Name; var timeoutStopwatch = new Stopwatch(); timeoutStopwatch.Start(); var wasWaitingForDependentJobs = RegisteredJobs[id].DependentJobs.Count > 0; if (wasWaitingForDependentJobs) { RegisteredJobs[id].State = JobState.Waiting; } while (RegisteredJobs[id].DependentJobs.Count > 0) { RegisteredJobs[id].DependentJobs.RemoveAll(item => !RegisteredJobs.ContainsKey(item)); if (RegisteredJobs[id].DependentJobs.Count > 0) { yield return(null); // Yield, as we are still waiting for at least one dependent job to complete if (timeoutStopwatch.ElapsedTicks >= RegisteredJobs[id].TimeoutTimeInTicks) { Debug.Log("JobManager: \"" + jobName + "\" has timed out (before it even started) and is therefore being cancelled"); OnCancelInternal(id, true); yield break; // And we're completely done } } } // Note: We put this here because if the job has been waiting for dependent jobs, and the last dependent job has finished, and in the same frame, // we're now starting this job, the budgeting can effectively give more time than it should to this job execution, because the basic per-job // budget is calculated once per frame and is based on the number of active jobs. if (!startImmediately || wasWaitingForDependentJobs) { yield return(null); } if (RegisteredJobs[id].State != JobState.Paused) { RegisteredJobs[id].State = JobState.Running; } //Debug.Log("JobManager: Started \"" + jobName + "\"" + (RegisteredJobs[id].State == JobState.Paused ? " but in paused state" : "")); var sw = new Stopwatch(); var timedOut = false; var exceptionOccurred = false; Exception exception = null; var savedCurrentlyRunningJobId = CurrentlyRunningJobId; CurrentlyRunningJobId = id; do { if (RegisteredJobs[id].FinishJobNow) { Debug.Log("JobManager: \"" + jobName + "\" request to complete entire job now..."); try { while (job.MoveNext()) { ; } } catch (Exception e) { DumpJobExceptionMessage(id, e); exceptionOccurred = true; exception = e; } break; // Break out of the outer do loop } var budgetedTimeInTicks = JobBudget - LastJobOverTicks; // Note that at this point, budgetedTimeInTicks can be negative. // In that case the job will execute exactly one iteration below, EXCEPT if CanSkipFrames is set to true, in which case we don't execute it at all //Debug.Log("budgetedTimeInTicks:" + budgetedTimeInTicks + " for job " + jobName); if (budgetedTimeInTicks < 0 && RegisteredJobs[id].CanSkipFrames) { //Debug.Log("JobManager: \"" + RegisteredJobs[id].Name + "\" is being skipped this frame"); LastJobOverTicks = 0 - budgetedTimeInTicks; CurrentlyRunningJobId = savedCurrentlyRunningJobId; yield return(null); savedCurrentlyRunningJobId = CurrentlyRunningJobId; CurrentlyRunningJobId = id; } else { long elapsedTicks = 0; if (RegisteredJobs[id].State != JobState.Paused) { sw.Start(); var executionCount = 0; do { try { if (!job.MoveNext()) // Call the actual code to be executed; returns false if entire job is done { jobDone = true; elapsedTicks = sw.ElapsedTicks; break; } } catch (Exception e) { DumpJobExceptionMessage(id, e); exceptionOccurred = true; exception = e; jobDone = true; elapsedTicks = sw.ElapsedTicks; break; } elapsedTicks = sw.ElapsedTicks; if (++executionCount >= RegisteredJobs[id].MaxExecutionsPerFrame) { break; } if (RegisteredJobs[id].State == JobState.Paused) { break; } } while (elapsedTicks < budgetedTimeInTicks); sw.Reset(); } LastJobOverTicks = elapsedTicks - budgetedTimeInTicks; // Note that this can be negative TimeSpentInJobsTicks += elapsedTicks; //if (elapsedTicks < budgetedTimeInTicks) // Debug.Log( "JobManager: Job slice finished earlier than budget!" ); } if (!jobDone) { if (timeoutStopwatch.ElapsedTicks < RegisteredJobs[id].TimeoutTimeInTicks) { CurrentlyRunningJobId = savedCurrentlyRunningJobId; yield return(null); savedCurrentlyRunningJobId = CurrentlyRunningJobId; CurrentlyRunningJobId = id; } else { Debug.Log("JobManager: \"" + jobName + "\" has timed out and is therefore being cancelled"); OnCancelInternal(id, true); jobDone = true; timedOut = true; } } } while (!jobDone); if (exceptionOccurred) { var spawningJobId = RegisteredJobs[id].JobIdToUnPauseWhenDone; // Cancel this job, and all jobs spawned by this job CancelJob(id, true, exception); // Cancel all job(s) that spawned this job while (spawningJobId != UInt32.MaxValue) { var nextSpawningJobId = RegisteredJobs[spawningJobId].JobIdToUnPauseWhenDone; CancelJob(spawningJobId); if (savedCurrentlyRunningJobId == spawningJobId) { savedCurrentlyRunningJobId = UInt32.MaxValue; } spawningJobId = nextSpawningJobId; } } else if (!timedOut) { var onCompletion = RegisteredJobs[id].JobCompletionHandler; var jobIdToUnPauseWhenDone = RegisteredJobs[id].JobIdToUnPauseWhenDone; RegisteredJobs.Remove(id); //Debug.Log("JobManager: \"" + jobName + "\" completed"); if (jobIdToUnPauseWhenDone != UInt32.MaxValue) { if (!UnPauseJob(jobIdToUnPauseWhenDone)) { Debug.Log("JobManager: Warning: \"" + jobName + "\" tried to unpause job id " + jobIdToUnPauseWhenDone + " but wasn't able to"); } } if (onCompletion != null) { onCompletion(id); // Call the caller's 'on completion' code if it was provided } } CurrentlyRunningJobId = savedCurrentlyRunningJobId; }