rubohash 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +14 -0
- data/Rakefile +7 -0
- data/benchmark/image_generation.rb +141 -0
- data/lib/rubohash/factory.rb +75 -40
- data/lib/rubohash/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7dfd304b72e037e11bf35ba9061a2158e19bc73965aac68ec0ca3ca08b3867ef
|
|
4
|
+
data.tar.gz: bf91a387bca0176f56b1f6e21f2ad58bd5d7179e30985003c1878a1baac096c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c9e10adc7548488c402c58cceb6f81870cef25b0e1de0003e1bd07c06f6a5e61f5ab82975d4924e37a6b28dd44be7262c3c1ab48c6766014cd8338813859f52
|
|
7
|
+
data.tar.gz: 55bfaab91a0408813dbe8a3909833baee8b3ed1f246b9c967c20f21a24a4a3693e4632ea17606606b9e46251186c070d550509578d0a49b83ee16820b7896e93
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -20,6 +20,20 @@ By default, generated files are written to `./output` from your current working
|
|
|
20
20
|
|
|
21
21
|
For local development in this repository, you can use `./bin/build.sh`.
|
|
22
22
|
|
|
23
|
+
## Benchmark
|
|
24
|
+
|
|
25
|
+
To measure how long image generation takes locally:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
bundle exec ruby benchmark/image_generation.rb
|
|
29
|
+
bundle exec rake benchmark:image_generation
|
|
30
|
+
ITERATIONS=25 WARMUP=5 bundle exec ruby benchmark/image_generation.rb
|
|
31
|
+
SCENARIOS=without_background bundle exec ruby benchmark/image_generation.rb
|
|
32
|
+
BATCH_SIZES=1,10,100 bundle exec ruby benchmark/image_generation.rb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The benchmark reports batch totals plus per-image averages in milliseconds for each scenario and batch size.
|
|
36
|
+
|
|
23
37
|
## Usage
|
|
24
38
|
|
|
25
39
|
From Ruby code:
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'tmpdir'
|
|
4
|
+
require_relative '../lib/rubohash'
|
|
5
|
+
|
|
6
|
+
module Rubohash
|
|
7
|
+
class BenchmarkRunner
|
|
8
|
+
DEFAULT_ITERATIONS = 10
|
|
9
|
+
DEFAULT_WARMUP = 2
|
|
10
|
+
DEFAULT_SCENARIOS = %w[with_background without_background].freeze
|
|
11
|
+
DEFAULT_BATCH_SIZES = [1, 10, 100].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(iterations:, warmup:, scenarios:, batch_sizes:)
|
|
14
|
+
@iterations = iterations
|
|
15
|
+
@warmup = warmup
|
|
16
|
+
@scenarios = scenarios
|
|
17
|
+
@batch_sizes = batch_sizes
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
print_header
|
|
22
|
+
@scenarios.each { |scenario| run_scenario(scenario) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run_scenario(scenario)
|
|
28
|
+
use_background = scenario == 'with_background'
|
|
29
|
+
|
|
30
|
+
with_benchmark_config(use_background: use_background) do
|
|
31
|
+
@warmup.times do |index|
|
|
32
|
+
image = build_image("warmup-#{scenario}-#{index}")
|
|
33
|
+
image.destroy!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "scenario: #{scenario}"
|
|
37
|
+
@batch_sizes.each do |batch_size|
|
|
38
|
+
durations = benchmark_batch(scenario, batch_size)
|
|
39
|
+
print_report(batch_size, durations)
|
|
40
|
+
end
|
|
41
|
+
puts
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_image(seed)
|
|
46
|
+
Rubohash::Factory.new(seed).assemble
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def benchmark_batch(scenario, batch_size)
|
|
50
|
+
durations = []
|
|
51
|
+
|
|
52
|
+
@iterations.times do |index|
|
|
53
|
+
started_at = monotonic_time
|
|
54
|
+
batch_size.times do |offset|
|
|
55
|
+
seed = "benchmark-#{scenario}-#{batch_size}-#{index}-#{offset}"
|
|
56
|
+
image = build_image(seed)
|
|
57
|
+
image.destroy!
|
|
58
|
+
end
|
|
59
|
+
durations << ((monotonic_time - started_at) * 1000.0)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
durations
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_benchmark_config(use_background:)
|
|
66
|
+
original = {
|
|
67
|
+
mounted: Rubohash.mounted,
|
|
68
|
+
use_background: Rubohash.use_background,
|
|
69
|
+
robot_output_path: Rubohash.robot_output_path
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Dir.mktmpdir('rubohash-benchmark') do |tmpdir|
|
|
73
|
+
Rubohash.configure do |config|
|
|
74
|
+
config.mounted = true
|
|
75
|
+
config.use_background = use_background
|
|
76
|
+
config.robot_output_path = tmpdir
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
yield
|
|
80
|
+
ensure
|
|
81
|
+
Rubohash.configure do |config|
|
|
82
|
+
config.mounted = original[:mounted]
|
|
83
|
+
config.use_background = original[:use_background]
|
|
84
|
+
config.robot_output_path = original[:robot_output_path]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def print_header
|
|
90
|
+
puts 'Rubohash image generation benchmark'
|
|
91
|
+
puts "iterations: #{@iterations}"
|
|
92
|
+
puts "warmup: #{@warmup}"
|
|
93
|
+
puts "batch_sizes: #{@batch_sizes.join(', ')}"
|
|
94
|
+
puts
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def print_report(batch_size, durations)
|
|
98
|
+
sorted = durations.sort
|
|
99
|
+
total = durations.sum
|
|
100
|
+
average = total / durations.length
|
|
101
|
+
median = percentile(sorted, 50)
|
|
102
|
+
p95 = percentile(sorted, 95)
|
|
103
|
+
per_image_average = average / batch_size
|
|
104
|
+
|
|
105
|
+
puts " batch_size: #{batch_size}"
|
|
106
|
+
puts format(' total: %.2f ms', total)
|
|
107
|
+
puts format(' avg_batch: %.2f ms', average)
|
|
108
|
+
puts format(' avg_per_image: %.2f ms', per_image_average)
|
|
109
|
+
puts format(' median_batch: %.2f ms', median)
|
|
110
|
+
puts format(' p95_batch: %.2f ms', p95)
|
|
111
|
+
puts format(' min/max: %.2f / %.2f ms', sorted.first, sorted.last)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def percentile(sorted_values, rank)
|
|
115
|
+
return sorted_values.first if sorted_values.length == 1
|
|
116
|
+
|
|
117
|
+
index = ((rank / 100.0) * (sorted_values.length - 1)).round
|
|
118
|
+
sorted_values[index]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def monotonic_time
|
|
122
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
iterations = Integer(ENV.fetch('ITERATIONS', Rubohash::BenchmarkRunner::DEFAULT_ITERATIONS))
|
|
128
|
+
warmup = Integer(ENV.fetch('WARMUP', Rubohash::BenchmarkRunner::DEFAULT_WARMUP))
|
|
129
|
+
scenarios = ENV.fetch('SCENARIOS', Rubohash::BenchmarkRunner::DEFAULT_SCENARIOS.join(',')).split(',').map(&:strip)
|
|
130
|
+
batch_sizes = ENV.fetch('BATCH_SIZES', Rubohash::BenchmarkRunner::DEFAULT_BATCH_SIZES.join(',')).split(',').map { |value| Integer(value.strip) }
|
|
131
|
+
|
|
132
|
+
unknown_scenarios = scenarios - Rubohash::BenchmarkRunner::DEFAULT_SCENARIOS
|
|
133
|
+
abort("Unknown scenarios: #{unknown_scenarios.join(', ')}") unless unknown_scenarios.empty?
|
|
134
|
+
abort('BATCH_SIZES must only contain positive integers') unless batch_sizes.all?(&:positive?)
|
|
135
|
+
|
|
136
|
+
Rubohash::BenchmarkRunner.new(
|
|
137
|
+
iterations: iterations,
|
|
138
|
+
warmup: warmup,
|
|
139
|
+
scenarios: scenarios,
|
|
140
|
+
batch_sizes: batch_sizes
|
|
141
|
+
).call
|
data/lib/rubohash/factory.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
|
+
require 'tempfile'
|
|
2
3
|
|
|
3
4
|
# Rubohash namespace
|
|
4
5
|
module Rubohash
|
|
@@ -49,34 +50,32 @@ module Rubohash
|
|
|
49
50
|
|
|
50
51
|
# List directories for the given path
|
|
51
52
|
def list_directories(path)
|
|
52
|
-
Dir.entries(path).select do |entry|
|
|
53
|
+
self.class.directory_cache[path] ||= Dir.entries(path).select do |entry|
|
|
53
54
|
next if %w[. ..].include?(entry)
|
|
54
55
|
File.directory?(File.join(path, entry))
|
|
55
|
-
end.sort
|
|
56
|
+
end.sort.freeze
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# List files from the given path
|
|
59
60
|
def list_files(path)
|
|
60
|
-
Dir.glob("#{path}/**").reject do |entry|
|
|
61
|
+
self.class.file_cache[path] ||= Dir.glob("#{path}/**").reject do |entry|
|
|
61
62
|
next if %w[. ..].include?(entry)
|
|
62
63
|
File.directory?(entry)
|
|
63
|
-
end.sort
|
|
64
|
+
end.sort.freeze
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# Get the random parts for the robot
|
|
67
68
|
# eyes, ears, etc...
|
|
68
69
|
def get_list_of_files(path)
|
|
69
|
-
#
|
|
70
|
-
# sets/set1/blue
|
|
71
|
-
directories = Dir.glob("#{path}/**").select do |entry|
|
|
70
|
+
directories = self.class.part_directory_cache[path] ||= Dir.glob("#{path}/**").select do |entry|
|
|
72
71
|
next if %w[. ..].include?(entry)
|
|
73
72
|
File.directory?(entry)
|
|
74
|
-
end.sort
|
|
73
|
+
end.sort.freeze
|
|
75
74
|
|
|
76
75
|
# This is to index into the proper place in the hash array
|
|
77
76
|
iter = 4
|
|
78
77
|
directories.map do |dir|
|
|
79
|
-
files = Dir.entries(dir).reject { |k| %w[. ..].include?(k) }.sort
|
|
78
|
+
files = self.class.directory_file_cache[dir] ||= Dir.entries(dir).reject { |k| %w[. ..].include?(k) }.sort.freeze
|
|
80
79
|
sample = files[my_hash_array[iter] % files.length]
|
|
81
80
|
iter += 1
|
|
82
81
|
[dir, sample].join('/')
|
|
@@ -112,52 +111,88 @@ module Rubohash
|
|
|
112
111
|
|
|
113
112
|
roboparts = get_list_of_files(robot.my_set).sort_by { |k| k.split('#')[1] }
|
|
114
113
|
robot.parts = roboparts
|
|
114
|
+
background = selected_background_for(robot)
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
output_path, tempfile = output_destination_for(robot)
|
|
117
|
+
compose_image(roboparts, background, output_path)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
robot.name = string
|
|
120
|
+
robot.my_digest = my_digest
|
|
121
|
+
|
|
122
|
+
return load_image(output_path, tempfile: tempfile) if Rubohash.mounted
|
|
123
|
+
|
|
124
|
+
puts "Writing Robot: '#{output_path}'"
|
|
125
|
+
robot
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.directory_cache
|
|
129
|
+
@directory_cache ||= {}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.file_cache
|
|
133
|
+
@file_cache ||= {}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.part_directory_cache
|
|
137
|
+
@part_directory_cache ||= {}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.directory_file_cache
|
|
141
|
+
@directory_file_cache ||= {}
|
|
142
|
+
end
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
# use the hash bits to get the actual sample
|
|
129
|
-
background_files = list_files(robot.my_background_set)
|
|
144
|
+
private
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
background
|
|
146
|
+
def compose_image(parts, background, output_path)
|
|
147
|
+
MiniMagick::Tool::Convert.new do |convert|
|
|
148
|
+
if background
|
|
149
|
+
convert << '('
|
|
150
|
+
convert << background
|
|
151
|
+
convert << '-resize' << '1024x1024!'
|
|
152
|
+
convert << ')'
|
|
153
|
+
else
|
|
154
|
+
convert << '-size' << '1024x1024'
|
|
155
|
+
convert << 'canvas:none'
|
|
156
|
+
end
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
parts.each do |part|
|
|
159
|
+
convert << '('
|
|
160
|
+
convert << part
|
|
161
|
+
convert << '-resize' << '1024x1024!'
|
|
162
|
+
convert << ')'
|
|
140
163
|
end
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
|
|
165
|
+
convert << '-background' << 'none'
|
|
166
|
+
convert << '-layers' << 'flatten'
|
|
167
|
+
|
|
168
|
+
convert << '-resize' << '300x300!'
|
|
169
|
+
convert << output_path
|
|
143
170
|
end
|
|
171
|
+
end
|
|
144
172
|
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
def selected_background_for(robot)
|
|
174
|
+
return unless Rubohash.use_background
|
|
147
175
|
|
|
176
|
+
background_files = list_files(robot.my_background_set)
|
|
177
|
+
background_hash_key = my_hash_array[3] % background_files.size
|
|
178
|
+
background_files[background_hash_key]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def output_destination_for(robot)
|
|
148
182
|
if Rubohash.mounted
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
tempfile = Tempfile.new([robot.name || 'rubohash', ".#{my_format}"])
|
|
184
|
+
[tempfile.path, tempfile]
|
|
151
185
|
else
|
|
152
186
|
FileUtils.mkdir_p(Rubohash.robot_output_path)
|
|
153
|
-
|
|
154
|
-
puts "Writing Robot: '#{path}'"
|
|
155
|
-
image.write path
|
|
156
|
-
robot
|
|
187
|
+
[File.join(Rubohash.robot_output_path, "#{string}.#{my_format}"), nil]
|
|
157
188
|
end
|
|
158
189
|
end
|
|
159
190
|
|
|
160
|
-
|
|
191
|
+
def load_image(path, tempfile: nil)
|
|
192
|
+
image = MiniMagick::Image.new(path)
|
|
193
|
+
image.instance_variable_set(:@rubohash_tempfile, tempfile) if tempfile
|
|
194
|
+
image
|
|
195
|
+
end
|
|
161
196
|
|
|
162
197
|
# Build out robot attributes
|
|
163
198
|
def build_robot_attrs
|
data/lib/rubohash/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubohash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mark Holmberg
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mini_magick
|
|
@@ -125,6 +125,7 @@ files:
|
|
|
125
125
|
- LICENSE.txt
|
|
126
126
|
- README.md
|
|
127
127
|
- Rakefile
|
|
128
|
+
- benchmark/image_generation.rb
|
|
128
129
|
- bin/build.sh
|
|
129
130
|
- bin/clean.sh
|
|
130
131
|
- bin/console
|