Skip to content

Commit dba400c

Browse files
authored
--benchmark, fixes #217 (#218)
1 parent 367b472 commit dba400c

File tree

9 files changed

+227
-5
lines changed

9 files changed

+227
-5
lines changed

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Metrics/BlockLength:
6767
- 'spec/**/*.rb'
6868

6969
Metrics/ClassLength:
70+
Exclude:
71+
- 'lib/image_optim.rb'
7072
Max: 150
7173

7274
Metrics/CyclomaticComplexity:

CHANGELOG.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## unreleased
44

55
* Correct environment variable to specify `jpeg-recompress` location [@toy](https://github.com/toy)
6+
* Added --benchmark, to compare performance of each tool [#217](https://github.com/toy/image_optim/issues/217) [#218](https://github.com/toy/image_optim/pull/218) [@gurgeous](https://github.com/gurgeous)
67

78
## v0.31.4 (2024-11-19)
89

README.markdown

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,29 @@ optipng:
301301
302302
`image_optim` uses standard ruby library for creating temporary files. Temporary directory can be changed using one of `TMPDIR`, `TMP` or `TEMP` environment variables.
303303

304+
### Benchmark
305+
306+
Run with `--benchmark` to compare the performance of each individual tool on your images:
307+
308+
```sh
309+
image_optim --benchmark=isolated -r /tmp/corpus/
310+
```
311+
312+
```
313+
benchmarking: 100.0% (elapsed: 3.9m)
314+
315+
BENCHMARK RESULTS
316+
317+
name files elapsed kb saved kb/s
318+
-------- ----- ------- -------- -------
319+
oxipng 50 8.906 1867.253 209.664
320+
pngquant 50 1.980 214.597 108.386
321+
pngcrush 50 22.529 1753.704 77.841
322+
optipng 50 142.940 1641.101 11.481
323+
advpng 50 137.753 962.549 6.987
324+
pngout 50 426.706 444.679 1.042
325+
```
326+
304327
## Options
305328
306329
* `:nice` — Nice level, priority of all used tools with higher value meaning lower priority, in range `-20..19`, negative values can be set only if run by root user *(defaults to `10`)*

lib/image_optim.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'image_optim/benchmark_result'
34
require 'image_optim/bin_resolver'
45
require 'image_optim/cache'
56
require 'image_optim/config'
@@ -8,6 +9,7 @@
89
require 'image_optim/image_meta'
910
require 'image_optim/optimized_path'
1011
require 'image_optim/path'
12+
require 'image_optim/table'
1113
require 'image_optim/timer'
1214
require 'image_optim/worker'
1315
require 'in_threads'
@@ -162,6 +164,22 @@ def optimize_image_data(original_data)
162164
end
163165
end
164166

167+
def benchmark_image(original)
168+
src = Path.convert(original)
169+
return unless (workers = workers_for_image(src))
170+
171+
dst = src.temp_path
172+
begin
173+
workers.map do |worker|
174+
start = ElapsedTime.now
175+
worker.optimize(src, dst)
176+
BenchmarkResult.new(src, dst, ElapsedTime.now - start, worker)
177+
end
178+
ensure
179+
dst.unlink
180+
end
181+
end
182+
165183
# Optimize multiple images
166184
# if block given yields path and result for each image and returns array of
167185
# yield results
@@ -186,6 +204,10 @@ def optimize_images_data(datas, &block)
186204
run_method_for(datas, :optimize_image_data, &block)
187205
end
188206

207+
def benchmark_images(paths, &block)
208+
run_method_for(paths, :benchmark_image, &block)
209+
end
210+
189211
class << self
190212
# Optimization methods with default options
191213
def method_missing(method, *args, &block)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class ImageOptim
4+
# Benchmark result for one worker+src
5+
class BenchmarkResult
6+
attr_reader :bytes, :elapsed, :worker
7+
8+
def initialize(src, dst, elapsed, worker)
9+
@bytes = bytes_saved(src, dst)
10+
@elapsed = elapsed
11+
@worker = worker.class.bin_sym.to_s
12+
end
13+
14+
private
15+
16+
def bytes_saved(src, dst)
17+
src, dst = src.size, dst.size
18+
return 0 if dst == 0 # failure
19+
return 0 if dst > src # the file got bigger
20+
21+
src - dst
22+
end
23+
end
24+
end

lib/image_optim/runner.rb

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,43 @@ def size_percent(size_a, size_b)
4545
end
4646
end
4747

48+
# files, elapsed, kb saved, kb/s
49+
class BenchmarkResults
50+
def initialize
51+
@all = []
52+
end
53+
54+
def add(rows)
55+
@all.concat(rows)
56+
end
57+
58+
def print
59+
if @all.empty?
60+
puts 'nothing to report'
61+
return
62+
end
63+
64+
report = @all.group_by(&:worker).map do |name, results|
65+
kb = (results.sum(&:bytes) / 1024.0)
66+
elapsed = results.sum(&:elapsed)
67+
{
68+
'name' => name,
69+
'files' => results.length,
70+
'elapsed' => elapsed,
71+
'kb saved' => kb,
72+
'kb/s' => (kb / elapsed),
73+
}
74+
end
75+
76+
report = report.sort_by do |row|
77+
[-row['kb/s'], row['name']]
78+
end
79+
80+
puts "\nBENCHMARK RESULTS\n\n"
81+
Table.new(report).write($stdout)
82+
end
83+
end
84+
4885
def initialize(options)
4986
options = HashHelpers.deep_symbolise_keys(options)
5087
@recursive = options.delete(:recursive)
@@ -53,26 +90,52 @@ def initialize(options)
5390
glob = options.delete(:"exclude_#{type}_glob") || '.*'
5491
GlobHelpers.expand_braces(glob)
5592
end
93+
94+
# --benchmark
95+
@benchmark = options.delete(:benchmark)
96+
if @benchmark
97+
unless options[:threads].nil?
98+
warning '--benchmark ignores --threads'
99+
options[:threads] = 1 # for consistency
100+
end
101+
if options[:timeout]
102+
warning '--benchmark ignores --timeout'
103+
end
104+
end
105+
56106
@image_optim = ImageOptim.new(options)
57107
end
58108

59109
def run!(args) # rubocop:disable Naming/PredicateMethod
60110
to_optimize = find_to_optimize(args)
61111
unless to_optimize.empty?
62-
results = Results.new
112+
if @benchmark
113+
benchmark_results = BenchmarkResults.new
114+
benchmark_images(to_optimize).each do |_original, rows| # rubocop:disable Style/HashEachMethods
115+
benchmark_results.add(rows)
116+
end
117+
benchmark_results.print
118+
else
119+
results = Results.new
63120

64-
optimize_images!(to_optimize).each do |original, optimized|
65-
results.add(original, optimized)
66-
end
121+
optimize_images!(to_optimize).each do |original, optimized|
122+
results.add(original, optimized)
123+
end
67124

68-
results.print
125+
results.print
126+
end
69127
end
70128

71129
!@warnings
72130
end
73131

74132
private
75133

134+
def benchmark_images(to_optimize, &block)
135+
to_optimize = to_optimize.with_progress('benchmarking') if @progress
136+
@image_optim.benchmark_images(to_optimize, &block)
137+
end
138+
76139
def optimize_images!(to_optimize, &block)
77140
to_optimize = to_optimize.with_progress('optimizing') if @progress
78141
@image_optim.optimize_images!(to_optimize, &block)

lib/image_optim/runner/option_parser.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ def wrap_regex(width)
153153
options[:pack] = pack
154154
end
155155

156+
op.separator nil
157+
op.on(
158+
'--benchmark TYPE',
159+
[:isolated],
160+
'Run benchmarks, to compare tools without modifying images. `isolated` is the only supported type so far.'
161+
) do |benchmark|
162+
options[:benchmark] = benchmark
163+
end
164+
156165
op.separator nil
157166
op.separator ' Caching:'
158167

lib/image_optim/table.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# frozen_string_literal: true
2+
3+
class ImageOptim
4+
# Handy class for pretty printing a table in the terminal. This is very simple, switch to Terminal
5+
# Table, Table Tennis or similar if we need more.
6+
class Table
7+
attr_reader :rows
8+
9+
def initialize(rows)
10+
@rows = rows
11+
end
12+
13+
def write(io)
14+
io.puts render_row(columns)
15+
io.puts render_sep
16+
rows.each do |row|
17+
io.puts render_row(row.values)
18+
end
19+
end
20+
21+
protected
22+
23+
# array of column names
24+
def columns
25+
@columns ||= rows.first.keys
26+
end
27+
28+
# should columns be justified left or right?
29+
def justs
30+
@justs ||= columns.map do |col|
31+
rows.first[col].is_a?(Numeric) ? :rjust : :ljust
32+
end
33+
end
34+
35+
# max width of each column
36+
def widths
37+
@widths ||= columns.map do |col|
38+
values = rows.map{ |row| fmt(row[col]) }
39+
([col] + values).map(&:length).max
40+
end
41+
end
42+
43+
# render an array of row values
44+
def render_row(values)
45+
values.zip(justs, widths).map do |value, just, width|
46+
fmt(value).send(just, width)
47+
end.join(' ')
48+
end
49+
50+
# render a separator line
51+
def render_sep
52+
render_row(widths.map{ |width| '-' * width })
53+
end
54+
55+
# format one cell value
56+
def fmt(value)
57+
if value.is_a?(Float)
58+
format('%0.3f', value)
59+
else
60+
value.to_s
61+
end
62+
end
63+
end
64+
end

spec/image_optim_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,20 @@ def temp_copy(image)
269269
end
270270
end
271271

272+
describe 'benchmark_images' do
273+
it 'does it' do
274+
image_optim = ImageOptim.new
275+
pairs = image_optim.benchmark_images(test_images)
276+
test_images.zip(pairs).each do |original, (src, bm)|
277+
expect(original).to equal(src)
278+
expect(bm[0]).to be_a(ImageOptim::BenchmarkResult)
279+
expect(bm[0].bytes).to be_a(Numeric)
280+
expect(bm[0].elapsed).to be_a(Numeric)
281+
expect(bm[0].worker).to be_a(String)
282+
end
283+
end
284+
end
285+
272286
%w[
273287
optimize_image
274288
optimize_image!

0 commit comments

Comments
 (0)