Class: Binpacker::Timing

Inherits:
Object
  • Object
show all
Defined in:
lib/binpacker/timing.rb,
sig/binpacker/timing.rbs

Defined Under Namespace

Classes: Entry

Constant Summary collapse

DEFAULT_WEIGHT =

Returns:

  • (Float)
1.0
MAX_SAMPLES_PER_TEST =

Samples retained per test by #compact! and consulted by the median in #load_per_file. Three samples make a single anomalous run (GC pause, noisy CI neighbour) unable to move the weight.

Returns:

  • (Integer)
3

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Timing

Returns a new instance of Timing.

Parameters:

  • (Object)


16
17
18
# File 'lib/binpacker/timing.rb', line 16

def initialize(path)
  @path = Pathname(path)
end

Instance Method Details

#append(file:, name:, time:) ⇒ Object



81
82
83
84
85
# File 'lib/binpacker/timing.rb', line 81

def append(file:, name:, time:)
  @path.dirname.mkpath unless @path.dirname.directory?
  @path.open('a', encoding: 'UTF-8') { |io| io.puts JSON.generate({ file: file, name: name, time: time }) }
  invalidate
end

#append_all(entries) ⇒ nil

Parameters:

  • (Object)

Returns:

  • (nil)


87
88
89
90
91
92
93
94
95
# File 'lib/binpacker/timing.rb', line 87

def append_all(entries)
  return if entries.empty?

  @path.dirname.mkpath unless @path.dirname.directory?
  @path.open('a', encoding: 'UTF-8') do |io|
    entries.each { |e| io.puts JSON.generate({ file: e[:file], name: e[:name], time: e[:time] }) }
  end
  invalidate
end

#calibrated?Boolean

True once any timing samples exist, i.e. the project has been calibrated at least once. Callers use this to tell a measured run (weights in seconds) from a pure cold start (fallbacks only).

Returns:

  • (Boolean)


23
24
25
# File 'lib/binpacker/timing.rb', line 23

def calibrated?
  !samples_by_test.empty?
end

#compact!nil

Rewrites the timing file keeping only the most recent MAX_SAMPLES_PER_TEST samples per test, so the append-only history (and any CI cache built from it) stays bounded instead of growing by one run per invocation.

Returns:

  • (nil)


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/binpacker/timing.rb', line 56

def compact!
  samples = samples_by_test
  return if samples.empty?

  tmp = Pathname("#{@path}.tmp")
  tmp.open('w', encoding: 'UTF-8') do |io|
    samples.each do |(file, name), times|
      times.last(MAX_SAMPLES_PER_TEST).each do |time|
        io.puts JSON.generate({ file: file, name: name, time: time })
      end
    end
  end
  File.rename(tmp.to_s, @path.to_s)
  invalidate
end

#load_per_fileHash[String, untyped], {}

Predicted weight per file: the median of each test's recent samples, summed per file. The append-only history must NOT be summed wholesale — a file present in N historical runs would weigh ~N times its true cost, so long-lived files dominate and newly added ones are starved, skewing the partition.

Returns:

  • (Hash[String, untyped], {})


45
46
47
48
49
50
# File 'lib/binpacker/timing.rb', line 45

def load_per_file
  samples_by_test.each_with_object({}) do |((file, _name), times), per_file|
    weight = median(times.last(MAX_SAMPLES_PER_TEST))
    per_file[file] = per_file.fetch(file, 0.0) + weight
  end
end

#load_rawHash[[String, untyped], untyped], {}

Returns:

  • (Hash[[String, untyped], untyped], {})


36
37
38
# File 'lib/binpacker/timing.rb', line 36

def load_raw
  @load_raw ||= samples_by_test.transform_values(&:last)
end

#load_with_fallback(tests) ⇒ Hash[untyped, Float]

Parameters:

  • (Object)

Returns:

  • (Hash[untyped, Float])


27
28
29
30
31
32
33
34
# File 'lib/binpacker/timing.rb', line 27

def load_with_fallback(tests)
  per_file = load_per_file
  coefficient = seconds_per_kb(per_file)
  tests.each_with_object({}) do |test, hash|
    key = normalize_path(test.file)
    hash[test.key] = per_file.fetch(key) { fallback_weight(test.file, coefficient) }
  end
end

#measured?(file:, name:) ⇒ Boolean

True when a measured Weight already exists for this Test.

Parameters:

  • file: (Object)
  • name: (Object)

Returns:

  • (Boolean)


77
78
79
# File 'lib/binpacker/timing.rb', line 77

def measured?(file:, name:)
  load_raw.key?([normalize_path(file), name])
end

#normalize_path(path) ⇒ String

Parameters:

  • (Object)

Returns:

  • (String)


72
73
74
# File 'lib/binpacker/timing.rb', line 72

def normalize_path(path)
  Pathname(path).cleanpath.to_s.sub(%r{\A\./}, '')
end