7#include "greedyscheduler.h"
9#include <ekos_scheduler_debug.h>
14#include "ui_scheduler.h"
15#include "schedulerjob.h"
16#include "schedulerutils.h"
18#define TEST_PRINT if (false) fprintf
21constexpr int SCHEDULE_RESOLUTION_MINUTES = 2;
26GreedyScheduler::GreedyScheduler()
30void GreedyScheduler::setParams(
bool restartImmediately,
bool restartQueue,
31 bool rescheduleErrors,
int abortDelay,
32 int errorHandlingDelay)
34 setRescheduleAbortsImmediate(restartImmediately);
35 setRescheduleAbortsQueue(restartQueue);
36 setRescheduleErrors(rescheduleErrors);
37 setAbortDelaySeconds(abortDelay);
38 setErrorDelaySeconds(errorHandlingDelay);
58 scheduledJob =
nullptr;
61 prepareJobsForEvaluation(jobs, now, capturedFramesCount, logger);
66 scheduledJob = selectNextJob(leadJobs, now,
nullptr, SIMULATE, &when,
nullptr,
nullptr, &capturedFramesCount);
67 auto schedule = getSchedule();
68 if (logger !=
nullptr)
70 if (!schedule.
empty())
75 for (
int i = schedule.
size() - 1; i >= 0; i--)
76 logger->appendLogText(GreedyScheduler::jobScheduleString(schedule[i]));
77 logger->appendLogText(
QString(
"Greedy Scheduler plan for the next 48 hours starting %1 (%2)s:")
78 .arg(now.
toString()).
arg(timer.elapsed() / 1000.0));
80 else logger->appendLogText(
QString(
"Greedy Scheduler: empty plan (%1s)").arg(timer.elapsed() / 1000.0));
82 if (scheduledJob !=
nullptr)
84 qCDebug(KSTARS_EKOS_SCHEDULER)
85 <<
QString(
"Greedy Scheduler scheduling next job %1 at %2")
86 .
arg(scheduledJob->getName(), when.
toString(
"hh:mm"));
88 scheduledJob->setStartupTime(when);
100 const SchedulerJob *
const currentJob)
103 if (currentJob && currentJob->getStateTime().secsTo(now) < 5)
110 SimulationType simType = SIMULATE_EACH_JOB_ONCE;
111 if (m_SimSeconds > 0.2 ||
112 (m_LastCheckJobSim.
isValid() && m_LastCheckJobSim.
secsTo(now) < 60))
113 simType = DONT_SIMULATE;
115 const SchedulerJob *
next = selectNextJob(jobs, now, currentJob, simType, &startTime);
116 if (next == currentJob && now.
secsTo(startTime) <= 1)
118 if (simType != DONT_SIMULATE)
119 m_LastCheckJobSim = now;
126 qCDebug(KSTARS_EKOS_SCHEDULER)
127 <<
QString(
"Greedy Scheduler bumping current job %1 for %2 at %3")
128 .
arg(currentJob->getName(), next ?
next->getName() :
"---", now.
toString(
"hh:mm"));
147void GreedyScheduler::prepareJobsForEvaluation(
152 foreach (SchedulerJob *job, jobs)
154 switch (job->getCompletionCondition())
158 if (job->getFinishAtTime().isValid() && job->getFinishAtTime() < now)
168 if (job->getRepeatsRemaining() == 0)
170 if (logger !=
nullptr)
logger->appendLogText(
i18n(
"Job '%1' has no more batches remaining.", job->getName()));
172 job->setEstimatedTime(0);
183 foreach (SchedulerJob *job, jobs)
185 switch (job->getState())
207 foreach (SchedulerJob *job, jobs)
215 if (reestimateJobTimes)
217 job->setEstimatedTime(-1);
218 if (SchedulerUtils::estimateJobTime(job, capturedFramesCount, logger) ==
false)
224 if (job->getEstimatedTime() == 0)
226 job->setRepeatsRemaining(0);
239bool allowJob(
const SchedulerJob *job,
bool rescheduleAbortsImmediate,
bool rescheduleAbortsQueue,
bool rescheduleErrors)
243 if (job->getState() ==
SCHEDJOB_ABORTED && !rescheduleAbortsImmediate && !rescheduleAbortsQueue)
254 bool rescheduleAbortsQueue,
int abortDelaySeconds,
255 bool rescheduleErrors,
int errorDelaySeconds)
258 const QDateTime &abortTime = job->getLastAbortTime();
259 const QDateTime &errorTime = job->getLastErrorTime();
261 if (abortTime.
isValid() && rescheduleAbortsQueue)
263 auto abortStartTime = abortTime.
addSecs(abortDelaySeconds);
264 if (abortStartTime > now)
265 possibleStart = abortStartTime;
269 if (errorTime.
isValid() && rescheduleErrors)
271 auto errorStartTime = errorTime.
addSecs(errorDelaySeconds);
272 if (errorStartTime > now)
273 possibleStart = errorStartTime;
276 if (!possibleStart.
isValid() || possibleStart < now)
278 return possibleStart;
301 const SchedulerJob *
const currentJob, SimulationType simType,
QDateTime *when,
306 constexpr int MIN_RUN_SECS = 10 * 60;
309 constexpr int MAX_INTERRUPT_SECS = 30;
312 bool currentJobIsStartAt = (currentJob && currentJob->getFileStartupCondition() == START_AT &&
313 currentJob->getStartAtTime().isValid());
315 SchedulerJob * nextJob =
nullptr;
318 for (
int i = 0; i < jobs.
size(); ++i)
320 SchedulerJob *
const job = jobs[i];
321 const bool evaluatingCurrentJob = (currentJob && (job == currentJob));
323 if (!allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
327 QDateTime startSearchingtAt = firstPossibleStart(
328 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds);
333 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES,
334 evaluatingCurrentJob);
337 if (nextJob ==
nullptr)
340 nextStart = startTime;
342 if (nextInterruption) *nextInterruption =
QDateTime();
345 else if (Options::greedyScheduling())
349 const int runSecs = evaluatingCurrentJob ? MAX_INTERRUPT_SECS : MIN_RUN_SECS;
352 if (evaluatingCurrentJob && currentJobIsStartAt)
354 if (nextInterruption) *nextInterruption =
QDateTime();
355 nextStart = startTime;
359 else if (startTime.
secsTo(nextStart) > runSecs)
363 if (nextInterruption) *nextInterruption = nextStart;
364 interruptStr =
QString(
"interrupted by %1").
arg(nextJob->getName());
365 nextStart = startTime;
371 if (!currentJob && nextStart.
isValid() && now.
secsTo(nextStart) < MIN_RUN_SECS)
374 else if (evaluatingCurrentJob)
382 if (evaluatingCurrentJob)
break;
384 if (nextJob !=
nullptr)
390 for (
int i = 0; i < jobs.
size(); ++i)
392 SchedulerJob *
const atJob = jobs[i];
393 if (atJob == nextJob)
395 const QDateTime atTime = atJob->getStartAtTime();
396 if (atJob->getFileStartupCondition() == START_AT && atTime.
isValid())
398 if (!allowJob(atJob, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
401 QDateTime startSearchingtAt = firstPossibleStart(
402 atJob, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors,
406 const QDateTime atJobStartTime = atJob->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, currentJob
407 && (atJob == currentJob));
411 const double startDelta = atJobStartTime.
secsTo(atTime);
412 if (fabs(startDelta) < 20 * 60)
418 const int gap = currentJob ==
nullptr ? MIN_RUN_SECS : 30;
419 if (nextStart.
secsTo(atJobStartTime) <= gap)
422 nextStart = atJobStartTime;
423 if (nextInterruption) *nextInterruption =
QDateTime();
425 else if (nextInterruption)
429 if (!nextInterruption->
isValid() ||
430 atJobStartTime.
secsTo(*nextInterruption) < 0)
432 *nextInterruption = atJobStartTime;
433 interruptStr =
QString(
"interrupted by %1").
arg(atJob->getName());
445 if (nextJob && !nextJob->getGroup().isEmpty() && Options::greedyScheduling() && nextJob->getCompletedIterations() > 0)
448 bool foundSelectedJob =
false;
449 for (
int i = 0; i < jobs.
size(); ++i)
451 SchedulerJob *
const job = jobs[i];
454 foundSelectedJob =
true;
462 if (!foundSelectedJob ||
463 (job->getGroup() != nextJob->getGroup()) ||
464 (job->getCompletedIterations() >= nextJob->getCompletedIterations()) ||
465 !allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
468 const bool evaluatingCurrentJob = (currentJob && (job == currentJob));
471 QDateTime startSearchingtAt = firstPossibleStart(
472 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds);
475 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES,
476 evaluatingCurrentJob);
479 if (!startTime.
isValid() || startTime.
secsTo(nextStart) > MAX_INTERRUPT_SECS)
483 if (evaluatingCurrentJob && currentJobIsStartAt)
485 if (nextInterruption) *nextInterruption =
QDateTime();
486 nextStart = startTime;
490 else if (startTime.
secsTo(nextStart) >= -MAX_INTERRUPT_SECS)
493 nextStart = startTime;
499 if (when !=
nullptr) *when = nextStart;
500 if (interruptReason !=
nullptr) *interruptReason = interruptStr;
507 unsetEvaluation(jobs);
511 constexpr int twoDays = 48 * 3600;
512 if (simType != DONT_SIMULATE && nextJob !=
nullptr)
516 QDateTime simEnd = simulate(jobs, now, simulationLimit, capturedFramesCount, simType);
520 if (!Options::rememberJobProgress() && Options::schedulerRepeatEverything())
522 int repeats = 0, maxRepeats = 5;
523 while (simEnd.
isValid() && simEnd.
secsTo(simulationLimit) > 0 && ++repeats < maxRepeats)
526 simEnd = simulate(jobs, simEnd, simulationLimit,
nullptr, simType);
530 m_SimSeconds = simTimer.
elapsed() / 1000.0;
539 TEST_PRINT(stderr,
"%d simulate()\n", __LINE__);
545 foreach (SchedulerJob *job, jobs)
547 SchedulerJob *newJob =
new SchedulerJob();
551 newJob->followerJobs().clear();
552 copiedJobs.
append(newJob);
558 int numStartupCandidates = 0, numStartups = 0;
560 foreach (SchedulerJob *job, copiedJobs)
563 const auto state = job->getState();
566 numStartupCandidates++;
570 if (capturedFramesCount !=
nullptr)
571 capturedFramesCopy = *capturedFramesCount;
573 prepareJobsForEvaluation(copiedJobs, time, capturedFramesCopy,
nullptr,
false);
577 bool exceededIterations =
false;
581 for(
int i = 0; i < simJobs.
size(); ++i)
582 workDone[simJobs[i]] = 0.0;
592 SchedulerJob *selectedJob = selectNextJob(
593 simJobs, simTime,
nullptr, DONT_SIMULATE, &jobStartTime, &jobInterruptTime, &interruptReason);
594 if (selectedJob ==
nullptr)
597 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
"%1 starting at %2 interrupted at \"%3\" reason \"%4\"")
598 .arg(selectedJob->getName()).
arg(jobStartTime.
toString(
"MM/dd hh:mm"))
601 if (endTime.
isValid() && jobStartTime.
secsTo(endTime) < 0)
break;
607 foreach (SchedulerJob *job, simJobs)
609 if (job != selectedJob &&
610 job->getStartupCondition() == START_AT &&
611 jobStartTime.
secsTo(job->getStartupTime()) > 0 &&
615 QDateTime startAtTime = job->getStartupTime();
616 if (!nextStartAtTime.
isValid() || nextStartAtTime.
secsTo(startAtTime) < 0)
617 nextStartAtTime = startAtTime;
621 QDateTime constraintStopTime = jobInterruptTime;
622 if (nextStartAtTime.
isValid() &&
623 (!constraintStopTime.
isValid() ||
624 nextStartAtTime.
secsTo(constraintStopTime) < 0))
625 constraintStopTime = nextStartAtTime;
629 QDateTime jobConstraintTime = selectedJob->getNextEndTime(jobStartTime, SCHEDULE_RESOLUTION_MINUTES, &constraintReason,
632 std::abs(jobConstraintTime.
secsTo(nextStartAtTime)) < 2 * SCHEDULE_RESOLUTION_MINUTES)
633 constraintReason =
"interrupted by start-at job";
634 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" constraint \"%1\" reason \"%2\"")
637 if (selectedJob->getEstimatedTime() > 0)
640 const int timeLeft = selectedJob->getEstimatedTime() - workDone[selectedJob];
641 jobCompletionTime = jobStartTime.
addSecs(timeLeft);
642 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" completion \"%1\" time left %2s")
647 QDateTime jobStopTime = jobInterruptTime;
648 QString stopReason = jobStopTime.
isValid() ? interruptReason :
"";
649 if (jobConstraintTime.
isValid() && (!jobStopTime.
isValid() || jobStopTime.
secsTo(jobConstraintTime) < 0))
651 stopReason = constraintReason;
652 jobStopTime = jobConstraintTime;
653 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" picked constraint").toLatin1().data());
655 if (jobCompletionTime.
isValid() && (!jobStopTime.
isValid() || jobStopTime.
secsTo(jobCompletionTime) < 0))
657 stopReason =
"job completion";
658 jobStopTime = jobCompletionTime;
659 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" picked completion").toLatin1().data());
664 if (!selectedJob->getGroup().isEmpty() &&
665 (selectedJob->getCompletionCondition() == FINISH_LOOP ||
666 selectedJob->getCompletionCondition() == FINISH_REPEAT ||
667 selectedJob->getCompletionCondition() == FINISH_AT))
669 if (originalIteration.
find(selectedJob) == originalIteration.
end())
670 originalIteration[selectedJob] = selectedJob->getCompletedIterations();
671 if (originalSecsLeftIteration.
find(selectedJob) == originalSecsLeftIteration.
end())
672 originalSecsLeftIteration[selectedJob] = selectedJob->getEstimatedTimeLeftThisRepeat();
675 int leftThisRepeat = selectedJob->getEstimatedTimeLeftThisRepeat();
676 int secsPerRepeat = selectedJob->getEstimatedTimePerRepeat();
677 int secsLeftThisRepeat = (workDone[selectedJob] < leftThisRepeat) ?
678 leftThisRepeat - workDone[selectedJob] : secsPerRepeat;
680 if (workDone[selectedJob] == 0)
681 secsLeftThisRepeat += selectedJob->getEstimatedStartupTime();
684 if (secsLeftThisRepeat > 0 &&
685 (!jobStopTime.
isValid() || secsLeftThisRepeat < jobStartTime.
secsTo(jobStopTime)))
687 auto tempStart = jobStartTime;
688 auto tempInterrupt = jobInterruptTime;
689 auto tempReason = stopReason;
690 SchedulerJob keepJob = *selectedJob;
692 auto t = jobStartTime.
addSecs(secsLeftThisRepeat);
693 int iteration = selectedJob->getCompletedIterations();
694 int iters = 0, maxIters = 20;
695 while ((!jobStopTime.
isValid() || t.secsTo(jobStopTime) > 0) && iters++ < maxIters)
697 selectedJob->setCompletedIterations(++iteration);
698 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" iteration=%1").arg(iteration).toLatin1().data());
699 SchedulerJob *
next = selectNextJob(simJobs, t,
nullptr, DONT_SIMULATE, &tempStart, &tempInterrupt, &tempReason);
700 if (next != selectedJob)
702 stopReason =
"interrupted for group member";
704 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" switched to group member %1 at %2")
705 .arg(next ==
nullptr ?
"null" :
next->getName()).
arg(t.toString(
"MM/dd hh:mm")).
toLatin1().
data());
709 t = t.addSecs(secsPerRepeat);
711 *selectedJob = keepJob;
718 const int secondsRun = jobStartTime.
secsTo(jobStopTime);
719 workDone[selectedJob] += secondsRun;
721 if ((originalIteration.
find(selectedJob) != originalIteration.
end()) &&
722 (originalSecsLeftIteration.
find(selectedJob) != originalSecsLeftIteration.
end()))
724 int completedIterations = originalIteration[selectedJob];
725 if (workDone[selectedJob] >= originalSecsLeftIteration[selectedJob] &&
726 selectedJob->getEstimatedTimePerRepeat() > 0)
727 completedIterations +=
728 1 + (workDone[selectedJob] - originalSecsLeftIteration[selectedJob]) / selectedJob->getEstimatedTimePerRepeat();
729 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
730 QString(
" work sets interations=%1").arg(completedIterations).toLatin1().data());
731 selectedJob->setCompletedIterations(completedIterations);
737 if (!selectedJob->getStartupTime().isValid())
740 selectedJob->setStartupTime(jobStartTime);
741 selectedJob->setStopTime(jobStopTime);
742 selectedJob->setStopReason(stopReason);
744 scheduledJobs.
append(selectedJob);
745 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" Scheduled: %1 %2 -> %3 %4 work done %5s")
746 .arg(selectedJob->getName()).
arg(selectedJob->getStartupTime().toString(
"MM/dd hh:mm"))
747 .
arg(selectedJob->getStopTime().toString(
"MM/dd hh:mm")).
arg(selectedJob->getStopReason())
752 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" Added: %1 %2 -> %3 %4 work done %5s")
753 .arg(selectedJob->getName()).
arg(selectedJob->getStartupTime().toString(
"MM/dd hh:mm"))
754 .
arg(selectedJob->getStopTime().toString(
"MM/dd hh:mm")).
arg(selectedJob->getStopReason())
759 if (selectedJob->getEstimatedTime() >= 0 &&
760 workDone[selectedJob] >= selectedJob->getEstimatedTime())
763 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
" job %1 is complete")
766 schedule.
append(JobSchedule(jobs[copiedJobs.
indexOf(selectedJob)], jobStartTime, jobStopTime, stopReason));
767 simEndTime = jobStopTime;
768 simTime = jobStopTime.
addSecs(60);
775 if (++iterations > std::max(20, numStartupCandidates))
777 exceededIterations =
true;
778 TEST_PRINT(stderr,
"%d %s\n", __LINE__,
QString(
"ending simulation after %1 iterations")
779 .arg(iterations).toLatin1().data());
783 if (simType == SIMULATE_EACH_JOB_ONCE)
785 bool allJobsProcessedOnce =
true;
786 for (
const auto job : simJobs)
788 if (allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors) &&
789 !job->getStartupTime().isValid())
791 allJobsProcessedOnce =
false;
795 if (allJobsProcessedOnce)
797 TEST_PRINT(stderr,
"%d ending simulation, all jobs processed once\n", __LINE__);
806 for (
int i = 0; i < jobs.
size(); ++i)
808 if (scheduledJobs.
indexOf(copiedJobs[i]) >= 0)
814 jobs[i]->setStartupTime(copiedJobs[i]->getStartupTime());
817 jobs[i]->setStopTime(copiedJobs[i]->getStopTime());
818 jobs[i]->setStopReason(copiedJobs[i]->getStopReason());
823 unsetEvaluation(jobs);
825 return exceededIterations ?
QDateTime() : simEndTime;
830 for (
int i = 0; i < jobs.
size(); ++i)
837QString GreedyScheduler::jobScheduleString(
const JobSchedule &jobSchedule)
839 return QString(
"%1\t%2 --> %3 \t%4")
840 .
arg(jobSchedule.job->getName(), -10)
841 .
arg(jobSchedule.startTime.toString(
"MM/dd hh:mm"),
842 jobSchedule.stopTime.toString(
"hh:mm"), jobSchedule.stopReason);
847 foreach (
auto &line, schedule)
849 fprintf(stderr,
"%s\n",
QString(
"%1 %2 --> %3 (%4)")
850 .arg(jobScheduleString(line)).toLatin1().data());
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
@ SCHEDJOB_ABORTED
Job encountered a transitory issue while processing, and will be rescheduled.
@ SCHEDJOB_INVALID
Job has an incorrect configuration, and cannot proceed.
@ SCHEDJOB_ERROR
Job encountered a fatal issue while processing, and must be reset manually.
@ SCHEDJOB_COMPLETE
Job finished all required captures.
@ SCHEDJOB_EVALUATION
Job is being evaluated.
@ SCHEDJOB_SCHEDULED
Job was evaluated, and has a schedule.
@ SCHEDJOB_BUSY
Job is being processed.
@ SCHEDJOB_IDLE
Job was just created, and is not evaluated yet.
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
QCA_EXPORT Logger * logger()
QDateTime addSecs(qint64 s) const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
QString toString(QStringView format, QCalendar cal) const const
qint64 elapsed() const const
iterator find(const Key &key)
void append(QList< T > &&value)
qsizetype indexOf(const AT &value, qsizetype from) const const
qsizetype size() const const
QString arg(Args &&... args) const const
QByteArray toLatin1() const const