[rubygems/rubygems] Use a constant empty tar header to avoid extra allocations
[ruby.git] / misc / call_fuzzer.rb
blobc3f9f90490c0bc41abc13bed59d1e89ef648e667
1 require 'optparse'
2 require 'set'
4 # Number of iterations to test
5 num_iters = 10_000
7 # Parse the command-line options
8 OptionParser.new do |opts|
9   opts.on("--num-iters=N") do |n|
10     num_iters = n.to_i
11   end
12 end.parse!
14 # Format large numbers with comma separators for readability
15 def format_number(pad, number)
16   s = number.to_s
17   i = s.index('.') || s.size
18   s.insert(i -= 3, ',') while i > 3
19   s.rjust(pad, ' ')
20 end
22 # Wrap an integer to pass as argument
23 # We use this so we can have some object arguments
24 class IntWrapper
25   def initialize(v)
26     # Force the object to have a random shape
27     if rand() < 50
28       @v0 = 1
29     end
30     if rand() < 50
31       @v1 = 1
32     end
33     if rand() < 50
34       @v2 = 1
35     end
36     if rand() < 50
37       @v3 = 1
38     end
39     if rand() < 50
40       @v4 = 1
41     end
42     if rand() < 50
43       @v5 = 1
44     end
45     if rand() < 50
46       @v6 = 1
47     end
49     @value = v
50   end
52   attr_reader :value
53 end
55 # Generate a random argument value, integer or string or object
56 def sample_arg()
57   c = ['int', 'string', 'object'].sample()
59   if c == 'int'
60     return rand(0...100)
61   end
63   if c == 'string'
64     return 'f' * rand(0...100)
65   end
67   if c == 'object'
68     return IntWrapper.new(rand(0...100))
69   end
71   raise "should not get here"
72 end
74 # Evaluate the value of an argument with respect to the checksum
75 def arg_val(arg)
76   if arg.kind_of? Integer
77     return arg
78   end
80   if arg.kind_of? String
81     return arg.length
82   end
84   if arg.kind_of? Object
85     return arg.value
86   end
88   raise "unknown arg type"
89 end
91 # List of parameters/arguments for a method
92 class ParamList
93   def initialize()
94     self.sample_params()
95     self.sample_args()
96   end
98   # Sample/generate a random set of parameters for a method
99   def sample_params()
100     # Choose how many positional arguments to use, and how many are optional
101     num_pargs = rand(10)
102     @opt_parg_idx = rand(num_pargs)
103     @num_opt_pargs = rand(num_pargs + 1 - @opt_parg_idx)
104     @num_pargs_req = num_pargs - @num_opt_pargs
105     @pargs = (0...num_pargs).map do |i|
106       {
107         :name => "p#{i}",
108         :optional => (i >= @opt_parg_idx && i < @opt_parg_idx + @num_opt_pargs)
109       }
110     end
112     # Choose how many kwargs to use, and how many are optional
113     num_kwargs = rand(10)
114     @kwargs = (0...num_kwargs).map do |i|
115       {
116         :name => "k#{i}",
117         :optional => rand() < 0.5
118       }
119     end
121     # Choose whether to have rest parameters or not
122     @has_rest = @num_opt_pargs == 0 && rand() < 0.5
123     @has_kwrest = rand() < 0.25
125     # Choose whether to have a named block parameter or not
126     @has_block_param = rand() < 0.25
127   end
129   # Sample/generate a random set of arguments corresponding to the parameters
130   def sample_args()
131     # Choose how many positional args to pass
132     num_pargs_passed = rand(@num_pargs_req..@pargs.size)
134     # How many optional arguments will be filled
135     opt_pargs_filled = num_pargs_passed - @num_pargs_req
137     @pargs.each_with_index do |parg, i|
138       if parg[:optional]
139         parg[:default] = rand(100)
140       end
142       if !parg[:optional] || i < @opt_parg_idx + opt_pargs_filled
143         parg[:argval] = rand(100)
144       end
145     end
147     @kwargs.each_with_index do |kwarg, i|
148       if kwarg[:optional]
149         kwarg[:default] = rand(100)
150       end
152       if !kwarg[:optional] || rand() < 0.5
153         kwarg[:argval] = rand(100)
154       end
155     end
157     # Randomly pass a block or not
158     @block_arg = nil
159     if rand() < 0.5
160       @block_arg = rand(100)
161     end
162   end
164   # Compute the expected checksum of arguments ahead of time
165   def compute_checksum()
166     checksum = 0
168     @pargs.each_with_index do |arg, i|
169       value = (arg.key? :argval)? arg[:argval]:arg[:default]
170       checksum += (i+1) * arg_val(value)
171     end
173     @kwargs.each_with_index do |arg, i|
174       value = (arg.key? :argval)? arg[:argval]:arg[:default]
175       checksum += (i+1) * arg_val(value)
176     end
178     if @block_arg
179       if @has_block_param
180         checksum += arg_val(@block_arg)
181       end
183       checksum += arg_val(@block_arg)
184     end
186     checksum
187   end
189   # Generate code for the method signature and method body
190   def gen_method_str()
191     m_str = "def m("
193     @pargs.each do |arg|
194       if !m_str.end_with?("(")
195         m_str += ", "
196       end
198       m_str += arg[:name]
200       # If this has a default value
201       if arg[:optional]
202         m_str += " = #{arg[:default]}"
203       end
204     end
206     if @has_rest
207       if !m_str.end_with?("(")
208         m_str += ", "
209       end
210       m_str += "*rest"
211     end
213     @kwargs.each do |arg|
214       if !m_str.end_with?("(")
215         m_str += ", "
216       end
218       m_str += "#{arg[:name]}:"
220       # If this has a default value
221       if arg[:optional]
222         m_str += " #{arg[:default]}"
223       end
224     end
226     if @has_kwrest
227       if !m_str.end_with?("(")
228         m_str += ", "
229       end
230       m_str += "**kwrest"
231     end
233     if @has_block_param
234       if !m_str.end_with?("(")
235         m_str += ", "
236       end
238       m_str += "&block"
239     end
241     m_str += ")\n"
243     # Add some useless locals
244     rand(0...16).times do |i|
245       m_str += "local#{i} = #{i}\n"
246     end
248     # Add some useless if statements
249     @pargs.each_with_index do |arg, i|
250       if rand() < 50
251         m_str += "if #{arg[:name]} > 4; end\n"
252       end
253     end
255     m_str += "checksum = 0\n"
257     @pargs.each_with_index do |arg, i|
258       m_str += "checksum += #{i+1} * arg_val(#{arg[:name]})\n"
259     end
261     @kwargs.each_with_index do |arg, i|
262       m_str += "checksum += #{i+1} * arg_val(#{arg[:name]})\n"
263     end
265     if @has_block_param
266       m_str += "if block; r = block.call; checksum += arg_val(r); end\n"
267     end
269     m_str += "if block_given?; r = yield; checksum += arg_val(r); end\n"
271     if @has_rest
272       m_str += "raise 'rest is not array' unless rest.kind_of?(Array)\n"
273       m_str += "raise 'rest size not integer' unless rest.size.kind_of?(Integer)\n"
274     end
276     if @has_kwrest
277       m_str += "raise 'kwrest is not a hash' unless kwrest.kind_of?(Hash)\n"
278       m_str += "raise 'kwrest size not integer' unless kwrest.size.kind_of?(Integer)\n"
279     end
281     m_str += "checksum\n"
282     m_str += "end"
284     m_str
285   end
287   # Generate code to call into the method and pass the arguments
288   def gen_call_str()
289     c_str = "m("
291     @pargs.each_with_index do |arg, i|
292       if !arg.key? :argval
293         next
294       end
296       if !c_str.end_with?("(")
297         c_str += ", "
298       end
300       c_str += "#{arg[:argval]}"
301     end
303     @kwargs.each_with_index do |arg, i|
304       if !arg.key? :argval
305         next
306       end
308       if !c_str.end_with?("(")
309         c_str += ", "
310       end
312       c_str += "#{arg[:name]}: #{arg[:argval]}"
313     end
315     c_str += ")"
317     # Randomly pass a block or not
318     if @block_arg
319       c_str += " { #{@block_arg} }"
320     end
322     c_str
323   end
326 iseqs_compiled_start = RubyVM::YJIT.runtime_stats[:compiled_iseq_entry]
327 start_time = Time.now.to_f
329 num_iters.times do |i|
330   puts "Iteration #{i}"
332   lst = ParamList.new()
333   m_str = lst.gen_method_str()
334   c_str = lst.gen_call_str()
335   checksum = lst.compute_checksum()
337   f = Object.new
339   # Define the method on f
340   puts "Defining"
341   p m_str
342   f.instance_eval(m_str)
343   #puts RubyVM::InstructionSequence.disasm(f.method(:m))
344   #exit 0
346   puts "Calling"
347   c_str = "f.#{c_str}"
348   p c_str
349   r = eval(c_str)
350   puts "checksum=#{r}"
352   if r != checksum
353     raise "return value #{r} doesn't match checksum #{checksum}"
354   end
356   puts ""
359 # Make sure that YJIT actually compiled the tests we ran
360 # Should be run with --yjit-call-threshold=1
361 iseqs_compiled_end = RubyVM::YJIT.runtime_stats[:compiled_iseq_entry]
362 if iseqs_compiled_end - iseqs_compiled_start < num_iters
363   raise "YJIT did not compile enough ISEQs"
366 puts "Code region size: #{ format_number(0, RubyVM::YJIT.runtime_stats[:code_region_size]) }"
368 end_time = Time.now.to_f
369 itrs_per_sec = num_iters / (end_time - start_time)
370 itrs_per_hour = 3600 * itrs_per_sec
371 puts "#{'%.1f' % itrs_per_sec} iterations/s"
372 puts "#{format_number(0, itrs_per_hour.round)} iterations/hour"