1#!/usr/bin/env ruby 2# 3# Copyright Louis Dionne 2013-2017 4# Distributed under the Boost Software License, Version 1.0. 5# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) 6# 7# 8# When called as a program, this script runs the command line given in 9# arguments and returns the total time. This is similar to the `time` 10# command from Bash. 11# 12# This file can also be required as a Ruby module to gain access to the 13# methods defined below. 14# 15# NOTE: 16# This file must not be used as-is. It must be processed by CMake first. 17 18require 'benchmark' 19require 'open3' 20require 'pathname' 21require 'ruby-progressbar' 22require 'tilt' 23 24 25def split_at(n, list) 26 before = list[0...n] || [] 27 after = list[n..-1] || [] 28 return [before, after] 29end 30 31# types : A sequence of strings to put in the mpl::vector. 32# Using this method requires including 33# - <boost/mpl/vector.hpp> 34# - <boost/mpl/push_back.hpp> 35def mpl_vector(types) 36 fast, rest = split_at(20, types) 37 rest.inject("boost::mpl::vector#{fast.length}<#{fast.join(', ')}>") { |v, t| 38 "boost::mpl::push_back<#{v}, #{t}>::type" 39 } 40end 41 42# types : A sequence of strings to put in the mpl::list. 43# Using this method requires including 44# - <boost/mpl/list.hpp> 45# - <boost/mpl/push_front.hpp> 46def mpl_list(types) 47 prefix, fast = split_at([types.length - 20, 0].max, types) 48 prefix.reverse.inject("boost::mpl::list#{fast.length}<#{fast.join(', ')}>") { |l, t| 49 "boost::mpl::push_front<#{l}, #{t}>::type" 50 } 51end 52 53# values : A sequence of strings representing values to put in the fusion::vector. 54# Using this method requires including 55# - <boost/fusion/include/make_vector.hpp> 56# - <boost/fusion/include/push_back.hpp> 57def fusion_vector(values) 58 fast, rest = split_at(10, values) 59 rest.inject("boost::fusion::make_vector(#{fast.join(', ')})") { |xs, v| 60 "boost::fusion::push_back(#{xs}, #{v})" 61 } 62end 63 64# values : A sequence of strings representing values to put in the fusion::list. 65# Using this method requires including 66# - <boost/fusion/include/make_list.hpp> 67# - <boost/fusion/include/push_back.hpp> 68def fusion_list(values) 69 fast, rest = split_at(10, values) 70 rest.inject("boost::fusion::make_list(#{fast.join(', ')})") { |xs, v| 71 "boost::fusion::push_back(#{xs}, #{v})" 72 } 73end 74 75# Turns a CMake-style boolean into a Ruby boolean. 76def cmake_bool(b) 77 return true if b.is_a? String and ["true", "yes", "1"].include?(b.downcase) 78 return true if b.is_a? Integer and b > 0 79 return false # otherwise 80end 81 82# aspect must be one of :compilation_time, :bloat, :execution_time 83def measure(aspect, template_relative, range, env = {}) 84 measure_file = Pathname.new("#{MEASURE_FILE}") 85 template = Pathname.new(template_relative).expand_path 86 range = range.to_a 87 88 if ENV["BOOST_HANA_JUST_CHECK_BENCHMARKS"] && range.length >= 2 89 range = [range[0], range[-1]] 90 end 91 92 make = -> (target) { 93 command = "@CMAKE_COMMAND@ --build @CMAKE_BINARY_DIR@ --target #{target}" 94 stdout, stderr, status = Open3.capture3(command) 95 } 96 97 progress = ProgressBar.create(format: '%p%% %t | %B |', 98 title: template_relative, 99 total: range.size, 100 output: STDERR) 101 range.map do |n| 102 # Evaluate the ERB template with the given environment, and save 103 # the result in the `measure.cpp` file. 104 code = Tilt::ERBTemplate.new(template).render(nil, input_size: n, env: env) 105 measure_file.write(code) 106 107 # Compile the file and get timing statistics. The timing statistics 108 # are output to stdout when we compile the file because of the way 109 # the `compile.benchmark.measure` CMake target is setup. 110 stdout, stderr, status = make["#{MEASURE_TARGET}"] 111 raise "compilation error: #{stdout}\n\n#{stderr}\n\n#{code}" if not status.success? 112 ctime = stdout.match(/\[compilation time: (.+)\]/i) 113 # Size of the generated executable in KB 114 size = File.size("@CMAKE_CURRENT_BINARY_DIR@/#{MEASURE_TARGET}").to_f / 1000 115 116 # If we didn't match anything, that's because we went too fast, CMake 117 # did not have the time to see the changes to the measure file and 118 # the target was not rebuilt. So we sleep for a bit and then retry 119 # this iteration. 120 (sleep 0.2; redo) if ctime.nil? 121 stat = ctime.captures[0].to_f if aspect == :compilation_time 122 stat = size if aspect == :bloat 123 124 # Run the resulting program and get timing statistics. The statistics 125 # should be written to stdout by the `measure` function of the 126 # `measure.hpp` header. 127 if aspect == :execution_time 128 stdout, stderr, status = make["#{MEASURE_TARGET}.run"] 129 raise "runtime error: #{stderr}\n\n#{code}" if not status.success? 130 match = stdout.match(/\[execution time: (.+)\]/i) 131 if match.nil? 132 raise ("Could not find [execution time: ...] bit in the output. " + 133 "Did you use the `measure` function in the `measure.hpp` header? " + 134 "stdout follows:\n#{stdout}") 135 end 136 stat = match.captures[0].to_f 137 end 138 139 progress.increment 140 [n, stat] 141 end 142ensure 143 measure_file.write("") 144 progress.finish if progress 145end 146 147def time_execution(erb_file, range, env = {}) 148 measure(:execution_time, erb_file, range, env) 149end 150 151def time_compilation(erb_file, range, env = {}) 152 measure(:compilation_time, erb_file, range, env) 153end 154 155if __FILE__ == $0 156 command = ARGV.join(' ') 157 time = Benchmark.realtime { `#{command}` } 158 159 puts "[command line: #{command}]" 160 puts "[compilation time: #{time}]" 161end 162