Skip to content

Instantly share code, notes, and snippets.

@eregon
Last active October 28, 2024 00:09
Show Gist options
  • Save eregon/27b56c686e238bafa6b46b73f17a4618 to your computer and use it in GitHub Desktop.
Save eregon/27b56c686e238bafa6b46b73f17a4618 to your computer and use it in GitHub Desktop.
require "objspace"
require "benchmark/ips"
require "prism"
require "parser/current"
require "ripper"
require "ruby_parser"
files = Dir.glob("railties-7.2.1.2/**/*.rb")
SOURCES = files.map { |file| File.read(file) }
total_lines = SOURCES.sum { |code| code.lines.size }
total_bytesize = SOURCES.sum(&:bytesize)
puts "#{files.size} files, #{total_lines} lines, #{total_bytesize} bytes"
def walk_prism(ast)
ast.child_nodes.each do |child|
walk_prism(child) if child.is_a?(Prism::Node)
end
ast
end
def walk_parser(ast)
ast.children.each do |child|
walk_parser(child) if child.is_a?(Parser::AST::Node)
end
ast
end
def walk_rubyparser(ast)
ast.each do |child|
walk_rubyparser(child) if child.is_a?(Sexp)
end
ast
end
def walk_ripper(ast)
ast.each do |child|
walk_ripper(child) if child.is_a?(Array)
end
ast
end
def walk_rubyvm(ast)
ast.children.each do |child|
walk_rubyvm(child) if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
end
ast
end
PARSERS = {
"CONTROL" => -> source {},
"Prism.parse" => -> source {
Prism.parse(source)
},
"Prism.parse + walk" => -> source {
walk_prism Prism.parse(source).value
},
"Parser gem" => -> source {
Parser::CurrentRuby.parse(source)
},
"Parser gem + walk" => -> source {
walk_parser Parser::CurrentRuby.parse(source)
},
"RubyParser" => -> source {
RubyParser.new.parse(source)
},
"RubyParser + walk" => -> source {
walk_rubyparser RubyParser.new.parse(source)
},
"Ripper.sexp" => -> source {
Ripper.sexp(source)
},
"Ripper.sexp + walk" => -> source {
walk_ripper Ripper.sexp(source)
},
"RubyVM::AbstractSyntaxTree" => -> source {
RubyVM::AbstractSyntaxTree.parse(source)
},
"RubyVM::AbstractSyntaxTree + walk" => -> source {
walk_rubyvm RubyVM::AbstractSyntaxTree.parse(source)
},
# "Prism.profile" => -> source {
# Prism.profile(source)
# },
# "RubyVM::AbstractSyntaxTree" => -> source {
# RubyVM::AbstractSyntaxTree.parse(source)
# },
# "RubyVM::InstructionSequence.compile_prism" => -> source {
# RubyVM::InstructionSequence.compile_prism(source)
# },
# "RubyVM::InstructionSequence.compile_parsey" => -> source {
# RubyVM::InstructionSequence.compile_parsey(source)
# },
}
PARSERS.each_pair { |parser, parse|
# warmup
ObjectSpace.count_objects
GC.stat(:total_allocated_objects)
SOURCES.each { |source| parse.call(source) }
node_class = if parser.include? 'Prism'
Prism::Node
else
parse.call(SOURCES.first).class
end
GC.start
GC.disable
nodes_before = ObjectSpace.each_object(node_class) {}
counts_before = ObjectSpace.count_objects
before = GC.stat(:total_allocated_objects)
SOURCES.each { |source| parse.call(source) }
after = GC.stat(:total_allocated_objects)
counts_after = ObjectSpace.count_objects
nodes_after = ObjectSpace.each_object(node_class) {}
nodes = nodes_after - nodes_before
counts_after[:T_HASH] -= 1
diff = {}
counts_after.each_pair { |type, count|
next if %i[TOTAL FREE].include?(type)
d = count - (counts_before[type] || 0)
diff[type] = d if d != 0
}
diff = diff.each_pair.map { |type, count| "#{count} #{type[2..].capitalize}s" }.join(", ")
puts "#{'%6d' % nodes} #{node_class} allocations for #{parser}"
puts "#{'%7d' % (after - before)} total allocations for #{parser}"
puts "#{after - before} total allocations for #{parser}, including #{nodes} #{node_class}:"
puts " #{diff}"
GC.enable
}
PARSERS.delete "CONTROL"
puts
# exit
GC.start
Benchmark.ips do |x|
PARSERS.each_pair { |parser, parse|
x.report parser do
SOURCES.each { |source| parse.call(source) }
end
}
x.compare!
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment