Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

89 lines
2.6 KiB
Go

package progress
import (
"fmt"
"time"
)
// Phase enumerates the typed sub-phases of an index / enrich run, so
// progress labels are consistent across reporters and surfaces instead of
// ad-hoc strings. They double as human-readable stage labels.
type Phase string
const (
PhaseDiscover Phase = "discovering files"
PhaseParse Phase = "parsing"
PhaseResolve Phase = "resolving references"
PhaseInfer Phase = "linking symbols"
PhasePersist Phase = "persisting"
)
// FormatElapsed renders a duration as a compact human string: "45s",
// "5m 12s", "1h 20m".
func FormatElapsed(d time.Duration) string {
if d < 0 {
d = 0
}
switch {
case d < time.Minute:
return fmt.Sprintf("%ds", int(d.Seconds()+0.5))
case d < time.Hour:
m := int(d / time.Minute)
s := int((d % time.Minute) / time.Second)
return fmt.Sprintf("%dm %02ds", m, s)
default:
h := int(d / time.Hour)
m := int((d % time.Hour) / time.Minute)
return fmt.Sprintf("%dh %02dm", h, m)
}
}
// ETA returns the throughput (items/sec) and estimated time remaining given
// a start time and within-phase counters. A zero/unknown total yields a
// zero eta. Rate is 0 until some time and work have elapsed.
func ETA(start time.Time, current, total int) (itemsPerSec float64, eta time.Duration) {
elapsed := time.Since(start)
if elapsed <= 0 || current <= 0 {
return 0, 0
}
itemsPerSec = float64(current) / elapsed.Seconds()
if total > current && itemsPerSec > 0 {
remaining := float64(total - current)
eta = time.Duration(remaining/itemsPerSec) * time.Second
}
return itemsPerSec, eta
}
// ProgressStats bundles the derived progress metrics for a phase, ready to
// drop into a readiness / health payload.
type ProgressStats struct {
Phase string `json:"phase"`
Current int `json:"current"`
Total int `json:"total"`
Percent int `json:"percent"`
ItemsPerSec float64 `json:"items_per_sec"`
Elapsed string `json:"elapsed"`
ETA string `json:"eta,omitempty"`
}
// Stats computes the derived progress metrics for a phase from its start
// time and counters — percent, throughput, human-formatted elapsed, and a
// human-formatted ETA (omitted when the total is unknown).
func Stats(phase string, start time.Time, current, total int) ProgressStats {
rate, eta := ETA(start, current, total)
ps := ProgressStats{
Phase: phase,
Current: current,
Total: total,
ItemsPerSec: rate,
Elapsed: FormatElapsed(time.Since(start)),
}
if total > 0 {
ps.Percent = int(float64(current) / float64(total) * 100)
}
if eta > 0 {
ps.ETA = FormatElapsed(eta)
}
return ps
}