github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/state/timeestimation.go (about)

     1  package state
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
     8  
     9  	"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    10  	"github.com/jfrog/jfrog-client-go/utils/log"
    11  )
    12  
    13  const (
    14  	milliSecsInSecond               = 1000
    15  	bytesPerMilliSecToMBPerSec      = float64(milliSecsInSecond) / float64(utils.SizeMiB)
    16  	minTransferTimeToShowEstimation = time.Minute * 5
    17  )
    18  
    19  type timeTypeSingular string
    20  
    21  const (
    22  	day    timeTypeSingular = "day"
    23  	hour   timeTypeSingular = "hour"
    24  	minute timeTypeSingular = "minute"
    25  )
    26  
    27  var numOfSpeedsToKeepPerWorkingThread = 10
    28  
    29  type TimeEstimationManager struct {
    30  	// Speeds of the last done chunks, in bytes/ms
    31  	LastSpeeds []float64 `json:"last_speeds,omitempty"`
    32  	// Sum of the speeds in LastSpeeds. The speeds are in bytes/ms.
    33  	LastSpeedsSum float64 `json:"last_speeds_sum,omitempty"`
    34  	// The last calculated sum of speeds, in bytes/ms
    35  	SpeedsAverage float64 `json:"speeds_average,omitempty"`
    36  	// Total transferred bytes since the beginning of the current transfer execution
    37  	CurrentTotalTransferredBytes uint64 `json:"current_total_transferred_bytes,omitempty"`
    38  	// The state manager
    39  	stateManager *TransferStateManager
    40  }
    41  
    42  func (tem *TimeEstimationManager) AddChunkStatus(chunkStatus api.ChunkStatus, durationMillis int64) {
    43  	if durationMillis == 0 {
    44  		return
    45  	}
    46  
    47  	tem.addDataChunkStatus(chunkStatus, durationMillis)
    48  }
    49  
    50  func (tem *TimeEstimationManager) addDataChunkStatus(chunkStatus api.ChunkStatus, durationMillis int64) {
    51  	var chunkSizeBytes int64
    52  	for _, file := range chunkStatus.Files {
    53  		if file.Status != api.Fail {
    54  			tem.CurrentTotalTransferredBytes += uint64(file.SizeBytes)
    55  		}
    56  		if (file.Status == api.Success || file.Status == api.SkippedLargeProps) && !file.ChecksumDeployed {
    57  			chunkSizeBytes += file.SizeBytes
    58  		}
    59  	}
    60  
    61  	// If no files were uploaded regularly (with no errors and not checksum-deployed), don't use this chunk for the time estimation calculation.
    62  	if chunkSizeBytes == 0 {
    63  		return
    64  	}
    65  
    66  	workingThreads, err := tem.stateManager.GetWorkingThreads()
    67  	if err != nil {
    68  		log.Error("Couldn't calculate time estimation:", err.Error())
    69  		return
    70  	}
    71  	speed := calculateChunkSpeed(workingThreads, chunkSizeBytes, durationMillis)
    72  	tem.LastSpeeds = append(tem.LastSpeeds, speed)
    73  	tem.LastSpeedsSum += speed
    74  	lastSpeedsSliceLen := workingThreads * numOfSpeedsToKeepPerWorkingThread
    75  	for len(tem.LastSpeeds) > lastSpeedsSliceLen {
    76  		// Remove the oldest calculated speed
    77  		tem.LastSpeedsSum -= tem.LastSpeeds[0]
    78  		tem.LastSpeeds = tem.LastSpeeds[1:]
    79  	}
    80  	if len(tem.LastSpeeds) == 0 {
    81  		tem.SpeedsAverage = 0
    82  		return
    83  	}
    84  	// Calculate speed in bytes/ms
    85  	tem.SpeedsAverage = tem.LastSpeedsSum / float64(len(tem.LastSpeeds))
    86  }
    87  
    88  func calculateChunkSpeed(workingThreads int, chunkSizeSum, chunkDuration int64) float64 {
    89  	return float64(workingThreads) * float64(chunkSizeSum) / float64(chunkDuration)
    90  }
    91  
    92  // getSpeed gets the transfer speed, in MB/s.
    93  func (tem *TimeEstimationManager) getSpeed() float64 {
    94  	// Convert from bytes/ms to MB/s
    95  	return tem.SpeedsAverage * bytesPerMilliSecToMBPerSec
    96  }
    97  
    98  // GetSpeedString gets the transfer speed as an easy-to-read string.
    99  func (tem *TimeEstimationManager) GetSpeedString() string {
   100  	if len(tem.LastSpeeds) == 0 {
   101  		return "Not available yet"
   102  	}
   103  	return fmt.Sprintf("%.3f MB/s", tem.getSpeed())
   104  }
   105  
   106  // GetEstimatedRemainingTimeString gets the estimated remaining time as an easy-to-read string.
   107  // Return "Not available yet" in the following cases:
   108  // 1. 5 minutes not passed since the beginning of the transfer
   109  // 2. No files transferred
   110  // 3. The transfer speed is less than 1 byte per second
   111  func (tem *TimeEstimationManager) GetEstimatedRemainingTimeString() string {
   112  	remainingTimeSec := tem.getEstimatedRemainingSeconds()
   113  	if remainingTimeSec == 0 {
   114  		return "Not available yet"
   115  	}
   116  
   117  	return SecondsToLiteralTime(int64(remainingTimeSec), "About ")
   118  }
   119  
   120  func (tem *TimeEstimationManager) getEstimatedRemainingSeconds() uint64 {
   121  	if tem.CurrentTotalTransferredBytes == 0 {
   122  		// No files transferred
   123  		return 0
   124  	}
   125  	duration := time.Since(tem.stateManager.startTimestamp)
   126  	if duration < minTransferTimeToShowEstimation {
   127  		// 5 minutes not yet passed
   128  		return 0
   129  	}
   130  
   131  	transferredBytesInSeconds := tem.CurrentTotalTransferredBytes / uint64(duration.Seconds())
   132  	if transferredBytesInSeconds == 0 {
   133  		// Less than 1 byte per second
   134  		return 0
   135  	}
   136  	remainingBytes := tem.stateManager.OverallTransfer.TotalSizeBytes - tem.stateManager.OverallTransfer.TransferredSizeBytes
   137  	return uint64(remainingBytes) / transferredBytesInSeconds
   138  }