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

KDE's Doxygen guidelines are available online.