Kstars

greedyscheduler.cpp
1/* Ekos Scheduler Greedy Algorithm
2 SPDX-FileCopyrightText: Hy Murveit <hy@murveit.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "greedyscheduler.h"
8
9#include <ekos_scheduler_debug.h>
10
11#include "Options.h"
12#include "scheduler.h"
13#include "schedulermodulestate.h"
14#include "ekos/ekos.h"
15#include "ui_scheduler.h"
16#include "schedulerjob.h"
17#include "schedulerutils.h"
18
19#define TEST_PRINT if (false) fprintf
20
21// Can make the scheduling a bit faster by sampling every other minute instead of every minute.
22constexpr int SCHEDULE_RESOLUTION_MINUTES = 2;
23
24namespace Ekos
25{
26
27GreedyScheduler::GreedyScheduler()
28{
29}
30
31void GreedyScheduler::setParams(bool restartImmediately, bool restartQueue,
32 bool rescheduleErrors, int abortDelay,
33 int errorHandlingDelay)
34{
35 setRescheduleAbortsImmediate(restartImmediately);
36 setRescheduleAbortsQueue(restartQueue);
37 setRescheduleErrors(rescheduleErrors);
38 setAbortDelaySeconds(abortDelay);
39 setErrorDelaySeconds(errorHandlingDelay);
40}
41
42// The possible changes made to a job in jobs are:
43// Those listed in prepareJobsForEvaluation()
44// Those listed in selectNextJob
45// job->clearCache()
46// job->updateJobCells()
47
48void GreedyScheduler::scheduleJobs(const QList<SchedulerJob *> &jobs,
49 const QDateTime &now,
50 const QMap<QString, uint16_t> &capturedFramesCount,
51 ModuleLogger *logger)
52{
53 for (auto job : jobs)
54 job->clearCache();
55
56 QDateTime when;
57 QElapsedTimer timer;
58 timer.start();
59 scheduledJob = nullptr;
60 schedule.clear();
61
62 prepareJobsForEvaluation(jobs, now, capturedFramesCount, logger);
63
64 // consider only lead jobs for scheduling, scheduling data is propagated to its follower jobs
65 const QList<SchedulerJob *> leadJobs = SchedulerUtils::filterLeadJobs(jobs);
66
67 scheduledJob = selectNextJob(leadJobs, now, nullptr, SIMULATE, &when, nullptr, nullptr, &capturedFramesCount);
68 auto schedule = getSchedule();
69 if (logger != nullptr)
70 {
71 if (!schedule.empty())
72 {
73 // Print in reverse order ?! The log window at the bottom of the screen
74 // prints "upside down" -- most recent on top -- and I believe that view
75 // is more important than the log file (where we can invert when debugging).
76 for (int i = schedule.size() - 1; i >= 0; i--)
77 logger->appendLogText(GreedyScheduler::jobScheduleString(schedule[i]));
78 logger->appendLogText(QString("Scheduler plan for the next 48 hours starting %1 (%2)s:")
79 .arg(now.toString()).arg(timer.elapsed() / 1000.0));
80 }
81 else
82 {
83 if (jobs.size() > 0)
84 {
85 const int numJobs = jobs.size();
86 QString reason;
87 auto now = SchedulerModuleState::getLocalTime().addSecs(12 * 3600);
88 QDateTime soon = now.addSecs(3600);
89 for (int i = numJobs; i > 0; i--)
90 {
91 const auto &job = jobs[i - 1];
92 QDateTime nextEnd = job->getNextEndTime(now, SCHEDULE_RESOLUTION_MINUTES, &reason, soon);
93 logger->appendLogText(QString("(%1) %2: cannot run because: %3").arg(i).arg(job->getName()).arg(reason));
94 }
95 logger->appendLogText(QString("*****************In 12 Hours******************"));
96 now = SchedulerModuleState::getLocalTime();
97 soon = now.addSecs(3600);
98 for (int i = numJobs; i > 0; i--)
99 {
100 const auto &job = jobs[i - 1];
101 QDateTime nextEnd = job->getNextEndTime(now, SCHEDULE_RESOLUTION_MINUTES, &reason, soon);
102 logger->appendLogText(QString("(%1) %2: cannot run because: %3").arg(i).arg(job->getName()).arg(reason));
103 }
104 logger->appendLogText(QString("******************** Now ********************"));
105 logger->appendLogText(QString("To debug, set the time to a time when you believe a job should be runnable."));
106 }
107 logger->appendLogText(QString("Scheduler: Sorry, no jobs are runnable for the next 3 days."));
108 }
109 }
110 if (scheduledJob != nullptr)
111 {
112 qCDebug(KSTARS_EKOS_SCHEDULER)
113 << QString("Greedy Scheduler scheduling next job %1 at %2")
114 .arg(scheduledJob->getName(), when.toString("hh:mm"));
115 scheduledJob->setState(SCHEDJOB_SCHEDULED);
116 scheduledJob->setStartupTime(when);
117 }
118
119 for (auto job : jobs)
120 job->clearCache();
121}
122
123// The changes made to a job in jobs are:
124// Those listed in selectNextJob()
125// Not a const method because it sets the schedule class variable.
126bool GreedyScheduler::checkJob(const QList<SchedulerJob *> &jobs,
127 const QDateTime &now,
128 const SchedulerJob * const currentJob)
129{
130 // Don't interrupt a job that just started.
131 if (currentJob && currentJob->getStateTime().secsTo(now) < 5)
132 return true;
133
134 QDateTime startTime;
135
136 // Simulating in checkJob() is only done to update the schedule which is a GUI convenience.
137 // Do it only if its quick and not more frequently than once per minute.
138 SimulationType simType = SIMULATE_EACH_JOB_ONCE;
139 if (m_SimSeconds > 0.5 ||
140 (m_LastCheckJobSim.isValid() && m_LastCheckJobSim.secsTo(now) < 60))
141 simType = DONT_SIMULATE;
142
143 const SchedulerJob *next = selectNextJob(jobs, now, currentJob, simType, &startTime);
144 if (next == currentJob && now.secsTo(startTime) <= 1)
145 {
146 if (simType != DONT_SIMULATE)
147 m_LastCheckJobSim = now;
148
149 return true;
150 }
151 else
152 {
153 // We need to interrupt the current job. There's a higher-priority one to run.
154 qCDebug(KSTARS_EKOS_SCHEDULER)
155 << QString("Greedy Scheduler bumping current job %1 for %2 at %3")
156 .arg(currentJob->getName(), next ? next->getName() : "---", now.toString("hh:mm"));
157 return false;
158 }
159}
160
161// The changes made to a job in jobs are:
162// job->setState(JOB_COMPLETE|JOB_EVALUATION|JOB_INVALID|JOB_COMPLETEno_change)
163// job->setEstimatedTime(0|-1|-2|time)
164// job->setInitialFilter(filter)
165// job->setLightFramesRequired(bool)
166// job->setInSequenceFocus(bool);
167// job->setCompletedIterations(completedIterations);
168// job->setCapturedFramesMap(capture_map);
169// job->setSequenceCount(count);
170// job->setEstimatedTimePerRepeat(time);
171// job->setEstimatedTimeLeftThisRepeat(time);
172// job->setEstimatedStartupTime(time);
173// job->setCompletedCount(count);
174
175void GreedyScheduler::prepareJobsForEvaluation(
176 const QList<SchedulerJob *> &jobs, const QDateTime &now,
177 const QMap<QString, uint16_t> &capturedFramesCount, ModuleLogger *logger, bool reestimateJobTimes) const
178{
179 // Remove some finished jobs from eval.
180 foreach (SchedulerJob *job, jobs)
181 {
182 job->clearSimulatedSchedule();
183 switch (job->getCompletionCondition())
184 {
185 case FINISH_AT:
186 /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */
187 if (job->getFinishAtTime().isValid() && job->getFinishAtTime() < now)
188 {
189 job->setState(SCHEDJOB_COMPLETE);
190 continue;
191 }
192 break;
193
194 case FINISH_REPEAT:
195 // In case of a repeating jobs, let's make sure we have more runs left to go
196 // If we don't, re-estimate imaging time for the scheduler job before concluding
197 if (job->getRepeatsRemaining() == 0)
198 {
199 if (logger != nullptr) logger->appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName()));
200 job->setState(SCHEDJOB_COMPLETE);
201 job->setEstimatedTime(0);
202 continue;
203 }
204 break;
205
206 default:
207 break;
208 }
209 }
210
211 // Change the state to eval or ERROR/ABORTED for all jobs that will be evaluated.
212 foreach (SchedulerJob *job, jobs)
213 {
214 switch (job->getState())
215 {
216 case SCHEDJOB_INVALID:
218 // If job is invalid or complete, bypass evaluation.
219 break;
220
221 case SCHEDJOB_ERROR:
222 case SCHEDJOB_ABORTED:
223 // These will be evaluated, but we'll have a delay to start.
224 break;
225 case SCHEDJOB_IDLE:
226 case SCHEDJOB_BUSY:
229 default:
230 job->setState(SCHEDJOB_EVALUATION);
231 break;
232 }
233 }
234
235 // Estimate the job times
236 foreach (SchedulerJob *job, jobs)
237 {
238 if (job->getState() == SCHEDJOB_INVALID || job->getState() == SCHEDJOB_COMPLETE)
239 continue;
240
241 // -1 = Job is not estimated yet
242 // -2 = Job is estimated but time is unknown
243 // > 0 Job is estimated and time is known
244 if (reestimateJobTimes)
245 {
246 job->setEstimatedTime(-1);
247 if (SchedulerUtils::estimateJobTime(job, capturedFramesCount, logger) == false)
248 {
249 job->setState(SCHEDJOB_INVALID);
250 continue;
251 }
252 }
253 if (job->getEstimatedTime() == 0)
254 {
255 job->setRepeatsRemaining(0);
256 // set job including its followers to complete state
257 job->setState(SCHEDJOB_COMPLETE, true);
258 continue;
259 }
260 TEST_PRINT(stderr, "JOB %s estimated time: %ld state %d\n", job->getName().toLatin1().data(), job->getEstimatedTime(),
261 job->getState());
262 }
263}
264
265namespace
266{
267// Don't Allow INVALID or COMPLETE jobs to be scheduled.
268// Allow ABORTED if one of the rescheduleAbort... options are true.
269// Allow ERROR if rescheduleErrors is true.
270bool allowJob(const SchedulerJob *job, bool rescheduleAbortsImmediate, bool rescheduleAbortsQueue, bool rescheduleErrors)
271{
272 if (job->getState() == SCHEDJOB_INVALID || job->getState() == SCHEDJOB_COMPLETE)
273 return false;
274 if (job->getState() == SCHEDJOB_ABORTED && !rescheduleAbortsImmediate && !rescheduleAbortsQueue)
275 return false;
276 if (job->getState() == SCHEDJOB_ERROR && !rescheduleErrors)
277 return false;
278 return true;
279}
280
281// Returns the first possible time a job may be scheduled. That is, it doesn't
282// evaluate the job, but rather just computes the needed delay (for ABORT and ERROR jobs)
283// or returns now for other jobs.
284QDateTime firstPossibleStart(const SchedulerJob *job, const QDateTime &now,
285 bool rescheduleAbortsQueue, int abortDelaySeconds,
286 bool rescheduleErrors, int errorDelaySeconds)
287{
288 QDateTime possibleStart = now;
289 const QDateTime &abortTime = job->getLastAbortTime();
290 const QDateTime &errorTime = job->getLastErrorTime();
291
292 if (abortTime.isValid() && rescheduleAbortsQueue)
293 {
294 auto abortStartTime = abortTime.addSecs(abortDelaySeconds);
295 if (abortStartTime > now)
296 possibleStart = abortStartTime;
297 }
298
299
300 if (errorTime.isValid() && rescheduleErrors)
301 {
302 auto errorStartTime = errorTime.addSecs(errorDelaySeconds);
303 if (errorStartTime > now)
304 possibleStart = errorStartTime;
305 }
306
307 if (!possibleStart.isValid() || possibleStart < now)
308 possibleStart = now;
309 return possibleStart;
310}
311} // namespace
312
313// Consider all jobs marked as JOB_EVALUATION/ABORT/ERROR. Assume ordered by highest priority first.
314// - Find the job with the earliest start time (given constraints like altitude, twilight, ...)
315// that can run for at least 10 minutes before a higher priority job.
316// - START_AT jobs are given the highest priority, whereever on the list they may be,
317// as long as they can start near their designated start times.
318// - Compute a schedule for the next 2 days, if fullSchedule is true, otherwise
319// just look for the next job.
320// - If currentJob is not nullptr, this method is really evaluating whether
321// that job can continue to be run, or if can't meet constraints, or if it
322// should be preempted for another job.
323//
324// This does not modify any of the jobs in jobs if there is no simType is DONT_SIMULATE.
325// If we are simulating, then jobs may change in the following ways:
326// job->setGreedyCompletionTime()
327// job->setState(state);
328// job->setStartupTime(time);
329// job->setStopReason(reason);
330// The only reason this isn't a const method is because it sets the schedule class variable.
331SchedulerJob *GreedyScheduler::selectNextJob(const QList<SchedulerJob *> &jobs, const QDateTime &now,
332 const SchedulerJob * const currentJob, SimulationType simType, QDateTime *when,
333 QDateTime *nextInterruption, QString *interruptReason,
334 const QMap<QString, uint16_t> *capturedFramesCount)
335{
336 TEST_PRINT(stderr, "selectNextJob(%s)\n", now.toString().toLatin1().data());
337 // Don't schedule a job that will be preempted in less than MIN_RUN_SECS.
338 constexpr int MIN_RUN_SECS = 10 * 60;
339
340 // Don't preempt a job for another job that is more than MAX_INTERRUPT_SECS in the future.
341 constexpr int MAX_INTERRUPT_SECS = 30;
342
343 // Don't interrupt START_AT jobs unless they can no longer run, or they're interrupted by another START_AT.
344 bool currentJobIsStartAt = (currentJob && currentJob->getFileStartupCondition() == START_AT &&
345 currentJob->getStartAtTime().isValid());
346 QDateTime nextStart;
347 SchedulerJob * nextJob = nullptr;
348 QString interruptStr;
349
350 for (int i = 0; i < jobs.size(); ++i)
351 {
352 SchedulerJob * const job = jobs[i];
353 const bool evaluatingCurrentJob = (currentJob && (job == currentJob));
354
355 TEST_PRINT(stderr, " considering %s (%s)\n", job->getName().toLatin1().data(), evaluatingCurrentJob ? "evaluating" : "");
356
357 if (!allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
358 {
359 TEST_PRINT(stderr, " not allowed\n");
360 continue;
361 }
362
363 // If the job state is abort or error, might have to delay the first possible start time.
364 QDateTime startSearchingtAt = firstPossibleStart(
365 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds);
366
367 TEST_PRINT(stderr, " start searching at %s\n", startSearchingtAt.toString().toLatin1().data());
368 // Find the first time this job can meet all its constraints.
369 // I found that passing in an "until" 4th argument actually hurt performance, as it reduces
370 // the effectiveness of the cache that getNextPossibleStartTime uses.
371 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES,
372 evaluatingCurrentJob);
373 TEST_PRINT(stderr, " startTime %s\n", startTime.toString().toLatin1().data());
374
375 if (startTime.isValid())
376 {
377 if (nextJob == nullptr)
378 {
379 // We have no other solutions--this is our best solution so far.
380 nextStart = startTime;
381 nextJob = job;
382 if (nextInterruption) *nextInterruption = QDateTime();
383 interruptStr = "";
384 }
385 else if (Options::greedyScheduling())
386 {
387 // Allow this job to be scheduled if it can run this many seconds
388 // before running into a higher priority job.
389 const int runSecs = evaluatingCurrentJob ? MAX_INTERRUPT_SECS : MIN_RUN_SECS;
390
391 // Don't interrupt a START_AT for higher priority job
392 if (evaluatingCurrentJob && currentJobIsStartAt)
393 {
394 if (nextInterruption) *nextInterruption = QDateTime();
395 nextStart = startTime;
396 nextJob = job;
397 interruptStr = "";
398 }
399 else if (startTime.secsTo(nextStart) > runSecs)
400 {
401 // We can start a lower priority job if it can run for at least runSecs
402 // before getting bumped by the previous higher priority job.
403 if (nextInterruption) *nextInterruption = nextStart;
404 interruptStr = QString("interrupted by %1").arg(nextJob->getName());
405 nextStart = startTime;
406 nextJob = job;
407 }
408 }
409 // If scheduling, and we have a solution close enough to now, none of the lower priority
410 // jobs can possibly be scheduled.
411 if (!currentJob && nextStart.isValid() && now.secsTo(nextStart) < MIN_RUN_SECS)
412 break;
413 }
414 else if (evaluatingCurrentJob)
415 {
416 // No need to keep searching past the current job if we're evaluating it
417 // and it had no startTime. It needs to be stopped.
418 *when = QDateTime();
419 return nullptr;
420 }
421
422 if (evaluatingCurrentJob) break;
423 }
424 if (nextJob != nullptr)
425 {
426 // The exception to the simple scheduling rules above are START_AT jobs, which
427 // are given highest priority, irrespective of order. If nextJob starts less than
428 // MIN_RUN_SECS before an on-time START_AT job, then give the START_AT job priority.
429 // However, in order for the START_AT job to interrupt a current job, it must start now.
430 for (int i = 0; i < jobs.size(); ++i)
431 {
432 SchedulerJob * const atJob = jobs[i];
433 if (atJob == nextJob)
434 continue;
435 const QDateTime atTime = atJob->getStartAtTime();
436 if (atJob->getFileStartupCondition() == START_AT && atTime.isValid())
437 {
438 if (!allowJob(atJob, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
439 continue;
440 // If the job state is abort or error, might have to delay the first possible start time.
441 QDateTime startSearchingtAt = firstPossibleStart(
442 atJob, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors,
443 errorDelaySeconds);
444 // atTime above is the user-specified start time. atJobStartTime is the time it can
445 // actually start, given all the constraints (altitude, twilight, etc).
446 const QDateTime atJobStartTime = atJob->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, currentJob
447 && (atJob == currentJob));
448 if (atJobStartTime.isValid())
449 {
450 // This difference between the user-specified start time, and the time it can really start.
451 const double startDelta = atJobStartTime.secsTo(atTime);
452 if (fabs(startDelta) < 20 * 60)
453 {
454 // If we're looking for a new job to start, then give the START_AT priority
455 // if it's within 10 minutes of its user-specified time.
456 // However, if we're evaluating the current job (called from checkJob() above)
457 // then only interrupt it if the START_AT job can start very soon.
458 const int gap = currentJob == nullptr ? MIN_RUN_SECS : 30;
459 if (nextStart.secsTo(atJobStartTime) <= gap)
460 {
461 nextJob = atJob;
462 nextStart = atJobStartTime;
463 if (nextInterruption) *nextInterruption = QDateTime(); // Not interrupting atJob
464 }
465 else if (nextInterruption)
466 {
467 // The START_AT job was not chosen to start now, but it's still possible
468 // that this atJob will be an interrupter.
469 if (!nextInterruption->isValid() ||
470 atJobStartTime.secsTo(*nextInterruption) < 0)
471 {
472 *nextInterruption = atJobStartTime;
473 interruptStr = QString("interrupted by %1").arg(atJob->getName());
474 }
475 }
476 }
477 }
478 }
479 }
480
481 // If the selected next job is part of a group, then we may schedule other members of the group if
482 // - the selected job is a repeating job and
483 // - another group member is runnable now and
484 // - that group mnember is behind the selected job's iteration.
485 if (nextJob && !nextJob->getGroup().isEmpty() && Options::greedyScheduling() && nextJob->getCompletedIterations() > 0)
486 {
487 TEST_PRINT(stderr, " Considering GROUPS (%d jobs) selected %s\n", jobs.size(), nextJob->getName().toLatin1().data());
488 // Iterate through the jobs list, first finding the selected job, the looking at all jobs after that.
489 bool foundSelectedJob = false;
490 for (int i = 0; i < jobs.size(); ++i)
491 {
492 SchedulerJob * const job = jobs[i];
493 if (job == nextJob)
494 {
495 foundSelectedJob = true;
496 continue;
497 }
498
499 TEST_PRINT(stderr, " Job %s (group %s) %s (%d vs %d iterations) %s\n",
500 job->getName().toLatin1().data(), (job->getGroup() != nextJob->getGroup()) ? "Different" : "Same",
501 foundSelectedJob ? "Found" : "not found yet",
502 job->getCompletedIterations(), nextJob->getCompletedIterations(),
503 allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors) ? "allowed" : "not allowed");
504 // Only jobs with lower priority than nextJob--higher priority jobs already have been considered and rejected.
505 // Only consider jobs in the same group as nextJob
506 // Only consider jobs with fewer iterations than nextJob.
507 // Only consider jobs that are allowed.
508 if (!foundSelectedJob ||
509 (job->getGroup() != nextJob->getGroup()) ||
510 (job->getCompletedIterations() >= nextJob->getCompletedIterations()) ||
511 !allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
512 continue;
513
514 const bool evaluatingCurrentJob = (currentJob && (job == currentJob));
515
516 // If the job state is abort or error, might have to delay the first possible start time.
517 QDateTime startSearchingtAt = firstPossibleStart(
518 job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds);
519
520 // Find the first time this job can meet all its constraints.
521 const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES,
522 evaluatingCurrentJob);
523
524 // Only consider jobs that can start soon.
525 if (!startTime.isValid() || startTime.secsTo(nextStart) > MAX_INTERRUPT_SECS)
526 continue;
527
528 // Don't interrupt a START_AT for higher priority job
529 if (evaluatingCurrentJob && currentJobIsStartAt)
530 {
531 if (nextInterruption) *nextInterruption = QDateTime();
532 nextStart = startTime;
533 nextJob = job;
534 interruptStr = "";
535 }
536 else if (startTime.secsTo(nextStart) >= -MAX_INTERRUPT_SECS)
537 {
538 // Use this group member, keeping the old interruption variables.
539 nextStart = startTime;
540 nextJob = job;
541 }
542 }
543 }
544 }
545 if (when != nullptr) *when = nextStart;
546 if (interruptReason != nullptr) *interruptReason = interruptStr;
547
548 // Needed so display says "Idle" for unscheduled jobs.
549 // This will also happen in simulate, but that isn't called if nextJob is null.
550 // Must test for !nextJob. setState() inside unsetEvaluation has a nasty side effect
551 // of clearing the estimated time.
552 if (!nextJob)
553 unsetEvaluation(jobs);
554
555 QElapsedTimer simTimer;
556 simTimer.start();
557 const int simDays = SIM_HOURS * 3600;
558 if (simType != DONT_SIMULATE && nextJob != nullptr)
559 {
560 QDateTime simulationLimit = now.addSecs(simDays);
561 schedule.clear();
562 QDateTime simEnd = simulate(jobs, now, simulationLimit, capturedFramesCount, simType);
563
564 // This covers the scheduler's "repeat after completion" option,
565 // which only applies if rememberJobProgress is false.
566 if (!Options::rememberJobProgress() && Options::schedulerRepeatEverything())
567 {
568 int repeats = 0, maxRepeats = 5;
569 while (simEnd.isValid() && simEnd.secsTo(simulationLimit) > 0 && ++repeats < maxRepeats)
570 {
571 simEnd = simEnd.addSecs(60);
572 simEnd = simulate(jobs, simEnd, simulationLimit, nullptr, simType);
573 }
574 }
575 m_SimSeconds = simTimer.elapsed() / 1000.0;
576 TEST_PRINT(stderr, "********************************* simulate(%s,%d) took %.3fs\n",
577 simType == SIMULATE ? "SIM" : "ONLY_1", SIM_HOURS, m_SimSeconds);
578 }
579
580 return nextJob;
581}
582
583// The only reason this isn't a const method is because it sets the schedule class variable
584QDateTime GreedyScheduler::simulate(const QList<SchedulerJob *> &jobs, const QDateTime &time, const QDateTime &endTime,
585 const QMap<QString, uint16_t> *capturedFramesCount, SimulationType simType)
586{
587 TEST_PRINT(stderr, "%d simulate()\n", __LINE__);
588 // Make a deep copy of jobs
589 QList<SchedulerJob *> copiedJobs;
590 QList<SchedulerJob *> scheduledJobs;
591 QDateTime simEndTime;
592
593 foreach (SchedulerJob *job, jobs)
594 {
595 SchedulerJob *newJob = new SchedulerJob();
596 // Make sure the copied class pointers aren't affected!
597 *newJob = *job;
598 // clear follower job lists to avoid links to existing jobs
599 newJob->followerJobs().clear();
600 newJob->clearSimulatedSchedule();
601 copiedJobs.append(newJob);
602 job->setStopTime(QDateTime());
603 }
604
605 // The number of jobs we have that can be scheduled,
606 // and the number of them where a simulated start has been scheduled.
607 int numStartupCandidates = 0, numStartups = 0;
608 // Reset the start times.
609 foreach (SchedulerJob *job, copiedJobs)
610 {
611 job->setStartupTime(QDateTime());
612 const auto state = job->getState();
613 if (state == SCHEDJOB_SCHEDULED || state == SCHEDJOB_EVALUATION ||
614 state == SCHEDJOB_BUSY || state == SCHEDJOB_IDLE)
615 numStartupCandidates++;
616 }
617
618 QMap<QString, uint16_t> capturedFramesCopy;
619 if (capturedFramesCount != nullptr)
620 capturedFramesCopy = *capturedFramesCount;
621 QList<SchedulerJob *>simJobs = copiedJobs;
622 prepareJobsForEvaluation(copiedJobs, time, capturedFramesCopy, nullptr, false);
623
624 QDateTime simTime = time;
625 int iterations = 0;
626 bool exceededIterations = false;
627 QHash<SchedulerJob*, int> workDone;
628 QHash<SchedulerJob*, int> originalIteration, originalSecsLeftIteration;
629
630 for(int i = 0; i < simJobs.size(); ++i)
631 workDone[simJobs[i]] = 0.0;
632
633 while (true)
634 {
635 QDateTime jobStartTime;
636 QDateTime jobInterruptTime;
637 QString interruptReason;
638 // Find the next job to be scheduled, when it starts, and when a higher priority
639 // job might preempt it, why it would be preempted.
640 // Note: 4th arg, fullSchedule, must be false or we'd loop forever.
641 SchedulerJob *selectedJob =
642 selectNextJob(simJobs, simTime, nullptr, DONT_SIMULATE, &jobStartTime, &jobInterruptTime, &interruptReason);
643 if (selectedJob == nullptr)
644 break;
645
646 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString("%1 starting at %2 interrupted at \"%3\" reason \"%4\"")
647 .arg(selectedJob->getName()).arg(jobStartTime.toString("MM/dd hh:mm"))
648 .arg(jobInterruptTime.toString("MM/dd hh:mm")).arg(interruptReason).toLatin1().data());
649 // Are we past the end time?
650 if (endTime.isValid() && jobStartTime.secsTo(endTime) < 0) break;
651
652 // It's possible there are start_at jobs that can preempt this job.
653 // Find the next start_at time, and use that as an end constraint to getNextEndTime
654 // if it's before jobInterruptTime.
655 QDateTime nextStartAtTime;
656 foreach (SchedulerJob *job, simJobs)
657 {
658 if (job != selectedJob &&
659 job->getStartupCondition() == START_AT &&
660 jobStartTime.secsTo(job->getStartupTime()) > 0 &&
661 (job->getState() == SCHEDJOB_EVALUATION ||
662 job->getState() == SCHEDJOB_SCHEDULED))
663 {
664 QDateTime startAtTime = job->getStartupTime();
665 if (!nextStartAtTime.isValid() || nextStartAtTime.secsTo(startAtTime) < 0)
666 nextStartAtTime = startAtTime;
667 }
668 }
669 // Check to see if the above start-at stop time is before the interrupt stop time.
670 QDateTime constraintStopTime = jobInterruptTime;
671 if (nextStartAtTime.isValid() &&
672 (!constraintStopTime.isValid() ||
673 nextStartAtTime.secsTo(constraintStopTime) < 0))
674 {
675 constraintStopTime = nextStartAtTime;
676 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" job will be interrupted by a START_AT job").toLatin1().data());
677 }
678
679 QString constraintReason;
680 // Get the time that this next job would fail its constraints, and a human-readable explanation.
681 QDateTime jobConstraintTime = selectedJob->getNextEndTime(jobStartTime, SCHEDULE_RESOLUTION_MINUTES, &constraintReason,
682 constraintStopTime);
683 if (nextStartAtTime.isValid() && jobConstraintTime.isValid() &&
684 std::abs(jobConstraintTime.secsTo(nextStartAtTime)) < 2 * SCHEDULE_RESOLUTION_MINUTES)
685 constraintReason = "interrupted by start-at job";
686 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" constraint \"%1\" reason \"%2\"")
687 .arg(jobConstraintTime.toString("MM/dd hh:mm")).arg(constraintReason).toLatin1().data());
688 QDateTime jobCompletionTime;
689 TEST_PRINT(stderr, "%d %s\n", __LINE__,
690 QString(" estimated time = %1").arg(selectedJob->getEstimatedTime()).toLatin1().data());
691 if (selectedJob->getEstimatedTime() > 0)
692 {
693 // Estimate when the job might complete, if it was allowed to run without interruption.
694 const int timeLeft = selectedJob->getEstimatedTime() - workDone[selectedJob];
695 jobCompletionTime = jobStartTime.addSecs(timeLeft);
696 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" completion \"%1\" time left %2s")
697 .arg(jobCompletionTime.toString("MM/dd hh:mm")).arg(timeLeft).toLatin1().data());
698 }
699 // Consider the 3 stopping times computed above (preemption, constraints missed, and completion),
700 // see which comes soonest, and set the jobStopTime and jobStopReason.
701 QDateTime jobStopTime = jobInterruptTime;
702 QString stopReason = jobStopTime.isValid() ? interruptReason : "";
703 if (jobConstraintTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobConstraintTime) < 0))
704 {
705 stopReason = constraintReason;
706 jobStopTime = jobConstraintTime;
707 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" picked constraint").toLatin1().data());
708 }
709 if (jobCompletionTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobCompletionTime) < 0))
710 {
711 stopReason = "job completion";
712 jobStopTime = jobCompletionTime;
713 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" picked completion").toLatin1().data());
714 }
715
716 // This if clause handles the simulation of scheduler repeat groups
717 // which applies to scheduler jobs with repeat-style completion conditions.
718 if (!selectedJob->getGroup().isEmpty() &&
719 (selectedJob->getCompletionCondition() == FINISH_LOOP ||
720 selectedJob->getCompletionCondition() == FINISH_REPEAT ||
721 selectedJob->getCompletionCondition() == FINISH_AT))
722 {
723 if (originalIteration.find(selectedJob) == originalIteration.end())
724 originalIteration[selectedJob] = selectedJob->getCompletedIterations();
725 if (originalSecsLeftIteration.find(selectedJob) == originalSecsLeftIteration.end())
726 originalSecsLeftIteration[selectedJob] = selectedJob->getEstimatedTimeLeftThisRepeat();
727
728 // Estimate the time it would take to complete the current repeat, if this is a repeated job.
729 int leftThisRepeat = selectedJob->getEstimatedTimeLeftThisRepeat();
730 int secsPerRepeat = selectedJob->getEstimatedTimePerRepeat();
731 int secsLeftThisRepeat = (workDone[selectedJob] < leftThisRepeat) ?
732 leftThisRepeat - workDone[selectedJob] : secsPerRepeat;
733
734 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" sec per repeat %1 sec left this repeat %2")
735 .arg(secsPerRepeat).arg(secsLeftThisRepeat).toLatin1().data());
736
737 if (workDone[selectedJob] == 0)
738 {
739 secsLeftThisRepeat += selectedJob->getEstimatedStartupTime();
740 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" adding %1 to secsLeftThisRepeat")
741 .arg(selectedJob->getEstimatedStartupTime()).arg(secsLeftThisRepeat).toLatin1().data());
742 }
743
744 // If it would finish a repeat, run one repeat and see if it would still be scheduled.
745 if (secsLeftThisRepeat > 0 &&
746 (!jobStopTime.isValid() || secsLeftThisRepeat < jobStartTime.secsTo(jobStopTime)))
747 {
748 auto tempStart = jobStartTime;
749 auto tempInterrupt = jobInterruptTime;
750 auto tempReason = stopReason;
751 SchedulerJob keepJob = *selectedJob;
752
753 auto t = jobStartTime.addSecs(secsLeftThisRepeat);
754 int iteration = selectedJob->getCompletedIterations();
755 int iters = 0, maxIters = 20; // just in case...
756 while ((!jobStopTime.isValid() || t.secsTo(jobStopTime) > 0) && iters++ < maxIters)
757 {
758 selectedJob->setCompletedIterations(++iteration);
759 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" iteration=%1").arg(iteration).toLatin1().data());
760 SchedulerJob *next = selectNextJob(simJobs, t, nullptr, DONT_SIMULATE, &tempStart, &tempInterrupt, &tempReason);
761 if (next != selectedJob)
762 {
763 stopReason = "interrupted for group member";
764 jobStopTime = t;
765 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" switched to group member %1 at %2")
766 .arg(next == nullptr ? "null" : next->getName()).arg(t.toString("MM/dd hh:mm")).toLatin1().data());
767
768 break;
769 }
770 t = t.addSecs(secsPerRepeat);
771 }
772 *selectedJob = keepJob;
773 }
774 }
775
776 // Increment the work done, for the next time this job might be scheduled in this simulation.
777 if (jobStopTime.isValid())
778 {
779 const int secondsRun = jobStartTime.secsTo(jobStopTime);
780 workDone[selectedJob] += secondsRun;
781
782 if ((originalIteration.find(selectedJob) != originalIteration.end()) &&
783 (originalSecsLeftIteration.find(selectedJob) != originalSecsLeftIteration.end()))
784 {
785 int completedIterations = originalIteration[selectedJob];
786 if (workDone[selectedJob] >= originalSecsLeftIteration[selectedJob] &&
787 selectedJob->getEstimatedTimePerRepeat() > 0)
788 completedIterations +=
789 1 + (workDone[selectedJob] - originalSecsLeftIteration[selectedJob]) / selectedJob->getEstimatedTimePerRepeat();
790 TEST_PRINT(stderr, "%d %s\n", __LINE__,
791 QString(" work sets interations=%1").arg(completedIterations).toLatin1().data());
792 selectedJob->setCompletedIterations(completedIterations);
793 }
794 }
795
796 // Set the job's startupTime, but only for the first time the job will be scheduled.
797 // This will be used by the scheduler's UI when displaying the job schedules.
798 if (!selectedJob->getStartupTime().isValid())
799 {
800 numStartups++;
801 selectedJob->setStartupTime(jobStartTime);
802 selectedJob->setStopTime(jobStopTime);
803 selectedJob->setStopReason(stopReason);
804 selectedJob->setState(SCHEDJOB_SCHEDULED);
805 scheduledJobs.append(selectedJob);
806 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" Scheduled: %1 %2 -> %3 %4 work done %5s")
807 .arg(selectedJob->getName()).arg(selectedJob->getStartupTime().toString("MM/dd hh:mm"))
808 .arg(selectedJob->getStopTime().toString("MM/dd hh:mm")).arg(selectedJob->getStopReason())
809 .arg(workDone[selectedJob]).toLatin1().data());
810 }
811 else
812 {
813 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" Added: %1 %2 -> %3 %4 work done %5s")
814 .arg(selectedJob->getName()).arg(jobStartTime.toString("MM/dd hh:mm"))
815 .arg(jobStopTime.toString("MM/dd hh:mm")).arg(stopReason)
816 .arg(workDone[selectedJob]).toLatin1().data());
817 }
818
819 // Compute if the simulated job should be considered complete because of work done.
820 if (selectedJob->getEstimatedTime() >= 0 &&
821 workDone[selectedJob] >= selectedJob->getEstimatedTime())
822 {
823 selectedJob->setState(SCHEDJOB_COMPLETE);
824 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString(" job %1 is complete")
825 .arg(selectedJob->getName()).toLatin1().data());
826 }
827 selectedJob->appendSimulatedSchedule(JobSchedule(nullptr, jobStartTime, jobStopTime, stopReason));
828 schedule.append(JobSchedule(jobs[copiedJobs.indexOf(selectedJob)], jobStartTime, jobStopTime, stopReason));
829 simEndTime = jobStopTime;
830 simTime = jobStopTime.addSecs(60);
831
832 // End the simulation if we've crossed endTime, or no further jobs could be started,
833 // or if we've simply run too long.
834 if (!simTime.isValid()) break;
835 if (endTime.isValid() && simTime.secsTo(endTime) < 0) break;
836
837 if (++iterations > std::max(20, numStartupCandidates))
838 {
839 exceededIterations = true;
840 TEST_PRINT(stderr, "%d %s\n", __LINE__, QString("ending simulation after %1 iterations")
841 .arg(iterations).toLatin1().data());
842
843 break;
844 }
845 if (simType == SIMULATE_EACH_JOB_ONCE)
846 {
847 bool allJobsProcessedOnce = true;
848 for (const auto job : simJobs)
849 {
850 if (allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors) &&
851 !job->getStartupTime().isValid())
852 {
853 allJobsProcessedOnce = false;
854 break;
855 }
856 }
857 if (allJobsProcessedOnce)
858 {
859 TEST_PRINT(stderr, "%d ending simulation, all jobs processed once\n", __LINE__);
860 break;
861 }
862 }
863 }
864
865 // This simulation has been run using a deep-copy of the jobs list, so as not to interfere with
866 // some of their stored data. However, we do wish to update several fields of the "real" scheduleJobs.
867 // Note that the original jobs list and "copiedJobs" should be in the same order..
868 for (int i = 0; i < jobs.size(); ++i)
869 {
870 if (scheduledJobs.indexOf(copiedJobs[i]) >= 0)
871 {
872 // If this is a simulation where the job is already running, don't change its state or startup time.
873 if (jobs[i]->getState() != SCHEDJOB_BUSY)
874 {
875 jobs[i]->setState(SCHEDJOB_SCHEDULED);
876 jobs[i]->setStartupTime(copiedJobs[i]->getStartupTime());
877 }
878 // Can't set the standard completionTime as it affects getEstimatedTime()
879 jobs[i]->setStopTime(copiedJobs[i]->getStopTime());
880 jobs[i]->setStopReason(copiedJobs[i]->getStopReason());
881 if (simType == SIMULATE)
882 jobs[i]->setSimulatedSchedule(copiedJobs[i]->getSimulatedSchedule());
883 }
884 }
885 // This should go after above loop. unsetEvaluation calls setState() which clears
886 // certain fields from the state for IDLE states.
887 unsetEvaluation(jobs);
888
889 return exceededIterations ? QDateTime() : simEndTime;
890}
891
892void GreedyScheduler::unsetEvaluation(const QList<SchedulerJob *> &jobs) const
893{
894 for (int i = 0; i < jobs.size(); ++i)
895 {
896 if (jobs[i]->getState() == SCHEDJOB_EVALUATION)
897 jobs[i]->setState(SCHEDJOB_IDLE);
898 }
899}
900
901QString GreedyScheduler::jobScheduleString(const JobSchedule &jobSchedule)
902{
903 return QString("%1\t%2 --> %3 \t%4")
904 .arg(jobSchedule.job->getName(), -10)
905 .arg(jobSchedule.startTime.toString("MM/dd hh:mm"),
906 jobSchedule.stopTime.toString("hh:mm"), jobSchedule.stopReason);
907}
908
909void GreedyScheduler::printSchedule(const QList<JobSchedule> &schedule)
910{
911 foreach (auto &line, schedule)
912 {
913 fprintf(stderr, "%s\n", QString("%1 %2 --> %3 (%4)")
914 .arg(jobScheduleString(line)).toLatin1().data());
915 }
916}
917
918} // namespace Ekos
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
@ 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.
const QList< QKeySequence > & next()
QCA_EXPORT Logger * logger()
char * data()
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 end()
iterator find(const Key &key)
void append(QList< T > &&value)
qsizetype indexOf(const AT &value, qsizetype from) const const
qsizetype size() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
QByteArray toLatin1() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:58:36 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.