Skip to content

Commit

Permalink
add instant download speed
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Zha <[email protected]>
  • Loading branch information
qweeah committed Oct 8, 2023
1 parent f49bdcb commit 8bc5c37
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 29 deletions.
2 changes: 1 addition & 1 deletion cmd/oras/internal/display/progress/humanize.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type bytes struct {
func ToBytes(sizeInBytes int64) bytes {
f := float64(sizeInBytes)
if f < base {
return bytes{f, "B"}
return bytes{f, units[0]}
}
e := math.Floor(math.Log(f) / math.Log(base))
p := f / math.Pow(base, e)
Expand Down
2 changes: 1 addition & 1 deletion cmd/oras/internal/display/progress/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Manager interface {
Close() error
}

const bufFlushDuration = 100 * time.Millisecond
const bufFlushDuration = 500 * time.Millisecond

type manager struct {
status []*status
Expand Down
69 changes: 44 additions & 25 deletions cmd/oras/internal/display/progress/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,34 @@ import (
)

const (
barLength = 40
barLength = 20
speedLength = 8
zeroDuration = "0s" // default zero value of time.Duration.String()
zeroStatus = "loading status..."
zeroDigest = " └─ loading digest..."
)

// status is used as message to update progress view.
type status struct {
done bool // done is true when the end time is set
prompt string
descriptor ocispec.Descriptor
offset int64
total bytes
done bool // done is true when the end time is set
prompt string
descriptor ocispec.Descriptor
offset int64
total bytes
lastOffset int64
lastRenderTime time.Time

startTime time.Time
endTime time.Time
mark spinner
lock sync.RWMutex
lock sync.Mutex
}

// newStatus generates a base empty status.
func newStatus() *status {
return &status{
offset: -1,
offset: -1,
lastRenderTime: time.Now(),
}
}

Expand Down Expand Up @@ -85,8 +89,8 @@ func (s *status) isZero() bool {

// String returns human-readable TTY strings of the status.
func (s *status) String(width int) (string, string) {
s.lock.RLock()
defer s.lock.RUnlock()
s.lock.Lock()
defer s.lock.Unlock()

if s.isZero() {
return zeroStatus, zeroDigest
Expand All @@ -103,33 +107,35 @@ func (s *status) String(width int) (string, string) {
name = s.descriptor.MediaType
}

// format: [left--------------------------------][margin][right---------------------------------]
// mark(1) bar(42) action(<10) name(<126) size_per_size(<=11) percent(8) time(>=6)
// format: [left--------------------------------------------][margin][right---------------------------------]
// mark(1) bar(22) speed(8) action(<=11) name(<126) size_per_size(<=13) percent(8) time(>=6)
// └─ digest(72)
var offset string
switch percent {
case 1: // 100%, show exact size
offset = fmt.Sprint(s.total.size)
default: // 0% ~ 99%, show 2-digit precision
offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent))
}
right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString())
lenRight := utf8.RuneCountInString(right)

var left string
lenLeft := 0
if !s.done {
lenBar := int(percent * barLength)
bar := fmt.Sprintf("[%s%s]", aec.Inverse.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar))
mark := s.mark.symbol()
left = fmt.Sprintf("%c %s %s %s", mark, bar, s.prompt, name)
// bar + wrapper(2) + space(1) = len(bar) + 3
lenLeft = barLength + 3
speed := s.calculateSpeed()
speedStr := fmt.Sprintf("%v%s/s", speed.size, speed.unit)
left = fmt.Sprintf("%c %s(%*s) %s %s", s.mark.symbol(), bar, speedLength, speedStr, s.prompt, name)
// bar + wrapper(2) + space(1) + speed + wrapper(2) = len(bar) + len(speed) + 5
lenLeft = barLength + speedLength + 5
} else {
left = fmt.Sprintf("√ %s %s", s.prompt, name)
}
// mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3
lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3

var offset string
switch percent {
case 1: // 100%, show exact size
offset = fmt.Sprint(s.total.size)
default: // 0% ~ 99%, show 2-digit precision
offset = fmt.Sprintf("%.2f", RoundTo(s.total.size*percent))
}
right := fmt.Sprintf(" %s/%v %s %6.2f%% %6s", offset, s.total.size, s.total.unit, percent*100, s.durationString())
lenRight := utf8.RuneCountInString(right)
lenMargin := width - lenLeft - lenRight
if lenMargin < 0 {
// hide partial name with one space left
Expand All @@ -139,6 +145,19 @@ func (s *status) String(width int) (string, string) {
return fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s", s.descriptor.Digest.String())
}

// calculateSpeed calculates the speed of the progress and update last status.
// caller must hold the lock.
func (s *status) calculateSpeed() bytes {
now := time.Now()
secondsTaken := now.Sub(s.lastRenderTime).Seconds()
bytes := float64(s.offset - s.lastOffset)

s.lastOffset = s.offset
s.lastRenderTime = now

return ToBytes(int64(bytes / secondsTaken))
}

// durationString returns a viewable TTY string of the status with duration.
func (s *status) durationString() string {
if s.startTime.IsZero() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/oras/internal/display/progress/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ func Test_status_String(t *testing.T) {
})
// full name
statusStr, digestStr := s.String(120)
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil {
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}
// partial name
statusStr, digestStr = s.String(console.MinWidth)
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m........................................]", s.prompt, "appli.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil {
if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/vnd.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil {
t.Error(err)
}

Expand Down

0 comments on commit 8bc5c37

Please sign in to comment.