Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## unreleased

* Correct environment variable to specify `jpeg-recompress` location [@toy](https://github.com/toy)
* Added --benchmark, to compare performance of each tool [#218]

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

Expand Down
21 changes: 21 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,27 @@ optipng:

`image_optim` uses standard ruby library for creating temporary files. Temporary directory can be changed using one of `TMPDIR`, `TMP` or `TEMP` environment variables.

### Benchmark

Run with `--benchmark` to compare the performance of each individual tool on your images:

```
$ image_optim --benchmark -r /tmp/corpus/

benchmarking: 100.0% (elapsed: 3.9m)

BENCHMARK RESULTS

name files elapsed kb saved kb/s
-------- ----- ------- -------- -------
oxipng 50 8.906 1867.253 209.664
pngquant 50 1.980 214.597 108.386
pngcrush 50 22.529 1753.704 77.841
optipng 50 142.940 1641.101 11.481
advpng 50 137.753 962.549 6.987
pngout 50 426.706 444.679 1.042
```

## Options

* `: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`)*
Expand Down
22 changes: 22 additions & 0 deletions lib/image_optim.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'image_optim/benchmark'
require 'image_optim/bin_resolver'
require 'image_optim/cache'
require 'image_optim/config'
Expand All @@ -8,6 +9,7 @@
require 'image_optim/image_meta'
require 'image_optim/optimized_path'
require 'image_optim/path'
require 'image_optim/table'
require 'image_optim/timer'
require 'image_optim/worker'
require 'in_threads'
Expand Down Expand Up @@ -162,6 +164,22 @@ def optimize_image_data(original_data)
end
end

def benchmark_image(original)
src = Path.convert(original)
return unless (workers = workers_for_image(src))

workers.map do |worker|
start = ElapsedTime.now
dst = src.temp_path
begin
worker.optimize(src, dst)
BenchmarkResult.new(src, dst, ElapsedTime.now - start, worker)
ensure
dst.unlink
end
end
end

# Optimize multiple images
# if block given yields path and result for each image and returns array of
# yield results
Expand All @@ -186,6 +204,10 @@ def optimize_images_data(datas, &block)
run_method_for(datas, :optimize_image_data, &block)
end

def benchmark_images(paths, &block)
run_method_for(paths, :benchmark_image, &block)
end

class << self
# Optimization methods with default options
def method_missing(method, *args, &block)
Expand Down
24 changes: 24 additions & 0 deletions lib/image_optim/benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class ImageOptim
# Benchmark result for one worker+src
class BenchmarkResult
attr_reader :bytes, :elapsed, :worker

def initialize(src, dst, elapsed, worker)
@bytes = bytes_saved(src, dst)
@elapsed = elapsed
@worker = worker.class.bin_sym.to_s
end

private

def bytes_saved(src, dst)
src, dst = src.size, dst.size
return 0 if dst == 0 # failure
return 0 if dst > src # the file got bigger

src - dst
end
end
end
3 changes: 3 additions & 0 deletions lib/image_optim/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def threads

case threads
when true, nil
# --benchmark defaults to one thread
return 1 if get!(:benchmark)

processor_count
when false
1
Expand Down
73 changes: 68 additions & 5 deletions lib/image_optim/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@ def size_percent(size_a, size_b)
end
end

# files, elapsed, kb saved, kb/s
class BenchmarkResults
def initialize
@all = []
end

def add(rows)
@all.concat(rows)
end

def print
if @all.empty?
puts 'nothing to report'
return
end

# group by worker
report = @all.group_by(&:worker).map do |name, results|
kb = (results.sum(&:bytes) / 1024.0)
elapsed = results.sum(&:elapsed)
{
'name' => name,
'files' => results.length,
'elapsed' => elapsed,
'kb saved' => kb,
'kb/s' => (kb / elapsed),
}
end

# sort
report = report.sort_by do |row|
[-row['kb/s'], row['name']]
end

# output
puts "\nBENCHMARK RESULTS\n\n"
Table.new(report).write($stdout)
end
end

def initialize(options)
options = HashHelpers.deep_symbolise_keys(options)
@recursive = options.delete(:recursive)
Expand All @@ -53,26 +93,49 @@ def initialize(options)
glob = options.delete(:"exclude_#{type}_glob") || '.*'
GlobHelpers.expand_braces(glob)
end

# --benchmark
@benchmark = options.delete(:benchmark)
if @benchmark
options[:threads] = 1 # for consistency
if options[:timeout]
warning '--benchmark ignores --timeout'
end
end

@image_optim = ImageOptim.new(options)
end

def run!(args) # rubocop:disable Naming/PredicateMethod
to_optimize = find_to_optimize(args)
unless to_optimize.empty?
results = Results.new
if @benchmark
benchmark_results = BenchmarkResults.new
benchmark_images(to_optimize).each do |_original, rows| # rubocop:disable Style/HashEachMethods
benchmark_results.add(rows)
end
benchmark_results.print
else
results = Results.new

optimize_images!(to_optimize).each do |original, optimized|
results.add(original, optimized)
end
optimize_images!(to_optimize).each do |original, optimized|
results.add(original, optimized)
end

results.print
results.print
end
end

!@warnings
end

private

def benchmark_images(to_optimize, &block)
to_optimize = to_optimize.with_progress('benchmarking') if @progress
@image_optim.benchmark_images(to_optimize, &block)
end

def optimize_images!(to_optimize, &block)
to_optimize = to_optimize.with_progress('optimizing') if @progress
@image_optim.optimize_images!(to_optimize, &block)
Expand Down
5 changes: 5 additions & 0 deletions lib/image_optim/runner/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ def wrap_regex(width)
options[:pack] = pack
end

op.separator nil
op.on('--benchmark', 'Run in benchmark mode, to compare tools without modifying images') do
options[:benchmark] = true
end

op.separator nil
op.separator ' Caching:'

Expand Down
64 changes: 64 additions & 0 deletions lib/image_optim/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

class ImageOptim
# Handy class for pretty printing a table in the terminal. This is very simple, switch to Terminal
# Table, Table Tennis or similar if we need more.
class Table
attr_reader :rows

def initialize(rows)
@rows = rows
end

def write(io)
io.puts render_row(columns)
io.puts render_sep
rows.each do |row|
io.puts render_row(row.values)
end
end

protected

# array of column names
def columns
@columns ||= rows.first.keys
end

# should columns be justified left or right?
def justs
@justs ||= columns.map do |col|
rows.first[col].is_a?(Numeric) ? :rjust : :ljust
end
end

# max width of each column
def widths
@widths ||= columns.map do |col|
values = rows.map{ |row| fmt(row[col]) }
([col] + values).map(&:length).max
end
end

# render an array of row values
def render_row(values)
values.zip(justs, widths).map do |value, just, width|
fmt(value).send(just, width)
end.join(' ')
end

# render a separator line
def render_sep
render_row(widths.map{ |width| '-' * width })
end

# format one cell value
def fmt(value)
if value.is_a?(Float)
format('%0.3f', value)
else
value.to_s
end
end
end
end
14 changes: 14 additions & 0 deletions spec/image_optim_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,20 @@ def temp_copy(image)
end
end

describe 'benchmark_images' do
it 'does it' do
image_optim = ImageOptim.new
pairs = image_optim.benchmark_images(test_images)
test_images.zip(pairs).each do |original, (src, bm)|
expect(original).to equal(src)
expect(bm[0]).to be_a(ImageOptim::BenchmarkResult)
expect(bm[0].bytes).to be_a(Numeric)
expect(bm[0].elapsed).to be_a(Numeric)
expect(bm[0].worker).to be_a(String)
end
end
end

%w[
optimize_image
optimize_image!
Expand Down
Loading