script/worker_analysis in image_optim-0.20.2 vs script/worker_analysis in image_optim-0.21.0
- old
+ new
@@ -7,11 +7,12 @@
require 'image_optim/cmd'
require 'progress'
require 'shellwords'
require 'gdbm'
require 'digest'
-require 'haml'
+require 'erb'
+require 'ostruct'
DIR = 'tmp'
Pathname(DIR).mkpath
Array.class_eval do
@@ -86,11 +87,11 @@
def digest
@digest ||= Digest::SHA256.file(to_s).hexdigest
end
- def cache_etag
+ def etag
[mtime, digest]
end
end
# Analyse efficency of workers
@@ -98,64 +99,57 @@
Cmd = ImageOptim::Cmd
HashHelpers = ImageOptim::HashHelpers
# Caching entries using GDBM
class Cache
- PATH = "#{DIR}/worker-analysis.db"
+ DB = GDBM.new("#{DIR}/worker-analysis.db")
class << self
- def get(key, etag, &block)
+ def get(context, key, etag, &block)
+ full_key = [context, key]
if block
- get!(key, etag) || set!(key, etag, &block)
+ get!(full_key, etag) || set!(full_key, etag, &block)
else
- get!(key, etag)
+ get!(full_key, etag)
end
end
- def set(key, etag, &block)
- set!(key, etag, &block)
+ def set(context, key, etag, &block)
+ set!([context, key], etag, &block)
end
private
- def open
- GDBM.open(PATH) do |db|
- yield db
- end
- end
-
def get!(key, etag)
- raw = open{ |db| db[Marshal.dump(key)] }
+ raw = DB[Marshal.dump(key)]
return unless raw
entry = Marshal.load(raw)
return unless entry[1] == etag
entry[0]
end
def set!(key, etag, &block)
value = block.call
- open{ |db| db[Marshal.dump(key)] = Marshal.dump([value, etag]) }
+ DB[Marshal.dump(key)] = Marshal.dump([value, etag])
value
end
end
end
# Delegate to worker with short id
class WorkerVariant < DelegateClass(ImageOptim::Worker)
- attr_reader :cons_id, :id
+ attr_reader :name, :id, :cons_id
def initialize(klass, image_optim, options)
allow_consecutive_on = Array(options.delete(:allow_consecutive_on))
@image_optim = image_optim
- @id = klass.bin_sym.to_s
- unless options.empty?
- @id << "(#{options.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
- end
+ @name = klass.bin_sym.to_s + options_string(options)
__setobj__(klass.new(image_optim, options))
+ @id = klass.bin_sym.to_s + options_string(self.options)
@cons_id = [klass, allow_consecutive_on.map{ |key| [key, send(key)] }]
end
- def cache_etag
+ def etag
[
id,
bin_versions,
source_digest,
]
@@ -173,32 +167,53 @@
@digest ||= begin
source_path = __getobj__.method(:optimize).source_location[0]
Digest::SHA256.file(source_path).hexdigest
end
end
+
+ def options_string(options)
+ return '' if options.empty?
+ "(#{options.sort.map{ |k, v| "#{k}:#{v.inspect}" }.join(', ')})"
+ end
end
# One worker result
StepResult = Struct.new(*[
:worker_id,
:success,
:time,
:src_size,
:dst_size,
+ :cache,
]) do
- def self.run(src, dst, worker)
+ def self.run(src, worker)
+ dst = src.temp_path
start = Process.times.sum
success = worker.optimize(src, dst)
time = Process.times.sum - start
- new(worker.id, success, time, src.size, success ? dst.size : nil)
+ dst_size = success ? dst.size : nil
+ cache = (success ? dst : src).digest.sub(/../, '\0/') + ".#{src.format}"
+ result = new(worker.id, success, time, src.size, dst_size, cache)
+ if success
+ path = result.path
+ unless path.exist?
+ path.dirname.mkpath
+ dst.rename(path)
+ end
+ end
+ result
end
def size
success ? dst_size : src_size
end
+ def path
+ ImageOptim::ImagePath.convert("#{DIR}/worker-analysis/#{cache}")
+ end
+
def inspect
"<S:#{worker_id} #{success ? '✓' : '✗'} #{time}s #{src_size}→#{dst_size}>"
end
end
@@ -239,17 +254,14 @@
@path = ImageOptim::ImagePath.convert(path)
@workers = workers
end
def results
- cache_etag = [@path.cache_etag, @workers.map(&:cache_etag).sort]
- Cache.get(@path.to_s, cache_etag) do
- results = []
- run_workers(@path, @workers){ |result| results << result }
- run_cache.clear
- results
- end
+ results = []
+ run_workers(@path, @workers){ |result| results << result }
+ run_cache.clear
+ results
end
private
def run_cache
@@ -272,25 +284,33 @@
chain_result = ChainResult.new(src.format, steps)
chain_result.difference = difference_with(result_image)
block.call(chain_result)
- workers_left = workers.reject{ |w| w.cons_id == worker.cons_id }
+ workers_left = workers.reject do |w|
+ w.cons_id == worker.cons_id || w.run_order < worker.run_order
+ end
run_workers(result_image, workers_left, chain_result, &block)
end
end
def run_worker(src, worker)
run_cache[:run][[src.digest, worker.id]] ||= begin
- dst = src.temp_path
- worker_result = StepResult.run(src, dst, worker)
- [worker_result, worker_result.success ? dst : src]
+ cache_args = [:result, [src.digest, worker.id], worker.etag]
+ result = Cache.get(*cache_args)
+ if !result || (result.success && !result.path.exist?)
+ result = Cache.set(*cache_args) do
+ StepResult.run(src, worker)
+ end
+ end
+ [result, result.success ? result.path : src]
end
end
def difference_with(other)
- run_cache[:difference][other.digest] ||= begin
+ run_cache[:difference][other.digest] ||=
+ Cache.get(:difference, [@path.digest, other.digest].sort, nil) do
images = [flatten_animation(@path), flatten_animation(other)]
alpha_presence = images.map do |image|
Cmd.capture("identify -format '%A' #{image.shellescape}")
end
@@ -363,34 +383,40 @@
attr_reader :worker_stats
attr_reader :unused_workers
attr_reader :entry_count
attr_reader :original_size, :optimized_size, :ratio, :avg_ratio
attr_reader :avg_difference, :max_difference, :warn_level
- attr_reader :time, :speed
+ attr_reader :time, :avg_time, :speed
- def initialize(worker_ids, results)
- steps_by_worker_id = results.flat_map(&:steps).group_by(&:worker_id)
- @worker_stats = worker_ids.map do |worker_id|
- Worker.new(worker_id, steps_by_worker_id[worker_id])
- end
+ def initialize(worker_ids, results, ids2names)
+ @worker_stats = build_worker_stats(worker_ids, results, ids2names)
@unused_workers = worker_stats.any?(&:unused?)
@entry_count = results.count
@original_size = results.sum(&:src_size)
@optimized_size = results.sum(&:dst_size)
@ratio = optimized_size.to_f / original_size
@avg_ratio = results.sum(&:ratio) / results.length
@avg_difference = results.sum(&:difference) / results.length
@max_difference = results.map(&:difference).max
@time = results.sum(&:time)
+ @avg_time = time / results.length
@warn_level = calculate_warn_level
@speed = calculate_speed
end
private
+ def build_worker_stats(worker_ids, results, ids2names)
+ steps_by_worker_id = results.flat_map(&:steps).group_by(&:worker_id)
+ worker_ids.map do |worker_id|
+ worker_name = ids2names[worker_id] || worker_id
+ Worker.new(worker_name, steps_by_worker_id[worker_id])
+ end
+ end
+
def calculate_warn_level
case
when max_difference >= 0.1 then 'high'
when max_difference >= 0.01 then 'medium'
when max_difference >= 0.001 then 'low'
@@ -408,29 +434,33 @@
# Worker usage
class Worker
attr_reader :name
attr_reader :success_count
+ attr_reader :time, :avg_time
def initialize(name, steps)
@name = name
@success_count = steps.count(&:success)
+ @time = steps.sum(&:time)
+ @avg_time = time / steps.length
end
def unused?
success_count.zero?
end
end
- attr_reader :name, :results
- def initialize(name, results)
+ attr_reader :name, :results, :ids2names
+ def initialize(name, results, ids2names)
@name = name.to_s
@results = results
+ @ids2names = ids2names
end
def each_chain(&block)
chains = results.group_by(&:worker_ids).map do |worker_ids, results|
- Chain.new(worker_ids, results)
+ Chain.new(worker_ids, results, ids2names)
end
chains.sort_by!{ |chain| [chain.optimized_size, chain.time] }
chains.each(&block)
end
end
@@ -453,47 +483,59 @@
end
worker_option_variants.each do |options|
options = HashHelpers.deep_symbolise_keys(options)
options[:allow_consecutive_on] = allow_consecutive_on
worker = WorkerVariant.new(klass, image_optim, options)
- puts worker.id
worker.image_formats.each do |format|
@workers_by_format[format] << worker
end
end
end
+ log_workers_by_format
+
fail "unknown variants: #{option_variants}" unless option_variants.empty?
end
def analyse(paths)
- results = process_paths(paths).shuffle.with_progress.flat_map do |path|
- WorkerRunner.new(path, workers_for_image(path)).results
- end
+ results = collect_results(paths)
- template = Haml::Engine.new(File.read("#{__FILE__}.haml"))
+ template = ERB.new(template_path.read, nil, '>')
by_format = results.group_by(&:format)
formats = by_format.keys.sort
basenames = Hash[formats.map do |format|
[format, "worker-analysis-#{format}.html"]
end]
formats.each do |format|
- stats = Stats.new('all', by_format[format])
+ stats = Stats.new('all', by_format[format], worker_ids2names)
+ path = FSPath("#{DIR}/#{basenames[format]}")
model = {
:stats_format => format,
:stats => stats,
:format_links => basenames,
+ :template_dir => template_path.dirname.relative_path_from(path.dirname),
}
- html = template.render(nil, model)
- path = FSPath("#{DIR}/#{basenames[format]}")
+ html = template.result(OpenStruct.new(model).instance_eval{ binding })
path.write(html)
puts "Created #{path}"
end
end
private
+ def worker_ids2names
+ Hash[@workers_by_format.values.flatten.map do |worker|
+ [worker.id, worker.name]
+ end]
+ end
+
+ def collect_results(paths)
+ process_paths(paths).shuffle.with_progress.flat_map do |path|
+ WorkerRunner.new(path, workers_for_image(path)).results
+ end
+ end
+
def process_paths(paths)
paths = paths.map{ |path| ImageOptim::ImagePath.convert(path) }
paths.select!{ |path| path.exist? || warn("#{path} doesn't exits") }
paths.select!{ |path| path.file? || warn("#{path} is not a file") }
paths.select!{ |path| path.format || warn("#{path} is not an image") }
@@ -503,9 +545,22 @@
paths
end
def workers_for_image(path)
@workers_by_format[ImageOptim::ImagePath.convert(path).format]
+ end
+
+ def log_workers_by_format
+ @workers_by_format.each do |format, workers|
+ puts "#{format}:"
+ workers.sort_by.with_index{ |w, i| [w.run_order, i] }.each do |worker|
+ puts " #{worker.name} [#{worker.run_order}]"
+ end
+ end
+ end
+
+ def template_path
+ FSPath("#{File.dirname(__FILE__)}/template/#{File.basename(__FILE__)}.erb")
end
end
def option_variants
path = '.analysis_variants.yml'