heapscope 0.6.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 +7 -0
- data/.rubocop.yml +119 -0
- data/CHANGELOG.md +86 -0
- data/CODE_OF_CONDUCT.md +38 -0
- data/CONTRIBUTING.md +72 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +342 -0
- data/Rakefile +49 -0
- data/SECURITY.md +39 -0
- data/assets/heapscope-logo.png +0 -0
- data/assets/logo.svg +24 -0
- data/assets/wordmark.svg +18 -0
- data/docs/README.md +20 -0
- data/docs/ROADMAP.md +29 -0
- data/docs/adr/0001-evidence-over-certainty.md +24 -0
- data/docs/adr/0002-local-only-privacy.md +19 -0
- data/docs/adr/0003-runtime-adapters.md +25 -0
- data/docs/adr/README.md +7 -0
- data/docs/allocators.md +13 -0
- data/docs/api/overview.md +70 -0
- data/docs/changelogs/0.1.0.md +22 -0
- data/docs/changelogs/0.2.0.md +49 -0
- data/docs/changelogs/0.3.0.md +38 -0
- data/docs/changelogs/0.4.0.md +32 -0
- data/docs/changelogs/0.5.0.md +18 -0
- data/docs/changelogs/0.6.0.md +23 -0
- data/docs/cli.md +98 -0
- data/docs/diagnostics/HS001_persistent_class_growth.md +39 -0
- data/docs/diagnostics/HS002_high_retention_ratio.md +24 -0
- data/docs/diagnostics/HS003_thread_local_retention.md +24 -0
- data/docs/diagnostics/HS004_unbounded_collection.md +19 -0
- data/docs/diagnostics/HS005_callback_accumulation.md +14 -0
- data/docs/diagnostics/HS006_closure_retention.md +15 -0
- data/docs/diagnostics/HS007_poor_gc_recovery.md +18 -0
- data/docs/diagnostics/HS008_baseline_regression.md +15 -0
- data/docs/diagnostics/HS009_high_allocation_pressure.md +13 -0
- data/docs/diagnostics/HS010_native_memory_mismatch.md +19 -0
- data/docs/guides/ci-budgets.md +43 -0
- data/docs/guides/first-retention-experiment.md +25 -0
- data/docs/guides/production-safe.md +17 -0
- data/docs/index.md +38 -0
- data/examples/cache_vs_leak.rb +30 -0
- data/examples/closure_capture.rb +20 -0
- data/examples/healthy_churn.rb +16 -0
- data/examples/heapscope.yml +12 -0
- data/examples/import_leak.rb +19 -0
- data/examples/probe_and_session.rb +22 -0
- data/examples/thread_local_leak.rb +31 -0
- data/exe/heapscope +7 -0
- data/heapscope.gemspec +73 -0
- data/lib/heapscope/aging.rb +114 -0
- data/lib/heapscope/analyzer.rb +333 -0
- data/lib/heapscope/baseline.rb +77 -0
- data/lib/heapscope/branding.rb +98 -0
- data/lib/heapscope/budget.rb +108 -0
- data/lib/heapscope/capabilities.rb +42 -0
- data/lib/heapscope/catalog.rb +71 -0
- data/lib/heapscope/cli/color.rb +67 -0
- data/lib/heapscope/cli/commands/capture.rb +289 -0
- data/lib/heapscope/cli/commands/diffing.rb +157 -0
- data/lib/heapscope/cli/commands/meta.rb +244 -0
- data/lib/heapscope/cli/commands/reporting.rb +228 -0
- data/lib/heapscope/cli/completion.rb +72 -0
- data/lib/heapscope/cli/help.rb +226 -0
- data/lib/heapscope/cli/support.rb +126 -0
- data/lib/heapscope/cli.rb +165 -0
- data/lib/heapscope/closures.rb +102 -0
- data/lib/heapscope/collector.rb +167 -0
- data/lib/heapscope/config.rb +196 -0
- data/lib/heapscope/detectors.rb +131 -0
- data/lib/heapscope/diff.rb +130 -0
- data/lib/heapscope/dominators.rb +87 -0
- data/lib/heapscope/errors.rb +13 -0
- data/lib/heapscope/extrapolation.rb +51 -0
- data/lib/heapscope/findings.rb +149 -0
- data/lib/heapscope/globals.rb +108 -0
- data/lib/heapscope/graph.rb +218 -0
- data/lib/heapscope/growth.rb +108 -0
- data/lib/heapscope/middleware.rb +35 -0
- data/lib/heapscope/minitest.rb +47 -0
- data/lib/heapscope/monitor.rb +160 -0
- data/lib/heapscope/noise.rb +48 -0
- data/lib/heapscope/notifications.rb +79 -0
- data/lib/heapscope/pack.rb +55 -0
- data/lib/heapscope/paths.rb +64 -0
- data/lib/heapscope/rails.rb +71 -0
- data/lib/heapscope/report/html.rb +283 -0
- data/lib/heapscope/report/markdown.rb +78 -0
- data/lib/heapscope/report/text.rb +204 -0
- data/lib/heapscope/report.rb +308 -0
- data/lib/heapscope/retention.rb +91 -0
- data/lib/heapscope/rspec.rb +118 -0
- data/lib/heapscope/runtime/base.rb +130 -0
- data/lib/heapscope/runtime/jruby.rb +28 -0
- data/lib/heapscope/runtime/mri.rb +86 -0
- data/lib/heapscope/runtime/truffleruby.rb +40 -0
- data/lib/heapscope/runtime/windows_rss.rb +75 -0
- data/lib/heapscope/runtime.rb +33 -0
- data/lib/heapscope/schema.rb +28 -0
- data/lib/heapscope/scorecard.rb +85 -0
- data/lib/heapscope/session.rb +90 -0
- data/lib/heapscope/sidekiq_middleware.rb +36 -0
- data/lib/heapscope/snapshot.rb +165 -0
- data/lib/heapscope/suggest.rb +99 -0
- data/lib/heapscope/tables.rb +58 -0
- data/lib/heapscope/trend_store.rb +41 -0
- data/lib/heapscope/version.rb +6 -0
- data/lib/heapscope.rb +307 -0
- metadata +211 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# HS001 — persistent_class_growth
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
A class's live object count increased across the measured window or retention cycles.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Class counts before/after (or multi-sample series)
|
|
10
|
+
- Growth pattern (`monotonic_growth`, `linear_growth`, `exponential_like`)
|
|
11
|
+
- Optional forced-GC between samples
|
|
12
|
+
|
|
13
|
+
## Likely causes
|
|
14
|
+
|
|
15
|
+
- Unbounded registry / array / hash
|
|
16
|
+
- Request or job objects retained globally or in thread-locals
|
|
17
|
+
- Memoization without eviction
|
|
18
|
+
- Callbacks retaining contexts
|
|
19
|
+
|
|
20
|
+
## False positives
|
|
21
|
+
|
|
22
|
+
- Cache warmup / lazy loading
|
|
23
|
+
- First-time autoload in development
|
|
24
|
+
- Batch size increases that are intentional
|
|
25
|
+
- Delayed GC (mitigate with `force_gc: true` in experiments)
|
|
26
|
+
|
|
27
|
+
## How to investigate
|
|
28
|
+
|
|
29
|
+
1. Run `HeapScope.retention_test` with forced GC
|
|
30
|
+
2. Enable allocation tracing for the class's sites
|
|
31
|
+
3. Inspect thread-locals, globals, and constants
|
|
32
|
+
4. Use deep mode retention paths for sampled objects
|
|
33
|
+
|
|
34
|
+
## Potential fixes
|
|
35
|
+
|
|
36
|
+
- Clear request/job context in `ensure`
|
|
37
|
+
- Bound caches / use TTL or LRU
|
|
38
|
+
- Unsubscribe listeners
|
|
39
|
+
- Avoid capturing large objects in long-lived closures
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# HS002 — high_retention_ratio
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
A large fraction of objects allocated during the workload remained reachable afterward.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- `total_allocated_objects` / `total_freed_objects` deltas
|
|
10
|
+
- Surviving estimate and retention ratio
|
|
11
|
+
|
|
12
|
+
## Likely causes
|
|
13
|
+
|
|
14
|
+
- Sticky object graphs retained by roots
|
|
15
|
+
- Accidental global retention of request data
|
|
16
|
+
|
|
17
|
+
## False positives
|
|
18
|
+
|
|
19
|
+
- Workload that intentionally builds long-lived structures
|
|
20
|
+
- Insufficient GC between measurements
|
|
21
|
+
|
|
22
|
+
## Investigation / fixes
|
|
23
|
+
|
|
24
|
+
Use class deltas + retention sessions; clear accidental roots; bound collections.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# HS003 — thread_local_retention
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Retention appears consistent with thread-local (or fiber-local) storage holding growing state — common under Puma/Sidekiq thread reuse.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Thread key inventory
|
|
10
|
+
- Growing populations reachable from thread contexts
|
|
11
|
+
- Persistence across request/job cycles
|
|
12
|
+
|
|
13
|
+
## Likely causes
|
|
14
|
+
|
|
15
|
+
- `Thread.current[:context]` not cleared
|
|
16
|
+
- Middleware storing request state on the worker thread
|
|
17
|
+
|
|
18
|
+
## False positives
|
|
19
|
+
|
|
20
|
+
- Intentional per-thread caches with bounds
|
|
21
|
+
|
|
22
|
+
## Fixes
|
|
23
|
+
|
|
24
|
+
Clear thread/fiber locals in `ensure` after each request/job; avoid storing full request graphs.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# HS004 — unbounded_collection
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
An Array/Hash/Set-like structure grows across samples without observed shrinkage after GC.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Size series across samples
|
|
10
|
+
- Owner class when identifiable
|
|
11
|
+
|
|
12
|
+
## False positives
|
|
13
|
+
|
|
14
|
+
- Bounded caches still filling toward max
|
|
15
|
+
- Warmup periods
|
|
16
|
+
|
|
17
|
+
## Fixes
|
|
18
|
+
|
|
19
|
+
Cap size, eviction, periodic compaction, or stop appending.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# HS005 — callback_accumulation
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Listeners, subscribers, or observer lists grow over time (often repeated registration without unsubscribe).
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Listener count series (via adapters)
|
|
10
|
+
- Proc/Array growth near event buses
|
|
11
|
+
|
|
12
|
+
## Fixes
|
|
13
|
+
|
|
14
|
+
Unsubscribe on teardown; register once at boot; use weak references where appropriate.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# HS006 — closure_retention
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Long-lived `Proc` objects appear to retain large object graphs via captured bindings.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Proc population growth
|
|
10
|
+
- Approximate retained size from bounded traversal
|
|
11
|
+
- Allocation site of the Proc when tracing is enabled
|
|
12
|
+
|
|
13
|
+
## Caveats
|
|
14
|
+
|
|
15
|
+
Runtime APIs may not expose closure captures cleanly — treat as hypothesis.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# HS007 — poor_gc_recovery
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
After opt-in forced GC, live populations or RSS remain elevated versus baseline.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Before / after GC / after idle phase snapshots
|
|
10
|
+
|
|
11
|
+
## False positives
|
|
12
|
+
|
|
13
|
+
- Native memory / fragmentation (see HS010)
|
|
14
|
+
- Intentional caches filled during workload
|
|
15
|
+
|
|
16
|
+
## Note
|
|
17
|
+
|
|
18
|
+
Forced GC distorts production behavior — experiments only.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# HS008 — baseline_regression
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Current retention exceeds a stored baseline beyond a configured threshold.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- Baseline retained objects/bytes vs current report
|
|
10
|
+
|
|
11
|
+
## Use in CI
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
heapscope compare baseline.json current.json --threshold 0.5
|
|
15
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# HS009 — high_allocation_pressure
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Many objects were allocated and mostly freed — **churn**, not necessarily a leak.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- High allocated Δ with high freed Δ / low retention ratio
|
|
10
|
+
|
|
11
|
+
## Action
|
|
12
|
+
|
|
13
|
+
Optimize hot allocation sites if GC/CPU time matters; do not treat as retention failure.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# HS010 — native_memory_mismatch
|
|
2
|
+
|
|
3
|
+
## What it means
|
|
4
|
+
|
|
5
|
+
Process RSS grew substantially while the Ruby heap (live slots / object estimates) did not.
|
|
6
|
+
|
|
7
|
+
## Evidence used
|
|
8
|
+
|
|
9
|
+
- RSS Δ vs heap live slots Δ and shallow byte estimates
|
|
10
|
+
|
|
11
|
+
## Likely causes
|
|
12
|
+
|
|
13
|
+
- Native extension allocations
|
|
14
|
+
- Allocator fragmentation (glibc / jemalloc / macOS)
|
|
15
|
+
- mmap / external buffers
|
|
16
|
+
|
|
17
|
+
## Important
|
|
18
|
+
|
|
19
|
+
**RSS growth ≠ Ruby object leak.** Do not blame object retention from this signal alone.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Guide: CI memory budgets
|
|
2
|
+
|
|
3
|
+
## Presets (0.6+)
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
HeapScope.budget_preset(:rails_request) # typical request cycle
|
|
7
|
+
HeapScope.budget_preset(:sidekiq_job) # background job
|
|
8
|
+
HeapScope.budget_preset(:ci_strict) # tight PR gate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
report = HeapScope.check_budget(budget: HeapScope.budget_preset(:ci_strict)) { scenario }
|
|
13
|
+
raise report.budget_result[:violations].join("\n") unless report.passed_budget?
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Capture a baseline on `main`
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle exec ruby script/memory_scenario.rb # writes report.json
|
|
20
|
+
heapscope baseline create report.json -o baseline.json
|
|
21
|
+
git add baseline.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## On PRs
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
heapscope compare baseline.json report.json --threshold 0.5
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Exit code `1` means regression.
|
|
31
|
+
|
|
32
|
+
## Hand-tuned budgets
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
budget = HeapScope::Budget.new(
|
|
36
|
+
max_retained_objects: 2_000,
|
|
37
|
+
max_rss_growth: 20 * 1024 * 1024,
|
|
38
|
+
severity_threshold: :high
|
|
39
|
+
)
|
|
40
|
+
report = HeapScope.check_budget(budget: budget) { scenario }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Keep thresholds noisy-tolerant; prefer `experiment(runs: 5)` medians for flaky heaps.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Guide: First retention experiment
|
|
2
|
+
|
|
3
|
+
Goal: prove whether a workload **retains** objects across GC, not merely allocates.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
1. Warm up the app (routes, autoload, DB connections).
|
|
8
|
+
2. Capture a baseline mental model: what *should* survive?
|
|
9
|
+
3. Run:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
report = HeapScope.retention_test(cycles: 10, force_gc: true) do
|
|
13
|
+
50.times { MyEndpoint.call }
|
|
14
|
+
end
|
|
15
|
+
puts report
|
|
16
|
+
report.save_html("retention.html")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
4. Read findings in order: facts → derived → hypothesis → suspected cause.
|
|
20
|
+
5. If HS001/HS002 fire, enable allocation tracing and re-run once.
|
|
21
|
+
6. Inspect thread-locals if HS003 appears.
|
|
22
|
+
|
|
23
|
+
## Interpreting HEALTHY
|
|
24
|
+
|
|
25
|
+
Temporary growth that reclaims after forced GC is success. Do not chase HS009 (churn) as a leak.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Guide: Production-safe sampling
|
|
2
|
+
|
|
3
|
+
```ruby
|
|
4
|
+
HeapScope.configure do |c|
|
|
5
|
+
c.mode = :production_safe
|
|
6
|
+
c.object_sample_rate = 0.05
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
use HeapScope::Middleware, sample_rate: 0.01, mode: :lightweight
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Rules of thumb:
|
|
13
|
+
|
|
14
|
+
- Never force GC in production paths
|
|
15
|
+
- Prefer `lightweight` snapshots
|
|
16
|
+
- Ship reports to local disk / log drains you already trust — HeapScope itself uploads nothing
|
|
17
|
+
- Treat HTML/JSON outputs as sensitive
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/wordmark.svg" alt="HeapScope" width="420"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Documentation hub
|
|
6
|
+
|
|
7
|
+
**Website:** [theworker02.github.io/heapscope](https://theworker02.github.io/heapscope/) · **Sponsor:** [thanks.dev/u/gh/theworker02](https://thanks.dev/u/gh/theworker02)
|
|
8
|
+
|
|
9
|
+
## Start here
|
|
10
|
+
|
|
11
|
+
| Doc | Purpose |
|
|
12
|
+
|-----|---------|
|
|
13
|
+
| [README](../README.md) | Product overview & quick start |
|
|
14
|
+
| [First retention experiment](guides/first-retention-experiment.md) | Hands-on workflow |
|
|
15
|
+
| [CI budgets](guides/ci-budgets.md) | Regression gates + presets |
|
|
16
|
+
| [Production-safe sampling](guides/production-safe.md) | Low-overhead prod use |
|
|
17
|
+
| [CLI reference](cli.md) | Command catalog |
|
|
18
|
+
| [API overview](api/overview.md) | Public Ruby API |
|
|
19
|
+
| [Diagnostic codes](diagnostics/) | HS001–HS010 encyclopedia |
|
|
20
|
+
| [Allocators](allocators.md) | RSS vs Ruby heap caveats |
|
|
21
|
+
| [Roadmap](ROADMAP.md) | What's next |
|
|
22
|
+
| [ADRs](adr/) | Architecture decisions |
|
|
23
|
+
| [Changelogs](changelogs/) | Release narratives |
|
|
24
|
+
|
|
25
|
+
## Design pledges
|
|
26
|
+
|
|
27
|
+
1. **Retention over allocation volume**
|
|
28
|
+
2. **Evidence over certainty**
|
|
29
|
+
3. **Local-only privacy by default**
|
|
30
|
+
4. **Safe degradation across Ruby engines**
|
|
31
|
+
|
|
32
|
+
## Version
|
|
33
|
+
|
|
34
|
+
Docs track **HeapScope 0.6.x**.
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="../assets/logo.svg" alt="" width="48"/>
|
|
38
|
+
</p>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
# Bounded cache vs unbounded registry — HeapScope should treat them differently.
|
|
7
|
+
CACHE = {}
|
|
8
|
+
CACHE_MAX = 100
|
|
9
|
+
REGISTRY = []
|
|
10
|
+
|
|
11
|
+
def fill_cache
|
|
12
|
+
300.times { |i| CACHE[i % CACHE_MAX] = "v#{i}" }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fill_registry
|
|
16
|
+
150.times { REGISTRY << ("row-" + rand(10_000).to_s) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts "=== CACHE (expect plateau / low suspicion) ==="
|
|
20
|
+
cache_report = HeapScope.retention_test(cycles: 5, force_gc: true, mode: :lightweight) { fill_cache }
|
|
21
|
+
puts cache_report.summary.inspect
|
|
22
|
+
puts cache_report.findings.map { |f| "#{f.code}/#{f.severity}" }.inspect
|
|
23
|
+
|
|
24
|
+
puts
|
|
25
|
+
puts "=== REGISTRY (expect persistent growth) ==="
|
|
26
|
+
reg_report = HeapScope.retention_test(cycles: 5, force_gc: true, mode: :lightweight) { fill_registry }
|
|
27
|
+
puts reg_report.summary.inspect
|
|
28
|
+
puts reg_report.findings.map { |f| "#{f.code}/#{f.severity} #{f.subject}" }.inspect
|
|
29
|
+
|
|
30
|
+
reg_report.save_html("examples/cache_vs_leak.html")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
# Closure capture demo — enable closure detection explicitly.
|
|
7
|
+
HOLDERS = []
|
|
8
|
+
big = Array.new(2_000) { "payload" }
|
|
9
|
+
|
|
10
|
+
HeapScope.configure do |c|
|
|
11
|
+
c.detect_closures = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
report = HeapScope.retention_test(cycles: 4, force_gc: true, mode: :lightweight) do
|
|
15
|
+
30.times { HOLDERS << -> { big.size } }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts report
|
|
19
|
+
puts "Closures sampled: #{Array(report.closures).size}"
|
|
20
|
+
report.save_markdown("examples/closure_report.md")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
# Healthy temporary allocation — should reclaim after GC.
|
|
7
|
+
report = HeapScope.measure(force_gc: true, mode: :lightweight) do
|
|
8
|
+
data = []
|
|
9
|
+
10_000.times { |i| data << { i: i, body: "x" * 20 } }
|
|
10
|
+
data.clear
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Framework/Hash noise from the profiler itself is ignored in findings by default.
|
|
15
|
+
puts report
|
|
16
|
+
puts "Healthy?=#{report.summary[:healthy].inspect} findings=#{report.findings.map(&:code)}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
# Demo: intentional leak via a growing array.
|
|
7
|
+
LEAK = []
|
|
8
|
+
|
|
9
|
+
puts "HeapScope #{HeapScope::VERSION}"
|
|
10
|
+
puts HeapScope.capabilities
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
report = HeapScope.retention_test(cycles: 5, force_gc: true, mode: :lightweight) do
|
|
14
|
+
200.times { LEAK << (+"item") << ("x" * 50) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
puts report
|
|
18
|
+
report.save("examples/import_leak_report.json")
|
|
19
|
+
puts "Wrote examples/import_leak_report.json"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
puts "Probe demo"
|
|
7
|
+
result = HeapScope.probe(title: "temporary objects", force_gc: true, mode: :lightweight) do
|
|
8
|
+
data = []
|
|
9
|
+
5_000.times { |i| data << { i: i } }
|
|
10
|
+
data.clear
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
puts result.scorecard.to_text
|
|
14
|
+
|
|
15
|
+
puts
|
|
16
|
+
puts "Session demo"
|
|
17
|
+
session = HeapScope.session("demo-probe")
|
|
18
|
+
session.measure(label: "churn", force_gc: true, mode: :lightweight) do
|
|
19
|
+
1_000.times { "x" * 10 }
|
|
20
|
+
end
|
|
21
|
+
puts "Session artifacts in .heapscope/sessions/demo-probe/"
|
|
22
|
+
puts "Latest: #{session.latest.inspect}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "heapscope"
|
|
5
|
+
|
|
6
|
+
# Demonstrates thread-local retention across "requests" on one thread.
|
|
7
|
+
HeapScopeFixtures = Module.new unless defined?(HeapScopeFixtures)
|
|
8
|
+
|
|
9
|
+
module ThreadLocalDemo
|
|
10
|
+
def self.handle_request
|
|
11
|
+
Thread.current[:request_context] ||= { presenters: [] }
|
|
12
|
+
50.times do
|
|
13
|
+
Thread.current[:request_context][:presenters] << ("Presenter:" + ("x" * 40))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.clear
|
|
18
|
+
Thread.current[:request_context] = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ThreadLocalDemo.clear
|
|
23
|
+
|
|
24
|
+
report = HeapScope.retention_test(cycles: 6, force_gc: true, mode: :lightweight) do
|
|
25
|
+
ThreadLocalDemo.handle_request
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts report
|
|
29
|
+
report.save("examples/thread_local_report.json")
|
|
30
|
+
report.save_html("examples/thread_local_report.html")
|
|
31
|
+
puts "Wrote examples/thread_local_report.{json,html}"
|
data/exe/heapscope
ADDED
data/heapscope.gemspec
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/heapscope/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "heapscope"
|
|
7
|
+
spec.version = HeapScope::VERSION
|
|
8
|
+
spec.authors = ["theworker02"]
|
|
9
|
+
spec.email = ["matthewlooney5@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby object retention, heap growth, and memory leak diagnostics"
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
HeapScope is a local, evidence-driven memory diagnostics toolkit for Ruby.
|
|
14
|
+
It helps identify object retention, abnormal heap growth, allocation hot spots,
|
|
15
|
+
long-lived objects, GC-surviving populations, and likely memory leaks in
|
|
16
|
+
long-running Ruby processes — without claiming a leak unless evidence is strong.
|
|
17
|
+
DESC
|
|
18
|
+
spec.homepage = "https://github.com/theworker02/heapscope"
|
|
19
|
+
spec.license = "MIT"
|
|
20
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
21
|
+
|
|
22
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
23
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
|
|
24
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
25
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
26
|
+
spec.metadata["documentation_uri"] = "https://theworker02.github.io/heapscope/"
|
|
27
|
+
spec.metadata["funding_uri"] = "https://thanks.dev/u/gh/theworker02"
|
|
28
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
29
|
+
|
|
30
|
+
spec.post_install_message = <<~MSG
|
|
31
|
+
|
|
32
|
+
HeapScope #{HeapScope::VERSION} installed.
|
|
33
|
+
heapscope doctor
|
|
34
|
+
heapscope about
|
|
35
|
+
https://theworker02.github.io/heapscope/
|
|
36
|
+
https://thanks.dev/u/gh/theworker02
|
|
37
|
+
|
|
38
|
+
Local-only diagnostics. No telemetry. No object-value dumps by default.
|
|
39
|
+
MSG
|
|
40
|
+
|
|
41
|
+
spec.files = Dir.chdir(__dir__) do
|
|
42
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
43
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github benchmark/ site/]) ||
|
|
44
|
+
f.end_with?(".gem")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
# Ensure packaging works before first commit
|
|
48
|
+
if spec.files.empty?
|
|
49
|
+
spec.files = Dir[
|
|
50
|
+
"lib/**/*",
|
|
51
|
+
"exe/*",
|
|
52
|
+
"assets/**/*",
|
|
53
|
+
"docs/**/*",
|
|
54
|
+
"examples/**/*",
|
|
55
|
+
"LICENSE*",
|
|
56
|
+
"README*",
|
|
57
|
+
"CHANGELOG*",
|
|
58
|
+
"CONTRIBUTING*",
|
|
59
|
+
"CODE_OF_CONDUCT*",
|
|
60
|
+
"SECURITY*",
|
|
61
|
+
"heapscope.gemspec"
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
spec.files.reject! { |f| f.start_with?("site/") }
|
|
65
|
+
|
|
66
|
+
spec.bindir = "exe"
|
|
67
|
+
spec.executables = ["heapscope"]
|
|
68
|
+
spec.require_paths = ["lib"]
|
|
69
|
+
|
|
70
|
+
spec.add_development_dependency "minitest", "~> 5.20"
|
|
71
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
72
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
73
|
+
end
|