trace_location gem を使ってみる

こんにちは!kossyです!




さて、今回はOSSソースコードを読む際に役に立つtrace_locationを使ってみたので、ブログに残してみたいと思います。



試す

まずは公式ドキュメントを参考にして進めます。

# Gemfile

gem 'trace_location'
$ bundle

以下、rails cで試しました。

$ config = Rails.application.config.database_configuration[Rails.env]
 {"adapter"=>"postgresql",
 "encoding"=>"unicode",
 "pool"=>5,
 "username"=>"root",
 "password"=>"password",
 "host"=>"db",
 "database"=>"sample_api_development"}

$ TraceLocation.trace do
  # You just surround you want to track the process.
  ActiveRecord::Base.establish_connection(config)
end

Created at /app/log/trace_location-2021081506081629010190.md

=> true

特に指定しなければMarkdown拡張子でlog配下に出力されます。

オプション名 内容
format :md, :log, :csv (default: :md) :md
match Regexp, Symbol, String or Array for allow list [:activerecord, :activesupport]
ignore Regexp, Symbol, String or Array for deny list /bootsnap activesupport/

約2500行のマークダウンが出力されたので詳細は割愛します、、、

establish_connetctionメソッドの処理から出力がされていました。

# ActiveRecord::ConnectionHandling.establish_connection

def establish_connection(config_or_env = nil)
  config_hash = resolve_config_for_connection(config_or_env)
  connection_handler.establish_connection(config_hash)
end

# called from (pry):4
# /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/connection_handling.rb:162

deviseのvalid_password?メソッドをtraceしてみる

こちらは300行ほどだったため、全て貼り付けてみます。

Generated by [trace_location](https://github.com/yhirano55/trace_location) at 2021-08-15 07:21:57 +0000

<details open>
<summary>/usr/local/bundle/gems/devise-4.8.0/lib/devise/models/database_authenticatable.rb:71</summary>

##### Devise::Models::DatabaseAuthenticatable#valid_password?

```ruby
def valid_password?(password)
  Devise::Encryptor.compare(self.class, encrypted_password, password)
end

# called from (pry):12
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/attribute_methods/read.rb:15</summary>

##### User::GeneratedAttributeMethods#encrypted_password

```ruby
def #{temp_method_name}
  name = #{attr_name_expr}
  _read_attribute(name) { |n| missing_attribute(n, caller) }
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/models/database_authenticatable.rb:72
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/attribute_methods/read.rb:37</summary>

##### ActiveRecord::AttributeMethods::Read#_read_attribute

```ruby
def _read_attribute(attr_name, &block) # :nodoc
  sync_with_transaction_state if @transaction_state&.finalized?
  @attributes.fetch_value(attr_name.to_s, &block)
end

# called from /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/attribute_methods/read.rb:17
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set.rb:40</summary>

##### ActiveModel::AttributeSet#fetch_value

```ruby
def fetch_value(name, &block)
  self[name].value(&block)
end

# called from /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/attribute_methods/read.rb:39
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set.rb:15</summary>

##### ActiveModel::AttributeSet#[]

```ruby
def [](name)
  attributes[name] || Attribute.null(name)
end

# called from /usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set.rb:41
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set/builder.rb:38</summary>

##### ActiveModel::LazyAttributeHash#[]

```ruby
def [](key)
  delegate_hash[key] || assign_default_value(key)
end

# called from /usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set.rb:16
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute.rb:40</summary>

##### ActiveModel::Attribute#value

```ruby
def value
  # `defined?` is cheaper than `||=` when we get back falsy values
  @value = type_cast(value_before_type_cast) unless defined?(@value)
  @value
end

# called from /usr/local/bundle/gems/activemodel-6.0.3.7/lib/active_model/attribute_set.rb:41
```
</details>
<details open>
<summary>/usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:14</summary>

##### Devise::Encryptor.compare

```ruby
def self.compare(klass, hashed_password, password)
  return false if hashed_password.blank?
  bcrypt   = ::BCrypt::Password.new(hashed_password)
  if klass.pepper.present?
    password = "#{password}#{klass.pepper}"
  end
  password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
  Devise.secure_compare(password, hashed_password)
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/models/database_authenticatable.rb:72
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:121</summary>

##### String#blank?

```ruby
def blank?
  # The regexp that matches blank strings is expensive. For the case of empty
  # strings we can speed up this method (~3.5x) with an empty? call. The
  # penalty for the rest of strings is marginal.
  empty? ||
    begin
      BLANK_RE.match?(self)
    rescue Encoding::CompatibilityError
      ENCODED_BLANKS[self.encoding].match?(self)
    end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:15
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:55</summary>

##### BCrypt::Password#initialize

```ruby
def initialize(raw_hash)
  if valid_hash?(raw_hash)
    self.replace(raw_hash)
    @version, @cost, @salt, @checksum = split_hash(self)
  else
    raise Errors::InvalidHash.new("invalid hash")
  end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:16
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:73</summary>

##### BCrypt::Password#valid_hash?

```ruby
def valid_hash?(h)
  self.class.valid_hash?(h)
end

# called from /usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:56
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:49</summary>

##### BCrypt::Password.valid_hash?

```ruby
def valid_hash?(h)
  /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/ === h
end

# called from /usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:74
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:81</summary>

##### BCrypt::Password#split_hash

```ruby
def split_hash(h)
  _, v, c, mash = h.split('$')
  return v.to_str, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str
end

# called from /usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/password.rb:58
```
</details>
<details open>
<summary>/usr/local/bundle/gems/devise-4.8.0/lib/devise/models.rb:37</summary>

##### Devise::Models::DatabaseAuthenticatable::ClassMethods.pepper

```ruby
def #{accessor}
  if defined?(@#{accessor})
    @#{accessor}
  elsif superclass.respond_to?(:#{accessor})
    superclass.#{accessor}
  else
    Devise.#{accessor}
  end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:17
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:6</summary>

##### ActiveRecord::DynamicMatchers.respond_to_missing?

```ruby
def respond_to_missing?(name, _)
  if self == Base
    super
  else
    match = Method.match(self, name)
    match && match.valid? || super
  end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/models.rb:40
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:32</summary>

##### ActiveRecord::DynamicMatchers::Method.match

```ruby
def match(model, name)
  klass = matchers.find { |k| k.pattern.match?(name) }
  klass.new(model, name) if klass
end

# called from /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:10
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:37</summary>

##### ActiveRecord::DynamicMatchers::Method.pattern

```ruby
def pattern
  @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end

# called from /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:33
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:37</summary>

##### ActiveRecord::DynamicMatchers::Method.pattern

```ruby
def pattern
  @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end

# called from /usr/local/bundle/gems/activerecord-6.0.3.7/lib/active_record/dynamic_matchers.rb:33
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/module/attribute_accessors.rb:57</summary>

##### Devise.pepper

```ruby
def self.#{sym}
  @@#{sym}
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/models.rb:43
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:25</summary>

##### Object#present?

```ruby
def present?
  !blank?
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:17
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:56</summary>

##### NilClass#blank?

```ruby
def blank?
  true
end

# called from /usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:26
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/engine.rb:47</summary>

##### BCrypt::Engine.hash_secret

```ruby
def self.hash_secret(secret, salt, _ = nil)
  if valid_secret?(secret)
    if valid_salt?(salt)
      if RUBY_PLATFORM == "java"
        Java.bcrypt_jruby.BCrypt.hashpw(secret.to_s.to_java_bytes, salt.to_s)
      else
        __bc_crypt(secret.to_s, salt)
      end
    else
      raise Errors::InvalidSalt.new("invalid salt")
    end
  else
    raise Errors::InvalidSecret.new("invalid secret")
  end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:20
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/engine.rb:87</summary>

##### BCrypt::Engine.valid_secret?

```ruby
def self.valid_secret?(secret)
  secret.respond_to?(:to_s)
end

# called from /usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/engine.rb:48
```
</details>
<details open>
<summary>/usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/engine.rb:82</summary>

##### BCrypt::Engine.valid_salt?

```ruby
def self.valid_salt?(salt)
  !!(salt =~ /^\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}$/)
end

# called from /usr/local/bundle/gems/bcrypt-3.1.16/lib/bcrypt/engine.rb:49
```
</details>
<details open>
<summary>/usr/local/bundle/gems/devise-4.8.0/lib/devise.rb:500</summary>

##### Devise.secure_compare

```ruby
def self.secure_compare(a, b)
  return false if a.blank? || b.blank? || a.bytesize != b.bytesize
  l = a.unpack "C#{a.bytesize}"

  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift }
  res == 0
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise/encryptor.rb:21
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:121</summary>

##### String#blank?

```ruby
def blank?
  # The regexp that matches blank strings is expensive. For the case of empty
  # strings we can speed up this method (~3.5x) with an empty? call. The
  # penalty for the rest of strings is marginal.
  empty? ||
    begin
      BLANK_RE.match?(self)
    rescue Encoding::CompatibilityError
      ENCODED_BLANKS[self.encoding].match?(self)
    end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise.rb:501
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/object/blank.rb:121</summary>

##### String#blank?

```ruby
def blank?
  # The regexp that matches blank strings is expensive. For the case of empty
  # strings we can speed up this method (~3.5x) with an empty? call. The
  # penalty for the rest of strings is marginal.
  empty? ||
    begin
      BLANK_RE.match?(self)
    rescue Encoding::CompatibilityError
      ENCODED_BLANKS[self.encoding].match?(self)
    end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise.rb:501
```
</details>
<details open>
<summary>/usr/local/bundle/gems/activesupport-6.0.3.7/lib/active_support/core_ext/numeric/conversions.rb:105</summary>

##### ActiveSupport::NumericWithFormat#to_s

```ruby
def to_s(format = nil, options = nil)
  case format
  when nil
    super()
  when Integer, String
    super(format)
  when :phone
    ActiveSupport::NumberHelper.number_to_phone(self, options || {})
  when :currency
    ActiveSupport::NumberHelper.number_to_currency(self, options || {})
  when :percentage
    ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
  when :delimited
    ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
  when :rounded
    ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
  when :human
    ActiveSupport::NumberHelper.number_to_human(self, options || {})
  when :human_size
    ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
  when Symbol
    super()
  else
    super(format)
  end
end

# called from /usr/local/bundle/gems/devise-4.8.0/lib/devise.rb:502
```
</details>

valid_password?メソッドは、引数に与えられたpasswordが正しいかどうかを検証するメソッドですが、 true か false かを判定するまでに多岐に渡るメソッドが呼び出されていることがわかります。

このGemがあれば、OSSのコードリーディングが捗りそうですね。