- Created internal/progress/estimator.go with ETAEstimator component - Tracks elapsed time and estimates remaining time based on progress - Enhanced Spinner and LineByLine indicators to display ETA info - Integrated into BackupCluster and RestoreCluster functions - Display format: 'Operation | X/Y (Z%) | Elapsed: Xm | ETA: ~Ym remaining' - Preserves spinner animation while showing progress/time estimates - Quick Win approach: no historical data storage, just current operation tracking
146 lines
3.7 KiB
Go
146 lines
3.7 KiB
Go
package progress
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ETAEstimator calculates estimated time remaining for operations
|
|
type ETAEstimator struct {
|
|
startTime time.Time
|
|
operation string
|
|
totalItems int
|
|
itemsComplete int
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
// NewETAEstimator creates a new ETA estimator
|
|
func NewETAEstimator(operation string, totalItems int) *ETAEstimator {
|
|
now := time.Now()
|
|
return &ETAEstimator{
|
|
startTime: now,
|
|
operation: operation,
|
|
totalItems: totalItems,
|
|
itemsComplete: 0,
|
|
lastUpdate: now,
|
|
}
|
|
}
|
|
|
|
// UpdateProgress updates the completed item count
|
|
func (e *ETAEstimator) UpdateProgress(itemsComplete int) {
|
|
e.itemsComplete = itemsComplete
|
|
e.lastUpdate = time.Now()
|
|
}
|
|
|
|
// GetElapsed returns elapsed time since start
|
|
func (e *ETAEstimator) GetElapsed() time.Duration {
|
|
return time.Since(e.startTime)
|
|
}
|
|
|
|
// GetETA calculates estimated time remaining
|
|
func (e *ETAEstimator) GetETA() time.Duration {
|
|
if e.itemsComplete == 0 || e.totalItems == 0 {
|
|
return 0
|
|
}
|
|
|
|
elapsed := e.GetElapsed()
|
|
avgTimePerItem := elapsed / time.Duration(e.itemsComplete)
|
|
remainingItems := e.totalItems - e.itemsComplete
|
|
|
|
return avgTimePerItem * time.Duration(remainingItems)
|
|
}
|
|
|
|
// GetProgress returns current progress as percentage
|
|
func (e *ETAEstimator) GetProgress() float64 {
|
|
if e.totalItems == 0 {
|
|
return 0
|
|
}
|
|
return float64(e.itemsComplete) / float64(e.totalItems) * 100
|
|
}
|
|
|
|
// FormatElapsed returns formatted elapsed time (e.g., "25m 30s")
|
|
func (e *ETAEstimator) FormatElapsed() string {
|
|
return FormatDuration(e.GetElapsed())
|
|
}
|
|
|
|
// FormatETA returns formatted ETA (e.g., "~40m remaining")
|
|
func (e *ETAEstimator) FormatETA() string {
|
|
eta := e.GetETA()
|
|
if eta == 0 {
|
|
return "calculating..."
|
|
}
|
|
return "~" + FormatDuration(eta) + " remaining"
|
|
}
|
|
|
|
// FormatProgress returns formatted progress string (e.g., "5/13 (38%)")
|
|
func (e *ETAEstimator) FormatProgress() string {
|
|
return fmt.Sprintf("%d/%d (%.0f%%)", e.itemsComplete, e.totalItems, e.GetProgress())
|
|
}
|
|
|
|
// GetFullStatus returns complete status line with all info
|
|
func (e *ETAEstimator) GetFullStatus(baseMessage string) string {
|
|
if e.totalItems == 0 {
|
|
// No items to track, just show elapsed
|
|
return fmt.Sprintf("%s | Elapsed: %s", baseMessage, e.FormatElapsed())
|
|
}
|
|
|
|
if e.itemsComplete == 0 {
|
|
// Just started
|
|
return fmt.Sprintf("%s | 0/%d | Starting...", baseMessage, e.totalItems)
|
|
}
|
|
|
|
// Full status with progress and ETA
|
|
return fmt.Sprintf("%s | %s | Elapsed: %s | ETA: %s",
|
|
baseMessage,
|
|
e.FormatProgress(),
|
|
e.FormatElapsed(),
|
|
e.FormatETA())
|
|
}
|
|
|
|
// FormatDuration formats a duration in human-readable format
|
|
func FormatDuration(d time.Duration) string {
|
|
if d < time.Second {
|
|
return "< 1s"
|
|
}
|
|
|
|
hours := int(d.Hours())
|
|
minutes := int(d.Minutes()) % 60
|
|
seconds := int(d.Seconds()) % 60
|
|
|
|
if hours > 0 {
|
|
if minutes > 0 {
|
|
return fmt.Sprintf("%dh %dm", hours, minutes)
|
|
}
|
|
return fmt.Sprintf("%dh", hours)
|
|
}
|
|
|
|
if minutes > 0 {
|
|
if seconds > 5 { // Only show seconds if > 5
|
|
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
|
}
|
|
return fmt.Sprintf("%dm", minutes)
|
|
}
|
|
|
|
return fmt.Sprintf("%ds", seconds)
|
|
}
|
|
|
|
// EstimateSizeBasedDuration estimates duration based on size (fallback when no progress tracking)
|
|
func EstimateSizeBasedDuration(sizeBytes int64, cores int) time.Duration {
|
|
sizeMB := float64(sizeBytes) / (1024 * 1024)
|
|
|
|
// Base estimate: ~100MB per minute on average hardware
|
|
baseMinutes := sizeMB / 100.0
|
|
|
|
// Adjust for CPU cores (more cores = faster, but not linear)
|
|
// Use square root to represent diminishing returns
|
|
if cores > 1 {
|
|
speedup := 1.0 + (0.3 * (float64(cores) - 1)) // 30% improvement per core
|
|
baseMinutes = baseMinutes / speedup
|
|
}
|
|
|
|
// Add 20% buffer for safety
|
|
baseMinutes = baseMinutes * 1.2
|
|
|
|
return time.Duration(baseMinutes * float64(time.Minute))
|
|
}
|