Class: GDKBox::Docker::PullProgress

Inherits:
Object
  • Object
show all
Defined in:
lib/gdkbox/docker.rb

Overview

Parses the terminal output of docker pull (see Shell#stream_tty) into an overall completion fraction and a download rate. Docker repaints one line per layer, each shaped like ": [==> ] 5MB/50MB". We track every layer we have seen and score its progress in 0.0..1.0: a download fills the first half of a layer and extraction the second, so the fraction rises smoothly rather than jumping only when a whole layer completes. Bytes reported on "Downloading" lines are summed across layers and sampled against a clock to derive the current download speed.

Constant Summary collapse

DONE =

States that mark a layer as fully pulled.

["Pull complete", "Already exists"].freeze
SAMPLE_INTERVAL =

Docker repaints many times a second across parallel layers; recomputing speed on every burst line would divide a small byte delta by a near-zero time and report absurd spikes. Instead accumulate bytes and only recompute the rate once this much time has elapsed.

0.25
UNITS =

SI byte units as docker prints them (base 1000).

{ "B" => 1, "kB" => 1000, "MB" => 1000**2,
"GB" => 1000**3, "TB" => 1000**4 }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(clock: nil) ⇒ PullProgress

clock returns a monotonically increasing time in seconds; it is injected in tests so speed is deterministic.



61
62
63
64
65
66
67
# File 'lib/gdkbox/docker.rb', line 61

def initialize(clock: nil)
  @layers = {}
  @downloaded = Hash.new(0) # layer id => bytes pulled so far
  @sizes = {}               # layer id => total bytes (once known)
  @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
  @speed = nil              # bytes/second, or nil until measurable
end

Instance Method Details

#completedObject

Number of layers fully pulled so far.



80
81
82
# File 'lib/gdkbox/docker.rb', line 80

def completed
  @layers.count { |_id, value| value >= 1.0 }
end

#fractionObject

Overall progress in 0.0..1.0 (0.0 before any layer is known).



90
91
92
93
94
# File 'lib/gdkbox/docker.rb', line 90

def fraction
  return 0.0 if @layers.empty?

  @layers.values.sum / @layers.size
end

#human_speedObject

Current download speed as a human string (e.g. "12.3 MB/s"), or nil.



103
104
105
106
107
# File 'lib/gdkbox/docker.rb', line 103

def human_speed
  return nil unless @speed && @speed.positive?

  "#{human_size(@speed)}/s"
end

#ingest(line) ⇒ Object



69
70
71
72
73
74
75
76
77
# File 'lib/gdkbox/docker.rb', line 69

def ingest(line)
  id, status = line.split(": ", 2)
  return unless status
  # The first line ("<tag>: Pulling from <repo>") is not a layer.
  return if status.start_with?("Pulling from")

  track_bytes(id, status)
  @layers[id] = score(status)
end

#speedObject

Current download speed in bytes/second, or nil before it can be measured (needs two samples with elapsed time and new bytes).



98
99
100
# File 'lib/gdkbox/docker.rb', line 98

def speed
  @speed
end

#totalObject

Number of distinct layers seen so far.



85
86
87
# File 'lib/gdkbox/docker.rb', line 85

def total
  @layers.size
end