Skip to content

Commit e4e38ee

Browse files
committed
scripts: Add heap snapshot analyzer
Add a script to analyze V8 heap snapshot files (.heapsnapshot) generated by JETLS's profiling feature. The script parses the snapshot and displays a summary of memory usage by object type, sorted by shallow size. Usage: julia --project=scripts scripts/analyze-heapsnapshot.jl <snapshot> Uses binary units (1 KB = 1024 bytes) consistent with macOS and htop. Written by Claude
1 parent ceebefc commit e4e38ee

File tree

4 files changed

+181
-1
lines changed

4 files changed

+181
-1
lines changed

DEVELOPMENT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ To investigate memory growth:
276276

277277
This helps identify which types are accumulating over time.
278278

279+
### Command-line analysis
280+
281+
For text-based analysis (useful for sharing or automated processing), use the
282+
`analyze-heapsnapshot.jl` script:
283+
284+
```bash
285+
julia --project=scripts scripts/analyze-heapsnapshot.jl JETLS_YYYYMMDD_HHMMSS.heapsnapshot
286+
```
287+
288+
This displays a summary of memory usage by object type, sorted by shallow size.
289+
Use `--top=N` to control how many entries to show (default: 50).
290+
279291
### Limitations
280292

281293
- Only Julia GC-managed heap is captured; memory allocated by external libraries

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.0.0"
44
authors = ["Shuhei Kadowaki <aviatesk@gmail.com>"]
55

66
[workspace]
7-
projects = ["docs", "test"]
7+
projects = ["docs", "test", "scripts"]
88

99
[deps]
1010
Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d"

scripts/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[deps]
2+
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"

scripts/analyze-heapsnapshot.jl

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env julia
2+
3+
using JSON3
4+
using Printf
5+
6+
struct HeapNode
7+
type::String
8+
name::String
9+
id::Int
10+
self_size::Int
11+
edge_count::Int
12+
end
13+
14+
struct TypeSummary
15+
count::Int
16+
shallow_size::Int
17+
end
18+
19+
function parse_heapsnapshot(path::String)
20+
data = open(path) do io
21+
JSON3.read(io)
22+
end
23+
24+
snapshot = data["snapshot"]
25+
meta = snapshot["meta"]
26+
node_fields = meta["node_fields"]
27+
node_types = meta["node_types"][1]
28+
strings = data["strings"]
29+
nodes_array = data["nodes"]
30+
31+
node_field_count = length(node_fields)
32+
node_count = snapshot["node_count"]
33+
34+
type_idx = findfirst(==("type"), node_fields)
35+
name_idx = findfirst(==("name"), node_fields)
36+
id_idx = findfirst(==("id"), node_fields)
37+
self_size_idx = findfirst(==("self_size"), node_fields)
38+
edge_count_idx = findfirst(==("edge_count"), node_fields)
39+
40+
nodes = Vector{HeapNode}(undef, node_count)
41+
for i in 1:node_count
42+
base = (i - 1) * node_field_count
43+
type_index = nodes_array[base + type_idx] + 1
44+
name_index = nodes_array[base + name_idx] + 1
45+
node_type = node_types[type_index]
46+
node_name = strings[name_index]
47+
node_id = nodes_array[base + id_idx]
48+
self_size = nodes_array[base + self_size_idx]
49+
edge_count = nodes_array[base + edge_count_idx]
50+
nodes[i] = HeapNode(node_type, node_name, node_id, self_size, edge_count)
51+
end
52+
53+
return nodes
54+
end
55+
56+
function summarize_by_type(nodes::Vector{HeapNode})
57+
summary = Dict{String,TypeSummary}()
58+
for node in nodes
59+
key = "($(node.type)) $(node.name)"
60+
existing = get(summary, key, TypeSummary(0, 0))
61+
summary[key] = TypeSummary(existing.count + 1, existing.shallow_size + node.self_size)
62+
end
63+
return summary
64+
end
65+
66+
# Uses binary units (1 KB = 1024 bytes), consistent with macOS and htop.
67+
# Note: Chrome DevTools uses SI units (1 KB = 1000 bytes), so values will differ slightly.
68+
function format_size(bytes::Int)
69+
if bytes >= 1024 * 1024
70+
return @sprintf("%.1f MB", bytes / (1024 * 1024))
71+
elseif bytes >= 1024
72+
return @sprintf("%.1f KB", bytes / 1024)
73+
else
74+
return "$bytes B"
75+
end
76+
end
77+
78+
function print_summary(summary::Dict{String,TypeSummary}; top_n::Int=50)
79+
sorted = sort(collect(summary); by = x -> x.second.shallow_size, rev = true)
80+
81+
total_size = sum(s.shallow_size for (_, s) in summary)
82+
total_count = sum(s.count for (_, s) in summary)
83+
84+
wide = 120
85+
86+
println()
87+
println("=" ^ wide)
88+
println("HEAP SNAPSHOT SUMMARY")
89+
println("=" ^ wide)
90+
println()
91+
@printf("Total objects: %d\n", total_count)
92+
@printf("Total shallow size: %s\n", format_size(total_size))
93+
println()
94+
println("-" ^ wide)
95+
@printf("%-80s %10s %14s %12s\n", "Type/Name", "Count", "Shallow Size", "% of Total")
96+
println("-" ^ wide)
97+
98+
for (i, (key, s)) in enumerate(sorted)
99+
i > top_n && break
100+
pct = 100.0 * s.shallow_size / total_size
101+
display_key = length(key) > 80 ? key[1:77] * "..." : key
102+
@printf("%-80s %10d %14s %11.1f%%\n", display_key, s.count, format_size(s.shallow_size), pct)
103+
end
104+
105+
println("-" ^ wide)
106+
println()
107+
end
108+
109+
function print_help()
110+
println("""
111+
scripts/analyze-heapsnapshot.jl - Heap Snapshot Analyzer
112+
113+
USAGE:
114+
julia scripts/analyze-heapsnapshot.jl <path-to-heapsnapshot> [--top=N]
115+
116+
DESCRIPTION:
117+
Analyzes V8 heap snapshot files (.heapsnapshot) generated by JETLS
118+
and displays memory usage summary by object type.
119+
120+
OPTIONS:
121+
--top=N Show top N entries (default: 50)
122+
--help, -h Show this help message
123+
124+
EXAMPLE:
125+
julia scripts/analyze-heapsnapshot.jl JETLS_20251203_120000.heapsnapshot
126+
julia scripts/analyze-heapsnapshot.jl JETLS_20251203_120000.heapsnapshot --top=100
127+
""")
128+
end
129+
130+
function parse_args(args::Vector{String})
131+
path = nothing
132+
top_n = 50
133+
134+
for arg in args
135+
if arg == "--help" || arg == "-h"
136+
print_help()
137+
exit(0)
138+
elseif startswith(arg, "--top=")
139+
top_n = parse(Int, split(arg, "="; limit=2)[2])
140+
elseif !startswith(arg, "-")
141+
path = arg
142+
else
143+
@warn "Unknown argument: $arg"
144+
println("\nRun with --help for usage information")
145+
exit(1)
146+
end
147+
end
148+
149+
if path === nothing
150+
error("Heap snapshot path is required\nRun with --help for usage information")
151+
end
152+
153+
return (path, top_n)
154+
end
155+
156+
function (@main)(args::Vector{String})
157+
(path, top_n) = parse_args(args)
158+
159+
if !isfile(path)
160+
error("File not found: $path")
161+
end
162+
163+
nodes = parse_heapsnapshot(path)
164+
summary = summarize_by_type(nodes)
165+
print_summary(summary; top_n)
166+
end

0 commit comments

Comments
 (0)