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

KDE's Doxygen guidelines are available online.