Class: Appom::Performance::Monitor

Inherits:
Object
  • Object
show all
Includes:
Logging
Defined in:
lib/appom/performance.rb

Overview

Performance monitoring and metrics collection

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Logging

level, level=, #log_debug, #log_element_action, #log_error, #log_info, #log_wait_end, #log_wait_start, #log_warn, #logger

Constructor Details

#initializeMonitor

Returns a new instance of Monitor.



12
13
14
15
16
17
# File 'lib/appom/performance.rb', line 12

def initialize
  @metrics = {}
  @started_at = Time.now
  @current_operations = {}
  reset_session_metrics
end

Instance Attribute Details

#metricsObject (readonly)

Returns the value of attribute metrics.



10
11
12
# File 'lib/appom/performance.rb', line 10

def metrics
  @metrics
end

#started_atObject (readonly)

Returns the value of attribute started_at.



10
11
12
# File 'lib/appom/performance.rb', line 10

def started_at
  @started_at
end

Instance Method Details

#check_regressions(baseline_file, threshold_percent = 20) ⇒ Object

Check for performance regressions



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/appom/performance.rb', line 187

def check_regressions(baseline_file, threshold_percent = 20)
  return {} unless File.exist?(baseline_file)

  baseline = load_baseline(baseline_file)
  regressions = {}

  @metrics.each do |name, current_metric|
    baseline_metric = baseline[name]
    next unless baseline_metric

    current_avg = current_metric[:total_duration] / current_metric[:total_calls]
    baseline_avg = baseline_metric['avg_duration'] || baseline_metric[:avg_duration]

    next unless current_avg > baseline_avg * (1 + (threshold_percent / 100.0))

    regression_percent = ((current_avg - baseline_avg) / baseline_avg * 100).round(2)
    regressions[name] = {
      current_avg: current_avg,
      baseline_avg: baseline_avg,
      regression_percent: regression_percent,
    }
  end

  regressions
end

#end_timing(operation_id, success: true, additional_context: {}) ⇒ Object

End timing an operation



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/appom/performance.rb', line 33

def end_timing(operation_id, success: true, additional_context: {})
  operation = @current_operations.delete(operation_id)
  return unless operation

  duration = Time.now - operation[:start_time]

  record_metric(
    operation[:name],
    duration,
    success: success,
    context: operation[:context].merge(additional_context),
  )

  log_debug("Completed timing: #{operation[:name]} (#{(duration * 1000).round(2)}ms)")
  duration
end

#export_metrics(format: :json, file_path: nil) ⇒ Object

Export metrics to file



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/appom/performance.rb', line 152

def export_metrics(format: :json, file_path: nil)
  file_path ||= "appom_metrics_#{Time.now.strftime('%Y%m%d_%H%M%S')}.#{format}"

  data = {
    exported_at: Time.now,
    session_started: @started_at,
    summary: summary,
    detailed_metrics: stats,
  }

  case format
  when :json
    File.write(file_path, JSON.pretty_generate(data))
  when :yaml
    File.write(file_path, YAML.dump(data))
  when :csv
    export_to_csv(file_path, data)
  else
    raise ArgumentError, "Unsupported format: #{format}"
  end

  log_info("Performance metrics exported to #{file_path}")
  file_path
end

#most_frequent_operations(limit = 10) ⇒ Object

Get most frequent operations



139
140
141
142
143
144
145
146
147
148
149
# File 'lib/appom/performance.rb', line 139

def most_frequent_operations(limit = 10)
  operations = @metrics.map do |name, metric|
    {
      name: name,
      total_calls: metric[:total_calls],
      avg_duration: metric[:total_duration] / metric[:total_calls],
      success_rate: (metric[:successful_calls].to_f / metric[:total_calls] * 100).round(2),
    }
  end
  operations.sort_by { |op| -op[:total_calls] }.first(limit)
end

#record_metric(name, duration, success: true, context: {}) ⇒ Object

Record a metric manually



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
# File 'lib/appom/performance.rb', line 72

def record_metric(name, duration, success: true, context: {})
  @metrics[name] ||= initialize_metric(name)
  metric = @metrics[name]

  metric[:total_calls] += 1
  metric[:total_duration] += duration
  metric[:successful_calls] += 1 if success
  metric[:failed_calls] += 1 unless success

  # Update min/max
  metric[:min_duration] = [metric[:min_duration], duration].min
  metric[:max_duration] = [metric[:max_duration], duration].max

  # Calculate rolling averages (last 10 calls)
  metric[:recent_durations] << duration
  metric[:recent_durations] = metric[:recent_durations].last(10)

  # Store context for analysis
  metric[:contexts] << context.merge(success: success, duration: duration)
  metric[:contexts] = metric[:contexts].last(50) # Keep last 50 contexts

  # Update percentiles for larger samples
  return unless (metric[:total_calls] % 10).zero?

  update_percentiles(metric)
end

#reset!Object

Reset all metrics



178
179
180
181
182
183
184
# File 'lib/appom/performance.rb', line 178

def reset!
  @metrics.clear
  @started_at = Time.now
  @current_operations.clear
  reset_session_metrics
  log_info('Performance metrics reset')
end

#slowest_operations(limit = 10) ⇒ Object

Get slowest operations



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/appom/performance.rb', line 126

def slowest_operations(limit = 10)
  operations = @metrics.map do |name, metric|
    {
      name: name,
      max_duration: metric[:max_duration],
      avg_duration: metric[:total_duration] / metric[:total_calls],
      total_calls: metric[:total_calls],
    }
  end
  operations.sort_by { |op| -op[:max_duration] }.first(limit)
end

#start_timing(operation_name, context = {}) ⇒ Object

Start timing an operation



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/appom/performance.rb', line 20

def start_timing(operation_name, context = {})
  operation_id = generate_operation_id
  @current_operations[operation_id] = {
    name: operation_name,
    start_time: Time.now,
    context: context,
  }

  log_debug("Started timing: #{operation_name}", context)
  operation_id
end

#stats(operation_name = nil) ⇒ Object

Get performance statistics



100
101
102
103
104
105
106
# File 'lib/appom/performance.rb', line 100

def stats(operation_name = nil)
  if operation_name
    calculate_stats(@metrics[operation_name]) if @metrics[operation_name]
  else
    @metrics.transform_values { |metric| calculate_stats(metric) }
  end
end

#summaryObject

Get performance summary



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/appom/performance.rb', line 109

def summary
  total_operations = @metrics.values.sum { |m| m[:total_calls] }
  total_duration = @metrics.values.sum { |m| m[:total_duration] }

  {
    session_duration: Time.now - @started_at,
    total_operations: total_operations,
    total_duration: total_duration,
    average_operation_time: total_operations.positive? ? total_duration / total_operations : 0,
    operations_per_second: total_operations / (Time.now - @started_at),
    slowest_operations: slowest_operations(5),
    most_frequent_operations: most_frequent_operations(5),
    success_rate: calculate_overall_success_rate,
  }
end

#time_operation(operation_name, context = {}) ⇒ Object

Time a block of code



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/appom/performance.rb', line 51

def time_operation(operation_name, context = {})
  operation_id = start_timing(operation_name, context)
  Time.now
  success = true
  result = nil

  begin
    result = yield
  rescue StandardError => e
    success = false
    raise e
  ensure
    end_timing(operation_id, success: success, additional_context: {
                 exception: success ? nil : e&.class&.name,
               },)
  end

  result
end