mirror of
https://github.com/samuelncui/yatm.git
synced 2026-11-08 04:18:03 +00:00
Unify file queries and Scan execution, retain indexed Identical results, improve Job and Preview workflows, and verify release packaging.
160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
package executor
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/samuelncui/yatm/entity"
|
|
)
|
|
|
|
const SpeedLen = 30
|
|
|
|
type speedEvent struct {
|
|
bytes int64
|
|
time time.Time
|
|
}
|
|
|
|
type Progress struct {
|
|
// stage is the attempt-local sampling window; it is created on first use so a Progress value
|
|
// that a runner constructs directly still samples with the default clock.
|
|
stage *stageMeter
|
|
stageOnce sync.Once
|
|
startTime time.Time
|
|
now func() time.Time
|
|
|
|
// atomic fields
|
|
totalBytes, totalFiles int64
|
|
baseBytes, baseFiles int64
|
|
sessionBytes, sessionFiles int64
|
|
sessionStartedAt int64
|
|
historicalBytes int64
|
|
historicalDurationNanoseconds int64
|
|
speed int64
|
|
|
|
// locked fields
|
|
lock sync.Mutex
|
|
speedEvents []speedEvent
|
|
speedLen int
|
|
speedIdx int
|
|
}
|
|
|
|
// SetClock replaces the clock this Progress and its stage window read. It exists so a test can fix
|
|
// attempt timing; production always uses the wall clock NewProgress installed.
|
|
func (p *Progress) SetClock(now func() time.Time) {
|
|
p.now = now
|
|
p.stageOnce = sync.Once{}
|
|
p.stage = nil
|
|
}
|
|
|
|
func NewProgress() *Progress {
|
|
return &Progress{
|
|
startTime: time.Now(),
|
|
now: time.Now,
|
|
speedEvents: make([]speedEvent, SpeedLen),
|
|
speedLen: SpeedLen,
|
|
}
|
|
}
|
|
|
|
func (p *Progress) ToEntity() *entity.Progress {
|
|
now := p.now()
|
|
sessionBytes := atomic.LoadInt64(&p.sessionBytes)
|
|
sessionDuration := time.Duration(0)
|
|
if startedAt := atomic.LoadInt64(&p.sessionStartedAt); startedAt > 0 {
|
|
sessionDuration = now.Sub(time.Unix(0, startedAt))
|
|
}
|
|
historicalBytes := atomic.LoadInt64(&p.historicalBytes) + sessionBytes
|
|
historicalDuration := time.Duration(atomic.LoadInt64(&p.historicalDurationNanoseconds)) + sessionDuration
|
|
return &entity.Progress{
|
|
TotalBytes: atomic.LoadInt64(&p.totalBytes),
|
|
TotalFileCount: atomic.LoadInt64(&p.totalFiles),
|
|
CopiedBytes: atomic.LoadInt64(&p.baseBytes) + sessionBytes,
|
|
CopiedFileCount: atomic.LoadInt64(&p.baseFiles) + atomic.LoadInt64(&p.sessionFiles),
|
|
SpeedBytesPerSecond: atomic.LoadInt64(&p.speed),
|
|
StartedAtNs: p.startTime.UnixNano(),
|
|
AverageSpeedBytesPerSecond: bytesPerSecond(sessionBytes, sessionDuration),
|
|
HistoricalAverageSpeedBytesPerSecond: bytesPerSecond(historicalBytes, historicalDuration),
|
|
TotalKnown: true,
|
|
}
|
|
}
|
|
|
|
func (p *Progress) SetGlobalTotal(bytes, files int64) {
|
|
atomic.StoreInt64(&p.totalBytes, bytes)
|
|
atomic.StoreInt64(&p.totalFiles, files)
|
|
}
|
|
|
|
func (p *Progress) SetGlobalCopied(bytes, files int64) {
|
|
atomic.StoreInt64(&p.baseBytes, bytes)
|
|
atomic.StoreInt64(&p.baseFiles, files)
|
|
}
|
|
|
|
func (p *Progress) StartSession() {
|
|
atomic.CompareAndSwapInt64(&p.sessionStartedAt, 0, p.now().UnixNano())
|
|
}
|
|
|
|
func (p *Progress) UpdateSessionCurrent(bytes, files int64) {
|
|
now := p.now()
|
|
p.StartSession()
|
|
atomic.StoreInt64(&p.sessionBytes, bytes)
|
|
atomic.StoreInt64(&p.sessionFiles, files)
|
|
|
|
// Derive the moving speed from the bounded event ring.
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
p.speedEvents[p.speedIdx] = speedEvent{bytes: bytes, time: now}
|
|
for earliest := p.speedIdx; ; {
|
|
earliest++
|
|
if earliest >= p.speedLen {
|
|
earliest = 0
|
|
}
|
|
if earliest == p.speedIdx {
|
|
break
|
|
}
|
|
|
|
if !p.speedEvents[earliest].time.IsZero() {
|
|
atomic.StoreInt64(&p.speed, bytesPerSecond(
|
|
bytes-p.speedEvents[earliest].bytes,
|
|
now.Sub(p.speedEvents[earliest].time),
|
|
))
|
|
break
|
|
}
|
|
}
|
|
|
|
p.speedIdx++
|
|
if p.speedIdx >= p.speedLen {
|
|
p.speedIdx = 0
|
|
}
|
|
}
|
|
|
|
func (p *Progress) CommitSession() {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
// Move the completed attempt into the global counters and observed throughput history.
|
|
bytes := atomic.SwapInt64(&p.sessionBytes, 0)
|
|
atomic.AddInt64(&p.baseBytes, bytes)
|
|
atomic.AddInt64(&p.baseFiles, atomic.SwapInt64(&p.sessionFiles, 0))
|
|
if startedAt := atomic.SwapInt64(&p.sessionStartedAt, 0); startedAt > 0 {
|
|
duration := p.now().Sub(time.Unix(0, startedAt))
|
|
if duration > 0 && bytes > 0 {
|
|
atomic.AddInt64(&p.historicalBytes, bytes)
|
|
atomic.AddInt64(&p.historicalDurationNanoseconds, duration.Nanoseconds())
|
|
}
|
|
}
|
|
atomic.StoreInt64(&p.speed, 0)
|
|
|
|
// Clear the attempt-local moving-speed window.
|
|
for i := range p.speedEvents {
|
|
p.speedEvents[i] = speedEvent{}
|
|
}
|
|
p.speedIdx = 0
|
|
}
|
|
|
|
func bytesPerSecond(bytes int64, duration time.Duration) int64 {
|
|
if bytes <= 0 || duration <= 0 {
|
|
return 0
|
|
}
|
|
return int64(float64(bytes) / duration.Seconds())
|
|
}
|