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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +53 -15
- data/docs/README.md +2 -1
- data/docs/ROADMAP.md +1 -0
- data/docs/changelogs/0.7.0.md +8 -0
- data/docs/cli.md +2 -1
- data/docs/index.md +3 -2
- data/heapscope.gemspec +3 -2
- data/lib/heapscope/branding.rb +4 -0
- data/lib/heapscope/cli/commands/reporting.rb +32 -0
- data/lib/heapscope/cli/help.rb +9 -0
- data/lib/heapscope/cli.rb +1 -0
- data/lib/heapscope/flamegraph.rb +169 -0
- data/lib/heapscope/version.rb +1 -1
- data/lib/heapscope.rb +5 -0
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf49d15007ca523b3c9fbfb463d55bb879ce6c73ad599412f8040238b9d5786e
|
|
4
|
+
data.tar.gz: bc97ee480b143c0079d6ee4762965ba94f517cae7370f3a0971616487be35646
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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&
|
|
12
|
-
<a href="https://
|
|
13
|
-
<a href="
|
|
14
|
-
<a href="
|
|
15
|
-
<a href="#
|
|
16
|
-
<a href="#
|
|
17
|
-
<a href="
|
|
18
|
-
<a href="https://
|
|
19
|
-
<a href="
|
|
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://
|
|
29
|
-
<a href="
|
|
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
|
-
|
|
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.
|
|
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://
|
|
36
|
-
https://
|
|
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
|
data/lib/heapscope/branding.rb
CHANGED
|
@@ -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
|
data/lib/heapscope/cli/help.rb
CHANGED
|
@@ -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
|
@@ -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
|
data/lib/heapscope/version.rb
CHANGED
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.
|
|
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.
|
|
189
|
+
HeapScope 0.7.0 installed.
|
|
188
190
|
heapscope doctor
|
|
189
191
|
heapscope about
|
|
190
|
-
https://
|
|
191
|
-
https://
|
|
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: []
|