Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tools/stack-usage-reporter.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
# The purpose of this script is to analyze disassembly generated by objdump or
5
# dumpbin to print (or to compare) the stack usage of functions/methods.
6
# This is a quickly written script, so it is quite possible it may not handle
7
# all code properly.
8
#
9
# The script expects the user to create a text assembly dump to be passed to
10
# the script.
11
#
12
# objdump Example
13
# objdump --demangle --disassemble objfile.o > objfile.s
14
#
15
# dumpbin Example
16
# dumpbin /disasm objfile.obj > objfile.s
17
#
18
# If the script is passed a single file, then all stack size information that
19
# is found it printed. If two files are passed, then the script compares the
20
# stack usage of the two files (useful for A/B comparisons).
21
# Currently more than two input files are not supported. (But adding support shouldn't
22
# be very difficult.)
23
#
24
# Note: The script only handles x64 disassembly. Supporting x86 is likely
25
# trivial, but ARM support could be difficult.
26
# Thus far the script has been tested with MSVC on Win64 and clang on OSX.
27
28
import argparse
29
import re
30
31
blank_re = re.compile('\s*')
32
33
class LineReader:
34
def __init__(self, lines):
35
self.lines = list(reversed(lines))
36
def get_line(self):
37
return self.lines.pop(-1)
38
def peek_line(self):
39
return self.lines[-1]
40
def consume_blank_lines(self):
41
while blank_re.fullmatch(self.peek_line()):
42
self.get_line()
43
def is_empty(self):
44
return len(self.lines) == 0
45
46
def parse_objdump_assembly(in_file):
47
results = {}
48
text_section_re = re.compile('Disassembly of section __TEXT,__text:\s*')
49
symbol_re = re.compile('[^<]*<(.*)>:\s*')
50
stack_alloc = re.compile('.*subq\s*\$(\d*), %rsp\s*')
51
52
lr = LineReader(in_file.readlines())
53
54
def find_stack_alloc_size():
55
while True:
56
if lr.is_empty():
57
return None
58
if blank_re.fullmatch(lr.peek_line()):
59
return None
60
61
line = lr.get_line()
62
mo = stack_alloc.fullmatch(line)
63
if mo:
64
lr.consume_blank_lines()
65
return int(mo.group(1))
66
67
# Find beginning of disassembly
68
while not text_section_re.fullmatch(lr.get_line()):
69
pass
70
71
# Scan for symbols
72
while not lr.is_empty():
73
lr.consume_blank_lines()
74
if lr.is_empty():
75
break
76
line = lr.get_line()
77
mo = symbol_re.fullmatch(line)
78
# Found a symbol
79
if mo:
80
symbol = mo.group(1)
81
stack_size = find_stack_alloc_size()
82
if stack_size != None:
83
results[symbol] = stack_size
84
85
return results
86
87
def parse_dumpbin_assembly(in_file):
88
results = {}
89
90
file_type_re = re.compile('File Type: COFF OBJECT\s*')
91
symbol_re = re.compile('[^(]*\((.*)\):\s*')
92
summary_re = re.compile('\s*Summary\s*')
93
stack_alloc = re.compile('.*sub\s*rsp,([A-Z0-9]*)h\s*')
94
95
lr = LineReader(in_file.readlines())
96
97
def find_stack_alloc_size():
98
while True:
99
if lr.is_empty():
100
return None
101
if blank_re.fullmatch(lr.peek_line()):
102
return None
103
104
line = lr.get_line()
105
mo = stack_alloc.fullmatch(line)
106
if mo:
107
lr.consume_blank_lines()
108
return int(mo.group(1), 16) # return value in decimal
109
110
# Find beginning of disassembly
111
while not file_type_re.fullmatch(lr.get_line()):
112
pass
113
114
# Scan for symbols
115
while not lr.is_empty():
116
lr.consume_blank_lines()
117
if lr.is_empty():
118
break
119
line = lr.get_line()
120
if summary_re.fullmatch(line):
121
break
122
mo = symbol_re.fullmatch(line)
123
# Found a symbol
124
if mo:
125
symbol = mo.group(1)
126
stack_size = find_stack_alloc_size()
127
if stack_size != None:
128
results[symbol] = stack_size
129
return results
130
131
def main():
132
parser = argparse.ArgumentParser(description='Tool used for reporting or comparing the stack usage of functions/methods')
133
parser.add_argument('--format', choices=['dumpbin', 'objdump'], required=True, help='Specifies the program used to generate the input files')
134
parser.add_argument('--input', action='append', required=True, help='Input assembly file. This option may be specified multiple times.')
135
parser.add_argument('--md-output', action='store_true', help='Show table output in markdown format')
136
parser.add_argument('--only-diffs', action='store_true', help='Only show stack info when it differs between the input files')
137
args = parser.parse_args()
138
139
parsers = {'dumpbin': parse_dumpbin_assembly, 'objdump' : parse_objdump_assembly}
140
parse_func = parsers[args.format]
141
142
input_results = []
143
for input_name in args.input:
144
with open(input_name) as in_file:
145
results = parse_func(in_file)
146
input_results.append(results)
147
148
if len(input_results) == 1:
149
# Print out the results sorted by size
150
size_sorted = sorted([(size, symbol) for symbol, size in results.items()], reverse=True)
151
print(input_name)
152
for size, symbol in size_sorted:
153
print(f'{size:10}\t{symbol}')
154
print()
155
elif len(input_results) == 2:
156
common_symbols = set(input_results[0].keys()).intersection(set(input_results[1].keys()))
157
print(f'Found {len(common_symbols)} common symbols')
158
stack_sizes = sorted([(input_results[0][sym], input_results[1][sym], sym) for sym in common_symbols], reverse=True)
159
if args.md_output:
160
print('Before | After | Symbol')
161
print('-- | -- | --')
162
for size0, size1, symbol in stack_sizes:
163
if args.only_diffs and size0 == size1:
164
continue
165
if args.md_output:
166
print(f'{size0} | {size1} | {symbol}')
167
else:
168
print(f'{size0:10}\t{size1:10}\t{symbol}')
169
else:
170
print("TODO support more than 2 inputs")
171
172
if __name__ == '__main__':
173
main()
174
175