Skip to content
Open
Show file tree
Hide file tree
Changes from all 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