PageRenderTime 39ms CodeModel.GetById 8ms RepoModel.GetById 1ms app.codeStats 0ms

/Ruby/lib/ruby/2.0.0/minitest/benchmark.rb

https://gitlab.com/orvi2014/rcs-db-ext
Ruby | 380 lines | 140 code | 60 blank | 180 comment | 1 complexity | 3da7366818adb8e2d7a011c9322bafc2 MD5 | raw file
  1. # encoding: utf-8
  2. ######################################################################
  3. # This file is imported from the minitest project.
  4. # DO NOT make modifications in this repo. They _will_ be reverted!
  5. # File a patch instead and assign it to Ryan Davis.
  6. ######################################################################
  7. require 'minitest/unit'
  8. require 'minitest/spec'
  9. class MiniTest::Unit # :nodoc:
  10. def run_benchmarks # :nodoc:
  11. _run_anything :benchmark
  12. end
  13. def benchmark_suite_header suite # :nodoc:
  14. "\n#{suite}\t#{suite.bench_range.join("\t")}"
  15. end
  16. class TestCase
  17. ##
  18. # Returns a set of ranges stepped exponentially from +min+ to
  19. # +max+ by powers of +base+. Eg:
  20. #
  21. # bench_exp(2, 16, 2) # => [2, 4, 8, 16]
  22. def self.bench_exp min, max, base = 10
  23. min = (Math.log10(min) / Math.log10(base)).to_i
  24. max = (Math.log10(max) / Math.log10(base)).to_i
  25. (min..max).map { |m| base ** m }.to_a
  26. end
  27. ##
  28. # Returns a set of ranges stepped linearly from +min+ to +max+ by
  29. # +step+. Eg:
  30. #
  31. # bench_linear(20, 40, 10) # => [20, 30, 40]
  32. def self.bench_linear min, max, step = 10
  33. (min..max).step(step).to_a
  34. rescue LocalJumpError # 1.8.6
  35. r = []; (min..max).step(step) { |n| r << n }; r
  36. end
  37. ##
  38. # Returns the benchmark methods (methods that start with bench_)
  39. # for that class.
  40. def self.benchmark_methods # :nodoc:
  41. public_instance_methods(true).grep(/^bench_/).map { |m| m.to_s }.sort
  42. end
  43. ##
  44. # Returns all test suites that have benchmark methods.
  45. def self.benchmark_suites
  46. TestCase.test_suites.reject { |s| s.benchmark_methods.empty? }
  47. end
  48. ##
  49. # Specifies the ranges used for benchmarking for that class.
  50. # Defaults to exponential growth from 1 to 10k by powers of 10.
  51. # Override if you need different ranges for your benchmarks.
  52. #
  53. # See also: ::bench_exp and ::bench_linear.
  54. def self.bench_range
  55. bench_exp 1, 10_000
  56. end
  57. ##
  58. # Runs the given +work+, gathering the times of each run. Range
  59. # and times are then passed to a given +validation+ proc. Outputs
  60. # the benchmark name and times in tab-separated format, making it
  61. # easy to paste into a spreadsheet for graphing or further
  62. # analysis.
  63. #
  64. # Ranges are specified by ::bench_range.
  65. #
  66. # Eg:
  67. #
  68. # def bench_algorithm
  69. # validation = proc { |x, y| ... }
  70. # assert_performance validation do |n|
  71. # @obj.algorithm(n)
  72. # end
  73. # end
  74. def assert_performance validation, &work
  75. range = self.class.bench_range
  76. io.print "#{__name__}"
  77. times = []
  78. range.each do |x|
  79. GC.start
  80. t0 = Time.now
  81. instance_exec(x, &work)
  82. t = Time.now - t0
  83. io.print "\t%9.6f" % t
  84. times << t
  85. end
  86. io.puts
  87. validation[range, times]
  88. end
  89. ##
  90. # Runs the given +work+ and asserts that the times gathered fit to
  91. # match a constant rate (eg, linear slope == 0) within a given
  92. # +threshold+. Note: because we're testing for a slope of 0, R^2
  93. # is not a good determining factor for the fit, so the threshold
  94. # is applied against the slope itself. As such, you probably want
  95. # to tighten it from the default.
  96. #
  97. # See http://www.graphpad.com/curvefit/goodness_of_fit.htm for
  98. # more details.
  99. #
  100. # Fit is calculated by #fit_linear.
  101. #
  102. # Ranges are specified by ::bench_range.
  103. #
  104. # Eg:
  105. #
  106. # def bench_algorithm
  107. # assert_performance_constant 0.9999 do |n|
  108. # @obj.algorithm(n)
  109. # end
  110. # end
  111. def assert_performance_constant threshold = 0.99, &work
  112. validation = proc do |range, times|
  113. a, b, rr = fit_linear range, times
  114. assert_in_delta 0, b, 1 - threshold
  115. [a, b, rr]
  116. end
  117. assert_performance validation, &work
  118. end
  119. ##
  120. # Runs the given +work+ and asserts that the times gathered fit to
  121. # match a exponential curve within a given error +threshold+.
  122. #
  123. # Fit is calculated by #fit_exponential.
  124. #
  125. # Ranges are specified by ::bench_range.
  126. #
  127. # Eg:
  128. #
  129. # def bench_algorithm
  130. # assert_performance_exponential 0.9999 do |n|
  131. # @obj.algorithm(n)
  132. # end
  133. # end
  134. def assert_performance_exponential threshold = 0.99, &work
  135. assert_performance validation_for_fit(:exponential, threshold), &work
  136. end
  137. ##
  138. # Runs the given +work+ and asserts that the times gathered fit to
  139. # match a straight line within a given error +threshold+.
  140. #
  141. # Fit is calculated by #fit_linear.
  142. #
  143. # Ranges are specified by ::bench_range.
  144. #
  145. # Eg:
  146. #
  147. # def bench_algorithm
  148. # assert_performance_linear 0.9999 do |n|
  149. # @obj.algorithm(n)
  150. # end
  151. # end
  152. def assert_performance_linear threshold = 0.99, &work
  153. assert_performance validation_for_fit(:linear, threshold), &work
  154. end
  155. ##
  156. # Runs the given +work+ and asserts that the times gathered curve
  157. # fit to match a power curve within a given error +threshold+.
  158. #
  159. # Fit is calculated by #fit_power.
  160. #
  161. # Ranges are specified by ::bench_range.
  162. #
  163. # Eg:
  164. #
  165. # def bench_algorithm
  166. # assert_performance_power 0.9999 do |x|
  167. # @obj.algorithm
  168. # end
  169. # end
  170. def assert_performance_power threshold = 0.99, &work
  171. assert_performance validation_for_fit(:power, threshold), &work
  172. end
  173. ##
  174. # Takes an array of x/y pairs and calculates the general R^2 value.
  175. #
  176. # See: http://en.wikipedia.org/wiki/Coefficient_of_determination
  177. def fit_error xys
  178. y_bar = sigma(xys) { |x, y| y } / xys.size.to_f
  179. ss_tot = sigma(xys) { |x, y| (y - y_bar) ** 2 }
  180. ss_err = sigma(xys) { |x, y| (yield(x) - y) ** 2 }
  181. 1 - (ss_err / ss_tot)
  182. end
  183. ##
  184. # To fit a functional form: y = ae^(bx).
  185. #
  186. # Takes x and y values and returns [a, b, r^2].
  187. #
  188. # See: http://mathworld.wolfram.com/LeastSquaresFittingExponential.html
  189. def fit_exponential xs, ys
  190. n = xs.size
  191. xys = xs.zip(ys)
  192. sxlny = sigma(xys) { |x,y| x * Math.log(y) }
  193. slny = sigma(xys) { |x,y| Math.log(y) }
  194. sx2 = sigma(xys) { |x,y| x * x }
  195. sx = sigma xs
  196. c = n * sx2 - sx ** 2
  197. a = (slny * sx2 - sx * sxlny) / c
  198. b = ( n * sxlny - sx * slny ) / c
  199. return Math.exp(a), b, fit_error(xys) { |x| Math.exp(a + b * x) }
  200. end
  201. ##
  202. # Fits the functional form: a + bx.
  203. #
  204. # Takes x and y values and returns [a, b, r^2].
  205. #
  206. # See: http://mathworld.wolfram.com/LeastSquaresFitting.html
  207. def fit_linear xs, ys
  208. n = xs.size
  209. xys = xs.zip(ys)
  210. sx = sigma xs
  211. sy = sigma ys
  212. sx2 = sigma(xs) { |x| x ** 2 }
  213. sxy = sigma(xys) { |x,y| x * y }
  214. c = n * sx2 - sx**2
  215. a = (sy * sx2 - sx * sxy) / c
  216. b = ( n * sxy - sx * sy ) / c
  217. return a, b, fit_error(xys) { |x| a + b * x }
  218. end
  219. ##
  220. # To fit a functional form: y = ax^b.
  221. #
  222. # Takes x and y values and returns [a, b, r^2].
  223. #
  224. # See: http://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
  225. def fit_power xs, ys
  226. n = xs.size
  227. xys = xs.zip(ys)
  228. slnxlny = sigma(xys) { |x, y| Math.log(x) * Math.log(y) }
  229. slnx = sigma(xs) { |x | Math.log(x) }
  230. slny = sigma(ys) { | y| Math.log(y) }
  231. slnx2 = sigma(xs) { |x | Math.log(x) ** 2 }
  232. b = (n * slnxlny - slnx * slny) / (n * slnx2 - slnx ** 2);
  233. a = (slny - b * slnx) / n
  234. return Math.exp(a), b, fit_error(xys) { |x| (Math.exp(a) * (x ** b)) }
  235. end
  236. ##
  237. # Enumerates over +enum+ mapping +block+ if given, returning the
  238. # sum of the result. Eg:
  239. #
  240. # sigma([1, 2, 3]) # => 1 + 2 + 3 => 7
  241. # sigma([1, 2, 3]) { |n| n ** 2 } # => 1 + 4 + 9 => 14
  242. def sigma enum, &block
  243. enum = enum.map(&block) if block
  244. enum.inject { |sum, n| sum + n }
  245. end
  246. ##
  247. # Returns a proc that calls the specified fit method and asserts
  248. # that the error is within a tolerable threshold.
  249. def validation_for_fit msg, threshold
  250. proc do |range, times|
  251. a, b, rr = send "fit_#{msg}", range, times
  252. assert_operator rr, :>=, threshold
  253. [a, b, rr]
  254. end
  255. end
  256. end
  257. end
  258. class MiniTest::Spec
  259. ##
  260. # This is used to define a new benchmark method. You usually don't
  261. # use this directly and is intended for those needing to write new
  262. # performance curve fits (eg: you need a specific polynomial fit).
  263. #
  264. # See ::bench_performance_linear for an example of how to use this.
  265. def self.bench name, &block
  266. define_method "bench_#{name.gsub(/\W+/, '_')}", &block
  267. end
  268. ##
  269. # Specifies the ranges used for benchmarking for that class.
  270. #
  271. # bench_range do
  272. # bench_exp(2, 16, 2)
  273. # end
  274. #
  275. # See Unit::TestCase.bench_range for more details.
  276. def self.bench_range &block
  277. return super unless block
  278. meta = (class << self; self; end)
  279. meta.send :define_method, "bench_range", &block
  280. end
  281. ##
  282. # Create a benchmark that verifies that the performance is linear.
  283. #
  284. # describe "my class" do
  285. # bench_performance_linear "fast_algorithm", 0.9999 do |n|
  286. # @obj.fast_algorithm(n)
  287. # end
  288. # end
  289. def self.bench_performance_linear name, threshold = 0.99, &work
  290. bench name do
  291. assert_performance_linear threshold, &work
  292. end
  293. end
  294. ##
  295. # Create a benchmark that verifies that the performance is constant.
  296. #
  297. # describe "my class" do
  298. # bench_performance_constant "zoom_algorithm!" do |n|
  299. # @obj.zoom_algorithm!(n)
  300. # end
  301. # end
  302. def self.bench_performance_constant name, threshold = 0.99, &work
  303. bench name do
  304. assert_performance_constant threshold, &work
  305. end
  306. end
  307. ##
  308. # Create a benchmark that verifies that the performance is exponential.
  309. #
  310. # describe "my class" do
  311. # bench_performance_exponential "algorithm" do |n|
  312. # @obj.algorithm(n)
  313. # end
  314. # end
  315. def self.bench_performance_exponential name, threshold = 0.99, &work
  316. bench name do
  317. assert_performance_exponential threshold, &work
  318. end
  319. end
  320. end