Class: HeapScope::Budget

Inherits:
Object
  • Object
show all
Defined in:
lib/heapscope/budget.rb

Overview

Explicit memory budgets for CI and regression tests. Integers only — no AS core_ext required.

Constant Summary collapse

PRESETS =
{
  rails_request: {
    max_retained_objects: 5_000,
    max_retained_bytes: 8 * 1024 * 1024,
    max_retention_ratio: 0.15,
    max_rss_growth: 20 * 1024 * 1024,
    max_heap_live_growth: 50_000,
    severity_threshold: :high
  },
  sidekiq_job: {
    max_retained_objects: 10_000,
    max_retained_bytes: 16 * 1024 * 1024,
    max_retention_ratio: 0.20,
    max_rss_growth: 50 * 1024 * 1024,
    max_heap_live_growth: 100_000,
    severity_threshold: :high
  },
  ci_strict: {
    max_retained_objects: 500,
    max_retained_bytes: 2 * 1024 * 1024,
    max_retention_ratio: 0.05,
    max_rss_growth: 5 * 1024 * 1024,
    max_heap_live_growth: 5_000,
    severity_threshold: :medium
  }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_retained_objects: nil, max_retained_bytes: nil, max_retention_ratio: nil, max_rss_growth: nil, max_heap_live_growth: nil, severity_threshold: :high, preset_name: nil) ⇒ Budget

Returns a new instance of Budget.



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/heapscope/budget.rb', line 48

def initialize(max_retained_objects: nil, max_retained_bytes: nil,
               max_retention_ratio: nil, max_rss_growth: nil,
               max_heap_live_growth: nil, severity_threshold: :high,
               preset_name: nil)
  @max_retained_objects = max_retained_objects
  @max_retained_bytes = max_retained_bytes
  @max_retention_ratio = max_retention_ratio
  @max_rss_growth = max_rss_growth
  @max_heap_live_growth = max_heap_live_growth
  @severity_threshold = severity_threshold
  @preset_name = preset_name
end

Instance Attribute Details

#max_heap_live_growthObject (readonly)

Returns the value of attribute max_heap_live_growth.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def max_heap_live_growth
  @max_heap_live_growth
end

#max_retained_bytesObject (readonly)

Returns the value of attribute max_retained_bytes.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def max_retained_bytes
  @max_retained_bytes
end

#max_retained_objectsObject (readonly)

Returns the value of attribute max_retained_objects.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def max_retained_objects
  @max_retained_objects
end

#max_retention_ratioObject (readonly)

Returns the value of attribute max_retention_ratio.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def max_retention_ratio
  @max_retention_ratio
end

#max_rss_growthObject (readonly)

Returns the value of attribute max_rss_growth.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def max_rss_growth
  @max_rss_growth
end

#preset_nameObject (readonly)

Returns the value of attribute preset_name.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def preset_name
  @preset_name
end

#severity_thresholdObject (readonly)

Returns the value of attribute severity_threshold.



33
34
35
# File 'lib/heapscope/budget.rb', line 33

def severity_threshold
  @severity_threshold
end

Class Method Details

.preset(name) ⇒ Object

Raises:

  • (ArgumentError)


36
37
38
39
40
41
42
# File 'lib/heapscope/budget.rb', line 36

def self.preset(name)
  key = name.to_sym
  attrs = PRESETS[key]
  raise ArgumentError, "Unknown budget preset: #{name} (#{PRESETS.keys.join(', ')})" unless attrs

  new(**attrs, preset_name: key)
end

.presetsObject



44
45
46
# File 'lib/heapscope/budget.rb', line 44

def self.presets
  PRESETS.keys
end

Instance Method Details

#evaluate(report) ⇒ Object



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
# File 'lib/heapscope/budget.rb', line 61

def evaluate(report)
  violations = []
  diff = report.diff

  if max_retained_objects && diff
    surviving = diff.surviving_estimate || report.classes.sum { |c| [c[:delta_count].to_i, 0].max }
    violations << "Retained objects #{surviving} exceed budget #{max_retained_objects}" if surviving && surviving > max_retained_objects
  end

  if max_retained_bytes && diff
    bytes = diff.heap_bytes_estimate_delta
    violations << "Retained bytes #{bytes} exceed budget #{max_retained_bytes}" if bytes > max_retained_bytes
  end

  if max_retention_ratio && diff&.retention_ratio && (diff.retention_ratio > max_retention_ratio)
    violations << "Retention ratio #{diff.retention_ratio} exceeds budget #{max_retention_ratio}"
  end

  violations << "RSS growth #{diff.rss_delta} exceeds budget #{max_rss_growth}" if max_rss_growth && diff&.rss_delta && diff.rss_delta > max_rss_growth

  if max_heap_live_growth && diff&.heap_live_delta && diff.heap_live_delta > max_heap_live_growth
    violations << "Heap live growth #{diff.heap_live_delta} exceeds budget #{max_heap_live_growth}"
  end

  rank = { low: 1, medium: 2, high: 3 }
  threshold = rank[severity_threshold] || 3
  report.findings.each do |f|
    next if (rank[f.severity] || 0) < threshold

    violations << "Finding #{f.code} severity #{f.severity} at/above threshold #{severity_threshold}"
  end

  { passed: violations.empty?, violations: violations, preset: preset_name }
end

#to_hObject



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/heapscope/budget.rb', line 96

def to_h
  {
    preset: preset_name,
    max_retained_objects: max_retained_objects,
    max_retained_bytes: max_retained_bytes,
    max_retention_ratio: max_retention_ratio,
    max_rss_growth: max_rss_growth,
    max_heap_live_growth: max_heap_live_growth,
    severity_threshold: severity_threshold
  }.compact
end