├── .rspec ├── spec ├── graph │ ├── faker.gif │ ├── memory.gif │ ├── rantly.gif │ ├── two_func.gif │ ├── comparison.gif │ ├── multitrial.gif │ ├── proc_v_method.gif │ ├── two_func.txt │ └── function_spec.rb └── spec_helper.rb ├── lib └── graph │ ├── function │ ├── version.rb │ ├── ints_comparison.rb │ ├── reformat_string.rb │ ├── plot_config.rb │ └── comparison.rb │ └── function.rb ├── examples ├── comparing_ints.gif ├── comparing_ints.rb └── comparing_ints.html ├── .travis.yml ├── Gemfile ├── bin ├── console └── setup ├── Rakefile ├── .gitignore ├── LICENSE ├── LICENSE.txt ├── graph-function.gemspec ├── CODE_OF_CONDUCT.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /spec/graph/faker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/faker.gif -------------------------------------------------------------------------------- /spec/graph/memory.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/memory.gif -------------------------------------------------------------------------------- /spec/graph/rantly.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/rantly.gif -------------------------------------------------------------------------------- /spec/graph/two_func.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/two_func.gif -------------------------------------------------------------------------------- /lib/graph/function/version.rb: -------------------------------------------------------------------------------- 1 | module Graph 2 | module Function 3 | VERSION = '0.2.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/graph/comparison.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/comparison.gif -------------------------------------------------------------------------------- /spec/graph/multitrial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/multitrial.gif -------------------------------------------------------------------------------- /examples/comparing_ints.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/examples/comparing_ints.gif -------------------------------------------------------------------------------- /spec/graph/proc_v_method.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreniemi/graph-function/HEAD/spec/graph/proc_v_method.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.2 5 | before_install: gem install bundler -v 1.12.5 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in graph-function.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "graph/function" 5 | 6 | require "pry" 7 | Pry.start 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'graph/function' 4 | Graph::Function.configure do |config| 5 | end 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/graph/function/ints_comparison.rb: -------------------------------------------------------------------------------- 1 | module Graph 2 | module Function 3 | class IntsComparison < Comparison 4 | def initialize 5 | @data_generator = Proc.new {|v| (-v/2 + 1 .. v/2).to_a.shuffle } 6 | end 7 | 8 | def self.of(*methods) 9 | comparison = self.new 10 | comparison.of(*methods) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/graph/function/reformat_string.rb: -------------------------------------------------------------------------------- 1 | module Graph 2 | module Function 3 | module ReformatString 4 | def escape_underscores(s) 5 | s.to_s.gsub("_", "\\_") 6 | end 7 | def camel_title(s) 8 | s.to_s.split('_').collect(&:capitalize).join 9 | end 10 | def extract_filename(s) 11 | matches = /([^\/]+:\d+)\>$/.match(s) 12 | !matches.nil? ? matches[1] : s 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/comparing_ints.rb: -------------------------------------------------------------------------------- 1 | require 'graph/function' 2 | 3 | Graph::Function.as_gif 4 | 5 | def bubble_sort(array) 6 | n = array.length 7 | loop do 8 | swapped = false 9 | (n-1).times do |i| 10 | if array[i] > array[i+1] 11 | array[i], array[i+1] = array[i+1], array[i] 12 | swapped = true 13 | end 14 | end 15 | break if not swapped 16 | end 17 | array 18 | end 19 | 20 | def sort(array) 21 | array.sort 22 | end 23 | 24 | Graph::Function::IntsComparison.of(method(:sort), method(:bubble_sort)) 25 | -------------------------------------------------------------------------------- /lib/graph/function/plot_config.rb: -------------------------------------------------------------------------------- 1 | module Graph 2 | module Function 3 | module PlotConfig 4 | def set_up(plot) 5 | plot.ylabel ylabel 6 | plot.xlabel 'input size' 7 | plot.terminal (t = Graph::Function.configuration.terminal) 8 | plot.output Graph::Function.configuration.output unless t == 'x11' 9 | end 10 | 11 | private 12 | def ylabel 13 | if Graph::Function.configuration.memory 14 | Graph::Function.configuration.trials == 1 ? 'total allocation memsize (bytes)' : 'average total allocation memsize (bytes)' 15 | else 16 | Graph::Function.configuration.trials == 1 ? 'execution time (seconds)' : 'average execution time (seconds)' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alex Moore-Niemi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Billy Everyteen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /graph-function.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'graph/function/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'graph-function' 8 | spec.version = Graph::Function::VERSION 9 | spec.authors = ['Alex Moore-Niemi', 'Cora Johnson-Roberson'] 10 | spec.email = ['moore.niemi@gmail.com'] 11 | 12 | spec.summary = %q{Graph function performance.} 13 | spec.description = %q{Using gnuplot and Ruby's benchmarking abilities, see the asymptotic behavior of your functions graphed.} 14 | spec.homepage = 'http://github.com/mooreniemi/graph-function' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'gnuplot' 22 | spec.add_dependency 'rantly' 23 | spec.add_dependency 'faker' 24 | spec.add_dependency 'ruby-progressbar' 25 | spec.add_dependency 'memory_profiler' 26 | spec.add_development_dependency 'bundler', '~> 1.12' 27 | spec.add_development_dependency 'rake', '~> 10.0' 28 | spec.add_development_dependency 'rspec', '~> 3.0' 29 | spec.add_development_dependency 'pry' 30 | end 31 | -------------------------------------------------------------------------------- /lib/graph/function.rb: -------------------------------------------------------------------------------- 1 | require 'gnuplot' 2 | require 'benchmark' 3 | require 'memory_profiler' 4 | require 'rantly' 5 | require 'faker' 6 | require 'ruby-progressbar' 7 | 8 | require 'graph/function/version' 9 | require 'graph/function/reformat_string' 10 | require 'graph/function/plot_config' 11 | require 'graph/function/comparison' 12 | require 'graph/function/ints_comparison' 13 | 14 | module Graph 15 | module Function 16 | # https://robots.thoughtbot.com/mygem-configure-block 17 | class << self 18 | attr_accessor :configuration 19 | end 20 | 21 | def self.configure 22 | self.configuration ||= Configuration.new 23 | yield(configuration) if block_given? 24 | end 25 | 26 | singleton_class.send(:alias_method, :as_x11, :configure) 27 | 28 | def self.file_location(ext) 29 | File.new("#{$0.slice(0..-4)}.#{ext}", 'w').path 30 | end 31 | 32 | def self.as_gif(file = file_location('gif')) 33 | self.configure do |config| 34 | config.terminal = 'gif' 35 | config.output = file 36 | end 37 | end 38 | 39 | def self.as_canvas(file = file_location('html')) 40 | self.configure do |config| 41 | config.terminal = 'canvas' 42 | config.output = file 43 | end 44 | end 45 | 46 | class Configuration 47 | attr_accessor :terminal, :output 48 | attr_accessor :step 49 | attr_accessor :trials, :memory 50 | 51 | # defaults 52 | # see https://github.com/rdp/ruby_gnuplot/blob/master/examples/output_image_file.rb 53 | # or http://mibai.tec.u-ryukyu.ac.jp/~oshiro/Doc/gnuplot_primer/gptermcmp.html 54 | def initialize 55 | @terminal = 'x11' 56 | @output = '.' 57 | @step = (0..10_000).step(1000).to_a 58 | # these are particular to GF, not Gnuplot 59 | @trials = 1 60 | @memory = false 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/graph/two_func.txt: -------------------------------------------------------------------------------- 1 | 2 | One vs Two 3 | 4 | 0.0007 +-+----------+------------+-------------+------------+----------+-+ 5 | + + + + + #B 6 | 0.0006 +-+ one ***A***-+ 7 | | tw###B#B### | 8 | | #B## | 9 | 0.0005 +-+ #### +-+ 10 | | ###B# *A***** ***A 11 | 0.0004 +-+ #B## ** A*** +-+ 12 | | #### ** | 13 | | #B# *A*****A* | 14 | 0.0003 +-+ #### **** +-+ 15 | | ###B# ***A* | 16 | 0.0002 +-+ *A*****A*** +-+ 17 | | **** | 18 | | ***A* | 19 | 0.0001 +-+ *A** +-+ 20 | + **** + + + + + 21 | 0 A*+----------+------------+-------------+------------+----------+-+ 22 | 0 2000 4000 6000 8000 10000 23 | input size 24 | 25 | -------------------------------------------------------------------------------- /lib/graph/function/comparison.rb: -------------------------------------------------------------------------------- 1 | module Graph 2 | module Function 3 | class Comparison 4 | include ReformatString 5 | include PlotConfig 6 | attr_accessor :data_generator 7 | 8 | def initialize(generator) 9 | @data_generator = generator 10 | end 11 | 12 | def of(*functions) 13 | fail unless functions.all? {|f| f.respond_to?(:call) } 14 | 15 | results = {} 16 | 17 | Gnuplot.open do |gp| 18 | Gnuplot::Plot.new(gp) do |plot| 19 | title = functions_to_title(functions) 20 | plot.title title 21 | set_up(plot) 22 | 23 | x = Graph::Function.configuration.step 24 | trials = Graph::Function.configuration.trials 25 | pb = ProgressBar.create(title: title, total: x.size) 26 | 27 | functions.each do |f| 28 | pb.reset 29 | 30 | name = fname(f) 31 | results[name] = {} 32 | 33 | y = x.collect do |v| 34 | pb.increment 35 | data = data_generator.call(v) 36 | current_trials = (1..trials).collect do |_| 37 | if Graph::Function.configuration.memory 38 | MemoryProfiler.report { f.call(data) }.total_allocated_memsize 39 | else 40 | Benchmark.measure { f.call(data) }.real 41 | end 42 | end 43 | results[name][v] = current_trials 44 | current_trials.reduce(0.0, :+) / trials 45 | end 46 | 47 | plot.data << Gnuplot::DataSet.new( [x, y] ) do |ds| 48 | ds.with = "linespoints" 49 | ds.title = "#{escape_underscores(name)}" 50 | end 51 | end 52 | end 53 | end 54 | results 55 | end 56 | 57 | private 58 | def fname(function) 59 | function.respond_to?(:name) ? function.name : extract_filename(function.to_s) 60 | end 61 | 62 | def functions_to_title(functions) 63 | case functions.size 64 | when 1 65 | "#{camel_title(fname(functions[0]))}" 66 | when 2 67 | "#{camel_title(fname(functions[0]))} vs #{camel_title(fname(functions[1]))}" 68 | else 69 | "#{functions.map {|f| camel_title(fname(f)) }.join(', ') }" 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at your_email@example.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /spec/graph/function_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def drops(results_for_function) 4 | # count how often the function takes more time for a smaller input than a larger one 5 | xs = results_for_function.values.map {|a| a.reduce(0, :+) } 6 | n = 0 7 | (0..xs.size - 2).each do |i| 8 | n = n + 1 if xs[i] > xs[i + 1] 9 | end 10 | n 11 | end 12 | 13 | describe Graph::Function do 14 | it 'has a version number' do 15 | expect(Graph::Function::VERSION).not_to be nil 16 | end 17 | 18 | describe Graph::Function::IntsComparison do 19 | def one(array) 20 | array.each {|e| e } 21 | end 22 | def two(array) 23 | array.each {|e| e * 2 } 24 | end 25 | it 'plots two functions and returns the times from each' do 26 | Graph::Function.configuration.trials = 3 27 | results = Graph::Function::IntsComparison.of(method(:one), method(:two)) 28 | expect(drops(results[:one])).to be <= 2 29 | expect(drops(results[:two])).to be <= 2 30 | end 31 | it 'can output to gif' do 32 | Graph::Function.configuration.terminal = 'gif' 33 | Graph::Function.configuration.output = File.expand_path('../two_func.gif', __FILE__) 34 | Graph::Function::IntsComparison.of(method(:one), method(:two)) 35 | end 36 | it 'can output to txt' do 37 | Graph::Function.configuration.terminal = 'dumb' 38 | Graph::Function.configuration.output = File.expand_path('../two_func.txt', __FILE__) 39 | Graph::Function::IntsComparison.of(method(:one), method(:two)) 40 | end 41 | end 42 | 43 | describe Graph::Function::Comparison do 44 | let(:rantly_generator) do 45 | proc {|size| 46 | Rantly { dict(size) { [string, integer] }} 47 | } 48 | end 49 | let(:hash_first_proc) { proc {|hash| hash.values.first}} 50 | def hash_last_value(hash) 51 | hash.values.last 52 | end 53 | def hash_first_value(hash) 54 | hash.values.first 55 | end 56 | it 'uses a Rantly generator for x data' do 57 | Graph::Function.configuration.terminal = 'gif' 58 | Graph::Function.configuration.output = File.expand_path('../comparison.gif', __FILE__) 59 | comparison = Graph::Function::Comparison.new(rantly_generator) 60 | comparison.of(method(:hash_last_value), method(:hash_first_value)) 61 | end 62 | it 'can take a proc as input' do 63 | Graph::Function.configuration.terminal = 'gif' 64 | Graph::Function.configuration.output = File.expand_path('../proc_v_method.gif', __FILE__) 65 | comparison = Graph::Function::Comparison.new(rantly_generator) 66 | comparison.of(hash_first_proc, method(:hash_first_value)) 67 | end 68 | it 'can perform multiple trials' do 69 | Graph::Function.configuration.terminal = 'gif' 70 | Graph::Function.configuration.output = File.expand_path('../multitrial.gif', __FILE__) 71 | Graph::Function.configuration.trials = 3 72 | comparison = Graph::Function::Comparison.new(rantly_generator) 73 | comparison.of(method(:hash_last_value), method(:hash_first_value)) 74 | end 75 | end 76 | 77 | describe "Comparison of one function" do 78 | let(:rantly_generator) do 79 | proc {|size| 80 | Rantly { array(size) {string} } 81 | } 82 | end 83 | def single_func(as) 84 | as.map(&:upcase) 85 | end 86 | let(:faker_generator) do 87 | proc {|size| 88 | Rantly(size) { call(Proc.new { Faker::Date.backward(14) }) } 89 | } 90 | end 91 | def custom_types(faked) 92 | if faked.is_a?(Array) 93 | faked.map {|e| e.strftime("at %I:%M%p") } 94 | else 95 | faked.strftime("at %I:%M%p") 96 | end 97 | end 98 | it 'uses a Rantly generator and acts on one function' do 99 | Graph::Function.configuration.terminal = 'gif' 100 | Graph::Function.configuration.output = File.expand_path('../rantly.gif', __FILE__) 101 | graph = Graph::Function::Comparison.new(rantly_generator) 102 | graph.of(method(:single_func)) 103 | end 104 | it 'can use a Faker/Rantly generator and acts on one function' do 105 | Graph::Function.configuration.terminal = 'gif' 106 | Graph::Function.configuration.output = File.expand_path('../faker.gif', __FILE__) 107 | graph = Graph::Function::Comparison.new(faker_generator) 108 | graph.of(method(:custom_types)) 109 | end 110 | it 'allows for basic memory profiling' do 111 | Graph::Function.configuration.terminal = 'gif' 112 | Graph::Function.configuration.memory = true 113 | Graph::Function.configuration.output = File.expand_path('../memory.gif', __FILE__) 114 | graph = Graph::Function::Comparison.new(faker_generator) 115 | graph.of(method(:custom_types)) 116 | end 117 | end 118 | 119 | describe Graph::Function::ReformatString do 120 | include Graph::Function::ReformatString 121 | 122 | it 'extracts a filename and line number from Proc#to_s' do 123 | proc_to_s = '#' 124 | expect(extract_filename(proc_to_s)).to eq('function_spec.rb:36') 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /examples/comparing_ints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gnuplot Canvas Graph 5 | 6 | 7 | 8 | 9 | 10 | 11 | 287 | 288 | 289 | 290 | 291 |
292 | 293 | 294 | 299 |
295 | 296 | Sorry, your browser seems not to support the HTML 5 canvas element 297 | 298 |
300 |
301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | .-. / \ _ 3 | ^^ / \ /^./\__ _/ \ 4 | _ .--'\/\_ \__/. \ / \ ^^ ___ 5 | / \_ _/ ^ \/ __ :' /\/\ /\ __/ \ 6 | / \ / .' _/ / \ ^ / \/ \/ .`'\_/\ 7 | /\/\ /\/ :' __ ^/ ^/ `--./.' ^ `-.\ _ _:\ _ 8 | / \/ \ _/ \-' __/.' ^ _ \_ .'\ _/ \ . __/ \ 9 | /\ .- `. \/ \ / -. _/ \ -. `_/ \ / `._/ ^ \ 10 | / `-.__ ^ / .-'.--' . / `--./ .-' `-. `-. `. - `. 11 | @/ `. / / `-. / .-' / . .' \ \ \ .- \% 12 | @(88%@)@%% @)&@&(88&@.-_=_-=_-=_-=_-=_.8@% &@&&8(8%@%8)(8@%8 8%@)% 13 | @88:::&(&8&&8::JGS:&`.~-_~~-~~_~-~_~-~~=.'@(&%::::%@8&8)::&#@8:::: 14 | `::::::8%@@%:::::@%&8:`.=~~-.~~-.~~=..~'8::::::::&@8:::::&8::::::' 15 | `::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::' 16 | 17 | ``` 18 | 19 | # Graph::Function 20 | 21 | This gem's goal is to make it easy to compare the [asymptotic performance](https://en.wikipedia.org/wiki/Asymptotic_analysis) of two or more functions via graphing. 22 | 23 | When I work on katas and exercises I found I often wanted to compare my implementations. After doing so a half dozen times I noticed some patterns, and figured it'd be valuable to capture those into an easier API to work with. While working on a kata I like the immediacy of replotting back on x11, but because of gnuplot's structure it is just as easy to get images or html canvas graphs. 24 | 25 | As a secondary performance metric, you can also [graph total allocated memsize](#graphing-memory). 26 | 27 | ## Installation 28 | 29 | Because this gem depends on `gnuplot` and `xquartz`, we need to follow their [prereq steps](https://github.com/rdp/ruby_gnuplot#pre-requisites-and-installation): 30 | 31 | ``` 32 | # these will vary by your system, mine is mac 33 | brew install Caskroom/cask/xquartz 34 | brew install gnuplot --with-x11 35 | # verify you have x11 36 | xpdyinfo | grep version 37 | ``` 38 | 39 | Now we're set. Add this line to your application's Gemfile: 40 | 41 | ```ruby 42 | gem 'graph-function' 43 | ``` 44 | 45 | And then execute: 46 | 47 | $ bundle 48 | 49 | Or install it yourself as: 50 | 51 | $ gem install graph-function 52 | 53 | ## Usage 54 | 55 | ### TL;DR 56 | 57 | From the [comparing ints example](examples/comparing_ints.rb): 58 | 59 | ```ruby 60 | require 'graph/function' 61 | Graph::Function.as_gif 62 | Graph::Function::IntsComparison.of(method(:sort), method(:bubble_sort)) 63 | ``` 64 | 65 | Produces: 66 | 67 | ![comparing ints](examples/comparing_ints.gif) 68 | 69 | ### Setup 70 | 71 | To set up, you only need the following: 72 | 73 | ```ruby 74 | require 'graph/function' 75 | Graph::Function.as_x11 76 | ``` 77 | 78 | If you don't want to output to [x11](https://www.xquartz.org/), just set `config.terminal` to a different option. Two convenience methods exist for `gif` and `canvas`: 79 | 80 | ```ruby 81 | # by default file will be set to name of the executing file and dumped in its dir 82 | # or you can set file yourself like so: 83 | Graph::Function.as_gif(File.expand_path('../comparing_ints.gif', __FILE__)) 84 | Graph::Function.as_canvas(File.expand_path('../comparing_ints.html', __FILE__)) 85 | ``` 86 | 87 | You can use anything else gnuplot [respects as a terminal](http://mibai.tec.u-ryukyu.ac.jp/~oshiro/Doc/gnuplot_primer/gptermcmp.html), even outputting to just `txt`! 88 | 89 | ```ruby 90 | Graph::Function.configure do |config| 91 | config.terminal = 'dumb' 92 | config.output = File.expand_path('../your_graph_name.txt', __FILE__) 93 | config.step = (0..10_000).step(1000).to_a # default value 94 | config.trials = 1 95 | end 96 | ``` 97 | 98 | In configuration, you can control the "step" size of `x` in the plot. Its default value is `(0..10_000).step(1000).to_a` (`[0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]`) but you can make it as fine or rough grained as you need up to any size. 99 | 100 | You can also set a number of trials over which to average execution times. 101 | 102 | ### Graphing 103 | 104 | The simplest usage (suitable for a large class of exercises, in my experience) is if you're comparing two functions that take a single argument of `Array[Int]` type: 105 | 106 | ```ruby 107 | c = YourClass.new # this class has #function_name_one & #function_name_two 108 | Graph::Function::IntsComparison.of(c.method(:function_name_one), c.method(:function_name_two)) 109 | # => will output an xquartz graph 110 | ``` 111 | 112 | ![comparison](spec/graph/two_func.gif) 113 | 114 | For more complex use cases, you'll be creating a `Graph::Function::Comparison` with some generator of data, and executing `#of` with `Method` objects or `Proc`s that operate on the same parameter types[1](#f1). (Note because `IntsComparison` *does not need a generator*, `.of` is a class method instead.) 115 | 116 | ### Graphing Memory 117 | 118 | To graph total allocated memsize rather than execution time, just set the following configuration: 119 | 120 | ``` 121 | Graph::Function.configure do |config| 122 | config.memory = true 123 | end 124 | ``` 125 | 126 | ### Generators 127 | 128 | To generate values of the type needed by your function, you can write 129 | a generator in Ruby or use the provided dependency 130 | [Rantly](https://github.com/hayeah/rantly). 131 | 132 | Here's an example of a simple Ruby generator, it's just a `Proc` parameterized on `size`: 133 | 134 | ```ruby 135 | tiny_int_generator = proc {|size| Array.new(size) { rand(-9...9) } } 136 | comparison = Graph::Function::Comparison.new(tiny_int_generator) 137 | ``` 138 | 139 | For Rantly usage, there's great documentation on generating many different kinds of data in 140 | their documentation, but here's an example of comparing two functions that 141 | take `Hash{String => Integer}`: 142 | 143 | ```ruby 144 | # you must put it in a proc taking size so Graph::Function can increase it 145 | generator = proc {|size| Rantly { dict(size) { [string, integer] } } 146 | dict_comparison = Graph::Function::Comparison.new(generator) 147 | # Comparison can take any number of Methods, but for now, 2 148 | dict_comparison.of(method(:hash_func_one), method(:hash_func_two)) 149 | # => will output an xquartz graph 150 | ``` 151 | 152 | ![comparison](spec/graph/comparison.gif) 153 | 154 | If you want to make use of more "real" fake data, [Faker](https://github.com/stympy/faker) is also included, and can be used like so in your generators: 155 | 156 | ```ruby 157 | # again, we need to parameterize our generator with size 158 | faker_generator = proc {|size| Rantly(size) { call(Proc.new { Faker::Date.backward(14) }) } 159 | graph = Graph::Function::Comparison.new(faker_generator) 160 | graph.of(method(:custom_types)) 161 | # => will output an xquartz graph 162 | ``` 163 | 164 | ![faker](spec/graph/faker.gif) 165 | 166 | The only downside here is that you can't parameterize `Faker`, but you could use random generators to mix it up. Using the above example, `graph-function` won't pass anything into the `faker_generator` but the `size`, so if we want the value to change, we could use `Faker::Date.backward(proc { rand(10) }.call)`. 167 | 168 | Check out the [spec file](spec/graph/function_spec.rb) to see all of these or see [examples](examples/). 169 | 170 | ### Functions that use `self` 171 | 172 | For graphing functions that operate on `self`, such as `String#upcase`, you must provide a `Method` or `Proc` that wraps the method call. For instance: 173 | 174 | ```ruby 175 | generator = proc {|size| Rantly { sized(size) { string } } } 176 | # wrap the call to upcase 177 | test_upcase = proc {|s| s.upcase } 178 | graph = Graph::Function::Comparison.new(generator) 179 | graph.of(test_upcase) 180 | ``` 181 | 182 | ## Development 183 | 184 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 185 | 186 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 187 | 188 | ## Contributing 189 | 190 | Bug reports and pull requests are welcome on GitHub at https://github.com/mooreniemi/graph-function. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 191 | 192 | 193 | ## License 194 | 195 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 196 | 197 | ## Footnotes 198 | 199 | 1 Why are we constrained to testing the same parameter types? The intent of this library is to graph _implementations_. Changing parameter types suggests a change in the _behavior_ of the function. That doesn't make for a very productive comparison. [↩](#a1) 200 | --------------------------------------------------------------------------------