rubohash 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21ae00543eb4f8f3b4a94616f1d730355e92635fdd95c5a0febc31b80fe2ae5f
4
- data.tar.gz: 87b6ff9172dd54bce6fecd3af1b3ed739fc3e3887b13502ae349de934f5c765c
3
+ metadata.gz: 7dfd304b72e037e11bf35ba9061a2158e19bc73965aac68ec0ca3ca08b3867ef
4
+ data.tar.gz: bf91a387bca0176f56b1f6e21f2ad58bd5d7179e30985003c1878a1baac096c2
5
5
  SHA512:
6
- metadata.gz: 03ee912152ae61bd84a5451c9f9f103cbdb0a620f5b35748d3a5aa47b2fa47718fe82af05185668d9ce5e2eced161fba045305cbc5baad6a7d6c4fe016233541
7
- data.tar.gz: e5ceb38b5d5b72213dc8e6ddcb608cc35af29af30bcd4cb309403ba59df255de597837aa2791a5b6ecdd8108317d5d7e8d89a388f290afb444cd63fa96339519
6
+ metadata.gz: 9c9e10adc7548488c402c58cceb6f81870cef25b0e1de0003e1bd07c06f6a5e61f5ab82975d4924e37a6b28dd44be7262c3c1ab48c6766014cd8338813859f52
7
+ data.tar.gz: 55bfaab91a0408813dbe8a3909833baee8b3ed1f246b9c967c20f21a24a4a3693e4632ea17606606b9e46251186c070d550509578d0a49b83ee16820b7896e93
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubohash (0.1.1)
4
+ rubohash (0.1.4)
5
5
  mini_magick (~> 4.9.4)
6
6
 
7
7
  GEM
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
@@ -3,4 +3,11 @@ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
+ namespace :benchmark do
7
+ desc 'Benchmark image generation'
8
+ task :image_generation do
9
+ ruby 'benchmark/image_generation.rb'
10
+ end
11
+ end
12
+
6
13
  task default: :spec
@@ -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
@@ -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
- # Get all subdirectories
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
- image = MiniMagick::Image.open(roboparts.first)
117
- image = image.resize('1024x1024')
116
+ output_path, tempfile = output_destination_for(robot)
117
+ compose_image(roboparts, background, output_path)
118
118
 
119
- roboparts.each do |part|
120
- img = MiniMagick::Image.open(part)
121
- img = img.resize('1024x1024')
122
- image = image.composite(img) do |c|
123
- c.compose 'Over'
124
- end
125
- end
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
- if Rubohash.use_background
128
- # use the hash bits to get the actual sample
129
- background_files = list_files(robot.my_background_set)
144
+ private
130
145
 
131
- # Set background itself from hash bits
132
- background_hash_key = my_hash_array[3] % background_files.size
133
- background = background_files[background_hash_key]
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
- bg = MiniMagick::Image.open(background)
136
- bg = bg.resize('1024x1024')
137
- image = image.composite(bg) do |c|
138
- c.compose 'Dst_Over'
139
- c.resize '300x300'
158
+ parts.each do |part|
159
+ convert << '('
160
+ convert << part
161
+ convert << '-resize' << '1024x1024!'
162
+ convert << ')'
140
163
  end
141
- else
142
- image = image.resize('300x300')
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
- robot.name = string
146
- robot.my_digest = my_digest
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
- # Just return the image
150
- image
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
- path = File.join(Rubohash.robot_output_path, "#{robot.name}.#{self.my_format}")
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
- private
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
@@ -1,5 +1,5 @@
1
1
  # Rubohash namespace
2
2
  module Rubohash
3
3
  # The version number of this gem
4
- VERSION = '0.1.1'.freeze
4
+ VERSION = '0.1.4'.freeze
5
5
  end
data/rubohash.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Mark Holmberg']
10
10
  spec.email = ['mark.holmberg@icentris.com']
11
11
 
12
- spec.summary = 'It generates SHA512 Robot Images.'
12
+ spec.summary = 'Ruby adaptation of robohash.org.'
13
13
  spec.description = 'Ruby adaptation of robohash.org'
14
14
  spec.homepage = 'https://github.com/nedzib/rubohash'
15
15
  spec.license = 'MIT'
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.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-18 00:00:00.000000000 Z
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
@@ -969,5 +970,5 @@ requirements: []
969
970
  rubygems_version: 3.4.10
970
971
  signing_key:
971
972
  specification_version: 4
972
- summary: It generates SHA512 Robot Images.
973
+ summary: Ruby adaptation of robohash.org.
973
974
  test_files: []