Class: HeapScope::Report

Inherits:
Object
  • Object
show all
Defined in:
lib/heapscope/report.rb,
lib/heapscope/report/html.rb,
lib/heapscope/report/text.rb,
lib/heapscope/report/markdown.rb

Overview

Versioned report combining snapshots, diffs, findings, and presentation.

Defined Under Namespace

Modules: HTML, Markdown, Text

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**attrs) ⇒ Report

Returns a new instance of Report.



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

def initialize(**attrs)
  @schema_version = attrs[:schema_version] || SCHEMA_VERSION
  @heapscope_version = attrs[:heapscope_version] || VERSION
  @runtime_info = attrs[:runtime_info] || {}
  @summary = attrs[:summary] || {}
  @before = attrs[:before]
  @after = attrs[:after]
  @diff = attrs[:diff]
  @findings = attrs[:findings] || []
  @suspects = attrs[:suspects] || []
  @classes = attrs[:classes] || []
  @allocation_sites = attrs[:allocation_sites] || []
  @retention = attrs[:retention]
  @metadata = attrs[:metadata] || {}
  @limitations = attrs[:limitations] || []
  @budget_result = attrs[:budget_result]
  @thread_locals = attrs[:thread_locals]
  @reproduction = attrs[:reproduction]
  @fibers = attrs[:fibers]
  @globals = attrs[:globals]
  @closures = attrs[:closures]
  @dominators = attrs[:dominators]
  @fragmentation = attrs[:fragmentation]
  @aging = attrs[:aging]
  @gc_correlation = attrs[:gc_correlation]
  @retention_paths = attrs[:retention_paths]
end

Instance Attribute Details

#afterObject (readonly)

Returns the value of attribute after.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def after
  @after
end

#agingObject (readonly)

Returns the value of attribute aging.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def aging
  @aging
end

#allocation_sitesObject (readonly)

Returns the value of attribute allocation_sites.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def allocation_sites
  @allocation_sites
end

#beforeObject (readonly)

Returns the value of attribute before.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def before
  @before
end

#budget_resultObject (readonly)

Returns the value of attribute budget_result.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def budget_result
  @budget_result
end

#classesObject (readonly)

Returns the value of attribute classes.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def classes
  @classes
end

#closuresObject (readonly)

Returns the value of attribute closures.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def closures
  @closures
end

#diffObject (readonly)

Returns the value of attribute diff.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def diff
  @diff
end

#dominatorsObject (readonly)

Returns the value of attribute dominators.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def dominators
  @dominators
end

#fibersObject (readonly)

Returns the value of attribute fibers.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def fibers
  @fibers
end

#findingsObject (readonly)

Returns the value of attribute findings.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def findings
  @findings
end

#fragmentationObject (readonly)

Returns the value of attribute fragmentation.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def fragmentation
  @fragmentation
end

#gc_correlationObject (readonly)

Returns the value of attribute gc_correlation.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def gc_correlation
  @gc_correlation
end

#globalsObject (readonly)

Returns the value of attribute globals.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def globals
  @globals
end

#heapscope_versionObject (readonly)

Returns the value of attribute heapscope_version.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def heapscope_version
  @heapscope_version
end

#limitationsObject (readonly)

Returns the value of attribute limitations.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def limitations
  @limitations
end

#metadataObject (readonly)

Returns the value of attribute metadata.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def 
  @metadata
end

#reproductionObject (readonly)

Returns the value of attribute reproduction.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def reproduction
  @reproduction
end

#retentionObject (readonly)

Returns the value of attribute retention.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def retention
  @retention
end

#retention_pathsObject (readonly)

Returns the value of attribute retention_paths.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def retention_paths
  @retention_paths
end

#runtime_infoObject (readonly)

Returns the value of attribute runtime_info.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def runtime_info
  @runtime_info
end

#schema_versionObject (readonly)

Returns the value of attribute schema_version.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def schema_version
  @schema_version
end

#summaryObject (readonly)

Returns the value of attribute summary.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def summary
  @summary
end

#suspectsObject (readonly)

Returns the value of attribute suspects.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def suspects
  @suspects
end

#thread_localsObject (readonly)

Returns the value of attribute thread_locals.



9
10
11
# File 'lib/heapscope/report.rb', line 9

def thread_locals
  @thread_locals
end

Class Method Details

.from_diff(diff, analysis:, metadata: {}) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/heapscope/report.rb', line 213

def self.from_diff(diff, analysis:, metadata: {})
  limitations = (diff.before.limitations + diff.after.limitations).uniq
  frag = Fragmentation.assess(diff.after)
  new(
    runtime_info: {
      ruby: RUBY_VERSION,
      engine: RUBY_ENGINE,
      platform: RUBY_PLATFORM
    },
    summary: analysis[:summary],
    before: diff.before,
    after: diff.after,
    diff: diff,
    findings: analysis[:findings],
    suspects: analysis[:suspects],
    classes: diff.class_deltas.sort_by { |c| -c[:delta_count].abs },
    allocation_sites: diff.allocation_deltas.first(50),
    fragmentation: frag,
    reproduction: {
      command: Reproduction.command(),
      captured_at: Time.now.utc.iso8601
    },
    metadata: ,
    limitations: limitations
  )
end

.from_h(data) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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/heapscope/report.rb', line 165

def self.from_h(data)
  before = data[:before] ? Snapshot.from_h(data[:before]) : nil
  after = data[:after] ? Snapshot.from_h(data[:after]) : nil
  diff = before && after ? Diff.new(before, after) : nil
  findings = Array(data[:findings]).map do |f|
    Finding.new(
      code: f[:code],
      severity: f[:severity]&.to_sym,
      title: f[:title],
      facts: f[:facts],
      derived: f[:derived],
      hypothesis: f[:hypothesis],
      suspected_cause: f[:suspected_cause],
      evidence: f[:evidence],
      suggestions: f[:suggestions],
      subject: f[:subject]
    )
  end

  new(
    schema_version: data[:schema_version],
    heapscope_version: data[:heapscope_version],
    runtime_info: data[:runtime] || {},
    summary: data[:summary] || {},
    before: before,
    after: after,
    diff: diff,
    findings: findings,
    suspects: data[:suspects] || [],
    classes: data[:classes] || diff&.class_deltas || [],
    allocation_sites: data[:allocation_sites] || [],
    retention: data[:retention],
    metadata: data[:metadata] || {},
    limitations: data[:limitations] || [],
    budget_result: data[:budget_result],
    thread_locals: data[:thread_locals],
    reproduction: data[:reproduction],
    fibers: data[:fibers],
    globals: data[:globals],
    closures: data[:closures],
    dominators: data[:dominators],
    fragmentation: data[:fragmentation],
    aging: data[:aging],
    gc_correlation: data[:gc_correlation],
    retention_paths: data[:retention_paths]
  )
end

.from_retention_session(session) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/heapscope/report.rb', line 240

def self.from_retention_session(session)
  analysis = Analyzer.new.analyze_retention(session)
  first = session.samples.first&.snapshot
  last = session.samples.last&.snapshot
  diff = first && last ? Diff.new(first, last) : nil
  diff_analysis = if diff
                    Analyzer.new.analyze_diff(diff,
                                              context: { force_gc: session.force_gc })
                  else
                    { findings: [], suspects: [],
                      summary: {} }
                  end

  extras = Analyzer.new.enrich_session(session, last)
  all_findings = Findings.rank_and_dedupe(
    analysis[:findings] + diff_analysis[:findings] + Array(extras[:findings])
  )
  new(
    runtime_info: { ruby: RUBY_VERSION, engine: RUBY_ENGINE, platform: RUBY_PLATFORM },
    summary: analysis[:summary].merge(diff_analysis[:summary] || {}).merge(
      healthy: analysis[:persistent].empty? && all_findings.none? { |f| f.severity == :high }
    ),
    before: first,
    after: last,
    diff: diff,
    findings: all_findings,
    suspects: diff_analysis[:suspects],
    classes: diff&.class_deltas || [],
    retention: {
      samples: session.samples.size,
      force_gc: session.force_gc,
      persistent: analysis[:persistent].first(20),
      class_series: analysis[:class_series].values
                                           .select { |e| e[:delta].abs > 0 }
                                           .sort_by { |e| -e[:delta].abs }
                                           .first(50),
      collections: analysis[:collections]
    },
    thread_locals: analysis[:thread_locals],
    fibers: extras[:fibers],
    globals: extras[:globals],
    closures: extras[:closures],
    dominators: extras[:dominators],
    fragmentation: extras[:fragmentation],
    aging: extras[:aging],
    gc_correlation: extras[:gc_correlation],
    retention_paths: extras[:retention_paths],
    reproduction: {
      command: Reproduction.command(kind: "retention_test", cycles: session.samples.size),
      captured_at: Time.now.utc.iso8601
    },
    metadata: { kind: "retention_session" },
    limitations: [first, last].compact.flat_map(&:limitations).uniq + Array(extras[:limitations])
  )
end

.load(path) ⇒ Object



159
160
161
162
163
# File 'lib/heapscope/report.rb', line 159

def self.load(path)
  data = JSON.parse(File.read(path), symbolize_names: true)
  Schema.validate!(data)
  from_h(data)
end

Instance Method Details

#findings_table(format: :ascii) ⇒ Object



142
143
144
# File 'lib/heapscope/report.rb', line 142

def findings_table(format: :ascii)
  Tables.findings(self, format: format)
end

#healthy?Boolean

Returns:

  • (Boolean)


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

def healthy?
  summary[:healthy] == true || (findings.empty? && suspects.none? { |s| s[:severity] == :high })
end

#next_steps(limit: 8) ⇒ Object



146
147
148
# File 'lib/heapscope/report.rb', line 146

def next_steps(limit: 8)
  Suggest.next_steps(self, limit: limit)
end

#passed_budget?Boolean

Returns:

  • (Boolean)


67
68
69
# File 'lib/heapscope/report.rb', line 67

def passed_budget?
  budget_result.nil? || budget_result[:passed]
end


71
72
73
74
# File 'lib/heapscope/report.rb', line 71

def print(io = $stdout)
  io.puts to_text
  self
end

#ranked_findingsObject



150
151
152
# File 'lib/heapscope/report.rb', line 150

def ranked_findings
  Findings.rank_and_dedupe(findings)
end

#retained_bytes_for(klass) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/heapscope/report.rb', line 59

def retained_bytes_for(klass)
  name = klass.is_a?(Module) ? klass.name : klass.to_s
  return 0 unless diff

  delta = diff.class_deltas.find { |c| c[:name] == name }
  delta ? delta[:delta_bytes] : 0
end

#retained_count_for(klass) ⇒ Object



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

def retained_count_for(klass)
  name = klass.is_a?(Module) ? klass.name : klass.to_s
  row = classes.find { |c| c[:name] == name || c["name"] == name }
  return row[:delta_count] || row["delta_count"] if row

  return 0 unless diff

  delta = diff.class_deltas.find { |c| c[:name] == name }
  delta ? delta[:delta_count] : 0
end

#save(path) ⇒ Object



127
128
129
130
# File 'lib/heapscope/report.rb', line 127

def save(path)
  File.write(path, to_json)
  path
end

#save_html(path) ⇒ Object



154
155
156
157
# File 'lib/heapscope/report.rb', line 154

def save_html(path)
  File.write(path, Report::HTML.render(self))
  path
end

#save_markdown(path) ⇒ Object



88
89
90
91
# File 'lib/heapscope/report.rb', line 88

def save_markdown(path)
  File.write(path, to_markdown)
  path
end

#scorecard(title: metadata[:title] || metadata["title"] || "HeapScope") ⇒ Object



132
133
134
# File 'lib/heapscope/report.rb', line 132

def scorecard(title: [:title] || ["title"] || "HeapScope")
  Scorecard.from_report(self, title: title)
end

#table(format: :ascii) ⇒ Object



136
137
138
139
140
# File 'lib/heapscope/report.rb', line 136

def table(format: :ascii)
  return "" unless diff

  Tables.class_growth(diff, format: format)
end

#to_hObject



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/heapscope/report.rb', line 93

def to_h
  {
    schema_version: schema_version,
    heapscope_version: heapscope_version,
    runtime: runtime_info,
    summary: summary,
    metadata: ,
    limitations: limitations,
    before: before&.to_h,
    after: after&.to_h,
    diff: diff_hash,
    classes: classes,
    allocation_sites: allocation_sites,
    findings: findings.map { |f| f.respond_to?(:to_h) ? f.to_h : f },
    suspects: suspects,
    retention: retention,
    budget_result: budget_result,
    thread_locals: thread_locals,
    reproduction: reproduction,
    fibers: fibers,
    globals: globals,
    closures: closures,
    dominators: dominators&.map { |d| d.respond_to?(:to_h) ? d.to_h : d },
    fragmentation: fragmentation,
    aging: aging,
    gc_correlation: gc_correlation,
    retention_paths: retention_paths
  }.compact
end

#to_json(*_args) ⇒ Object



123
124
125
# File 'lib/heapscope/report.rb', line 123

def to_json(*_args)
  JSON.pretty_generate(to_h)
end

#to_markdownObject



84
85
86
# File 'lib/heapscope/report.rb', line 84

def to_markdown
  Report::Markdown.render(self)
end

#to_sObject



76
77
78
# File 'lib/heapscope/report.rb', line 76

def to_s
  to_text
end

#to_textObject



80
81
82
# File 'lib/heapscope/report.rb', line 80

def to_text
  Report::Text.render(self)
end