2021-10-30 04:25:12 +08:00
|
|
|
#!/usr/bin/python
|
|
|
|
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
|
|
|
|
# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile
|
|
|
|
# The result of analysis is a .svg file which can be viewed in a browser
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import svg
|
2022-08-05 06:35:33 +08:00
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
|
|
|
|
argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps')
|
|
|
|
argumentParser.add_argument('source_file', type=open)
|
|
|
|
argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON')
|
2021-10-30 04:25:12 +08:00
|
|
|
|
|
|
|
class Node(svg.Node):
|
|
|
|
def __init__(self):
|
|
|
|
svg.Node.__init__(self)
|
|
|
|
self.function = ""
|
|
|
|
self.source = ""
|
|
|
|
self.line = 0
|
|
|
|
self.ticks = 0
|
|
|
|
|
|
|
|
def text(self):
|
|
|
|
return self.function
|
|
|
|
|
|
|
|
def title(self):
|
|
|
|
if self.line > 0:
|
|
|
|
return "{}\n{}:{}".format(self.function, self.source, self.line)
|
|
|
|
else:
|
|
|
|
return self.function
|
|
|
|
|
|
|
|
def details(self, root):
|
|
|
|
return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks)
|
|
|
|
|
|
|
|
|
2022-08-05 06:35:33 +08:00
|
|
|
def nodeFromCallstackListFile(source_file):
|
|
|
|
dump = source_file.readlines()
|
|
|
|
root = Node()
|
|
|
|
|
|
|
|
for l in dump:
|
|
|
|
ticks, stack = l.strip().split(" ", 1)
|
|
|
|
node = root
|
|
|
|
|
|
|
|
for f in reversed(stack.split(";")):
|
|
|
|
source, function, line = f.split(",")
|
|
|
|
|
|
|
|
child = node.child(f)
|
|
|
|
child.function = function
|
|
|
|
child.source = source
|
|
|
|
child.line = int(line) if len(line) > 0 else 0
|
|
|
|
|
|
|
|
node = child
|
|
|
|
|
|
|
|
node.ticks += int(ticks)
|
|
|
|
|
|
|
|
return root
|
|
|
|
|
2022-08-19 05:32:08 +08:00
|
|
|
def getDuration(obj):
|
|
|
|
total = obj['TotalDuration']
|
2022-08-05 06:35:33 +08:00
|
|
|
|
2022-08-19 05:32:08 +08:00
|
|
|
if 'Children' in obj:
|
|
|
|
for key, obj in obj['Children'].items():
|
|
|
|
total -= obj['TotalDuration']
|
|
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
|
|
def nodeFromJSONObject(node, key, obj):
|
2022-08-05 06:35:33 +08:00
|
|
|
source, function, line = key.split(",")
|
|
|
|
|
|
|
|
node.function = function
|
|
|
|
node.source = source
|
|
|
|
node.line = int(line) if len(line) > 0 else 0
|
|
|
|
|
2022-08-19 05:32:08 +08:00
|
|
|
node.ticks = getDuration(obj)
|
2022-08-05 06:35:33 +08:00
|
|
|
|
2022-08-19 05:32:08 +08:00
|
|
|
if 'Children' in obj:
|
|
|
|
for key, obj in obj['Children'].items():
|
|
|
|
nodeFromJSONObject(node.child(key), key, obj)
|
2022-08-05 06:35:33 +08:00
|
|
|
|
|
|
|
return node
|
|
|
|
|
|
|
|
|
|
|
|
def nodeFromJSONFile(source_file):
|
|
|
|
dump = json.load(source_file)
|
|
|
|
|
|
|
|
root = Node()
|
|
|
|
|
2022-08-19 05:32:08 +08:00
|
|
|
if 'Children' in dump:
|
|
|
|
for key, obj in dump['Children'].items():
|
|
|
|
nodeFromJSONObject(root.child(key), key, obj)
|
2022-08-05 06:35:33 +08:00
|
|
|
|
|
|
|
return root
|
2021-10-30 04:25:12 +08:00
|
|
|
|
|
|
|
|
2022-08-05 06:35:33 +08:00
|
|
|
arguments = argumentParser.parse_args()
|
2021-10-30 04:25:12 +08:00
|
|
|
|
2022-08-05 06:35:33 +08:00
|
|
|
if arguments.useJson:
|
|
|
|
root = nodeFromJSONFile(arguments.source_file)
|
|
|
|
else:
|
|
|
|
root = nodeFromCallstackListFile(arguments.source_file)
|
2021-10-30 04:25:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
svg.layout(root, lambda n: n.ticks)
|
|
|
|
svg.display(root, "Flame Graph", "hot", flip = True)
|