File indexing completed on 2024-04-28 03:43:42
0001 /* Ekos Scheduler Greedy Algorithm 0002 SPDX-FileCopyrightText: Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "greedyscheduler.h" 0008 0009 #include <ekos_scheduler_debug.h> 0010 0011 #include "Options.h" 0012 #include "scheduler.h" 0013 #include "ekos/ekos.h" 0014 #include "ui_scheduler.h" 0015 #include "schedulerjob.h" 0016 #include "schedulerutils.h" 0017 0018 #define TEST_PRINT if (false) fprintf 0019 0020 // Can make the scheduling a bit faster by sampling every other minute instead of every minute. 0021 constexpr int SCHEDULE_RESOLUTION_MINUTES = 2; 0022 0023 namespace Ekos 0024 { 0025 0026 GreedyScheduler::GreedyScheduler() 0027 { 0028 } 0029 0030 void GreedyScheduler::setParams(bool restartImmediately, bool restartQueue, 0031 bool rescheduleErrors, int abortDelay, 0032 int errorHandlingDelay) 0033 { 0034 setRescheduleAbortsImmediate(restartImmediately); 0035 setRescheduleAbortsQueue(restartQueue); 0036 setRescheduleErrors(rescheduleErrors); 0037 setAbortDelaySeconds(abortDelay); 0038 setErrorDelaySeconds(errorHandlingDelay); 0039 } 0040 0041 // The possible changes made to a job in jobs are: 0042 // Those listed in prepareJobsForEvaluation() 0043 // Those listed in selectNextJob 0044 // job->clearCache() 0045 // job->updateJobCells() 0046 0047 void GreedyScheduler::scheduleJobs(const QList<SchedulerJob *> &jobs, 0048 const QDateTime &now, 0049 const QMap<QString, uint16_t> &capturedFramesCount, 0050 ModuleLogger *logger) 0051 { 0052 for (auto job : jobs) 0053 job->clearCache(); 0054 0055 QDateTime when; 0056 QElapsedTimer timer; 0057 timer.start(); 0058 scheduledJob = nullptr; 0059 schedule.clear(); 0060 0061 prepareJobsForEvaluation(jobs, now, capturedFramesCount, logger); 0062 0063 scheduledJob = selectNextJob(jobs, now, nullptr, SIMULATE, &when, nullptr, nullptr, &capturedFramesCount); 0064 auto schedule = getSchedule(); 0065 if (logger != nullptr) 0066 { 0067 if (!schedule.empty()) 0068 { 0069 // Print in reverse order ?! The log window at the bottom of the screen 0070 // prints "upside down" -- most recent on top -- and I believe that view 0071 // is more important than the log file (where we can invert when debugging). 0072 for (int i = schedule.size() - 1; i >= 0; i--) 0073 logger->appendLogText(GreedyScheduler::jobScheduleString(schedule[i])); 0074 logger->appendLogText(QString("Greedy Scheduler plan for the next 48 hours starting %1 (%2)s:") 0075 .arg(now.toString()).arg(timer.elapsed() / 1000.0)); 0076 } 0077 else logger->appendLogText(QString("Greedy Scheduler: empty plan (%1s)").arg(timer.elapsed() / 1000.0)); 0078 } 0079 if (scheduledJob != nullptr) 0080 { 0081 qCDebug(KSTARS_EKOS_SCHEDULER) 0082 << QString("Greedy Scheduler scheduling next job %1 at %2") 0083 .arg(scheduledJob->getName(), when.toString("hh:mm")); 0084 scheduledJob->setState(SCHEDJOB_SCHEDULED); 0085 scheduledJob->setStartupTime(when); 0086 } 0087 0088 for (auto job : jobs) 0089 job->clearCache(); 0090 } 0091 0092 // The changes made to a job in jobs are: 0093 // Those listed in selectNextJob() 0094 // Not a const method because it sets the schedule class variable. 0095 bool GreedyScheduler::checkJob(const QList<SchedulerJob *> &jobs, 0096 const QDateTime &now, 0097 const SchedulerJob * const currentJob) 0098 { 0099 // Don't interrupt a job that just started. 0100 if (currentJob && currentJob->getStateTime().secsTo(now) < 5) 0101 return true; 0102 0103 QDateTime startTime; 0104 0105 // Simulating in checkJob() is only done to update the schedule which is a GUI convenience. 0106 // Do it only if its quick and not more frequently than once per minute. 0107 SimulationType simType = SIMULATE_EACH_JOB_ONCE; 0108 if (m_SimSeconds > 0.2 || 0109 (m_LastCheckJobSim.isValid() && m_LastCheckJobSim.secsTo(now) < 60)) 0110 simType = DONT_SIMULATE; 0111 0112 const SchedulerJob *next = selectNextJob(jobs, now, currentJob, simType, &startTime); 0113 if (next == currentJob && now.secsTo(startTime) <= 1) 0114 { 0115 if (simType != DONT_SIMULATE) 0116 m_LastCheckJobSim = now; 0117 0118 return true; 0119 } 0120 else 0121 { 0122 // We need to interrupt the current job. There's a higher-priority one to run. 0123 qCDebug(KSTARS_EKOS_SCHEDULER) 0124 << QString("Greedy Scheduler bumping current job %1 for %2 at %3") 0125 .arg(currentJob->getName(), next ? next->getName() : "---", now.toString("hh:mm")); 0126 return false; 0127 } 0128 } 0129 0130 // The changes made to a job in jobs are: 0131 // job->setState(JOB_COMPLETE|JOB_EVALUATION|JOB_INVALID|JOB_COMPLETEno_change) 0132 // job->setEstimatedTime(0|-1|-2|time) 0133 // job->setInitialFilter(filter) 0134 // job->setLightFramesRequired(bool) 0135 // job->setInSequenceFocus(bool); 0136 // job->setCompletedIterations(completedIterations); 0137 // job->setCapturedFramesMap(capture_map); 0138 // job->setSequenceCount(count); 0139 // job->setEstimatedTimePerRepeat(time); 0140 // job->setEstimatedTimeLeftThisRepeat(time); 0141 // job->setEstimatedStartupTime(time); 0142 // job->setCompletedCount(count); 0143 0144 void GreedyScheduler::prepareJobsForEvaluation( 0145 const QList<SchedulerJob *> &jobs, const QDateTime &now, 0146 const QMap<QString, uint16_t> &capturedFramesCount, ModuleLogger *logger, bool reestimateJobTimes) const 0147 { 0148 // Remove some finished jobs from eval. 0149 foreach (SchedulerJob *job, jobs) 0150 { 0151 switch (job->getCompletionCondition()) 0152 { 0153 case FINISH_AT: 0154 /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */ 0155 if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) 0156 { 0157 job->setState(SCHEDJOB_COMPLETE); 0158 continue; 0159 } 0160 break; 0161 0162 case FINISH_REPEAT: 0163 // In case of a repeating jobs, let's make sure we have more runs left to go 0164 // If we don't, re-estimate imaging time for the scheduler job before concluding 0165 if (job->getRepeatsRemaining() == 0) 0166 { 0167 if (logger != nullptr) logger->appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); 0168 job->setState(SCHEDJOB_COMPLETE); 0169 job->setEstimatedTime(0); 0170 continue; 0171 } 0172 break; 0173 0174 default: 0175 break; 0176 } 0177 } 0178 0179 // Change the state to eval or ERROR/ABORTED for all jobs that will be evaluated. 0180 foreach (SchedulerJob *job, jobs) 0181 { 0182 switch (job->getState()) 0183 { 0184 case SCHEDJOB_INVALID: 0185 case SCHEDJOB_COMPLETE: 0186 // If job is invalid or complete, bypass evaluation. 0187 break; 0188 0189 case SCHEDJOB_ERROR: 0190 case SCHEDJOB_ABORTED: 0191 // These will be evaluated, but we'll have a delay to start. 0192 break; 0193 case SCHEDJOB_IDLE: 0194 case SCHEDJOB_BUSY: 0195 case SCHEDJOB_SCHEDULED: 0196 case SCHEDJOB_EVALUATION: 0197 default: 0198 job->setState(SCHEDJOB_EVALUATION); 0199 break; 0200 } 0201 } 0202 0203 // Estimate the job times 0204 foreach (SchedulerJob *job, jobs) 0205 { 0206 if (job->getState() == SCHEDJOB_INVALID || job->getState() == SCHEDJOB_COMPLETE) 0207 continue; 0208 0209 // -1 = Job is not estimated yet 0210 // -2 = Job is estimated but time is unknown 0211 // > 0 Job is estimated and time is known 0212 if (reestimateJobTimes) 0213 { 0214 job->setEstimatedTime(-1); 0215 if (SchedulerUtils::estimateJobTime(job, capturedFramesCount, logger) == false) 0216 { 0217 job->setState(SCHEDJOB_INVALID); 0218 continue; 0219 } 0220 } 0221 if (job->getEstimatedTime() == 0) 0222 { 0223 job->setRepeatsRemaining(0); 0224 job->setState(SCHEDJOB_COMPLETE); 0225 continue; 0226 } 0227 } 0228 } 0229 0230 namespace 0231 { 0232 // Don't Allow INVALID or COMPLETE jobs to be scheduled. 0233 // Allow ABORTED if one of the rescheduleAbort... options are true. 0234 // Allow ERROR if rescheduleErrors is true. 0235 bool allowJob(const SchedulerJob *job, bool rescheduleAbortsImmediate, bool rescheduleAbortsQueue, bool rescheduleErrors) 0236 { 0237 if (job->getState() == SCHEDJOB_INVALID || job->getState() == SCHEDJOB_COMPLETE) 0238 return false; 0239 if (job->getState() == SCHEDJOB_ABORTED && !rescheduleAbortsImmediate && !rescheduleAbortsQueue) 0240 return false; 0241 if (job->getState() == SCHEDJOB_ERROR && !rescheduleErrors) 0242 return false; 0243 return true; 0244 } 0245 0246 // Returns the first possible time a job may be scheduled. That is, it doesn't 0247 // evaluate the job, but rather just computes the needed delay (for ABORT and ERROR jobs) 0248 // or returns now for other jobs. 0249 QDateTime firstPossibleStart(const SchedulerJob *job, const QDateTime &now, 0250 bool rescheduleAbortsQueue, int abortDelaySeconds, 0251 bool rescheduleErrors, int errorDelaySeconds) 0252 { 0253 QDateTime possibleStart = now; 0254 const QDateTime &abortTime = job->getLastAbortTime(); 0255 const QDateTime &errorTime = job->getLastErrorTime(); 0256 0257 if (abortTime.isValid() && rescheduleAbortsQueue) 0258 { 0259 auto abortStartTime = abortTime.addSecs(abortDelaySeconds); 0260 if (abortStartTime > now) 0261 possibleStart = abortStartTime; 0262 } 0263 0264 0265 if (errorTime.isValid() && rescheduleErrors) 0266 { 0267 auto errorStartTime = errorTime.addSecs(errorDelaySeconds); 0268 if (errorStartTime > now) 0269 possibleStart = errorStartTime; 0270 } 0271 0272 if (!possibleStart.isValid() || possibleStart < now) 0273 possibleStart = now; 0274 return possibleStart; 0275 } 0276 } // namespace 0277 0278 // Consider all jobs marked as JOB_EVALUATION/ABORT/ERROR. Assume ordered by highest priority first. 0279 // - Find the job with the earliest start time (given constraints like altitude, twilight, ...) 0280 // that can run for at least 10 minutes before a higher priority job. 0281 // - START_AT jobs are given the highest priority, whereever on the list they may be, 0282 // as long as they can start near their designated start times. 0283 // - Compute a schedule for the next 2 days, if fullSchedule is true, otherwise 0284 // just look for the next job. 0285 // - If currentJob is not nullptr, this method is really evaluating whether 0286 // that job can continue to be run, or if can't meet constraints, or if it 0287 // should be preempted for another job. 0288 // 0289 // This does not modify any of the jobs in jobs if there is no simType is DONT_SIMULATE. 0290 // If we are simulating, then jobs may change in the following ways: 0291 // job->setGreedyCompletionTime() 0292 // job->setState(state); 0293 // job->setStartupTime(time); 0294 // job->setStopReason(reason); 0295 // The only reason this isn't a const method is because it sets the schedule class variable. 0296 SchedulerJob *GreedyScheduler::selectNextJob(const QList<SchedulerJob *> &jobs, const QDateTime &now, 0297 const SchedulerJob * const currentJob, SimulationType simType, QDateTime *when, 0298 QDateTime *nextInterruption, QString *interruptReason, 0299 const QMap<QString, uint16_t> *capturedFramesCount) 0300 { 0301 // Don't schedule a job that will be preempted in less than MIN_RUN_SECS. 0302 constexpr int MIN_RUN_SECS = 10 * 60; 0303 0304 // Don't preempt a job for another job that is more than MAX_INTERRUPT_SECS in the future. 0305 constexpr int MAX_INTERRUPT_SECS = 30; 0306 0307 // Don't interrupt START_AT jobs unless they can no longer run, or they're interrupted by another START_AT. 0308 bool currentJobIsStartAt = (currentJob && currentJob->getFileStartupCondition() == START_AT && 0309 currentJob->getFileStartupTime().isValid()); 0310 QDateTime nextStart; 0311 SchedulerJob * nextJob = nullptr; 0312 QString interruptStr; 0313 0314 for (int i = 0; i < jobs.size(); ++i) 0315 { 0316 SchedulerJob * const job = jobs[i]; 0317 const bool evaluatingCurrentJob = (currentJob && (job == currentJob)); 0318 0319 if (!allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors)) 0320 continue; 0321 0322 // If the job state is abort or error, might have to delay the first possible start time. 0323 QDateTime startSearchingtAt = firstPossibleStart( 0324 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds); 0325 0326 // Find the first time this job can meet all its constraints. 0327 // I found that passing in an "until" 4th argument actually hurt performance, as it reduces 0328 // the effectiveness of the cache that getNextPossibleStartTime uses. 0329 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, 0330 evaluatingCurrentJob); 0331 if (startTime.isValid()) 0332 { 0333 if (nextJob == nullptr) 0334 { 0335 // We have no other solutions--this is our best solution so far. 0336 nextStart = startTime; 0337 nextJob = job; 0338 if (nextInterruption) *nextInterruption = QDateTime(); 0339 interruptStr = ""; 0340 } 0341 else if (Options::greedyScheduling()) 0342 { 0343 // Allow this job to be scheduled if it can run this many seconds 0344 // before running into a higher priority job. 0345 const int runSecs = evaluatingCurrentJob ? MAX_INTERRUPT_SECS : MIN_RUN_SECS; 0346 0347 // Don't interrupt a START_AT for higher priority job 0348 if (evaluatingCurrentJob && currentJobIsStartAt) 0349 { 0350 if (nextInterruption) *nextInterruption = QDateTime(); 0351 nextStart = startTime; 0352 nextJob = job; 0353 interruptStr = ""; 0354 } 0355 else if (startTime.secsTo(nextStart) > runSecs) 0356 { 0357 // We can start a lower priority job if it can run for at least runSecs 0358 // before getting bumped by the previous higher priority job. 0359 if (nextInterruption) *nextInterruption = nextStart; 0360 interruptStr = QString("interrupted by %1").arg(nextJob->getName()); 0361 nextStart = startTime; 0362 nextJob = job; 0363 } 0364 } 0365 // If scheduling, and we have a solution close enough to now, none of the lower priority 0366 // jobs can possibly be scheduled. 0367 if (!currentJob && nextStart.isValid() && now.secsTo(nextStart) < MIN_RUN_SECS) 0368 break; 0369 } 0370 else if (evaluatingCurrentJob) 0371 { 0372 // No need to keep searching past the current job if we're evaluating it 0373 // and it had no startTime. It needs to be stopped. 0374 *when = QDateTime(); 0375 return nullptr; 0376 } 0377 0378 if (evaluatingCurrentJob) break; 0379 } 0380 if (nextJob != nullptr) 0381 { 0382 // The exception to the simple scheduling rules above are START_AT jobs, which 0383 // are given highest priority, irrespective of order. If nextJob starts less than 0384 // MIN_RUN_SECS before an on-time START_AT job, then give the START_AT job priority. 0385 // However, in order for the START_AT job to interrupt a current job, it must start now. 0386 for (int i = 0; i < jobs.size(); ++i) 0387 { 0388 SchedulerJob * const atJob = jobs[i]; 0389 if (atJob == nextJob) 0390 continue; 0391 const QDateTime atTime = atJob->getFileStartupTime(); 0392 if (atJob->getFileStartupCondition() == START_AT && atTime.isValid()) 0393 { 0394 if (!allowJob(atJob, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors)) 0395 continue; 0396 // If the job state is abort or error, might have to delay the first possible start time. 0397 QDateTime startSearchingtAt = firstPossibleStart( 0398 atJob, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, 0399 errorDelaySeconds); 0400 // atTime above is the user-specified start time. atJobStartTime is the time it can 0401 // actually start, given all the constraints (altitude, twilight, etc). 0402 const QDateTime atJobStartTime = atJob->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, currentJob 0403 && (atJob == currentJob)); 0404 if (atJobStartTime.isValid()) 0405 { 0406 // This difference between the user-specified start time, and the time it can really start. 0407 const double startDelta = atJobStartTime.secsTo(atTime); 0408 if (fabs(startDelta) < 20 * 60) 0409 { 0410 // If we're looking for a new job to start, then give the START_AT priority 0411 // if it's within 10 minutes of its user-specified time. 0412 // However, if we're evaluating the current job (called from checkJob() above) 0413 // then only interrupt it if the START_AT job can start very soon. 0414 const int gap = currentJob == nullptr ? MIN_RUN_SECS : 30; 0415 if (nextStart.secsTo(atJobStartTime) <= gap) 0416 { 0417 nextJob = atJob; 0418 nextStart = atJobStartTime; 0419 if (nextInterruption) *nextInterruption = QDateTime(); // Not interrupting atJob 0420 } 0421 else if (nextInterruption) 0422 { 0423 // The START_AT job was not chosen to start now, but it's still possible 0424 // that this atJob will be an interrupter. 0425 if (!nextInterruption->isValid() || 0426 atJobStartTime.secsTo(*nextInterruption) < 0) 0427 { 0428 *nextInterruption = atJobStartTime; 0429 interruptStr = QString("interrupted by %1").arg(atJob->getName()); 0430 } 0431 } 0432 } 0433 } 0434 } 0435 } 0436 0437 // If the selected next job is part of a group, then we may schedule other members of the group if 0438 // - the selected job is a repeating job and 0439 // - another group member is runnable now and 0440 // - that group mnember is behind the selected job's iteration. 0441 if (nextJob && !nextJob->getGroup().isEmpty() && Options::greedyScheduling() && nextJob->getCompletedIterations() > 0) 0442 { 0443 // Iterate through the jobs list, first finding the selected job, the looking at all jobs after that. 0444 bool foundSelectedJob = false; 0445 for (int i = 0; i < jobs.size(); ++i) 0446 { 0447 SchedulerJob * const job = jobs[i]; 0448 if (job == nextJob) 0449 { 0450 foundSelectedJob = true; 0451 continue; 0452 } 0453 0454 // Only jobs with lower priority than nextJob--higher priority jobs already have been considered and rejected. 0455 // Only consider jobs in the same group as nextJob 0456 // Only consider jobs with fewer iterations than nextJob. 0457 // Only consider jobs that are allowed. 0458 if (!foundSelectedJob || 0459 (job->getGroup() != nextJob->getGroup()) || 0460 (job->getCompletedIterations() >= nextJob->getCompletedIterations()) || 0461 !allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors)) 0462 continue; 0463 0464 const bool evaluatingCurrentJob = (currentJob && (job == currentJob)); 0465 0466 // If the job state is abort or error, might have to delay the first possible start time. 0467 QDateTime startSearchingtAt = firstPossibleStart( 0468 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds); 0469 0470 // Find the first time this job can meet all its constraints. 0471 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, 0472 evaluatingCurrentJob); 0473 0474 // Only consider jobs that can start soon. 0475 if (!startTime.isValid() || startTime.secsTo(nextStart) > MAX_INTERRUPT_SECS) 0476 continue; 0477 0478 // Don't interrupt a START_AT for higher priority job 0479 if (evaluatingCurrentJob && currentJobIsStartAt) 0480 { 0481 if (nextInterruption) *nextInterruption = QDateTime(); 0482 nextStart = startTime; 0483 nextJob = job; 0484 interruptStr = ""; 0485 } 0486 else if (startTime.secsTo(nextStart) >= -MAX_INTERRUPT_SECS) 0487 { 0488 // Use this group member, keeping the old interruption variables. 0489 nextStart = startTime; 0490 nextJob = job; 0491 } 0492 } 0493 } 0494 } 0495 if (when != nullptr) *when = nextStart; 0496 if (interruptReason != nullptr) *interruptReason = interruptStr; 0497 0498 // Needed so display says "Idle" for unscheduled jobs. 0499 // This will also happen in simulate, but that isn't called if nextJob is null. 0500 // Must test for !nextJob. setState() inside unsetEvaluation has a nasty side effect 0501 // of clearing the estimated time. 0502 if (!nextJob) 0503 unsetEvaluation(jobs); 0504 0505 QElapsedTimer simTimer; 0506 simTimer.start(); 0507 constexpr int twoDays = 48 * 3600; 0508 if (simType != DONT_SIMULATE && nextJob != nullptr) 0509 { 0510 QDateTime simulationLimit = now.addSecs(twoDays); 0511 schedule.clear(); 0512 QDateTime simEnd = simulate(jobs, now, simulationLimit, capturedFramesCount, simType); 0513 0514 // This covers the scheduler's "repeat after completion" option, 0515 // which only applies if rememberJobProgress is false. 0516 if (!Options::rememberJobProgress() && Options::schedulerRepeatSequences()) 0517 { 0518 int repeats = 0, maxRepeats = 5; 0519 while (simEnd.isValid() && simEnd.secsTo(simulationLimit) > 0 && ++repeats < maxRepeats) 0520 { 0521 simEnd = simEnd.addSecs(60); 0522 simEnd = simulate(jobs, simEnd, simulationLimit, nullptr, simType); 0523 } 0524 } 0525 } 0526 m_SimSeconds = simTimer.elapsed() / 1000.0; 0527 0528 return nextJob; 0529 } 0530 0531 // The only reason this isn't a const method is because it sets the schedule class variable 0532 QDateTime GreedyScheduler::simulate(const QList<SchedulerJob *> &jobs, const QDateTime &time, const QDateTime &endTime, 0533 const QMap<QString, uint16_t> *capturedFramesCount, SimulationType simType) 0534 { 0535 TEST_PRINT(stderr, "%d simulate()\n", __LINE__); 0536 // Make a deep copy of jobs 0537 QList<SchedulerJob *> copiedJobs; 0538 QList<SchedulerJob *> scheduledJobs; 0539 QDateTime simEndTime; 0540 0541 foreach (SchedulerJob *job, jobs) 0542 { 0543 SchedulerJob *newJob = new SchedulerJob(); 0544 // Make sure the copied class pointers aren't affected! 0545 *newJob = *job; 0546 copiedJobs.append(newJob); 0547 job->setGreedyCompletionTime(QDateTime()); 0548 } 0549 0550 // The number of jobs we have that can be scheduled, 0551 // and the number of them where a simulated start has been scheduled. 0552 int numStartupCandidates = 0, numStartups = 0; 0553 // Reset the start times. 0554 foreach (SchedulerJob *job, copiedJobs) 0555 { 0556 job->setStartupTime(QDateTime()); 0557 const auto state = job->getState(); 0558 if (state == SCHEDJOB_SCHEDULED || state == SCHEDJOB_EVALUATION || 0559 state == SCHEDJOB_BUSY || state == SCHEDJOB_IDLE) 0560 numStartupCandidates++; 0561 } 0562 0563 QMap<QString, uint16_t> capturedFramesCopy; 0564 if (capturedFramesCount != nullptr) 0565 capturedFramesCopy = *capturedFramesCount; 0566 QList<SchedulerJob *>simJobs = copiedJobs; 0567 prepareJobsForEvaluation(copiedJobs, time, capturedFramesCopy, nullptr, false); 0568 0569 QDateTime simTime = time; 0570 int iterations = 0; 0571 bool exceededIterations = false; 0572 QHash<SchedulerJob*, int> workDone; 0573 QHash<SchedulerJob*, int> originalIteration, originalSecsLeftIteration; 0574 0575 for(int i = 0; i < simJobs.size(); ++i) 0576 workDone[simJobs[i]] = 0.0; 0577 0578 while (true) 0579 { 0580 QDateTime jobStartTime; 0581 QDateTime jobInterruptTime; 0582 QString interruptReason; 0583 // Find the next job to be scheduled, when it starts, and when a higher priority 0584 // job might preempt it, why it would be preempted. 0585 // Note: 4th arg, fullSchedule, must be false or we'd loop forever. 0586 SchedulerJob *selectedJob = selectNextJob( 0587 simJobs, simTime, nullptr, DONT_SIMULATE, &jobStartTime, &jobInterruptTime, &interruptReason); 0588 if (selectedJob == nullptr) 0589 break; 0590 0591 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString("%1 starting at %2 interrupted at \"%3\" reason \"%4\"") 0592 .arg(selectedJob->getName()).arg(jobStartTime.toString("MM/dd hh:mm")) 0593 .arg(jobInterruptTime.toString("MM/dd hh:mm")).arg(interruptReason).toLatin1().data()); 0594 // Are we past the end time? 0595 if (endTime.isValid() && jobStartTime.secsTo(endTime) < 0) break; 0596 0597 // It's possible there are start_at jobs that can preempt this job. 0598 // Find the next start_at time, and use that as an end constraint to getNextEndTime 0599 // if it's before jobInterruptTime. 0600 QDateTime nextStartAtTime; 0601 foreach (SchedulerJob *job, simJobs) 0602 { 0603 if (job != selectedJob && 0604 job->getStartupCondition() == START_AT && 0605 jobStartTime.secsTo(job->getStartupTime()) > 0 && 0606 (job->getState() == SCHEDJOB_EVALUATION || 0607 job->getState() == SCHEDJOB_SCHEDULED)) 0608 { 0609 QDateTime startAtTime = job->getStartupTime(); 0610 if (!nextStartAtTime.isValid() || nextStartAtTime.secsTo(startAtTime) < 0) 0611 nextStartAtTime = startAtTime; 0612 } 0613 } 0614 // Check to see if the above start-at stop time is before the interrupt stop time. 0615 QDateTime constraintStopTime = jobInterruptTime; 0616 if (nextStartAtTime.isValid() && 0617 (!constraintStopTime.isValid() || 0618 nextStartAtTime.secsTo(constraintStopTime) < 0)) 0619 constraintStopTime = nextStartAtTime; 0620 0621 QString constraintReason; 0622 // Get the time that this next job would fail its constraints, and a human-readable explanation. 0623 QDateTime jobConstraintTime = selectedJob->getNextEndTime(jobStartTime, SCHEDULE_RESOLUTION_MINUTES, &constraintReason, 0624 constraintStopTime); 0625 if (nextStartAtTime.isValid() && jobConstraintTime.isValid() && 0626 std::abs(jobConstraintTime.secsTo(nextStartAtTime)) < 2 * SCHEDULE_RESOLUTION_MINUTES) 0627 constraintReason = "interrupted by start-at job"; 0628 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" constraint \"%1\" reason \"%2\"") 0629 .arg(jobConstraintTime.toString("MM/dd hh:mm")).arg(constraintReason).toLatin1().data()); 0630 QDateTime jobCompletionTime; 0631 if (selectedJob->getEstimatedTime() > 0) 0632 { 0633 // Estimate when the job might complete, if it was allowed to run without interruption. 0634 const int timeLeft = selectedJob->getEstimatedTime() - workDone[selectedJob]; 0635 jobCompletionTime = jobStartTime.addSecs(timeLeft); 0636 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" completion \"%1\" time left %2s") 0637 .arg(jobCompletionTime.toString("MM/dd hh:mm")).arg(timeLeft).toLatin1().data()); 0638 } 0639 // Consider the 3 stopping times computed above (preemption, constraints missed, and completion), 0640 // see which comes soonest, and set the jobStopTime and jobStopReason. 0641 QDateTime jobStopTime = jobInterruptTime; 0642 QString stopReason = jobStopTime.isValid() ? interruptReason : ""; 0643 if (jobConstraintTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobConstraintTime) < 0)) 0644 { 0645 stopReason = constraintReason; 0646 jobStopTime = jobConstraintTime; 0647 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" picked constraint").toLatin1().data()); 0648 } 0649 if (jobCompletionTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobCompletionTime) < 0)) 0650 { 0651 stopReason = "job completion"; 0652 jobStopTime = jobCompletionTime; 0653 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" picked completion").toLatin1().data()); 0654 } 0655 0656 // This if clause handles the simulation of scheduler repeat groups 0657 // which applies to scheduler jobs with repeat-style completion conditions. 0658 if (!selectedJob->getGroup().isEmpty() && 0659 (selectedJob->getCompletionCondition() == FINISH_LOOP || 0660 selectedJob->getCompletionCondition() == FINISH_REPEAT || 0661 selectedJob->getCompletionCondition() == FINISH_AT)) 0662 { 0663 if (originalIteration.find(selectedJob) == originalIteration.end()) 0664 originalIteration[selectedJob] = selectedJob->getCompletedIterations(); 0665 if (originalSecsLeftIteration.find(selectedJob) == originalSecsLeftIteration.end()) 0666 originalSecsLeftIteration[selectedJob] = selectedJob->getEstimatedTimeLeftThisRepeat(); 0667 0668 // Estimate the time it would take to complete the current repeat, if this is a repeated job. 0669 int leftThisRepeat = selectedJob->getEstimatedTimeLeftThisRepeat(); 0670 int secsPerRepeat = selectedJob->getEstimatedTimePerRepeat(); 0671 int secsLeftThisRepeat = (workDone[selectedJob] < leftThisRepeat) ? 0672 leftThisRepeat - workDone[selectedJob] : secsPerRepeat; 0673 0674 if (workDone[selectedJob] == 0) 0675 secsLeftThisRepeat += selectedJob->getEstimatedStartupTime(); 0676 0677 // If it would finish a repeat, run one repeat and see if it would still be scheduled. 0678 if (secsLeftThisRepeat > 0 && 0679 (!jobStopTime.isValid() || secsLeftThisRepeat < jobStartTime.secsTo(jobStopTime))) 0680 { 0681 auto tempStart = jobStartTime; 0682 auto tempInterrupt = jobInterruptTime; 0683 auto tempReason = stopReason; 0684 SchedulerJob keepJob = *selectedJob; 0685 0686 auto t = jobStartTime.addSecs(secsLeftThisRepeat); 0687 int iteration = selectedJob->getCompletedIterations(); 0688 int iters = 0, maxIters = 20; // just in case... 0689 while ((!jobStopTime.isValid() || t.secsTo(jobStopTime) > 0) && iters++ < maxIters) 0690 { 0691 selectedJob->setCompletedIterations(++iteration); 0692 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" iteration=%1").arg(iteration).toLatin1().data()); 0693 SchedulerJob *next = selectNextJob(simJobs, t, nullptr, DONT_SIMULATE, &tempStart, &tempInterrupt, &tempReason); 0694 if (next != selectedJob) 0695 { 0696 stopReason = "interrupted for group member"; 0697 jobStopTime = t; 0698 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" switched to group member %1 at %2") 0699 .arg(next == nullptr ? "null" : next->getName()).arg(t.toString("MM/dd hh:mm")).toLatin1().data()); 0700 0701 break; 0702 } 0703 t = t.addSecs(secsPerRepeat); 0704 } 0705 *selectedJob = keepJob; 0706 } 0707 } 0708 0709 // Increment the work done, for the next time this job might be scheduled in this simulation. 0710 if (jobStopTime.isValid()) 0711 { 0712 const int secondsRun = jobStartTime.secsTo(jobStopTime); 0713 workDone[selectedJob] += secondsRun; 0714 0715 if ((originalIteration.find(selectedJob) != originalIteration.end()) && 0716 (originalSecsLeftIteration.find(selectedJob) != originalSecsLeftIteration.end())) 0717 { 0718 int completedIterations = originalIteration[selectedJob]; 0719 if (workDone[selectedJob] >= originalSecsLeftIteration[selectedJob] && 0720 selectedJob->getEstimatedTimePerRepeat() > 0) 0721 completedIterations += 0722 1 + (workDone[selectedJob] - originalSecsLeftIteration[selectedJob]) / selectedJob->getEstimatedTimePerRepeat(); 0723 TEST_PRINT(stderr, "%d %s\n", __LINE__, 0724 QString(" work sets interations=%1").arg(completedIterations).toLatin1().data()); 0725 selectedJob->setCompletedIterations(completedIterations); 0726 } 0727 } 0728 0729 // Set the job's startupTime, but only for the first time the job will be scheduled. 0730 // This will be used by the scheduler's UI when displaying the job schedules. 0731 if (!selectedJob->getStartupTime().isValid()) 0732 { 0733 numStartups++; 0734 selectedJob->setStartupTime(jobStartTime); 0735 selectedJob->setGreedyCompletionTime(jobStopTime); 0736 selectedJob->setStopReason(stopReason); 0737 selectedJob->setState(SCHEDJOB_SCHEDULED); 0738 scheduledJobs.append(selectedJob); 0739 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" Scheduled: %1 %2 -> %3 %4 work done %5s") 0740 .arg(selectedJob->getName()).arg(selectedJob->getStartupTime().toString("MM/dd hh:mm")) 0741 .arg(selectedJob->getGreedyCompletionTime().toString("MM/dd hh:mm")).arg(selectedJob->getStopReason()) 0742 .arg(workDone[selectedJob]).toLatin1().data()); 0743 } 0744 0745 // Compute if the simulated job should be considered complete because of work done. 0746 if (selectedJob->getEstimatedTime() >= 0 && 0747 workDone[selectedJob] >= selectedJob->getEstimatedTime()) 0748 { 0749 selectedJob->setState(SCHEDJOB_COMPLETE); 0750 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" job %1 is complete") 0751 .arg(selectedJob->getName()).toLatin1().data()); 0752 } 0753 schedule.append(JobSchedule(jobs[copiedJobs.indexOf(selectedJob)], jobStartTime, jobStopTime, stopReason)); 0754 simEndTime = jobStopTime; 0755 simTime = jobStopTime.addSecs(60); 0756 0757 // End the simulation if we've crossed endTime, or no further jobs could be started, 0758 // or if we've simply run too long. 0759 if (!simTime.isValid()) break; 0760 if (endTime.isValid() && simTime.secsTo(endTime) < 0) break; 0761 0762 if (++iterations > std::max(20, numStartupCandidates)) 0763 { 0764 exceededIterations = true; 0765 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString("ending simulation after %1 iterations") 0766 .arg(iterations).toLatin1().data()); 0767 0768 break; 0769 } 0770 if (simType == SIMULATE_EACH_JOB_ONCE) 0771 { 0772 bool allJobsProcessedOnce = true; 0773 for (const auto job : simJobs) 0774 { 0775 if (allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors) && 0776 !job->getStartupTime().isValid()) 0777 { 0778 allJobsProcessedOnce = false; 0779 break; 0780 } 0781 } 0782 if (allJobsProcessedOnce) 0783 { 0784 TEST_PRINT(stderr, "%d ending simulation, all jobs processed once\n", __LINE__); 0785 break; 0786 } 0787 } 0788 } 0789 0790 // This simulation has been run using a deep-copy of the jobs list, so as not to interfere with 0791 // some of their stored data. However, we do wish to update several fields of the "real" scheduleJobs. 0792 // Note that the original jobs list and "copiedJobs" should be in the same order.. 0793 for (int i = 0; i < jobs.size(); ++i) 0794 { 0795 if (scheduledJobs.indexOf(copiedJobs[i]) >= 0) 0796 { 0797 // If this is a simulation where the job is already running, don't change its state or startup time. 0798 if (jobs[i]->getState() != SCHEDJOB_BUSY) 0799 { 0800 jobs[i]->setState(SCHEDJOB_SCHEDULED); 0801 jobs[i]->setStartupTime(copiedJobs[i]->getStartupTime()); 0802 } 0803 // Can't set the standard completionTime as it affects getEstimatedTime() 0804 jobs[i]->setGreedyCompletionTime(copiedJobs[i]->getGreedyCompletionTime()); 0805 jobs[i]->setStopReason(copiedJobs[i]->getStopReason()); 0806 } 0807 } 0808 // This should go after above loop. unsetEvaluation calls setState() which clears 0809 // certain fields from the state for IDLE states. 0810 unsetEvaluation(jobs); 0811 0812 return exceededIterations ? QDateTime() : simEndTime; 0813 } 0814 0815 void GreedyScheduler::unsetEvaluation(const QList<SchedulerJob *> &jobs) const 0816 { 0817 for (int i = 0; i < jobs.size(); ++i) 0818 { 0819 if (jobs[i]->getState() == SCHEDJOB_EVALUATION) 0820 jobs[i]->setState(SCHEDJOB_IDLE); 0821 } 0822 } 0823 0824 QString GreedyScheduler::jobScheduleString(const JobSchedule &jobSchedule) 0825 { 0826 return QString("%1\t%2 --> %3 \t%4") 0827 .arg(jobSchedule.job->getName(), -10) 0828 .arg(jobSchedule.startTime.toString("MM/dd hh:mm"), 0829 jobSchedule.stopTime.toString("hh:mm"), jobSchedule.stopReason); 0830 } 0831 0832 void GreedyScheduler::printSchedule(const QList<JobSchedule> &schedule) 0833 { 0834 foreach (auto &line, schedule) 0835 { 0836 fprintf(stderr, "%s\n", QString("%1 %2 --> %3 (%4)") 0837 .arg(jobScheduleString(line)).toLatin1().data()); 0838 } 0839 } 0840 0841 } // namespace Ekos