Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tools/heapgraph.py
2723 views
1
#!/usr/bin/python3
2
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
3
4
# Given two heap snapshots (A & B), this tool performs reachability analysis on new objects allocated in B
5
# This is useful to find memory leaks - reachability analysis answers the question "why is this set of objects not freed"
6
# This tool can also be ran with just one snapshot, in which case it displays all allocated objects
7
# The result of analysis is a .svg file which can be viewed in a browser
8
# To generate these dumps, use luaC_dump, ideally preceded by luaC_fullgc
9
10
import argparse
11
import json
12
import sys
13
import svg
14
15
argumentParser = argparse.ArgumentParser(description='Luau heap snapshot analyzer')
16
17
argumentParser.add_argument('--split', dest = 'split', type = str, default = 'none', help = 'Perform additional root split using memory categories', choices = ['none', 'custom', 'all'])
18
19
argumentParser.add_argument('snapshot')
20
argumentParser.add_argument('snapshotnew', nargs='?')
21
22
arguments = argumentParser.parse_args()
23
24
class Node(svg.Node):
25
def __init__(self):
26
svg.Node.__init__(self)
27
self.size = 0
28
self.count = 0
29
# data for memory category filtering
30
self.objects = []
31
self.categories = set()
32
33
def text(self):
34
return self.name
35
36
def title(self):
37
return self.name
38
39
def details(self, root):
40
return "{} ({:,} bytes, {:.1%}); self: {:,} bytes in {:,} objects".format(self.name, self.width, self.width / root.width, self.size, self.count)
41
42
def getkey(heap, obj, key):
43
pairs = obj.get("pairs", [])
44
for i in range(0, len(pairs), 2):
45
if pairs[i] and heap[pairs[i]]["type"] == "string" and heap[pairs[i]]["data"] == key:
46
if pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string":
47
return heap[pairs[i + 1]]["data"]
48
else:
49
return None
50
return None
51
52
# load files
53
if arguments.snapshotnew == None:
54
dumpold = None
55
with open(arguments.snapshot) as f:
56
dump = json.load(f)
57
else:
58
with open(arguments.snapshot) as f:
59
dumpold = json.load(f)
60
with open(arguments.snapshotnew) as f:
61
dump = json.load(f)
62
63
heap = dump["objects"]
64
65
# reachability analysis: how much of the heap is reachable from roots?
66
visited = set()
67
queue = []
68
offset = 0
69
root = Node()
70
71
for name, addr in dump["roots"].items():
72
queue.append((addr, root.child(name)))
73
74
while offset < len(queue):
75
addr, node = queue[offset]
76
offset += 1
77
if addr in visited:
78
continue
79
80
visited.add(addr)
81
obj = heap[addr]
82
83
if not dumpold or not addr in dumpold["objects"]:
84
node.count += 1
85
node.size += obj["size"]
86
node.objects.append(obj)
87
88
if obj["type"] == "table":
89
pairs = obj.get("pairs", [])
90
weakkey = False
91
weakval = False
92
93
if "metatable" in obj:
94
modemt = getkey(heap, heap[obj["metatable"]], "__mode")
95
if modemt:
96
weakkey = "k" in modemt
97
weakval = "v" in modemt
98
99
for i in range(0, len(pairs), 2):
100
key = pairs[i+0]
101
val = pairs[i+1]
102
if key and heap[key]["type"] == "string":
103
# string keys are always strong
104
queue.append((key, node))
105
if val and not weakval:
106
queue.append((val, node.child(heap[key]["data"])))
107
else:
108
if key and not weakkey:
109
queue.append((key, node))
110
if val and not weakval:
111
queue.append((val, node))
112
113
for a in obj.get("array", []):
114
queue.append((a, node))
115
if "metatable" in obj:
116
queue.append((obj["metatable"], node.child("__meta")))
117
elif obj["type"] == "function":
118
queue.append((obj["env"], node.child("__env")))
119
120
source = ""
121
if "proto" in obj:
122
proto = heap[obj["proto"]]
123
if "source" in proto:
124
source = proto["source"]
125
126
if "proto" in obj:
127
queue.append((obj["proto"], node.child("__proto")))
128
for a in obj.get("upvalues", []):
129
queue.append((a, node.child(source)))
130
elif obj["type"] == "userdata":
131
if "metatable" in obj:
132
queue.append((obj["metatable"], node.child("__meta")))
133
elif obj["type"] == "thread":
134
queue.append((obj["env"], node.child("__env")))
135
stack = obj.get("stack")
136
stacknames = obj.get("stacknames", [])
137
stacknode = node.child("__stack")
138
framenode = None
139
for i in range(len(stack)):
140
name = stacknames[i] if stacknames else None
141
if name and name.startswith("frame:"):
142
framenode = stacknode.child(name[6:])
143
name = None
144
queue.append((stack[i], framenode.child(name) if framenode and name else framenode or stacknode))
145
elif obj["type"] == "proto":
146
for a in obj.get("constants", []):
147
queue.append((a, node))
148
for a in obj.get("protos", []):
149
queue.append((a, node))
150
elif obj["type"] == "upvalue":
151
if "object" in obj:
152
queue.append((obj["object"], node))
153
154
def annotateContainedCategories(node, start):
155
for obj in node.objects:
156
if obj["cat"] < start:
157
obj["cat"] = 0
158
159
node.categories.add(obj["cat"])
160
161
for child in node.children.values():
162
annotateContainedCategories(child, start)
163
164
for cat in child.categories:
165
node.categories.add(cat)
166
167
def filteredTreeForCategory(node, category):
168
children = {}
169
170
for c in node.children.values():
171
if category in c.categories:
172
filtered = filteredTreeForCategory(c, category)
173
174
if filtered:
175
children[filtered.name] = filtered
176
177
if len(children):
178
result = Node()
179
result.name = node.name
180
181
# re-count the objects with the correct category that we have
182
for obj in node.objects:
183
if obj["cat"] == category:
184
result.count += 1
185
result.size += obj["size"]
186
187
result.children = children
188
return result
189
else:
190
result = Node()
191
result.name = node.name
192
193
# re-count the objects with the correct category that we have
194
for obj in node.objects:
195
if obj["cat"] == category:
196
result.count += 1
197
result.size += obj["size"]
198
199
if result.count != 0:
200
return result
201
202
return None
203
204
def splitIntoCategories(root):
205
result = Node()
206
207
for i in range(0, 256):
208
filtered = filteredTreeForCategory(root, i)
209
210
if filtered:
211
name = dump["stats"]["categories"][str(i)]["name"]
212
213
filtered.name = name
214
result.children[name] = filtered
215
216
return result
217
218
if dump["stats"].get("categories") and arguments.split != 'none':
219
if arguments.split == 'custom':
220
annotateContainedCategories(root, 128)
221
else:
222
annotateContainedCategories(root, 0)
223
224
root = splitIntoCategories(root)
225
226
svg.layout(root, lambda n: n.size)
227
svg.display(root, "Memory Graph", "cold")
228
229