heapscope 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1ac0f631627ce36c708e8a675265170999f9692b5a3d1c883941c01338a814c
4
- data.tar.gz: ccf46c30e092d097872925d3a5cfcf124f2a8dd4d252beaf7f5666f9d48d38d1
3
+ metadata.gz: cf49d15007ca523b3c9fbfb463d55bb879ce6c73ad599412f8040238b9d5786e
4
+ data.tar.gz: bc97ee480b143c0079d6ee4762965ba94f517cae7370f3a0971616487be35646
5
5
  SHA512:
6
- metadata.gz: 22906c262dd5681cde4b144388818220aa991084679fd709186720e7c50cb4455bb0f7a51a51044b37cc4cb91392364bf55dd9857ad613fe3c96e2f482e9bcc8
7
- data.tar.gz: 246a6b67bb0d24839ac72320e458e2955a01d5452da4aca8b1a0f41aa43d7592d2082cd0546f38e371af5b63ec4204efd6f9ae50d4f9e3f277792483fd1ce63d
6
+ metadata.gz: 22f834117fff726913f913c406c93b88eae648b9caf51ff64801b040787f6d6e8b35454080318f1a812d070ef78f448fda31dee39e1d62ec225414138a75bed5
7
+ data.tar.gz: e27d706f6ffae232f418d84b9849a719321ef8d5b1ec2b3eee10257de4c6e1cb23e711efefa392c79f39b2207c7f56fbd225368fb51a90023d25c32b97de45b7
data/CHANGELOG.md CHANGED
@@ -9,6 +9,14 @@ Detailed release write-ups live in [`docs/changelogs/`](docs/changelogs/).
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.7.0] — 2026-08-14
13
+
14
+ ### Added
15
+
16
+ - Allocation flamegraphs from snapshot sites (`HeapScope.flamegraph`, `heapscope flamegraph`)
17
+ - Folded-stack output for inferno / flamegraph.pl and Speedscope JSON (`--format folded|speedscope`)
18
+ - Optional `--unit count|bytes` weighting for allocation counts vs shallow bytes
19
+
12
20
  ## [0.6.0] — 2026-08-13
13
21
 
14
22
  ### Added
@@ -77,7 +85,8 @@ Branding, detectors, richer HTML, eval harness, security/CoC.
77
85
 
78
86
  Initial retention toolkit.
79
87
 
80
- [Unreleased]: https://github.com/theworker02/heapscope/compare/v0.6.0...HEAD
88
+ [Unreleased]: https://github.com/theworker02/heapscope/compare/v0.7.0...HEAD
89
+ [0.7.0]: https://github.com/theworker02/heapscope/releases/tag/v0.7.0
81
90
  [0.6.0]: https://github.com/theworker02/heapscope/releases/tag/v0.6.0
82
91
  [0.5.0]: https://github.com/theworker02/heapscope/releases/tag/v0.5.0
83
92
  [0.4.0]: https://github.com/theworker02/heapscope/releases/tag/v0.4.0
data/README.md CHANGED
@@ -8,25 +8,29 @@
8
8
  </p>
9
9
 
10
10
  <p align="center">
11
- <a href="https://rubygems.org/gems/heapscope"><img alt="Gem version" src="https://img.shields.io/gem/v/heapscope?color=0f766e&style=for-the-badge"></a>
12
- <a href="https://github.com/theworker02/heapscope/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/theworker02/heapscope/ci.yml?branch=main&style=for-the-badge&label=CI"></a>
13
- <a href="LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-334155?style=for-the-badge"></a>
14
- <a href="#ruby-compatibility"><img alt="Ruby" src="https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D?style=for-the-badge&logo=ruby&logoColor=white"></a>
15
- <a href="#privacy"><img alt="Privacy" src="https://img.shields.io/badge/privacy-local%20only-0f766e&style=for-the-badge"></a>
16
- <a href="#no-saas"><img alt="No SaaS" src="https://img.shields.io/badge/SaaS-none-64748b&style=for-the-badge"></a>
17
- <a href="https://thanks.dev/u/gh/theworker02"><img alt="thanks.dev" src="https://img.shields.io/badge/thanks.dev-theworker02-0f766e&style=for-the-badge"></a>
18
- <a href="https://theworker02.github.io/heapscope/"><img alt="Docs" src="https://img.shields.io/badge/docs-Pages-0f766e?style=for-the-badge"></a>
19
- <a href="CHANGELOG.md"><img alt="Changelog" src="https://img.shields.io/badge/changelog-0.6.0-0f766e&style=for-the-badge"></a>
11
+ <a href="https://rubygems.org/gems/heapscope"><img alt="Gem version" src="https://img.shields.io/gem/v/heapscope?style=plastic&color=0f766e&label=gem"></a>
12
+ <a href="https://rubygems.org/gems/heapscope"><img alt="RubyGems" src="https://img.shields.io/badge/RubyGems-heapscope-CC342D?style=plastic&logo=ruby&logoColor=white"></a>
13
+ <a href="https://github.com/theworker02/heapscope/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/theworker02/heapscope/ci.yml?branch=main&style=plastic&label=CI"></a>
14
+ <a href="LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-334155?style=plastic"></a>
15
+ <a href="#ruby-compatibility"><img alt="Ruby" src="https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D?style=plastic&logo=ruby&logoColor=white"></a>
16
+ <a href="#privacy"><img alt="Privacy" src="https://img.shields.io/badge/privacy-local%20only-0f766e?style=plastic"></a>
17
+ <a href="#no-saas"><img alt="No SaaS" src="https://img.shields.io/badge/SaaS-none-64748b?style=plastic"></a>
18
+ <a href="https://thanks.dev/u/gh/theworker02"><img alt="thanks.dev" src="https://img.shields.io/badge/thanks.dev-theworker02-0f766e?style=plastic"></a>
19
+ <a href="https://theworker02.github.io/heapscope/"><img alt="Docs" src="https://img.shields.io/badge/docs-Pages-0f766e?style=plastic"></a>
20
+ <a href="CHANGELOG.md"><img alt="Changelog" src="https://img.shields.io/badge/changelog-keep%20a%20changelog-0f766e?style=plastic"></a>
20
21
  </p>
21
22
 
22
23
  <p align="center">
24
+ <a href="https://rubygems.org/gems/heapscope"><strong>RubyGems</strong></a> ·
23
25
  <a href="https://theworker02.github.io/heapscope/">Website</a> ·
24
26
  <a href="docs/index.md">Docs hub</a> ·
25
27
  <a href="#quick-start">Quick start</a> ·
26
28
  <a href="docs/cli.md">CLI</a> ·
27
29
  <a href="#privacy">Privacy</a> ·
28
- <a href="https://thanks.dev/u/gh/theworker02">Sponsor</a> ·
29
- <a href="CHANGELOG.md">Changelog</a>
30
+ <a href="https://github.com/sponsors/theworker02">GitHub Sponsors</a> ·
31
+ <a href="https://thanks.dev/u/gh/theworker02">thanks.dev</a> ·
32
+ <a href="CHANGELOG.md">Changelog</a> ·
33
+ <a href="https://github.com/theworker02/heapscope/releases/tag/v0.7.0">Release notes</a>
30
34
  </p>
31
35
 
32
36
  ---
@@ -42,7 +46,14 @@ allocation pressure from retention, and intentional caches from suspicious growt
42
46
 
43
47
  > **No SaaS.** No uploads. No telemetry. Everything runs in-process on your machine.
44
48
 
45
- Release notes: [`docs/changelogs/0.6.0.md`](docs/changelogs/0.6.0.md)
49
+ | | |
50
+ |--|--|
51
+ | **Install (gem)** | [rubygems.org/gems/heapscope](https://rubygems.org/gems/heapscope) · `gem install heapscope` |
52
+ | **Site / docs** | [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) |
53
+ | **Source** | [github.com/theworker02/heapscope](https://github.com/theworker02/heapscope) |
54
+ | **Changelog** | [`CHANGELOG.md`](CHANGELOG.md) · [narrative 0.7.0](docs/changelogs/0.7.0.md) |
55
+ | **Release notes** | [GitHub v0.7.0](https://github.com/theworker02/heapscope/releases/tag/v0.7.0) |
56
+ | **Sponsor** | [GitHub Sponsors](https://github.com/sponsors/theworker02) · [thanks.dev](https://thanks.dev/u/gh/theworker02) |
46
57
 
47
58
  ---
48
59
 
@@ -59,17 +70,41 @@ Every finding separates **observed facts**, **derived behavior**, **hypothesis**
59
70
 
60
71
  ## Installation
61
72
 
73
+ Install from [RubyGems](https://rubygems.org/gems/heapscope) (MRI Ruby ≥ 3.1 recommended):
74
+
75
+ ```bash
76
+ gem install heapscope
77
+ ```
78
+
79
+ Or add to your Gemfile:
80
+
62
81
  ```ruby
63
82
  # Gemfile
64
- gem "heapscope", "~> 0.6"
83
+ gem "heapscope", "~> 0.7"
65
84
  ```
66
85
 
67
86
  ```bash
68
87
  bundle add heapscope
69
- # or
70
- gem install heapscope
71
88
  ```
72
89
 
90
+ Confirm the install:
91
+
92
+ ```bash
93
+ heapscope about
94
+ heapscope doctor
95
+ ```
96
+
97
+ From source (development / contributing):
98
+
99
+ ```bash
100
+ git clone https://github.com/theworker02/heapscope.git
101
+ cd heapscope
102
+ bundle install
103
+ bundle exec rake test
104
+ ```
105
+
106
+ Release notes and source tags: [GitHub Releases](https://github.com/theworker02/heapscope/releases) · narrative: [`docs/changelogs/0.7.0.md`](docs/changelogs/0.7.0.md)
107
+
73
108
  ---
74
109
 
75
110
  ## Ruby compatibility
@@ -188,8 +223,11 @@ report = monitor.stop
188
223
 
189
224
  ```bash
190
225
  heapscope watch --interval 10 --duration 600 -o monitor.json
226
+ heapscope flamegraph snapshot.json --format speedscope -o alloc.json
191
227
  ```
192
228
 
229
+ `heapscope flamegraph` turns captured allocation sites into folded stacks (inferno / flamegraph.pl) or Speedscope JSON. Capture with `--track-allocations` (or `mode: :deep`) so sites are present.
230
+
193
231
  Alerts fire on RSS / live-slot spikes between samples (thresholds configurable).
194
232
 
195
233
  ---
data/docs/README.md CHANGED
@@ -13,7 +13,8 @@ Start at [`index.md`](index.md).
13
13
  - [Roadmap](ROADMAP.md)
14
14
  - [Changelogs](changelogs/)
15
15
 
16
- Website: [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) · Sponsor: [thanks.dev/u/gh/theworker02](https://thanks.dev/u/gh/theworker02)
16
+ Website: [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) · Gem: [rubygems.org/gems/heapscope](https://rubygems.org/gems/heapscope) · Sponsor: [thanks.dev/u/gh/theworker02](https://thanks.dev/u/gh/theworker02)
17
+
17
18
 
18
19
  <p align="center">
19
20
  <img src="../assets/logo.svg" alt="" width="48"/>
data/docs/ROADMAP.md CHANGED
@@ -14,6 +14,7 @@ report packs, and a modular CLI — all local-only.
14
14
  | 0.4 | Sessions, scorecards, schema, GitHub polish |
15
15
  | 0.5 | Funding, Pages site, pack/suggest, CLI expansion |
16
16
  | **0.6** | Product consolidation — presets, ranking, watch, slim JSON, doctor --fix |
17
+ | **0.7** | Allocation flamegraphs (folded stacks + Speedscope) |
17
18
 
18
19
  ## Next
19
20
 
@@ -0,0 +1,8 @@
1
+ # HeapScope 0.7.0 — Allocation flamegraphs
2
+
3
+ Turn snapshot allocation sites into folded stacks or Speedscope JSON so hot retainers are visible as a flame chart instead of a table.
4
+
5
+ ```bash
6
+ heapscope snapshot --track-allocations -o snap.json
7
+ heapscope flamegraph snap.json --format speedscope -o alloc.json
8
+ ```
data/docs/cli.md CHANGED
@@ -91,7 +91,8 @@ heapscope pack report.json -o ./pack
91
91
  heapscope explain HS001
92
92
  ```
93
93
 
94
- Sponsor: https://thanks.dev/u/gh/theworker02 · Site: https://theworker02.github.io/heapscope/
94
+ Sponsor: https://thanks.dev/u/gh/theworker02 · Site: https://theworker02.github.io/heapscope/ · Gem: https://rubygems.org/gems/heapscope
95
+
95
96
 
96
97
  <p align="center">
97
98
  <img src="../assets/logo.svg" alt="" width="48"/>
data/docs/index.md CHANGED
@@ -4,13 +4,14 @@
4
4
 
5
5
  # Documentation hub
6
6
 
7
- **Website:** [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) · **Sponsor:** [thanks.dev/u/gh/theworker02](https://thanks.dev/u/gh/theworker02)
7
+ **Website:** [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) · **RubyGems:** [rubygems.org/gems/heapscope](https://rubygems.org/gems/heapscope) · **Sponsor:** [thanks.dev/u/gh/theworker02](https://thanks.dev/u/gh/theworker02)
8
8
 
9
9
  ## Start here
10
10
 
11
11
  | Doc | Purpose |
12
12
  |-----|---------|
13
13
  | [README](../README.md) | Product overview & quick start |
14
+ | [RubyGems](https://rubygems.org/gems/heapscope) | Install the published gem (`gem install heapscope`) |
14
15
  | [First retention experiment](guides/first-retention-experiment.md) | Hands-on workflow |
15
16
  | [CI budgets](guides/ci-budgets.md) | Regression gates + presets |
16
17
  | [Production-safe sampling](guides/production-safe.md) | Low-overhead prod use |
@@ -31,7 +32,7 @@
31
32
 
32
33
  ## Version
33
34
 
34
- Docs track **HeapScope 0.6.x**.
35
+ Docs track **HeapScope 0.6.x**. Install from [RubyGems](https://rubygems.org/gems/heapscope).
35
36
 
36
37
  <p align="center">
37
38
  <img src="../assets/logo.svg" alt="" width="48"/>
data/heapscope.gemspec CHANGED
@@ -32,8 +32,9 @@ Gem::Specification.new do |spec|
32
32
  HeapScope #{HeapScope::VERSION} installed.
33
33
  heapscope doctor
34
34
  heapscope about
35
- https://theworker02.github.io/heapscope/
36
- https://thanks.dev/u/gh/theworker02
35
+ gem: https://rubygems.org/gems/heapscope
36
+ docs: https://theworker02.github.io/heapscope/
37
+ sponsor: https://thanks.dev/u/gh/theworker02
37
38
 
38
39
  Local-only diagnostics. No telemetry. No object-value dumps by default.
39
40
  MSG
@@ -7,6 +7,7 @@ module HeapScope
7
7
  TAGLINE = "Ruby retention & heap growth diagnostics"
8
8
  GITHUB_USER = "theworker02"
9
9
  GITHUB_REPO = "https://github.com/theworker02/heapscope"
10
+ RUBYGEMS_URL = "https://rubygems.org/gems/heapscope"
10
11
  PAGES_URL = "https://theworker02.github.io/heapscope/"
11
12
  SPONSORS_URL = "https://github.com/sponsors/theworker02"
12
13
  THANKS_DEV_URL = "https://thanks.dev/u/gh/theworker02"
@@ -45,10 +46,12 @@ module HeapScope
45
46
  lines = []
46
47
  lines << compact_banner
47
48
  lines << ""
49
+ lines << "RubyGems: #{RUBYGEMS_URL}"
48
50
  lines << "Repository: #{GITHUB_REPO}"
49
51
  lines << "Website: #{PAGES_URL}"
50
52
  funding_lines.each { |l| lines << l }
51
53
  lines << ""
54
+ lines << "Install: gem install heapscope"
52
55
  lines << "Privacy: local-only diagnostics — no telemetry, no object-value dumps by default."
53
56
  lines.join("\n")
54
57
  end
@@ -85,6 +88,7 @@ module HeapScope
85
88
  version: VERSION,
86
89
  github_user: GITHUB_USER,
87
90
  github_repo: GITHUB_REPO,
91
+ rubygems_url: RUBYGEMS_URL,
88
92
  pages_url: PAGES_URL,
89
93
  sponsors_url: SPONSORS_URL,
90
94
  thanks_dev_url: THANKS_DEV_URL,
@@ -222,6 +222,38 @@ module HeapScope
222
222
  say "OK schema_version=#{data[:schema_version]} heapscope=#{data[:heapscope_version]}"
223
223
  EXIT_OK
224
224
  end
225
+
226
+ def cmd_flamegraph(argv)
227
+ return help_exit("flamegraph") if command_wants_help?(argv)
228
+
229
+ options = { format: "folded", unit: "count", output: nil }
230
+ OptionParser.new do |opts|
231
+ opts.banner = "Usage: heapscope flamegraph SNAPSHOT.json [options]"
232
+ opts.on("--format FMT", %w[folded speedscope]) { |f| options[:format] = f }
233
+ opts.on("--unit UNIT", %w[count bytes]) { |u| options[:unit] = u }
234
+ opts.on("-o", "--output PATH") { |p| options[:output] = p }
235
+ opts.on("--json") { options[:json] = true }
236
+ opts.on("-h", "--help") { options[:help] = true }
237
+ end.parse!(argv)
238
+ return help_exit("flamegraph") if options[:help]
239
+
240
+ path = argv.shift or raise ArgumentError, "flamegraph requires SNAPSHOT.json"
241
+ snapshot = Snapshot.load(path)
242
+ graph = HeapScope.flamegraph(snapshot, unit: options[:unit].to_sym)
243
+ content =
244
+ if json_mode? || options[:json]
245
+ JSON.pretty_generate(graph.to_h)
246
+ else
247
+ graph.render(format: options[:format].to_sym)
248
+ end
249
+ if options[:output]
250
+ File.write(options[:output], content)
251
+ say "Wrote #{options[:output]}"
252
+ else
253
+ puts content
254
+ end
255
+ EXIT_OK
256
+ end
225
257
  end
226
258
  end
227
259
  end
@@ -24,6 +24,7 @@ module HeapScope
24
24
  "probe" => "Measure a Ruby file/snippet and print a scorecard",
25
25
  "measure" => "CLI wrapper around HeapScope.measure",
26
26
  "retention" => "CLI wrapper around HeapScope.retention_test",
27
+ "flamegraph" => "Export allocation sites as folded stacks or Speedscope JSON",
27
28
  "findings" => "List/filter findings from a report",
28
29
  "scorecard" => "Print scorecard only from a report",
29
30
  "table" => "Print growth table from a report",
@@ -75,6 +76,7 @@ module HeapScope
75
76
  lines << " 3 runtime capability unavailable"
76
77
  lines << ""
77
78
  lines << "Privacy: no network, no telemetry, no object value serialization by default."
79
+ lines << "Gem: #{Branding::RUBYGEMS_URL}"
78
80
  lines << "Sponsor: #{Branding::THANKS_DEV_URL}"
79
81
  lines << "Docs: #{Branding::PAGES_URL}"
80
82
  lines << ""
@@ -164,6 +166,13 @@ module HeapScope
164
166
  -o, --output PATH --html PATH --json
165
167
  --fail-on-high / --fail-on-medium
166
168
  U
169
+ "flamegraph" => <<~U,
170
+ Usage: heapscope flamegraph SNAPSHOT.json [options]
171
+ --format FMT folded (default) or speedscope
172
+ --unit UNIT count (default) or bytes
173
+ -o, --output PATH Write the flamegraph instead of stdout
174
+ --json Print frame table JSON
175
+ U
167
176
  "findings" => <<~U,
168
177
  Usage: heapscope findings REPORT.json [options]
169
178
  --severity LEVEL Filter: low|medium|high
data/lib/heapscope/cli.rb CHANGED
@@ -47,6 +47,7 @@ module HeapScope
47
47
  "probe" => :cmd_probe,
48
48
  "measure" => :cmd_measure,
49
49
  "retention" => :cmd_retention,
50
+ "flamegraph" => :cmd_flamegraph,
50
51
  "findings" => :cmd_findings,
51
52
  "scorecard" => :cmd_scorecard,
52
53
  "table" => :cmd_table,
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HeapScope
4
+ # Turns snapshot allocation sites into folded stacks or Speedscope JSON.
5
+ #
6
+ # Folded output is compatible with inferno / flamegraph.pl. Speedscope JSON
7
+ # can be dropped onto https://www.speedscope.app (local file, no upload required
8
+ # by HeapScope itself).
9
+ class Flamegraph
10
+ FORMATS = %i[folded speedscope].freeze
11
+
12
+ Frame = Struct.new(:site, :method_id, :klass, :allocations, :bytes, keyword_init: true)
13
+
14
+ attr_reader :frames, :snapshot_id, :unit
15
+
16
+ def initialize(frames:, snapshot_id: nil, unit: :count)
17
+ @frames = frames
18
+ @snapshot_id = snapshot_id
19
+ @unit = unit
20
+ end
21
+
22
+ def self.from_snapshot(snapshot, unit: :count)
23
+ raise ArgumentError, "unit must be :count or :bytes" unless %i[count bytes].include?(unit.to_sym)
24
+
25
+ snapshot = Snapshot.load(snapshot) if snapshot.is_a?(String)
26
+ frames = []
27
+ snapshot.allocation_sites.each do |site_key, stats|
28
+ stats = symbolize(stats)
29
+ site = stats[:site] || site_key.to_s
30
+ method_id = stats[:method_id]
31
+ classes = stats[:classes] || {}
32
+ if classes.empty?
33
+ frames << Frame.new(
34
+ site: site,
35
+ method_id: method_id,
36
+ klass: nil,
37
+ allocations: stats[:count].to_i,
38
+ bytes: stats[:shallow_bytes].to_i
39
+ )
40
+ else
41
+ classes.each do |klass, class_count|
42
+ share = stats[:count].to_i.positive? ? (class_count.to_f / stats[:count]) : 0.0
43
+ frames << Frame.new(
44
+ site: site,
45
+ method_id: method_id,
46
+ klass: klass.to_s,
47
+ allocations: class_count.to_i,
48
+ bytes: (stats[:shallow_bytes].to_i * share).round
49
+ )
50
+ end
51
+ end
52
+ end
53
+ new(frames: frames.sort_by { |frame| [-value_for(frame, unit), frame.site.to_s] },
54
+ snapshot_id: snapshot.id, unit: unit.to_sym)
55
+ end
56
+
57
+ def empty?
58
+ frames.empty? || total.zero?
59
+ end
60
+
61
+ def total
62
+ frames.sum { |frame| value_for(frame, unit) }
63
+ end
64
+
65
+ def to_folded
66
+ frames.filter_map do |frame|
67
+ weight = value_for(frame, unit)
68
+ next if weight <= 0
69
+
70
+ "#{stack_for(frame)} #{weight}"
71
+ end.join("\n")
72
+ end
73
+
74
+ def to_speedscope
75
+ names = []
76
+ name_index = {}
77
+ samples = []
78
+ weights = []
79
+
80
+ frames.each do |frame|
81
+ weight = value_for(frame, unit)
82
+ next if weight <= 0
83
+
84
+ stack = []
85
+ stack_for(frame).split(";").each do |name|
86
+ unless name_index.key?(name)
87
+ name_index[name] = names.length
88
+ names << name
89
+ end
90
+ stack << name_index[name]
91
+ end
92
+ samples << stack
93
+ weights << weight
94
+ end
95
+
96
+ {
97
+ "$schema" => "https://www.speedscope.app/file-format-schema.json",
98
+ shared: { frames: names.map { |name| { name: name } } },
99
+ profiles: [
100
+ {
101
+ type: "sampled",
102
+ name: "HeapScope allocations (#{unit})",
103
+ unit: unit == :bytes ? "bytes" : "none",
104
+ startValue: 0,
105
+ endValue: weights.sum,
106
+ samples: samples,
107
+ weights: weights
108
+ }
109
+ ],
110
+ exporter: "heapscope #{VERSION}",
111
+ name: snapshot_id && "heapscope-#{snapshot_id}"
112
+ }.compact
113
+ end
114
+
115
+ def render(format: :folded)
116
+ format = format.to_sym
117
+ raise ArgumentError, "unknown flamegraph format #{format}" unless FORMATS.include?(format)
118
+
119
+ format == :speedscope ? JSON.pretty_generate(to_speedscope) : to_folded
120
+ end
121
+
122
+ def to_h
123
+ {
124
+ snapshot_id: snapshot_id,
125
+ unit: unit,
126
+ total: total,
127
+ frames: frames.map do |frame|
128
+ {
129
+ site: frame.site,
130
+ method_id: frame.method_id,
131
+ class: frame.klass,
132
+ count: frame.allocations,
133
+ bytes: frame.bytes,
134
+ stack: stack_for(frame)
135
+ }
136
+ end
137
+ }
138
+ end
139
+
140
+ class << self
141
+ def value_for(frame, unit)
142
+ unit.to_sym == :bytes ? frame.bytes.to_i : frame.allocations.to_i
143
+ end
144
+
145
+ def symbolize(value)
146
+ return value unless value.is_a?(Hash)
147
+
148
+ value.each_with_object({}) do |(key, nested), acc|
149
+ acc[key.to_sym] = nested.is_a?(Hash) ? symbolize(nested) : nested
150
+ end
151
+ end
152
+ end
153
+
154
+ private_class_method :value_for, :symbolize
155
+
156
+ private
157
+
158
+ def value_for(frame, unit)
159
+ self.class.send(:value_for, frame, unit)
160
+ end
161
+
162
+ def stack_for(frame)
163
+ parts = [frame.site.to_s]
164
+ parts << frame.method_id if frame.method_id && !frame.method_id.to_s.empty?
165
+ parts << frame.klass if frame.klass && !frame.klass.to_s.empty?
166
+ parts.join(";")
167
+ end
168
+ end
169
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HeapScope
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  SCHEMA_VERSION = 1
6
6
  end
data/lib/heapscope.rb CHANGED
@@ -35,6 +35,7 @@ require_relative "heapscope/branding"
35
35
  require_relative "heapscope/suggest"
36
36
  require_relative "heapscope/catalog"
37
37
  require_relative "heapscope/pack"
38
+ require_relative "heapscope/flamegraph"
38
39
 
39
40
  # HeapScope — Ruby object retention, heap growth, and memory leak diagnostics.
40
41
  #
@@ -282,6 +283,10 @@ module HeapScope
282
283
  ConfigLoader.write_starter!(path, force: force)
283
284
  end
284
285
 
286
+ def flamegraph(snapshot, unit: :count)
287
+ Flamegraph.from_snapshot(snapshot, unit: unit)
288
+ end
289
+
285
290
  private
286
291
 
287
292
  def recovery_stats(before, immediate, after_gc, after_idle)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heapscope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - theworker02
@@ -89,6 +89,7 @@ files:
89
89
  - docs/changelogs/0.4.0.md
90
90
  - docs/changelogs/0.5.0.md
91
91
  - docs/changelogs/0.6.0.md
92
+ - docs/changelogs/0.7.0.md
92
93
  - docs/cli.md
93
94
  - docs/diagnostics/HS001_persistent_class_growth.md
94
95
  - docs/diagnostics/HS002_high_retention_ratio.md
@@ -139,6 +140,7 @@ files:
139
140
  - lib/heapscope/errors.rb
140
141
  - lib/heapscope/extrapolation.rb
141
142
  - lib/heapscope/findings.rb
143
+ - lib/heapscope/flamegraph.rb
142
144
  - lib/heapscope/globals.rb
143
145
  - lib/heapscope/graph.rb
144
146
  - lib/heapscope/growth.rb
@@ -184,11 +186,12 @@ metadata:
184
186
  rubygems_mfa_required: 'true'
185
187
  post_install_message: |2
186
188
 
187
- HeapScope 0.6.0 installed.
189
+ HeapScope 0.7.0 installed.
188
190
  heapscope doctor
189
191
  heapscope about
190
- https://theworker02.github.io/heapscope/
191
- https://thanks.dev/u/gh/theworker02
192
+ gem: https://rubygems.org/gems/heapscope
193
+ docs: https://theworker02.github.io/heapscope/
194
+ sponsor: https://thanks.dev/u/gh/theworker02
192
195
 
193
196
  Local-only diagnostics. No telemetry. No object-value dumps by default.
194
197
  rdoc_options: []