Progress and time remaining

By R. S. Doiel, 2022-11-05

I often find myself logging output when I'm developing tools. This is typically the case where I am iterating over data and transforming it. Overtime I've come to realize I really want a few specific pieces of information for non-error logging (e.g. -verbose which monitors progress as well as errors).

To do that I need three pieces of information.

  1. the count of the current iteration(e.g. i)
  2. the total number of iterations required (e.g. tot)
  3. The time just before I started iterating(e.g. t0)

The values for i and tot let me compute the percent completed. The percent completed is trivial (i/tot) * 100.0. Note on the first pass (i.e. i == 0) you can skip the percentage calculation.

import (
      	"time"
      	"fmt"
      )
      
      // Show progress with amount of time running
      func progress(t0 time.Time, i int, tot int) string {
          if i == 0 {
              return ""
          }
      	percent := (float64(i) / float64(tot)) * 100.0
      	t1 := time.Now()
      	// NOTE: Truncating the duration to seconds
      	return fmt.Sprintf("%.2f%% %v", percent, t1.Sub(t0).Truncate(time.Second))
      }
      

Here's how you might use it.

	tot := len(ids)
      	t0 := time.Now()
      	for i, id := range ids {
      		// ... processing stuff here ... and display progress every 1000 records
      		if (i % 1000) == 0 {
      			log.Printf("%s records processed", progress(t0, i, tot))
      		}
      	}
      

An improvement on this is to include an time remaining. I need to calculated the estimated time allocation (i.e. ETA). I know t0 so I can estimate that with this formula estimated time allocation = (((current running time since t0)/ the number of items processed) * total number of items)1. ETA adjusted for time running gives us time remaining2. The first pass of the function progress has a trivial optimization since we don't have enough delta t0 to compute an estimate. Calls after that are computed using our formula.

func progress(t0 time.Time, i int, tot int) string {
      	if i == 0 {
      		return "0.00 ETR Unknown"
      	}
      	// percent completed
      	percent := (float64(i) / float64(tot)) * 100.0
      	// running time
          rt := time.Now().Sub(t0)
          // estimated time allocation - running time = time remaining
          eta := time.Duration((float64(rt)/float64(i)*float64(tot)) - float64(rt))
          return fmt.Sprintf("%.2f%% ETR %v", percent, eta.Truncate(time.Second))
      }
      

  1. In code (rt/i)*tot is estimated time allocation ↩︎

  2. Estimated Time Remaining, in code ((rt/i)*tot) - rt ↩︎