Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
27 changes: 27 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,27 @@ def optimize_image_data(original_data)
end
end

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

timer = timeout && Timer.new(timeout)
workers.map do |worker|
start = ElapsedTime.now
dst = src.temp_path
begin
begin
worker.optimize(src, dst, timeout: timer)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if applying worker always to the original is more informative then doing it in chain (on result of previous worker) as will be done during normal operation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add a --chain later if people like this. I think those results would be interesting as well, especially now that I've seen the worker_analysis script

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worker_analysis is there to understand tool interaction, try different options, in the end decide about usefulness, order. When using --benchmark I think it may be confusing if two tools report reduction by 50%, but when combined - only by 51%

rescue Errors::TimeoutExceeded
# nop
end
Benchmark.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 +209,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
22 changes: 22 additions & 0 deletions lib/image_optim/benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class ImageOptim
# Benchmark result for one worker+src
class Benchmark
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

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
64 changes: 59 additions & 5 deletions lib/image_optim/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,51 @@ def size_percent(size_a, size_b)
end
end

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

def add(_original, 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"
puts Table.new(report)
end
end

def initialize(options)
options = HashHelpers.deep_symbolise_keys(options)
@recursive = options.delete(:recursive)
@progress = options.delete(:show_progress) != false
@benchmark = options.delete(:benchmark)
@exclude_dir_globs, @exclude_file_globs = %w[dir file].map do |type|
glob = options.delete(:"exclude_#{type}_glob") || '.*'
GlobHelpers.expand_braces(glob)
Expand All @@ -59,20 +100,33 @@ def initialize(options)
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, bm|
benchmark_results.add(original, bm)
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
66 changes: 66 additions & 0 deletions lib/image_optim/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 to_s
lines = []
lines << render_row(columns)
lines << render_sep
rows.each do |row|
lines << render_row(row.values)
end
lines.join("\n")
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.map.with_index do |value, ii|
fmt(value).send(justs[ii], widths[ii])
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
Loading