Displaying progress with time estimations in Common Lisp

04 Mar 2023 -

I’m using a variation of cl-progress-bar library for displaying progress of long-running tasks. Although instead of printing a progress bar, a text with completion percentage, speed, elapsed and estimated remaining time is printed. I think it is very useful for long-running tasks. I’m using it for some database imports at the moment.

Output looks like this:

Mapping table: REL_ORGANIZATION_PERSON_EMPLOYEES_IS_EMPLOYED_BY
Progress: 2% (100 of 4369) at 163.39763/sec. Elapsed: 0 seconds. Remaining: 26 seconds.
Progress: 6% (300 of 4369) at 345.6205/sec. Elapsed: 0 seconds. Remaining: 11 seconds.
Progress: 11% (500 of 4369) at 413.90558/sec. Elapsed: 1 seconds. Remaining: 9 seconds.
Progress: 18% (800 of 4369) at 505.04794/sec. Elapsed: 1 seconds. Remaining: 7 seconds.
Progress: 22% (1000 of 4369) at 502.00577/sec. Elapsed: 1 seconds. Remaining: 6 seconds.
Progress: 25% (1100 of 4369) at 486.7235/sec. Elapsed: 2 seconds. Remaining: 6 seconds.

Implementation:

(defpackage estimated-time-progress
  (:local-nicknames
   (:pb :cl-progress-bar.progress))
  (:use :cl)
  (:export :with-estimated-time-progress))

(in-package :estimated-time-progress)

(defconstant +seconds-in-one-hour+ 3600)
(defconstant +seconds-in-one-minute+ 60)

(defun format-time-in-seconds-minutes-hours (stream in-seconds)
  (when (>= in-seconds +seconds-in-one-hour+)
    (let* ((hours (floor (/ in-seconds +seconds-in-one-hour+))))
      (decf in-seconds (* hours +seconds-in-one-hour+))
      (format stream " ~a hour~p " hours hours)))
  (when (>= in-seconds +seconds-in-one-minute+)
    (let* ((minutes (floor (/ in-seconds +seconds-in-one-minute+))))
      (decf in-seconds (* minutes +seconds-in-one-minute+))
      (format stream "~a minute~p " minutes minutes)))
  (unless (zerop in-seconds)
    (format stream "~d seconds" (truncate in-seconds))))

;; (format-time-in-seconds-minutes-hours t 160)


(defclass estimated-time-progress (pb::progress-bar)
  ()
  (:documentation "Displays progress estimated time for completion."))

(defmethod pb::update-display ((progress-bar estimated-time-progress))
  (incf (pb::progress progress-bar) (pb::pending progress-bar))
  (setf (pb::pending progress-bar) 0)
  (setf (pb::last-update-time progress-bar) (get-internal-real-time))
  (unless (zerop (pb::progress progress-bar))
    (let ((current-time (- (pb::last-update-time progress-bar)
                           (pb::start-time progress-bar))))
      (format t "Progress: ~d% (~a of ~a) at ~f/sec. "
              (truncate
               (* (/ (pb::progress progress-bar)
                     (pb::total progress-bar)) 100))
              (pb::progress progress-bar)
              (pb::total progress-bar)
	      (* (/ (pb::progress progress-bar)
		    current-time)
		 1000000))
      (write-string "Elapsed: ")
      (format-time-in-seconds-minutes-hours t (/ current-time 1000000))
      (write-string ". ")
      (let ((estimated-seconds (/
                                (-
                                 (/ (* (pb::total progress-bar)
                                       current-time)
                                    (pb::progress progress-bar))
                                 current-time)
                                1000000)))
        (write-string "Remaining: ")
        (format-time-in-seconds-minutes-hours t estimated-seconds)
        (write-string "."))
      (terpri)
      (finish-output))))

(defmacro with-estimated-time-progress ((steps-count description &rest desc-args) &body body)
  (let ((!old-bar (gensym)))
    `(let* ((,!old-bar cl-progress-bar::*progress-bar*)
            (cl-progress-bar::*progress-bar* (or ,!old-bar
                                                 (when cl-progress-bar::*progress-bar-enabled*
                                                   (make-instance 'estimated-time-progress :total ,steps-count)))))
       (unless (eq ,!old-bar cl-progress-bar::*progress-bar*)
         (fresh-line)
         (format t ,description ,@desc-args)
         (cl-progress-bar.progress:start-display cl-progress-bar::*progress-bar*))
       (prog1 (progn ,@body)
         (unless (eq ,!old-bar cl-progress-bar::*progress-bar*)
           (cl-progress-bar.progress:finish-display cl-progress-bar::*progress-bar*))))))
		   

Use:

(defun perform-step () ; Calls to the update can occur anywhere.
  (sleep 1.7)
  (cl-progress-bar:update 1))

(with-estimated-time-progress (5 "This is just a example. Number of steps is ~a." 5)
  (dotimes (i 5) (perform-step)))

Page design by Ankit Sultana