benny 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e40e81931380263c65f91bf4473aae25c84c82f9924f8d5e0b4901acf5b4772
4
+ data.tar.gz: cf0b191ac09dc6721824f49ce6c90f5afce68b2c54947f55ea490e92e8fc2cc9
5
+ SHA512:
6
+ metadata.gz: c20d7b905bb13bf0b9e6bcda2099f39966a5936321cd207976016df79ce58942a44569e951bcb78d89d11cfcfe14a009aea3b652c9cf8dbad489d9b9b1aeb0b3
7
+ data.tar.gz: 821a9b332b3d6e4cae6f5bfe59aa17841123f781828cedf8608951062b789169c1cea1a16003c05521255197a7f4c4db35c50199b9cb1e8d12783eb977c51510
data/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ The MIT License (MIT)
2
+ Copyright © 2023 Joakim Antman
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Benny, your Ruby benchmarking buddy
2
+
3
+ This tool was created to answer the question: How does this version of a library compare to another version of the same library?
4
+ For a familiar feel for Rubyists, [rspec](https://github.com/rspec/rspec-core) has been as inspiration for how benchmarks are structured and defined.
5
+
6
+ ## Basic structure of a benchmarking project
7
+
8
+ A simple benchmark project looks something like this. To generate such a project you can execute:
9
+
10
+ ```
11
+ gem install benny
12
+ benny init my_benny_project
13
+ ```
14
+
15
+ ```
16
+ my_benny_project/
17
+ ├── benchmarks/
18
+ │ ├── bench_helper.rb
19
+ │ └── test_bench.rb
20
+ ├── gemfiles/
21
+ │ ├── version-1.gemfile
22
+ │ └── version-2.gemfile
23
+ └── Gemfile
24
+ ```
25
+
26
+ ### Gemfile
27
+ Dependencies are managed by [bundler](https://github.com/rubygems/rubygems/tree/master/bundler) and therefore a Gemfile is required
28
+
29
+ ```
30
+ source 'https://rubygems.org'
31
+
32
+ gem 'benny'
33
+ ```
34
+
35
+ ### benchmarks/ folder
36
+ Contains files that define environments and benchmarks.
37
+
38
+ ### benchmarks/bench_helper.rb
39
+ Configures the benchmarks and defines the different environments
40
+
41
+ A simple configuration configuring two environments to be compared:
42
+ ```
43
+ Benny.configure do |config|
44
+ config.environment 'Version 1' do
45
+ gemfile 'gemfiles/version-1.gemfile'
46
+ end
47
+ config.environment 'Version 2' do
48
+ gemfile 'gemfiles/version-2.gemfile'
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### benchmarks/*_bench.rb files
54
+ All the files ending with the suffix `_bench.rb` will be evaluated and added to the benchmark suite.
55
+
56
+
57
+ ```
58
+ Benny.define do
59
+ benchmark 'Simple calculation' do
60
+ 700_000.times do
61
+ sum = 1+1
62
+ end
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### gemfiles/ folder
68
+
69
+ The gemfile folder contains the different Gemfiles for version variations. These are referenced from the [benchmark/bench_helper.rb](#benchmarksbench_helperrb) file.
70
+ [Aappraisal](https://github.com/thoughtbot/appraisal) can be used in combination with Benny for managing the different gemfiles.
71
+
72
+ ## Defining benchmarks
73
+
74
+ TBD
75
+
76
+ ## Reporters
77
+
78
+ TBD
79
+
80
+ # Development and Tests
81
+
82
+ ## How to contribute
83
+ See [CONTRIBUTING](CONTRIBUTING.md).
84
+
85
+ ## License
86
+
87
+ See [LICENSE](LICENSE).
data/benny.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/benny/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'benny'
7
+ spec.version = Benny::VERSION
8
+ spec.authors = ['Joakim Antman']
9
+ spec.email = ['antmanj@gmail.com']
10
+
11
+ spec.summary = 'Benny the benchmark gem'
12
+ spec.description = 'Define and maintain your benchmarks in a familiarway.' \
13
+ 'Execute on different variations of environments'
14
+ spec.homepage = 'https://github.com/anakinj/benny'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 2.5.0'
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/anakinj/benny'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/anakinj/benny/blob/main/CHANGELOG.md'
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").select do |f|
23
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[exe/ lib/ README benny.gemspec LICENSE])
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_dependency 'gruff', '~> 0.23'
31
+ spec.add_dependency 'terminal-table', '~> 3.0'
32
+ spec.add_dependency 'thor', '~> 1.3'
33
+
34
+ spec.metadata['rubygems_mfa_required'] = 'true'
35
+ end
data/exe/benny ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'benny/cli'
5
+
6
+ Benny::CLI.start
data/lib/benny/cli.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'thor'
5
+
6
+ module Benny
7
+ class CLI < Thor
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+ default_task :all
12
+
13
+ desc 'init NAME', 'Create a new benny project'
14
+ def init(name)
15
+ require_relative 'init'
16
+ Init.create(name)
17
+ end
18
+
19
+ desc 'all', 'Run all bechmarks in all environments'
20
+ def all
21
+ require 'benny'
22
+ Benny.load_env
23
+ Benny.execute
24
+ end
25
+
26
+ desc 'execute', 'Execute a set of benchmarks'
27
+ method_options 'result-path' => :string
28
+ method_options 'env-name' => :string
29
+ def execute
30
+ require 'benny'
31
+ Benny.load_env
32
+ Executors::Benchmark.execute(benchmarks: Benny.benchmarks,
33
+ reporter: Reporters::File.new(env_name: options['env-name'],
34
+ path: options['result-path']))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Benny
4
+ class Configuration
5
+ attr_reader :environments
6
+
7
+ class EnvironmentEntry
8
+ attr_reader :name
9
+
10
+ def initialize(name, &block)
11
+ @name = name
12
+ instance_exec(&block)
13
+ end
14
+
15
+ def gemfile(path = nil)
16
+ @gemfile = path unless path.nil?
17
+ @gemfile
18
+ end
19
+ end
20
+
21
+ def environment(name, &block)
22
+ @environments ||= []
23
+ @environments << EnvironmentEntry.new(name, &block)
24
+ end
25
+
26
+ def reporter(reporter = nil)
27
+ @reporter = reporter
28
+ @reporter || Reporters::Stdout.new
29
+ end
30
+
31
+ def benchmark_path(path = nil)
32
+ @benchmark_path = path unless path.nil?
33
+ @benchmark_path || 'benchmarks/**/*_bench.rb'
34
+ end
35
+ end
36
+ end
data/lib/benny/dsl.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Benny
4
+ module DSL
5
+ class Definition
6
+ class WarmupOptions
7
+ attr_reader :iterations
8
+
9
+ def initialize(iterations:)
10
+ @iterations = iterations
11
+ end
12
+ end
13
+
14
+ class BenchmarkEntry
15
+ attr_reader :name, :block, :definition
16
+
17
+ def initialize(definition, name, &block)
18
+ @name = name
19
+ @block = block
20
+ @definition = definition
21
+ end
22
+ end
23
+
24
+ attr_reader :benchmarks, :before_blocks, :warmup_options
25
+
26
+ def initialize(&block)
27
+ @before_blocks = []
28
+ @benchmarks = []
29
+ @warmup_options = WarmupOptions.new(iterations: 1)
30
+ instance_eval(&block)
31
+ end
32
+
33
+ def warmup(iterations:)
34
+ @warmup_options = WarmupOptions.new(iterations: iterations)
35
+ end
36
+
37
+ def before(&block)
38
+ @before_blocks << block
39
+ end
40
+
41
+ def benchmark(name, &block)
42
+ @benchmarks << BenchmarkEntry.new(self, name, &block)
43
+ end
44
+ end
45
+
46
+ def define(&block)
47
+ @definitions ||= []
48
+ @definitions << Definition.new(&block)
49
+ end
50
+
51
+ def benchmarks
52
+ definitions.flat_map(&:benchmarks)
53
+ end
54
+
55
+ def load(path)
56
+ instance_eval(File.read(path), path, 1)
57
+ end
58
+
59
+ def configuration
60
+ @configuration ||= Configuration.new
61
+ end
62
+
63
+ def configure
64
+ yield(configuration)
65
+ end
66
+
67
+ attr_reader :environments, :definitions
68
+ end
69
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Benny
6
+ module Executors
7
+ class Benchmark
8
+ attr_reader :benchmarks, :reporter
9
+
10
+ def initialize(benchmarks:, reporter:)
11
+ @benchmarks = benchmarks
12
+ @reporter = reporter
13
+ end
14
+
15
+ def execute
16
+ benchmarks.each do |benchmark|
17
+ execute_one(benchmark)
18
+ end
19
+ end
20
+
21
+ def execute_one(benchmark)
22
+ benchmark.definition.before_blocks.each(&:call)
23
+
24
+ benchmark.definition.warmup_options.iterations.times do
25
+ benchmark.block.call
26
+ end
27
+
28
+ measure = ::Benchmark.measure do
29
+ benchmark.block.call
30
+ end
31
+
32
+ reporter.report(benchmark, measure)
33
+ end
34
+
35
+ def self.execute(benchmarks:, reporter:)
36
+ new(benchmarks: benchmarks, reporter: reporter).execute
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+ require 'tmpdir'
5
+
6
+ module Benny
7
+ module Executors
8
+ class Environment
9
+ attr_reader :environments, :reporter
10
+
11
+ def initialize(environments:, reporter:)
12
+ @environments = environments
13
+ @reporter = reporter
14
+ end
15
+
16
+ def run!
17
+ Output.puts("Running benchmark for #{environments.size} environments. Reporter: #{reporter.class.name}")
18
+
19
+ Dir.mktmpdir('benny') do |dir|
20
+ environments.each do |e|
21
+ run_one(e, dir)
22
+ end
23
+ report(dir)
24
+ end
25
+ end
26
+
27
+ def run_one(environment, dir)
28
+ Bundler.with_original_env do
29
+ ENV['BUNDLE_GEMFILE'] = environment.gemfile
30
+ Output.puts('Installing dependencies...')
31
+ Kernel.system(Shellwords.join(%w[bundle install]))
32
+ Output.puts("Executing benchmark for '#{environment.name}'..")
33
+ Kernel.system(Shellwords.join(['bundle', 'exec', 'benny', 'execute',
34
+ '--result-path', dir,
35
+ '--env-name', environment.name]))
36
+ end
37
+ end
38
+
39
+ def report(dir)
40
+ Output.puts('Generating report...')
41
+ metrics = Dir.glob(File.join(dir, '*.json')).map { |file| JSON.parse(File.read(file), symbolize_names: true) }
42
+ reporter.report(metrics)
43
+ end
44
+
45
+ def self.run!(*args)
46
+ new(*args).run!
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gem 'benny'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Benny.configure do |config|
4
+ config.environment 'latest' do
5
+ gemfile 'gemfiles/latest.gemfile'
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Benny.define do
4
+ before do
5
+ # Require dependencies or other setup
6
+ end
7
+
8
+ benchmark 'Description of benchmark' do
9
+ # Content of benchmark
10
+ end
11
+
12
+ benchmark 'Another benchmark' do
13
+ # Content of another benchmark
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "benny"
data/lib/benny/init.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Benny
6
+ module Init
7
+ def self.create(name)
8
+ raise ArgumentError, "The dir '#{name}' already exists" if Dir.exist?(name)
9
+
10
+ FileUtils.copy_entry(File.join(__dir__, 'init'), name)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Benny
4
+ module Output
5
+ def self.puts(txt)
6
+ $stdout.puts("[Benny] #{txt}")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Benny
6
+ module Reporters
7
+ class File
8
+ def initialize(path:, env_name:)
9
+ @path = path
10
+ @data = {
11
+ env_name: env_name,
12
+ benchmarks: []
13
+ }
14
+
15
+ at_exit { ::File.write(::File.join(@path, "#{Process.pid}.json"), JSON.dump(@data)) }
16
+ end
17
+
18
+ def report(benchmark, measure)
19
+ raise ArgumentError, "Measure of type #{measure.class} not supported." unless measure.is_a?(Benchmark::Tms)
20
+
21
+ @data[:benchmarks] << {
22
+ type: 'Benchmark::Tms',
23
+ name: benchmark.name,
24
+ utime: measure.utime,
25
+ stime: measure.stime,
26
+ total: measure.total
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff'
4
+
5
+ module Benny
6
+ module Reporters
7
+ class Gruff
8
+ def report(metrics) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
9
+ g = ::Gruff::SideBar.new(800)
10
+ g.title = 'Benchmark results'
11
+ result = {}
12
+ labels = {}
13
+
14
+ metrics.each.with_index do |metric, i|
15
+ labels[i] = metric[:env_name]
16
+ metric[:benchmarks].each do |bm|
17
+ result[bm[:name]] ||= []
18
+ result[bm[:name]] << bm[:total]
19
+ end
20
+ end
21
+
22
+ g.labels = labels
23
+ result.each { |k, v| g.data(k, v) }
24
+
25
+ g.write('result.png')
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module Benny
6
+ module Reporters
7
+ class Stdout
8
+ def initialize
9
+ @io = $stdout
10
+ end
11
+
12
+ def report(metrics) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
13
+ result = {}
14
+ labels = ['']
15
+
16
+ metrics.each.with_index do |metric, _i|
17
+ labels << metric[:env_name]
18
+ metric[:benchmarks].each do |bm|
19
+ result[bm[:name]] ||= []
20
+ result[bm[:name]] << bm[:total].round(5)
21
+ end
22
+ end
23
+
24
+ rows = result.map do |k, v|
25
+ ([k] + v)
26
+ end
27
+
28
+ puts Terminal::Table.new(headings: labels, rows: rows)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Benny
4
+ VERSION = '0.0.1'
5
+ end
data/lib/benny.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'benny/dsl'
4
+ require_relative 'benny/cli'
5
+ require_relative 'benny/output'
6
+ require_relative 'benny/configuration'
7
+ require_relative 'benny/executors/benchmark'
8
+ require_relative 'benny/executors/environment'
9
+ require_relative 'benny/reporters/stdout'
10
+ require_relative 'benny/reporters/gruff'
11
+ require_relative 'benny/reporters/file'
12
+ require_relative 'benny/version'
13
+
14
+ module Benny
15
+ extend DSL
16
+
17
+ def self.load_env
18
+ load('benchmarks/bench_helper.rb')
19
+ load_benchmarks(configuration.benchmark_path)
20
+ Output.puts("Loaded #{benchmarks.size} benchmark(s)")
21
+ end
22
+
23
+ def self.execute
24
+ Executors::Environment.new(environments: configuration.environments,
25
+ reporter: configuration.reporter).run!
26
+ end
27
+
28
+ def self.load_benchmarks(path)
29
+ Dir[path].each do |file|
30
+ load(file)
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: benny
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joakim Antman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gruff
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.23'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.23'
27
+ - !ruby/object:Gem::Dependency
28
+ name: terminal-table
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ description: Define and maintain your benchmarks in a familiarway.Execute on different
56
+ variations of environments
57
+ email:
58
+ - antmanj@gmail.com
59
+ executables:
60
+ - benny
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - benny.gemspec
67
+ - exe/benny
68
+ - lib/benny.rb
69
+ - lib/benny/cli.rb
70
+ - lib/benny/configuration.rb
71
+ - lib/benny/dsl.rb
72
+ - lib/benny/executors/benchmark.rb
73
+ - lib/benny/executors/environment.rb
74
+ - lib/benny/init.rb
75
+ - lib/benny/init/Gemfile
76
+ - lib/benny/init/benchmarks/bench_helper.rb
77
+ - lib/benny/init/benchmarks/parse_bench.rb
78
+ - lib/benny/init/gemfiles/latest.gemfile
79
+ - lib/benny/output.rb
80
+ - lib/benny/reporters/file.rb
81
+ - lib/benny/reporters/gruff.rb
82
+ - lib/benny/reporters/stdout.rb
83
+ - lib/benny/version.rb
84
+ homepage: https://github.com/anakinj/benny
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/anakinj/benny
89
+ source_code_uri: https://github.com/anakinj/benny
90
+ changelog_uri: https://github.com/anakinj/benny/blob/main/CHANGELOG.md
91
+ rubygems_mfa_required: 'true'
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.5.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.3.7
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Benny the benchmark gem
111
+ test_files: []