$30 off During Our Annual Pro Sale. View Details »

TracePointを活用してモデル名変更の負債解消をした話

alpaca-tc
October 27, 2023

 TracePointを活用してモデル名変更の負債解消をした話

2023-10-27 Kaigi on Rails 発表資料
https://kaigionrails.org/2023/talks/alpaca-tc/

誰もが開発時に直面する「技術的負債」。 あなたもRailsの世界でこの大敵との戦いで手をこまねいていませんか? 特にRubyのような動的言語では、一見シンプルな置換作業でも予期しない障壁が待ち受けています。

私たちのチームは、命名規則の誤りから生じたモデル名の技術的負債と向き合い、60,000行以上の変更を成功させました。 その成功のカギとなったのは「TracePoint」。 さまざまなイベント(メソッド呼び出しなど)をトレースすることができるRubyの強力な標準ライブラリでした。

このセッションでは、TracePointの基本的な使い方や、さらには技術的負債の解消のノウハウを実例とともに紹介します。 この機会に、巨大な技術的負債との戦いに悩むあなたの手札にTracePointという武器を加えてみませんか。

alpaca-tc

October 27, 2023
Tweet

More Decks by alpaca-tc

Other Decks in Programming

Transcript

  1. ͳͥࢲ͸ࠓ೔ൃදΛ͢Δͷ͔ w ѱ໋໊͍نଇͩͬͨͨΊɺ͢΂ͯͷϞσϧ໊Λมߋ͢Δෛ࠴ղফ w  DPNNJU  ߦ w Өڹൣғ͸ΞϓϦέʔγϣϯશମ

    w 5SBDF1PJOU ΠϕϯτΛτϨʔε͢Δඪ४ϥΠϒϥϦ Λ׆༻ͯ͠׬਱ w ࣄྫ͕গͳ͍ͷͰɺօ༷ʹ໾ཱͭώϯτΛൃ৴Ͱ͖Δ͔΋
  2. 1SPKFDU4IFFU4FDUJPO*UFN*OQVU/VNCFS w ϞσϧΛBQQNPEFMT௚Լʹ഑ஔ w มߋ͕ඞཁͳ΋ͷ w ఆ਺ Ϟσϧ໊ ɺϑΝΠϧύε w

    ؔ࿈໊ͷϝλϓϩDSFBUF@YYY CVJME@YYY w ΧϥϜ໊YYY@JE w 'BDUPSZ#PUɺ9YY%FDPSBUPSɺ"3ϝιου܈XIFSFKPJOTʜ
  3. 5SBDF1PJOU ର৅ ར༻ՕॴΛಛఆ ஔ׵ॲཧ w ఆ਺໊ w ؔ࿈໊ w ΧϥϜ໊

    T0ME/FXH GJMFQBUIUPBSC- GJMFQBUIUPCSC- GJMFQBUIUPD@TQFDSC-
  4. 5SBDF1PJOUͷجຊ w ඪ४ϥΠϒϥϦ w 3VCZ্ͷ༷ʑͳΠϕϯτΛϑοΫͰ͖Δ w DBMM3VCZͰهड़͞Εͨϝιουͷݺͼग़͠ w MJOFࣜͷධՁ w

    SFUVSO3VCZͰهड़͞Εͨϝιουݺͼग़͔͠ΒͷϦλʔϯ w SBJTFྫ֎ͷൃੜ w ʜଞଟ਺
  5. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello
  6. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello ΠϕϯτͷτϨʔεΛ։࢝
  7. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello ϝιουݺͼग़͠
  8. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello ϒϩοΫ͕࣮ߦ #=> [:call, :say_hello, "/private/tmp/xxx.rb", Object]
  9. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello #=> [:call, :say_hello, "/private/tmp/xxx.rb", Object] ϝιουݺͼग़͠
  10. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello ϒϩοΫ͕࣮ߦ #=> [:call, :say_hello, "/private/tmp/xxx.rb", Object] #=> [:call, :hello, "/private/tmp/xxx.rb", Object]
  11. trace_point = TracePoint.new(:call) do |tp| pp([tp.event, tp.method_id, tp.path, tp.defined_class]) end

    def hello "hello" end def say_hello hello end trace_point.enable say_hello w EF fi OFE@DMBTTϝιουΛఆٛͨ͠Ϋϥε͔Ϟδϡʔϧ w TFMGΠϕϯτΛൃੜͤͨ͞ΦϒδΣΫτ w NFUIPE@JEϝιουͷఆٛ࣌ͷ໊લ w QBSNFUFSTϝιουͷύϥϝʔλఆٛ w CJOEJOHϝιουͷCJOEJOHɻFWBMΛ࢖͑͹ͳΜͰ΋Ͱ͖Δ
  12. # 今回の置換では、appかspec配下にあるファイルが対象 TARGETS = [ Rails.root.join('app').to_s, Rails.root.join('spec').to_s ].freeze # @return

    [Thread::Backtrace::Location] ファイルパスや行数情報 def find_caller_location Thread.each_caller_location do |location| path = location.path next if path == __FILE__ return location if TARGETS.any? { path.start_with?(_1) } end end #=> "app/models/input_number.rb:11:in `tenant'" location = find_caller_location
  13. # 今回の置換では、appかspec配下にあるファイルが対象 TARGETS = [ Rails.root.join('app').to_s, Rails.root.join('spec').to_s ].freeze # @return

    [Thread::Backtrace::Location] ファイルパスや行数情報 def find_caller_location Thread.each_caller_location do |location| path = location.path next if path == __FILE__ return location if TARGETS.any? { path.start_with?(_1) } end end #=> "app/models/input_number.rb:11:in `tenant'" location = find_caller_location ஔ׵ର৅Λࢦఆ
  14. # 今回の置換では、appかspec配下にあるファイルが対象 TARGETS = [ Rails.root.join('app').to_s, Rails.root.join('spec').to_s ].freeze # @return

    [Thread::Backtrace::Location] ファイルパスや行数情報 def find_caller_location Thread.each_caller_location do |location| path = location.path next if path == __FILE__ return location if TARGETS.any? { path.start_with?(_1) } end end #=> "app/models/input_number.rb:11:in `tenant'" location = find_caller_location ελοΫΛॱʹḪΔ
  15. # 今回の置換では、appかspec配下にあるファイルが対象 TARGETS = [ Rails.root.join('app').to_s, Rails.root.join('spec').to_s ].freeze # @return

    [Thread::Backtrace::Location] ファイルパスや行数情報 def find_caller_location Thread.each_caller_location do |location| path = location.path next if path == __FILE__ return location if TARGETS.any? { path.start_with?(_1) } end end #=> "app/models/input_number.rb:11:in `tenant'" location = find_caller_location ஔ׵ର৅ͳΒ஋Λฦ͢
  16. # 今回の置換では、appかspec配下にあるファイルが対象 TARGETS = [ Rails.root.join('app').to_s, Rails.root.join('spec').to_s ].freeze # @return

    [Thread::Backtrace::Location] ファイルパスや行数情報 def find_caller_location Thread.each_caller_location do |location| path = location.path next if path == __FILE__ return location if TARGETS.any? { path.start_with?(_1) } end end #=> "app/models/input_number.rb:11:in `tenant'" location = find_caller_location ύεͱߦ͕Θ͔Δ
  17. class Hello def hello puts "hello" end end 3VCZ7."CTUSBDU4ZOUBY5SFFQBSTF SCOPE@#13

    CLASS@#12 COLON2(:Hello)@#0 SCOPE@#11 BLOCK@#9 BEGIN@#1 DEFN(:hello)@#3 SCOPE@#8 ARGS@#4 FCALL(puts)@#5 LIST@#7 STR(hello)@#6 ιʔείʔυ ந৅ߏจ໦ "45
  18. class Hello def hello puts "hello" end end SCOPE@#13 CLASS@#12

    COLON2(:Hello)@#0 SCOPE@#11 BLOCK@#9 BEGIN@#1 DEFN(:hello)@#3 SCOPE@#8 ARGS@#4 FCALL(puts)@#5 LIST@#7 STR(hello)@#6 3VCZ7."CTUSBDU4ZOUBY5SFFQBSTF ֤ϊʔυ͸ϢχʔΫͳOPEF@JEΛ࣋ͭ ιʔείʔυ ந৅ߏจ໦ "45
  19. # 呼び出し元のThread::Backtrace::Locationを取得 caller_location = find_caller_location # node_idを取得 node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(

    caller_location ) # ファイルパス + node_idで正確なソースコードを取得できる ast = RubyVM::AbstractSyntaxTree.parse_file( caller_location.path, keep_script_lines: true ) node = find_node_id(ast, node_id) #=> 正確な位置情報やソースコードを取得できる [node.first_lineno, node.first_column, node.last_lineno, node.last_column] node.source
  20. # 呼び出し元のThread::Backtrace::Locationを取得 caller_location = find_caller_location # node_idを取得 node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(

    caller_location ) # ファイルパス + node_idで正確なソースコードを取得できる ast = RubyVM::AbstractSyntaxTree.parse_file( caller_location.path, keep_script_lines: true ) node = find_node_id(ast, node_id) #=> 正確な位置情報やソースコードを取得できる [node.first_lineno, node.first_column, node.last_lineno, node.last_column] node.source ݺͼग़͠ݩͷ৘ใΛऔಘ
  21. # 呼び出し元のThread::Backtrace::Locationを取得 caller_location = find_caller_location # node_idを取得 node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(

    caller_location ) # ファイルパス + node_idで正確なソースコードを取得できる ast = RubyVM::AbstractSyntaxTree.parse_file( caller_location.path, keep_script_lines: true ) node = find_node_id(ast, node_id) #=> 正確な位置情報やソースコードを取得できる [node.first_lineno, node.first_column, node.last_lineno, node.last_column] node.source OPEF@JEʹม׵
  22. # 呼び出し元のThread::Backtrace::Locationを取得 caller_location = find_caller_location # node_idを取得 node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(

    caller_location ) # ファイルパス + node_idで正確なソースコードを取得できる ast = RubyVM::AbstractSyntaxTree.parse_file( caller_location.path, keep_script_lines: true ) node = find_node_id(ast, node_id) #=> 正確な位置情報やソースコードを取得できる [node.first_lineno, node.first_column, node.last_lineno, node.last_column] node.source "45͔ΒҰக͢ΔOPEFΛऔಘ
  23. # 呼び出し元のThread::Backtrace::Locationを取得 caller_location = find_caller_location # node_idを取得 node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(

    caller_location ) # ファイルパス + node_idで正確なソースコードを取得できる ast = RubyVM::AbstractSyntaxTree.parse_file( caller_location.path, keep_script_lines: true ) node = find_node_id(ast, node_id) #=> 正確な位置情報やソースコードを取得できる [node.first_lineno, node.first_column, node.last_lineno, node.last_column] node.source ιʔείʔυͷ಺༰
  24. count = 0 TracePoint.new(:call) do count += 1 end.enable #

    この1行で、内部のメソッド呼び出しは何回起こる? User.where(id: 1).load puts count #=> ??? DPVOU͸͍ͭ͘ʹͳΔͰ͠ΐ͏͔
  25. DPVOU͸͍ͭ͘ʹͳΔͰ͠ΐ͏͔ count = 0 TracePoint.new(:call) do count += 1 end.enable

    # この1行で、内部のメソッド呼び出しは何回起こる? User.where(id: 1).load puts count #=> 約5,000回
  26. TARGET_METHODS = [ ActiveRecord::Base.method(:has_one), ActiveRecord::Base.method(:has_many), ActiveRecord::Base.method(:belongs_to), ActiveRecord::Base.method(:has_and_belongs_to_many), ].map { [_1.owner,

    _1.name] }.to_set trace_point = TracePoint.new(:call) do |tp| next unless TARGET_METHODS.include?([tp.defined_class, tp_method_id]]) ... end ඞཁͳϝιουҎ֎͸ૣظϦλʔϯ
  27. TARGET_METHODS = [ ActiveRecord::Base.method(:has_one), ActiveRecord::Base.method(:has_many), ActiveRecord::Base.method(:belongs_to), ActiveRecord::Base.method(:has_and_belongs_to_many), ].map { [_1.owner,

    _1.name] }.to_set trace_point = TracePoint.new(:call) do |tp| next unless TARGET_METHODS.include?([tp.defined_class, tp_method_id]]) ... end ඞཁͳϝιουҎ֎͸ૣظϦλʔϯ ؔ࿈ఆٛͷϝιουΛࢦఆ
  28. TARGET_METHODS = [ ActiveRecord::Base.method(:has_one), ActiveRecord::Base.method(:has_many), ActiveRecord::Base.method(:belongs_to), ActiveRecord::Base.method(:has_and_belongs_to_many), ].map { [_1.owner,

    _1.name] }.to_set trace_point = TracePoint.new(:call) do |tp| next unless TARGET_METHODS.include?([tp.defined_class, tp_method_id]]) ... end ඞཁͳϝιουҎ֎͸ૣظϦλʔϯ τϨʔεର৅֎͸ૣظϦλʔϯ
  29. trace_point = TracePoint.new(:call) do |tp| names = tp.parameters.map { _2

    } parameters = names.to_h do [_1, tp.binding.local_variable_get(_1)] end pp(parameters) end
  30. trace_point = TracePoint.new(:call) do |tp| names = tp.parameters.map { _2

    } parameters = names.to_h do [_1, tp.binding.local_variable_get(_1)] end pp(parameters) end ύϥϝʔλ໊Λऔಘ
  31. trace_point = TracePoint.new(:call) do |tp| names = tp.parameters.map { _2

    } parameters = names.to_h do [_1, tp.binding.local_variable_get(_1)] end pp(parameters) end ஋Λऔಘ
  32. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable
  33. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable ແ໊ͳSFTUҾ਺໊ΛऔΓग़͢
  34. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable FYUSBDU@SFTU@QBSBNFUFST 
  35. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable ม਺ʹଋറ͞ΕΔ
  36. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable ύϥϝʔλ໊Λऔಘ
  37. def extract_rest_parameters(*args, **options, &block) { :* => args, :** =>

    options, :"..." => [args, options, block], :& => block } end trace_point = TracePoint.new(:call) do |tp| rest_names = tp.parameters.filter_map { _2 if [:*, :**, :&].include?(_2) } rest_variables = begin tp.binding.eval("extract_rest_parameters(#{rest_names.join(', ')})").slice(*rest_names) rescue SyntaxError tp.binding.eval("extract_rest_parameters(...)").slice(:"...") end pp rest_variables end trace_point.enable ͷ৔߹͸ͪ͜Β͕ॲཧ͞ΕΔ
  38. ·ͱΊ w 5SBDF1PJOUΛ׆༻͢Δͱϝιουݺͼग़͠ͷ৘ใΛऔಘՄೳ w Өڹൣғௐࠪ΍ஔ׵ॲཧʹ׆͔ͤΔ w ஔ׵ॲཧ͸τϥϒϧͳ͘׬਱Ͱ͖ͨͷ͔ʁ w ʮػցతͳஔ׵ ม਺ͳͲҰ෦ͷखಈஔ׵ʯͰ׬਱

    w ςετΛॻ͍͍ͯͳ͔ͬͨՕॴͰΤϥʔ w ʮ4NBSU)3Ϟσϧ໊ʯͰݕࡧ͢ΔͱςοΫϒϩά͕ݟΕ·͢ w 5SBDF1PJOUͷ໘ന͞ɺෛ࠴ղফʹ׆͔ͤͦ͏ͳώϯτ͸఻ΘΓ·͔ͨ͠ʁ