Kstars

greedyscheduler.cpp
1 /* Ekos Scheduler Greedy Algorithm
2  SPDX-FileCopyrightText: Hy Murveit <[email protected]>
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 
16 // Can make the scheduling a bit faster by sampling every other minute instead of every minute.
17 constexpr int SCHEDULE_RESOLUTION_MINUTES = 2;
18 
19 namespace Ekos
20 {
21 
22 GreedyScheduler::GreedyScheduler()
23 {
24 }
25 
26 void GreedyScheduler::setParams(bool restartImmediately, bool restartQueue,
27  bool rescheduleErrors, int abortDelay,
28  int errorHandlingDelay)
29 {
30  setRescheduleAbortsImmediate(restartImmediately);
31  setRescheduleAbortsQueue(restartQueue);
32  setRescheduleErrors(rescheduleErrors);
33  setAbortDelaySeconds(abortDelay);
34  setErrorDelaySeconds(errorHandlingDelay);
35 }
36 
37 QList<SchedulerJob *> GreedyScheduler::scheduleJobs(const QList<SchedulerJob *> &jobs,
38  const QDateTime &now,
39  const QMap<QString, uint16_t> &capturedFramesCount,
40  Scheduler *scheduler)
41 {
42  for (auto job : jobs)
43  job->clearCache();
44 
45  SchedulerJob::enableGraphicsUpdates(false);
46  QDateTime when;
47  QElapsedTimer timer;
48  timer.start();
49  scheduledJob = nullptr;
50  schedule.clear();
51 
52  QList<SchedulerJob *> sortedJobs =
53  prepareJobsForEvaluation(jobs, now, capturedFramesCount, scheduler);
54 
55  scheduledJob = selectNextJob(sortedJobs, now, nullptr, true, &when, nullptr, nullptr, &capturedFramesCount);
56  auto schedule = getSchedule();
57  if (!schedule.empty())
58  {
59  // Print in reverse order ?! The log window at the bottom of the screen
60  // prints "upside down" -- most recent on top -- and I believe that view
61  // is more important than the log file (where we can invert when debugging).
62  for (int i = schedule.size() - 1; i >= 0; i--)
63  scheduler->appendLogText(GreedyScheduler::jobScheduleString(schedule[i]));
64  scheduler->appendLogText(QString("Greedy Scheduler plan for the next 48 hours starting %1 (%2)s:")
65  .arg(now.toString()).arg(timer.elapsed() / 1000.0));
66  }
67  else scheduler->appendLogText(QString("Greedy Scheduler: empty plan (%1s)").arg(timer.elapsed() / 1000.0));
68  if (scheduledJob != nullptr)
69  {
70  qCDebug(KSTARS_EKOS_SCHEDULER)
71  << QString("Greedy Scheduler scheduling next job %1 at %2")
72  .arg(scheduledJob->getName(), when.toString("hh:mm"));
73  scheduledJob->setState(SchedulerJob::JOB_SCHEDULED);
74  scheduledJob->setStartupTime(when);
75  foreach (auto job, sortedJobs)
76  job->updateJobCells();
77  }
78  // The graphics would get updated many times during scheduling, which can
79  // cause significant cpu usage. No need for that, so we turn off updates
80  // at the start of this method, and then update all jobs once here.
81  SchedulerJob::enableGraphicsUpdates(true);
82  for (auto job : sortedJobs)
83  {
84  job->updateJobCells();
85  job->clearCache();
86  }
87 
88  return sortedJobs;
89 }
90 
91 bool GreedyScheduler::checkJob(const QList<SchedulerJob *> &jobs,
92  const QDateTime &now,
93  SchedulerJob *currentJob)
94 {
95  QDateTime startTime;
96  SchedulerJob *next = selectNextJob(jobs, now, currentJob, false, &startTime);
97  if (next == currentJob && now.secsTo(startTime) <= 1)
98  {
99  return true;
100  }
101  else
102  {
103  // We need to interrupt the current job. There's a higher-priority one to run.
104  qCDebug(KSTARS_EKOS_SCHEDULER)
105  << QString("Greedy Scheduler bumping current job %1 for %2 at %3")
106  .arg(currentJob->getName(), next ? next->getName() : "---", now.toString("hh:mm"));
107  return false;
108  }
109 }
110 
111 QList<SchedulerJob *> GreedyScheduler::prepareJobsForEvaluation(
112  const QList<SchedulerJob *> &jobs, const QDateTime &now,
113  const QMap<QString, uint16_t> &capturedFramesCount, Scheduler *scheduler, bool reestimateJobTimes)
114 {
115  QList<SchedulerJob *> sortedJobs = jobs;
116  // Remove some finished jobs from eval.
117  foreach (SchedulerJob *job, sortedJobs)
118  {
119  switch (job->getCompletionCondition())
120  {
121  case SchedulerJob::FINISH_AT:
122  /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */
123  if (job->getCompletionTime().isValid() && job->getCompletionTime() < now)
124  {
125  job->setState(SchedulerJob::JOB_COMPLETE);
126  continue;
127  }
128  break;
129 
130  case SchedulerJob::FINISH_REPEAT:
131  // In case of a repeating jobs, let's make sure we have more runs left to go
132  // If we don't, re-estimate imaging time for the scheduler job before concluding
133  if (job->getRepeatsRemaining() == 0)
134  {
135  if (scheduler != nullptr) scheduler->appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName()));
136  job->setState(SchedulerJob::JOB_COMPLETE);
137  job->setEstimatedTime(0);
138  continue;
139  }
140  break;
141 
142  default:
143  break;
144  }
145  }
146 
147  // Change the state to eval or ERROR/ABORTED for all jobs that will be evaluated.
148  foreach (SchedulerJob *job, sortedJobs)
149  {
150  switch (job->getState())
151  {
152  case SchedulerJob::JOB_INVALID:
153  case SchedulerJob::JOB_COMPLETE:
154  // If job is invalid or complete, bypass evaluation.
155  break;
156 
157  case SchedulerJob::JOB_ERROR:
158  case SchedulerJob::JOB_ABORTED:
159  // These will be evaluated, but we'll have a delay to start.
160  break;
161  case SchedulerJob::JOB_IDLE:
162  case SchedulerJob::JOB_BUSY:
163  case SchedulerJob::JOB_SCHEDULED:
164  case SchedulerJob::JOB_EVALUATION:
165  default:
166  job->setState(SchedulerJob::JOB_EVALUATION);
167  break;
168  }
169  }
170 
171  // Estimate the job times
172  foreach (SchedulerJob *job, sortedJobs)
173  {
174  if (job->getState() == SchedulerJob::JOB_INVALID || job->getState() == SchedulerJob::JOB_COMPLETE)
175  continue;
176 
177  // -1 = Job is not estimated yet
178  // -2 = Job is estimated but time is unknown
179  // > 0 Job is estimated and time is known
180  if (reestimateJobTimes)
181  {
182  job->setEstimatedTime(-1);
183  if (Scheduler::estimateJobTime(job, capturedFramesCount, scheduler) == false)
184  {
185  job->setState(SchedulerJob::JOB_INVALID);
186  continue;
187  }
188  }
189  if (job->getEstimatedTime() == 0)
190  {
191  job->setRepeatsRemaining(0);
192  job->setState(SchedulerJob::JOB_COMPLETE);
193  continue;
194  }
195  }
196 
197  return sortedJobs;
198 }
199 
200 namespace
201 {
202 // Don't Allow INVALID or COMPLETE jobs to be scheduled.
203 // Allow ABORTED if one of the rescheduleAbort... options are true.
204 // Allow ERROR if rescheduleErrors is true.
205 bool allowJob(SchedulerJob *job, bool rescheduleAbortsImmediate, bool rescheduleAbortsQueue, bool rescheduleErrors)
206 {
207  if (job->getState() == SchedulerJob::JOB_INVALID || job->getState() == SchedulerJob::JOB_COMPLETE)
208  return false;
209  if (job->getState() == SchedulerJob::JOB_ABORTED && !rescheduleAbortsImmediate && !rescheduleAbortsQueue)
210  return false;
211  if (job->getState() == SchedulerJob::JOB_ERROR && !rescheduleErrors)
212  return false;
213  return true;
214 }
215 
216 // Returns the first possible time a job may be scheduled. That is, it doesn't
217 // evaluate the job, but rather just computes the needed delay (for ABORT and ERROR jobs)
218 // or returns now for other jobs.
219 QDateTime firstPossibleStart(SchedulerJob *job, const QDateTime &now,
220  bool rescheduleAbortsQueue, int abortDelaySeconds,
221  bool rescheduleErrors, int errorDelaySeconds)
222 {
223  QDateTime possibleStart = now;
224  const QDateTime &abortTime = job->getLastAbortTime();
225  const QDateTime &errorTime = job->getLastErrorTime();
226 
227  if (abortTime.isValid() && rescheduleAbortsQueue)
228  {
229  auto abortStartTime = abortTime.addSecs(abortDelaySeconds);
230  if (abortStartTime > now)
231  possibleStart = abortStartTime;
232  }
233 
234 
235  if (errorTime.isValid() && rescheduleErrors)
236  {
237  auto errorStartTime = errorTime.addSecs(errorDelaySeconds);
238  if (errorStartTime > now)
239  possibleStart = errorStartTime;
240  }
241 
242  if (!possibleStart.isValid() || possibleStart < now)
243  possibleStart = now;
244  return possibleStart;
245 }
246 } // namespace
247 
248 // Consider all jobs marked as JOB_EVALUATION/ABORT/ERROR. Assume ordered by highest priority first.
249 // - Find the job with the earliest start time (given constraints like altitude, twilight, ...)
250 // that can run for at least 10 minutes before a higher priority job.
251 // - START_AT jobs are given the highest priority, whereever on the list they may be,
252 // as long as they can start near their designated start times.
253 // - Compute a schedule for the next 2 days, if fullSchedule is true, otherwise
254 // just look for the next job.
255 // - If currentJob is not nullptr, this method is really evaluating whether
256 // that job can continue to be run, or if can't meet constraints, or if it
257 // should be preempted for another job.
258 SchedulerJob *GreedyScheduler::selectNextJob(const QList<SchedulerJob *> &jobs, const QDateTime &now,
259  SchedulerJob *currentJob, bool fullSchedule, QDateTime *when,
260  QDateTime *nextInterruption, QString *interruptReason,
261  const QMap<QString, uint16_t> *capturedFramesCount)
262 {
263  // Don't schedule a job that will be preempted in less than MIN_RUN_SECS.
264  constexpr int MIN_RUN_SECS = 10 * 60;
265 
266  // Don't preempt a job for another job that is more than MAX_INTERRUPT_SECS in the future.
267  constexpr int MAX_INTERRUPT_SECS = 30;
268 
269  // Don't interrupt START_AT jobs unless they can no longer run, or they're interrupted by another START_AT.
270  bool currentJobIsStartAt = (currentJob && currentJob->getFileStartupCondition() == SchedulerJob::START_AT &&
271  currentJob->getFileStartupTime().isValid());
272  QDateTime nextStart;
273  SchedulerJob *nextJob = nullptr;
274  QString interruptStr;
275 
276  for (int i = 0; i < jobs.size(); ++i)
277  {
278  SchedulerJob *job = jobs[i];
279  const bool evaluatingCurrentJob = (currentJob && (job == currentJob));
280 
281  if (!allowJob(job, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
282  continue;
283 
284  // If the job state is abort or error, might have to delay the first possible start time.
285  QDateTime startSearchingtAt = firstPossibleStart(
286  job, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors, errorDelaySeconds);
287 
288  // Find the first time this job can meet all its constraints.
289  // I found that passing in an "until" 4th argument actually hurt performance, as it reduces
290  // the effectiveness of the cache that getNextPossibleStartTime uses.
291  const QDateTime startTime = job->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES,
292  evaluatingCurrentJob);
293  if (startTime.isValid())
294  {
295  if (nextJob == nullptr)
296  {
297  // We have no other solutions--this is our best solution so far.
298  nextStart = startTime;
299  nextJob = job;
300  if (nextInterruption) *nextInterruption = QDateTime();
301  interruptStr = "";
302  }
303  else
304  {
305  // Allow this job to be scheduled if it can run this many seconds
306  // before running into a higher priority job.
307  const int runSecs = evaluatingCurrentJob ? MAX_INTERRUPT_SECS : MIN_RUN_SECS;
308 
309  // Don't interrupt a START_AT for higher priority job
310  if (evaluatingCurrentJob && currentJobIsStartAt)
311  {
312  if (nextInterruption) *nextInterruption = QDateTime();
313  nextStart = startTime;
314  nextJob = job;
315  interruptStr = "";
316  }
317  else if (startTime.secsTo(nextStart) > runSecs)
318  {
319  // We can start a lower priority job if it can run for at least runSecs
320  // before getting bumped by the previous higher priority job.
321  if (nextInterruption) *nextInterruption = nextStart;
322  interruptStr = QString("interrupted by %1").arg(nextJob->getName());
323  nextStart = startTime;
324  nextJob = job;
325  }
326  }
327  // If scheduling, and we have a solution close enough to now, none of the lower priority
328  // jobs can possibly be scheduled.
329  if (!currentJob && nextStart.isValid() && now.secsTo(nextStart) < MIN_RUN_SECS)
330  break;
331  }
332  else if (evaluatingCurrentJob)
333  {
334  // No need to keep searching past the current job if we're evaluating it
335  // and it had no startTime. It needs to be stopped.
336  *when = QDateTime();
337  return nullptr;
338  }
339 
340  if (evaluatingCurrentJob) break;
341  }
342  if (nextJob != nullptr)
343  {
344  // The exception to the simple scheduling rules above are START_AT jobs, which
345  // are given highest priority, irrespective of order. If nextJob starts less than
346  // MIN_RUN_SECS before an on-time START_AT job, then give the START_AT job priority.
347  // However, in order for the START_AT job to interrupt a current job, it must start now.
348  for (int i = 0; i < jobs.size(); ++i)
349  {
350  SchedulerJob *atJob = jobs[i];
351  const QDateTime atTime = atJob->getFileStartupTime();
352  if (atJob->getFileStartupCondition() == SchedulerJob::START_AT && atTime.isValid())
353  {
354  if (!allowJob(atJob, rescheduleAbortsImmediate, rescheduleAbortsQueue, rescheduleErrors))
355  continue;
356  // If the job state is abort or error, might have to delay the first possible start time.
357  QDateTime startSearchingtAt = firstPossibleStart(
358  atJob, now, rescheduleAbortsQueue, abortDelaySeconds, rescheduleErrors,
359  errorDelaySeconds);
360  // atTime above is the user-specified start time. atJobStartTime is the time it can
361  // actually start, given all the constraints (altitude, twilight, etc).
362  const QDateTime atJobStartTime = atJob->getNextPossibleStartTime(startSearchingtAt, SCHEDULE_RESOLUTION_MINUTES, currentJob
363  && (atJob == currentJob));
364  if (atJobStartTime.isValid())
365  {
366  // This difference between the user-specified start time, and the time it can really start.
367  const double startDelta = atJobStartTime.secsTo(atTime);
368  if (fabs(startDelta) < 10 * 60)
369  {
370  // If we're looking for a new job to start, then give the START_AT priority
371  // if it's within 10 minutes of its user-specified time.
372  // However, if we're evaluating the current job (called from checkJob() above)
373  // then only interrupt it if the START_AT job can start very soon.
374  const int gap = currentJob == nullptr ? MIN_RUN_SECS : 30;
375  if (nextStart.secsTo(atJobStartTime) <= gap)
376  {
377  nextJob = atJob;
378  nextStart = atJobStartTime;
379  if (nextInterruption) *nextInterruption = QDateTime(); // Not interrupting atJob
380  }
381  else if (nextInterruption)
382  {
383  // The START_AT job was not chosen to start now, but it's still possible
384  // that this atJob will be an interrupter.
385  if (!nextInterruption->isValid() ||
386  atJobStartTime.secsTo(*nextInterruption) < 0)
387  {
388  *nextInterruption = atJobStartTime;
389  interruptStr = QString("interrupted by %1").arg(atJob->getName());
390  }
391  }
392  }
393  }
394  }
395  }
396  }
397  if (when != nullptr) *when = nextStart;
398  if (interruptReason != nullptr) *interruptReason = interruptStr;
399 
400  // Needed so display says "Idle" for unscheduled jobs.
401  // This will also happen in simulate, but that isn't called if nextJob is null.
402  // Must test for !nextJob. setState() inside unsetEvaluation has a nasty side effect
403  // of clearing the estimated time.
404  if (!nextJob)
405  unsetEvaluation(jobs);
406 
407  constexpr int twoDays = 48 * 3600;
408  if (fullSchedule && nextJob != nullptr)
409  simulate(jobs, now, now.addSecs(twoDays), capturedFramesCount);
410 
411  return nextJob;
412 }
413 
414 void GreedyScheduler::simulate(const QList<SchedulerJob *> &jobs, const QDateTime &time, const QDateTime &endTime,
415  const QMap<QString, uint16_t> *capturedFramesCount)
416 {
417  schedule.clear();
418 
419  // Make a deep copy of jobs
420  QList<SchedulerJob *> copiedJobs;
421  QList<SchedulerJob *> scheduledJobs;
422 
423  foreach (SchedulerJob *job, jobs)
424  {
425  SchedulerJob *newJob = new SchedulerJob();
426  // Make sure the copied class pointers aren't affected!
427  *newJob = *job;
428  // Don't want to affect the UI
429  newJob->setStatusCell(nullptr);
430  newJob->setStartupCell(nullptr);
431  copiedJobs.append(newJob);
432  job->setGreedyCompletionTime(QDateTime());
433  }
434 
435  // The number of jobs we have that can be scheduled,
436  // and the number of them where a simulated start has been scheduled.
437  int numStartupCandidates = 0, numStartups = 0;
438  // Reset the start times.
439  foreach (SchedulerJob *job, copiedJobs)
440  {
441  job->setStartupTime(QDateTime());
442  const auto state = job->getState();
443  if (state == SchedulerJob::JOB_SCHEDULED || state == SchedulerJob::JOB_EVALUATION ||
444  state == SchedulerJob::JOB_BUSY || state == SchedulerJob::JOB_IDLE)
445  numStartupCandidates++;
446  }
447 
448  QMap<QString, uint16_t> capturedFramesCopy;
449  if (capturedFramesCount != nullptr)
450  capturedFramesCopy = *capturedFramesCount;
451  QList<SchedulerJob *>simJobs =
452  prepareJobsForEvaluation(copiedJobs, time, capturedFramesCopy, nullptr, false);
453 
454  QDateTime simTime = time;
455  int iterations = 0;
456  QHash<SchedulerJob*, int> workDone;
457  for(int i = 0; i < simJobs.size(); ++i)
458  workDone[simJobs[i]] = 0.0;
459 
460  while (true)
461  {
462  QDateTime jobStartTime;
463  QDateTime jobInterruptTime;
464  QString interruptReason;
465  // Find the next job to be scheduled, when it starts, and when a higher priority
466  // job might preempt it, why it would be preempted.
467  // Note: 4th arg, fullSchedule, must be false or we'd loop forever.
468  SchedulerJob *selectedJob = selectNextJob(
469  simJobs, simTime, nullptr, false, &jobStartTime, &jobInterruptTime, &interruptReason);
470  if (selectedJob == nullptr)
471  break;
472 
473  // Are we past the end time?
474  if (endTime.isValid() && jobStartTime.secsTo(endTime) < 0) break;
475 
476  QString constraintReason;
477  // Get the time that this next job would fail its constraints, and a human-readable explanation.
478  QDateTime jobConstraintTime = selectedJob->getNextEndTime(jobStartTime, SCHEDULE_RESOLUTION_MINUTES, &constraintReason,
479  jobInterruptTime);
480  QDateTime jobCompletionTime;
481  if (selectedJob->getEstimatedTime() > 0)
482  {
483  // Estimate when the job might complete, if it was allowed to run without interruption.
484  const int timeLeft = selectedJob->getEstimatedTime() - workDone[selectedJob];
485  jobCompletionTime = jobStartTime.addSecs(timeLeft);
486  }
487  // Consider the 3 stopping times computed above (preemption, constraints missed, and completion),
488  // see which comes soonest, and set the jobStopTime and jobStopReason.
489  QDateTime jobStopTime = jobInterruptTime;
490  QString stopReason = jobStopTime.isValid() ? interruptReason : "";
491  if (jobConstraintTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobConstraintTime) < 0))
492  {
493  stopReason = constraintReason;
494  jobStopTime = jobConstraintTime;
495  }
496  if (jobCompletionTime.isValid() && (!jobStopTime.isValid() || jobStopTime.secsTo(jobCompletionTime) < 0))
497  {
498  stopReason = "job completion";
499  jobStopTime = jobCompletionTime;
500  }
501  // Increment the work done, for the next time this job might be scheduled in this simulation.
502  if (jobStopTime.isValid())
503  workDone[selectedJob] += jobStartTime.secsTo(jobStopTime);
504 
505  // Set the job's startupTime, but only for the first time the job will be scheduled.
506  // This will be used by the scheduler's UI when displaying the job schedules.
507  if (!selectedJob->getStartupTime().isValid())
508  {
509  numStartups++;
510  selectedJob->setStartupTime(jobStartTime);
511  selectedJob->setGreedyCompletionTime(jobStopTime);
512  selectedJob->setStopReason(stopReason);
513  selectedJob->setState(SchedulerJob::JOB_SCHEDULED);
514  scheduledJobs.append(selectedJob);
515  }
516 
517  // Compute if the simulated job should be considered complete because of work done.
518  if (selectedJob->getEstimatedTime() >= 0 &&
519  workDone[selectedJob] >= selectedJob->getEstimatedTime())
520  selectedJob->setState(SchedulerJob::JOB_COMPLETE);
521 
522  schedule.append(JobSchedule(jobs[copiedJobs.indexOf(selectedJob)], jobStartTime, jobStopTime, stopReason));
523  simTime = jobStopTime.addSecs(60);
524 
525  // End the simulation if we've crossed endTime, or no further jobs could be started,
526  // or if we've simply run too long.
527  if (!simTime.isValid()) break;
528  if (endTime.isValid() && simTime.secsTo(endTime) < 0) break;
529  if (++iterations > 20) break;
530  }
531 
532  // This simulation has been run using a deep-copy of the jobs list, so as not to interfere with
533  // some of their stored data. However, we do wish to update several fields of the "real" scheduleJobs.
534  // Note that the original jobs list and "copiedJobs" should be in the same order..
535  for (int i = 0; i < jobs.size(); ++i)
536  {
537  if (scheduledJobs.indexOf(copiedJobs[i]) >= 0)
538  {
539  jobs[i]->setState(SchedulerJob::JOB_SCHEDULED);
540  jobs[i]->setStartupTime(copiedJobs[i]->getStartupTime());
541  // Can't set the standard completionTime as it affects getEstimatedTime()
542  jobs[i]->setGreedyCompletionTime(copiedJobs[i]->getGreedyCompletionTime());
543  jobs[i]->setStopReason(copiedJobs[i]->getStopReason());
544  }
545  }
546  // This should go after above loop. unsetEvaluation calls setState() which clears
547  // certain fields from the state for IDLE states.
548  unsetEvaluation(jobs);
549 
550  return;
551 }
552 
553 void GreedyScheduler::unsetEvaluation(const QList<SchedulerJob *> &jobs)
554 {
555  for (int i = 0; i < jobs.size(); ++i)
556  {
557  if (jobs[i]->getState() == SchedulerJob::JOB_EVALUATION)
558  jobs[i]->setState(SchedulerJob::JOB_IDLE);
559  }
560 }
561 
562 QString GreedyScheduler::jobScheduleString(const JobSchedule &jobSchedule)
563 {
564  return QString("%1\t%2 --> %3 \t%4")
565  .arg(jobSchedule.job->getName(), -10)
566  .arg(jobSchedule.startTime.toString("MM/dd hh:mm"),
567  jobSchedule.stopTime.toString("hh:mm"), jobSchedule.stopReason);
568 }
569 
570 void GreedyScheduler::printSchedule(const QList<JobSchedule> &schedule)
571 {
572  foreach (auto &line, schedule)
573  {
574  fprintf(stderr, "%s\n", QString("%1 %2 --> %3 (%4)")
575  .arg(jobScheduleString(line)).toLatin1().data());
576  }
577 }
578 
579 } // namespace Ekos
void append(const T &value)
QDateTime addSecs(qint64 s) const const
Ekos is an advanced Astrophotography tool for Linux. It is based on a modular extensible framework to...
Definition: align.cpp:69
int size() const const
QString i18n(const char *text, const TYPE &arg...)
qint64 secsTo(const QDateTime &other) const const
int indexOf(const T &value, int from) const const
qint64 elapsed() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
static bool estimateJobTime(SchedulerJob *schedJob, const QMap< QString, uint16_t > &capturedFramesCount, Scheduler *scheduler)
estimateJobTime Estimates the time the job takes to complete based on the sequence file and what modu...
Definition: scheduler.cpp:5887
const QList< QKeySequence > & next()
bool isValid() const const
QString toString(Qt::DateFormat format) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Jun 6 2023 03:56:47 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.