github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/util/progress.go (about)

     1  package util
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"time"
     7  )
     8  
     9  type ProgressCallbackType int
    10  
    11  const (
    12  	// Process is figuring out what to do
    13  	ProgressCalculate ProgressCallbackType = iota
    14  	// Process is transferring data
    15  	ProgressTransferBytes ProgressCallbackType = iota
    16  	// Process is skipping data because it's already up to date
    17  	ProgressSkip ProgressCallbackType = iota
    18  	// Process did not find the requested data, moving on
    19  	ProgressNotFound ProgressCallbackType = iota
    20  	// Non-fatal error
    21  	ProgressError ProgressCallbackType = iota
    22  )
    23  
    24  // Collected callback data for a progress operation
    25  type ProgressCallbackData struct {
    26  	// What stage of the process this is for, preparing, transferring or skipping something
    27  	Type ProgressCallbackType
    28  	// Either a general message or an item name (e.g. file name in download stage)
    29  	Desc string
    30  	// If applicable, how many bytes transferred for this item
    31  	ItemBytesDone int64
    32  	// If applicable, how many bytes comprise this item
    33  	ItemBytes int64
    34  	// The number of bytes transferred for all items
    35  	TotalBytesDone int64
    36  	// The number of bytes needed to transfer all of this process
    37  	TotalBytes int64
    38  }
    39  
    40  // Summarised results of some progress action
    41  type ProgressResults struct {
    42  	// Items transferred fully
    43  	TransferredCount int
    44  	// Items skipped (not needed)
    45  	SkippedCount int
    46  	// Items that failed (but did not stop process)
    47  	ErrorCount int
    48  	// Items which were not found in source
    49  	NotFoundCount int
    50  }
    51  
    52  // Callback when progress is made during process
    53  // return true to abort the (entire) process
    54  type ProgressCallback func(data *ProgressCallbackData) (abort bool)
    55  
    56  // Function to periodically (based on freq) report progress of a transfer process to the console
    57  // callbackChan must be a channel of updates which is being populated with ProgressCallbackData
    58  // from a goroutine at an unknown frequency. This function will then print updates every freq seconds
    59  // of the updates received so far, collapsing duplicates (in the case of very frequent transfer updates)
    60  // and filling in the blanks with an updated transfer rate in the case of no updates in the time.
    61  func ReportProgressToConsole(callbackChan <-chan *ProgressCallbackData, op string, freq time.Duration) *ProgressResults {
    62  	// Update the console once every half second regardless of how many callbacks
    63  	// (or zero callbacks, so we can reduce xfer rate)
    64  	tickChan := time.Tick(freq)
    65  	// samples of data transferred over the last 4 ticks (2s average)
    66  	transferRate := NewTransferRateCalculator(4)
    67  
    68  	var lastTotalBytesDone int64
    69  	var lastTime = time.Now()
    70  	var lastProgress *ProgressCallbackData
    71  	complete := false
    72  	lastConsoleLineLen := 0
    73  	results := &ProgressResults{}
    74  	for _ = range tickChan {
    75  		// We run this every 0.5s
    76  		var finalDownloadProgress *ProgressCallbackData
    77  		for stop := false; !stop && !complete; {
    78  			select {
    79  			case data := <-callbackChan:
    80  				if data == nil {
    81  					// channel was closed, we've finished
    82  					stop = true
    83  					complete = true
    84  					break
    85  				}
    86  				// Some progress data is available
    87  				// May get many of these and we only want to display the last one
    88  				// unless it's general infoo or we're in verbose mode
    89  				switch data.Type {
    90  				case ProgressCalculate:
    91  					finalDownloadProgress = nil
    92  					LogConsole(data.Desc)
    93  				case ProgressError:
    94  					finalDownloadProgress = nil
    95  					LogConsole(data.Desc)
    96  				case ProgressSkip:
    97  					finalDownloadProgress = nil
    98  					results.SkippedCount++
    99  					// Only print if verbose
   100  					LogConsoleDebugf("Skipped: %v (Up to date)\n", data.Desc)
   101  				case ProgressNotFound:
   102  					finalDownloadProgress = nil
   103  					results.NotFoundCount++
   104  					LogConsolef("Not found: %v (Continuing)\n", data.Desc)
   105  				case ProgressTransferBytes:
   106  					finalDownloadProgress = data
   107  					// Print completion in verbose mode
   108  					if data.ItemBytesDone == data.ItemBytes {
   109  						results.TransferredCount++
   110  						if GlobalOptions.Verbose {
   111  							msg := fmt.Sprintf("%ved: %v 100%%", op, data.Desc)
   112  							LogConsoleOverwrite(msg, lastConsoleLineLen)
   113  							lastConsoleLineLen = len(msg)
   114  							// Clear line on completion in verbose mode
   115  							// Don't do this as \n in string above since we need to clear spaces after
   116  							LogConsole("")
   117  							finalDownloadProgress = nil
   118  							lastProgress = nil
   119  						}
   120  					}
   121  				}
   122  			default:
   123  				// No (more) progress data
   124  				stop = true
   125  			}
   126  		}
   127  		// Write progress data for this 0.5s if relevant
   128  		// If either we have new progress data, or unfinished progress data from previous
   129  		if finalDownloadProgress != nil || lastProgress != nil {
   130  			var bytesPerSecond int64
   131  			if finalDownloadProgress != nil && finalDownloadProgress.ItemBytes != 0 && finalDownloadProgress.TotalBytes != 0 {
   132  				lastProgress = finalDownloadProgress
   133  				bytesDoneThisTick := finalDownloadProgress.TotalBytesDone - lastTotalBytesDone
   134  				lastTotalBytesDone = finalDownloadProgress.TotalBytesDone
   135  				seconds := float32(time.Since(lastTime).Seconds())
   136  				if seconds > 0 {
   137  					bytesPerSecond = int64(float32(bytesDoneThisTick) / seconds)
   138  				}
   139  			} else {
   140  				// Actually the default but lets be specific
   141  				bytesPerSecond = 0
   142  			}
   143  			// Calculate transfer rate
   144  			transferRate.AddSample(bytesPerSecond)
   145  			avgRate := transferRate.Average()
   146  			lastTime = time.Now()
   147  
   148  			if lastProgress.ItemBytes != 0 || lastProgress.TotalBytes != 0 {
   149  				buf := bytes.NewBufferString(fmt.Sprintf("%ving: ", op))
   150  				if lastProgress.ItemBytes > 0 && GlobalOptions.Verbose {
   151  					itemPercent := int((100 * lastProgress.ItemBytesDone) / lastProgress.ItemBytes)
   152  					buf.WriteString(fmt.Sprintf("%v %d%%", lastProgress.Desc, itemPercent))
   153  					if lastProgress.TotalBytes != 0 {
   154  						buf.WriteString(" Overall: ")
   155  					}
   156  				}
   157  				if lastProgress.TotalBytes > 0 && avgRate > 0 {
   158  					overallPercent := int((100 * lastProgress.TotalBytesDone) / lastProgress.TotalBytes)
   159  					bytesRemaining := lastProgress.TotalBytes - lastProgress.TotalBytesDone
   160  					secondsRemaining := bytesRemaining / avgRate
   161  					timeRemaining := time.Duration(secondsRemaining) * time.Second
   162  
   163  					buf.WriteString(fmt.Sprintf("%d%% (%v ETA %v)", overallPercent, FormatTransferRate(avgRate), timeRemaining))
   164  				}
   165  				msg := buf.String()
   166  				LogConsoleOverwrite(msg, lastConsoleLineLen)
   167  				lastConsoleLineLen = len(msg)
   168  			}
   169  		}
   170  
   171  		if complete {
   172  			break
   173  		}
   174  
   175  	}
   176  	if !GlobalOptions.Verbose && lastConsoleLineLen > 0 {
   177  		// Write final line with newline
   178  		LogConsoleOverwrite(fmt.Sprintf("%ving: 100%%", op), lastConsoleLineLen)
   179  		LogConsole("")
   180  	}
   181  	return results
   182  
   183  }