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