Module: Axn::Extras::Strategies::Vernier

Defined in:
lib/axn/extras/strategies/vernier.rb

Class Method Summary collapse

Class Method Details

.configure(if: nil, sample_rate: 0.1, output_dir: nil) ⇒ Module

Returns A configured module that adds profiling to the action.

Parameters:

  • if (Proc, Symbol, #call, nil) (defaults to: nil)

    Optional condition to determine when to profile

  • sample_rate (Float) (defaults to: 0.1)

    Sampling rate (0.0 to 1.0, default: 0.1)

  • output_dir (String, Pathname) (defaults to: nil)

    Output directory for profile files (default: Rails.root/tmp/profiles or tmp/profiles)

Returns:

  • (Module)

    A configured module that adds profiling to the action



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/axn/extras/strategies/vernier.rb', line 14

def self.configure(if: nil, sample_rate: 0.1, output_dir: nil)
  condition = binding.local_variable_get(:if)
  sample_rate_value = sample_rate
  output_dir_value = output_dir || _default_output_dir

  Module.new do
    extend ActiveSupport::Concern

    included do
      class_attribute :_vernier_condition, default: condition
      class_attribute :_vernier_sample_rate, default: sample_rate_value
      class_attribute :_vernier_output_dir, default: output_dir_value

      around do |hooked|
        _with_vernier_profiling { hooked.call }
      end
    end

    private

    def _with_vernier_profiling(&)
      return yield unless _should_profile?

      _profile_with_vernier(&)
    end

    def _profile_with_vernier(&)
      _ensure_vernier_available!

      class_name = self.class.name.presence || "AnonymousAction"
      profile_name = "axn_#{class_name}_#{Time.now.to_i}"

      # Ensure output directory exists (only once per instance)
      _ensure_output_directory_exists

      # Build output file path
      output_dir = self.class._vernier_output_dir || _default_output_dir
      output_file = File.join(output_dir, "#{profile_name}.json")

      # Configure Vernier with our settings
      collector_options = {
        out: output_file,
        allocation_sample_rate: (self.class._vernier_sample_rate * 1000).to_i,
      }

      ::Vernier.profile(**collector_options, &)
    end

    def _ensure_output_directory_exists
      return if @_vernier_directory_created

      output_dir = self.class._vernier_output_dir || _default_output_dir
      FileUtils.mkdir_p(output_dir)
      @_vernier_directory_created = true
    end

    def _should_profile?
      # Fast path: no condition means always profile
      return true unless self.class._vernier_condition

      # Slow path: evaluate condition (only when needed)
      Axn::Core::Flow::Handlers::Invoker.call(
        action: self,
        handler: self.class._vernier_condition,
        operation: "determining if profiling should run",
      )
    end

    def _ensure_vernier_available!
      return if defined?(::Vernier) && ::Vernier.is_a?(Module)

      begin
        require "vernier"
      rescue LoadError
        raise LoadError, <<~ERROR
          Vernier profiler is not available. To use profiling, add 'vernier' to your Gemfile:

            gem 'vernier', '~> 1.0'

          Then run: bundle install
        ERROR
      end
    end

    def _default_output_dir
      if defined?(Rails) && Rails.respond_to?(:root)
        Rails.root.join("tmp", "profiles")
      else
        Pathname.new("tmp/profiles")
      end
    end
  end
end