Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tools/heapsnapshot.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 a Luau heap dump, this tool generates a heap snapshot which can be imported by Chrome's DevTools Memory panel
5
# To generate a snapshot, use luaC_dump, ideally preceded by luaC_fullgc
6
# To import in Chrome, ensure the snapshot has the .heapsnapshot extension and go to: Inspect -> Memory -> Load Profile
7
# A reference for the heap snapshot schema can be found here: https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/memory-problems/heap-snapshot-schema
8
9
# Usage: python3 heapsnapshot.py luauDump.json heapSnapshot.heapsnapshot
10
11
import json
12
import sys
13
14
# Header describing the snapshot format, copied from a real Chrome heap snapshot
15
snapshotMeta = {
16
"node_fields": ["type", "name", "id", "self_size", "edge_count", "trace_node_id", "detachedness"],
17
"node_types": [
18
["hidden", "array", "string", "object", "code", "closure", "regexp", "number", "native", "synthetic", "concatenated string", "sliced string", "symbol", "bigint", "object shape"],
19
"string", "number", "number", "number", "number", "number"
20
],
21
"edge_fields": ["type", "name_or_index", "to_node"],
22
"edge_types": [
23
["context", "element", "property", "internal", "hidden", "shortcut", "weak"],
24
"string_or_number", "node"
25
],
26
"trace_function_info_fields": ["function_id", "name", "script_name", "script_id", "line", "column"],
27
"trace_node_fields": ["id", "function_info_index", "count", "size", "children"],
28
"sample_fields": ["timestamp_us", "last_assigned_id"],
29
"location_fields": ["object_index", "script_id", "line", "column"],
30
}
31
32
# These indices refer to the index in the snapshot's metadata header
33
nodeTypeToMetaIndex = {type: i for i, type in enumerate(snapshotMeta["node_types"][0])}
34
edgeTypeToMetaIndex = {type: i for i, type in enumerate(snapshotMeta["edge_types"][0])}
35
36
nodeFieldCount = len(snapshotMeta["node_fields"])
37
edgeFieldCount = len(snapshotMeta["edge_fields"])
38
39
40
def readAddresses(data):
41
# Ordered list of addresses to ensure the registry is the first node, and also so we can process nodes in index order
42
addresses = []
43
addressToNodeIndex = {}
44
45
def addAddress(address):
46
assert address not in addressToNodeIndex, f"Address already exists in the snapshot: '{address}'"
47
addresses.append(address)
48
addressToNodeIndex[address] = len(addresses) - 1
49
50
# The registry is a special case that needs to be either the first or last node to ensure gc "distances" are calculated correctly
51
registryAddress = data["roots"]["registry"]
52
addAddress(registryAddress)
53
54
for address, obj in data["objects"].items():
55
if address == registryAddress:
56
continue
57
addAddress(address)
58
59
return addresses, addressToNodeIndex
60
61
62
def convertToSnapshot(data):
63
addresses, addressToNodeIndex = readAddresses(data)
64
65
# Some notable idiosyncrasies with the heap snapshot format:
66
# 1. The snapshot format contains a flat array of nodes and edges. Oddly, edges must reference the "absolute" index of a node's first element after flattening.
67
# 2. A node's outgoing edges are implicitly represented by a contiguous block of edges in the edges array which correspond to the node's position
68
# in the nodes array and its edge count. So if the first node has 3 edges, the first 3 edges in the edges array are its edges, and so on.
69
70
nodes = []
71
edges = []
72
strings = []
73
74
stringToSnapshotIndex = {}
75
76
def getUniqueId(address):
77
# TODO: we should hash this to an int32 instead of using the address directly
78
# Addresses are hexadecimal strings
79
return int(address, 16)
80
81
def addNode(node):
82
assert len(node) == nodeFieldCount, f"Expected {nodeFieldCount} fields, got {len(node)}"
83
nodes.append(node)
84
85
def addEdge(edge):
86
assert len(edge) == edgeFieldCount, f"Expected {edgeFieldCount} fields, got {len(edge)}"
87
edges.append(edge)
88
89
def getStringSnapshotIndex(string):
90
assert isinstance(string, str), f"'{string}' is not of type string"
91
if string not in stringToSnapshotIndex:
92
strings.append(string)
93
stringToSnapshotIndex[string] = len(strings) - 1
94
return stringToSnapshotIndex[string]
95
96
def getNodeSnapshotIndex(address):
97
# This is the index of the first element of the node in the flattened nodes array
98
return addressToNodeIndex[address] * nodeFieldCount
99
100
for address in addresses:
101
obj = data["objects"][address]
102
edgeCount = 0
103
104
if obj["type"] == "table":
105
# TODO: support weak references
106
name = f"Registry ({address})" if address == data["roots"]["registry"] else f"Luau table ({address})"
107
if "pairs" in obj:
108
for i in range(0, len(obj["pairs"]), 2):
109
key = obj["pairs"][i]
110
value = obj["pairs"][i + 1]
111
if key is None and value is None:
112
# Both the key and value are value types, nothing meaningful to add here
113
continue
114
elif key is None:
115
edgeCount += 1
116
addEdge([edgeTypeToMetaIndex["property"], getStringSnapshotIndex("(Luau table key value type)"), getNodeSnapshotIndex(value)])
117
elif value is None:
118
edgeCount += 1
119
addEdge([edgeTypeToMetaIndex["internal"], getStringSnapshotIndex(f'Luau table key ref: {data["objects"][key]["type"]} ({key})'), getNodeSnapshotIndex(key)])
120
elif data["objects"][key]["type"] == "string":
121
edgeCount += 2
122
# This is a special case where the key is a string, so we can use it as the edge name
123
addEdge([edgeTypeToMetaIndex["property"], getStringSnapshotIndex(data["objects"][key]["data"]), getNodeSnapshotIndex(value)])
124
addEdge([edgeTypeToMetaIndex["internal"], getStringSnapshotIndex(f'Luau table key ref: {data["objects"][key]["type"]} ({key})'), getNodeSnapshotIndex(key)])
125
else:
126
edgeCount += 2
127
addEdge([edgeTypeToMetaIndex["property"], getStringSnapshotIndex(f'{data["objects"][key]["type"]} ({key})'), getNodeSnapshotIndex(value)])
128
addEdge([edgeTypeToMetaIndex["internal"], getStringSnapshotIndex(f'Luau table key ref: {data["objects"][key]["type"]} ({key})'), getNodeSnapshotIndex(key)])
129
if "array" in obj:
130
for i, element in enumerate(obj["array"]):
131
edgeCount += 1
132
addEdge([edgeTypeToMetaIndex["element"], i, getNodeSnapshotIndex(element)])
133
if "metatable" in obj:
134
edgeCount += 1
135
addEdge([edgeTypeToMetaIndex["internal"], getStringSnapshotIndex(f'metatable ({obj["metatable"]})'), getNodeSnapshotIndex(obj["metatable"])])
136
# TODO: consider distinguishing "object" and "array" node types
137
addNode([nodeTypeToMetaIndex["object"], getStringSnapshotIndex(name), getUniqueId(address), obj["size"], edgeCount, 0, 0])
138
elif obj["type"] == "thread":
139
name = f'Luau thread: {obj["source"]}:{obj["line"]} ({address})' if "source" in obj else f"Luau thread ({address})"
140
if address == data["roots"]["mainthread"]:
141
name += " (main thread)"
142
if "env" in obj:
143
edgeCount += 1
144
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f'env ({obj["env"]})'), getNodeSnapshotIndex(obj["env"])])
145
if "stack" in obj:
146
for i, frame in enumerate(obj["stack"]):
147
edgeCount += 1
148
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f"callstack[{i}]"), getNodeSnapshotIndex(frame)])
149
addNode([nodeTypeToMetaIndex["native"], getStringSnapshotIndex(name), getUniqueId(address), obj["size"], edgeCount, 0, 0])
150
elif obj["type"] == "function":
151
name = f'Luau function: {obj["name"]} ({address})' if "name" in obj else f"Luau anonymous function ({address})"
152
if "env" in obj:
153
edgeCount += 1
154
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f'env ({obj["env"]})'), getNodeSnapshotIndex(obj["env"])])
155
if "proto" in obj:
156
edgeCount += 1
157
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f'proto ({obj["proto"]})'), getNodeSnapshotIndex(obj["proto"])])
158
if "upvalues" in obj:
159
for i, upvalue in enumerate(obj["upvalues"]):
160
edgeCount += 1
161
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f"up value ({upvalue})"), getNodeSnapshotIndex(upvalue)])
162
addNode([nodeTypeToMetaIndex["closure"], getStringSnapshotIndex(name), getUniqueId(address), obj["size"], edgeCount, 0, 0])
163
elif obj["type"] == "upvalue":
164
if "object" in obj:
165
edgeCount += 1
166
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(f'upvalue object ({obj["object"]})'), getNodeSnapshotIndex(obj["object"])])
167
addNode([nodeTypeToMetaIndex["native"], getStringSnapshotIndex(f"Luau upvalue ({address})"), getUniqueId(address), obj["size"], edgeCount, 0, 0])
168
elif obj["type"] == "userdata":
169
if "metatable" in obj:
170
edgeCount += 1
171
addEdge([edgeTypeToMetaIndex["internal"], getStringSnapshotIndex(f'metatable ({obj["metatable"]})'), getNodeSnapshotIndex(obj["metatable"])])
172
addNode([nodeTypeToMetaIndex["native"], getStringSnapshotIndex(f"Luau userdata ({address})"), getUniqueId(address), obj["size"], edgeCount, 0, 0])
173
elif obj["type"] == "proto":
174
name = f'Luau proto: {obj["source"]}:{obj["line"]} ({address})' if "source" in obj else f"Luau proto ({address})"
175
if "constants" in obj:
176
for constant in obj["constants"]:
177
edgeCount += 1
178
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(constant), getNodeSnapshotIndex(constant)])
179
if "protos" in obj:
180
for proto in obj["protos"]:
181
edgeCount += 1
182
addEdge([edgeTypeToMetaIndex["context"], getStringSnapshotIndex(proto), getNodeSnapshotIndex(proto)])
183
addNode([nodeTypeToMetaIndex["code"], getStringSnapshotIndex(name), getUniqueId(address), obj["size"], edgeCount, 0, 0])
184
elif obj["type"] == "string":
185
addNode([nodeTypeToMetaIndex["string"], getStringSnapshotIndex(obj["data"]), getUniqueId(address), obj["size"], 0, 0, 0])
186
elif obj["type"] == "buffer":
187
addNode([nodeTypeToMetaIndex["native"], getStringSnapshotIndex(f'buffer ({address})'), getUniqueId(address), obj["size"], 0, 0, 0])
188
else:
189
raise Exception(f"Unknown object type: '{obj['type']}'")
190
191
return {
192
"snapshot": {
193
"meta": snapshotMeta,
194
"node_count": len(nodes),
195
"edge_count": len(edges),
196
"trace_function_count": 0,
197
},
198
# flatten the nodes and edges arrays
199
"nodes": [field for node in nodes for field in node],
200
"edges": [field for edge in edges for field in edge],
201
"trace_function_infos": [],
202
"trace_tree": [],
203
"samples": [],
204
"locations": [],
205
"strings": strings,
206
}
207
208
209
if __name__ == "__main__":
210
luauDump = sys.argv[1]
211
heapSnapshot = sys.argv[2]
212
213
with open(luauDump, "r") as file:
214
dump = json.load(file)
215
216
snapshot = convertToSnapshot(dump)
217
218
with open(heapSnapshot, "w") as file:
219
json.dump(snapshot, file)
220
221
print(f"Heap snapshot written to: '{heapSnapshot}'")
222
223