Skip to content

Instantly share code, notes, and snippets.

@ShopifyEng
Created March 30, 2020 20:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShopifyEng/67a8c115bc6acc26122d43d275e9b901 to your computer and use it in GitHub Desktop.
Save ShopifyEng/67a8c115bc6acc26122d43d275e9b901 to your computer and use it in GitHub Desktop.
Optimizing Ruby Lazy Initialization in TruffleRuby with Deoptimization - Code ||= Lazy Initialization Profiling
# When run on the following 20 repositories, it found 2082 ||= and it was used as lazy init 64.3% of the time
# Those numbers are fuzzy, as there is not certain way to prove if something was used as a lazy init
# This script only marks a usage as a lazy init if the variable is being set to a constant,
# in which case if it is not a lazy init then the developers have done something weird.
# It is also marked as a lazy init if the variable is an instance variable or a class variable (either @var or @@var)
# AND the lazy init is used in the top level of a function, AND the function name is contained by the variable or vice versa.
# These are almost certain to be lazy initialized because the assumed behaviour is to always call the method instead of the variable,
# and other usages would be both weird usage of instance/class variables and/or weird usage of the method naming
# This of course, misses a variety of cases but likely does not get any cases that do not lazy initialize.
# A better way to profile could be at run time, but then less viable code samples could be used
# List of repositories;
# https://github.com/Homebrew/brew/tree/2f283b70eb2f3d334af1d4526932bbcb7dbbc94d
# https://github.com/rubygems/bundler/tree/d852b30b66165dbb88682f72a8929568af7e7c57
# https://github.com/chef/chef/tree/b32573ecddd5fb09413c69b4aace029be94a1fa3
# https://github.com/erikhuda/thor/tree/fb625b223465692a9d8a88cc2a483e126f1a8978
# https://github.com/github/choosealicense.com/tree/cb897a7471cbf4d8ac013267c18ebd44267cbaae
# https://github.com/Shopify/dashing/tree/bc1c106e42d52d3bfc042cba7bb0181d09413068
# https://github.com/thepracticaldev/dev.to/tree/4e41e4a2ac893fa2a6c36990cfe475858ffb086a
# https://github.com/elastic/elasticsearch-ruby/tree/32e3952c96482a38791785fe9db6d144f69a7946
# https://github.com/github/explore/tree/8a7b7a402df6f756bb0c83c2ede9fed7dac641fe
# https://github.com/Shopify/identity_cache/tree/0cbe7ec09516569f79fdee7a3ac25c9e16084229
# https://github.com/omniauth/omniauth/tree/894cb9c2f6fc06ba3777084c47999dea8a71bd3e
# https://github.com/ruby/rake/tree/313721164307fca6375239c636fc7cd8680f175b
# https://github.com/mperham/sidekiq/tree/3510de7df3c2eaacec9061184666aa853a0abb13
# https://github.com/spree/spree/tree/70c3ddf194de2b16e20de9477ada96b77debe0a0
# https://github.com/stripe/stripe-ruby/tree/dcb503dc7120ea3f5783c463e04e83368e027d1a
# https://github.com/airbnb/synapse/tree/c94125d3b6bb3e48d50dcb98aa28717c82f18322
# https://github.com/rubocop-hq/rubocop/tree/ddb1ba77c7947e04a226fe57ae7789c2eca7a74b
# https://github.com/github/scientist/tree/ced649720b0e8d5d58487f0b9bfca2e085d3e88b
# https://github.com/Shopify/shipit-engine/tree/b872c658eb8fcea56fe6dc16e91ddc5dd4da45c5
# https://github.com/Shopify/shopify_app/tree/4e6e6ea49716983d43db1d6e6c1dad86de3a6b63
require 'ripper'
require 'pp'
EMPTY_PARAMS = [:params, nil, nil, nil, nil, nil, nil, nil]
$certain_lazy = 0
$total = 0
def constant_check(c)
f = true
if c[0] == :array
return true if c[1].nil?
c[1].each do |val|
if !constant_check(val)
f = false
end
end
return f
elsif c[0] == :hash
return true if c[1].nil?
c[1][1].each do |val|
if !constant_check(val[2])
f = false
end
end
return f
elsif %i[nil string_literal @const @int symbol_literal].include?(c[0])
return true
else
return false
end
end
def constant(statement)
constant_check(statement[3])
end
def in_func(def_tree)
return false if !(def_tree[2] == EMPTY_PARAMS &&
def_tree[3][0] == :bodystmt)
def_tree[3][1].each do |statement|
next if !(statement[0] == :opassign && statement[2][0] == :@op && statement[2][1] == "||=")
if ((statement[1][1][0] == :@ivar &&
(def_tree[1][1].include?(statement[1][1][1][1..]) || statement[1][1][1][1..].include?(def_tree[1][1]))) ||
(statement[1][1][0] == :@cvar &&
(def_tree[1][1].include?(statement[1][1][1][2..]) || statement[1][1][1][2..].include?(def_tree[1][1]))))
return true
end
end
false
end
def traverse(exp)
return if exp.nil?
l = exp.size
(0...l).each do |i|
if exp[i].class == Array && exp[i][0].class == Symbol
if exp[i][0] == :def
if in_func(exp[i])
$certain_lazy += 1
$total += 1
next
end
elsif exp[i][0] == :opassign && exp[i][2][1] == "||=" && constant(exp[i])
$certain_lazy += 1
$total += 1
next
elsif exp[i][0] == :@op && exp[i][1] == "||="
$total += 1
next
end
end
if exp[i].class == Array
traverse(exp[i])
end
end
end
Dir.glob('./**/*.rb').each do|f|
next if File.directory?(f)
tree = Ripper.sexp(File.read(f))
last_total = $total
last_certain = $certain_lazy
traverse(tree)
# prints files where ||= was found potentially without lazy init
if ($total - last_total) > ($certain_lazy - last_certain)
pp f
end
end
pp "fairly certain lazy init usage " + ($certain_lazy / $total.to_f * 100).to_s + "%"
pp "total double pipes " + $total.to_s
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment