Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/toolchain_profiler.py
4128 views
1
# Copyright 2016 The Emscripten Authors. All rights reserved.
2
# Emscripten is available under two separate licenses, the MIT license and the
3
# University of Illinois/NCSA Open Source License. Both these licenses can be
4
# found in the LICENSE file.
5
6
import atexit
7
import logging
8
import os
9
import sys
10
import subprocess
11
import tempfile
12
import time
13
from contextlib import ContextDecorator
14
15
logger = logging.getLogger('profiler')
16
17
from . import response_file
18
19
EMPROFILE = int(os.getenv('EMPROFILE', '0'))
20
21
22
class Logger(ContextDecorator):
23
depth = 0
24
25
def __init__(self, name):
26
self.name = name
27
28
def __call__(self, func):
29
if self.name is None:
30
self.name = func.__name__
31
return super().__call__(func)
32
33
def __enter__(self):
34
if EMPROFILE == 2:
35
indentation = ' ' * Logger.depth
36
logger.info('%sstart block "%s"', indentation, self.name)
37
Logger.depth += 1
38
self.start = time.time()
39
40
def __exit__(self, exc_type, value, traceback):
41
# When a block ends debug log the total duration.
42
now = time.time()
43
duration = now - self.start
44
if exc_type:
45
msg = 'block "%s" raised an exception after %.3f seconds'
46
else:
47
msg = 'block "%s" took %.3f seconds'
48
if EMPROFILE == 2:
49
Logger.depth -= 1
50
indentation = ' ' * Logger.depth
51
logger.info(indentation + msg, self.name, duration)
52
else:
53
logger.debug(msg, self.name, duration)
54
55
56
if EMPROFILE == 1:
57
original_sys_exit = sys.exit
58
original_subprocess_call = subprocess.call
59
original_subprocess_check_call = subprocess.check_call
60
original_subprocess_check_output = subprocess.check_output
61
original_Popen = subprocess.Popen
62
process_returncode = None
63
64
def profiled_sys_exit(returncode):
65
# Stack the returncode which then gets used in the atexit handler
66
global process_returncode
67
process_returncode = returncode
68
original_sys_exit(returncode)
69
70
def profiled_call(cmd, *args, **kw):
71
pid = ToolchainProfiler.imaginary_pid()
72
ToolchainProfiler.record_subprocess_spawn(pid, cmd)
73
try:
74
returncode = original_subprocess_call(cmd, *args, **kw)
75
except Exception:
76
ToolchainProfiler.record_subprocess_finish(pid, 1)
77
raise
78
ToolchainProfiler.record_subprocess_finish(pid, returncode)
79
return returncode
80
81
def profiled_check_call(cmd, *args, **kw):
82
pid = ToolchainProfiler.imaginary_pid()
83
ToolchainProfiler.record_subprocess_spawn(pid, cmd)
84
try:
85
ret = original_subprocess_check_call(cmd, *args, **kw)
86
except Exception as e:
87
ToolchainProfiler.record_subprocess_finish(pid, e.returncode)
88
raise
89
ToolchainProfiler.record_subprocess_finish(pid, 0)
90
return ret
91
92
def profiled_check_output(cmd, *args, **kw):
93
pid = ToolchainProfiler.imaginary_pid()
94
ToolchainProfiler.record_subprocess_spawn(pid, cmd)
95
try:
96
ret = original_subprocess_check_output(cmd, *args, **kw)
97
except Exception as e:
98
ToolchainProfiler.record_subprocess_finish(pid, e.returncode)
99
raise
100
ToolchainProfiler.record_subprocess_finish(pid, 0)
101
return ret
102
103
class ProfiledPopen(original_Popen):
104
def __init__(self, args, *otherargs, **kwargs):
105
super().__init__(args, *otherargs, **kwargs)
106
ToolchainProfiler.record_subprocess_spawn(self.pid, args)
107
108
def communicate(self, *args, **kwargs):
109
ToolchainProfiler.record_subprocess_wait(self.pid)
110
output = super().communicate(*args, **kwargs)
111
ToolchainProfiler.record_subprocess_finish(self.pid, self.returncode)
112
return output
113
114
sys.exit = profiled_sys_exit
115
subprocess.call = profiled_call
116
subprocess.check_call = profiled_check_call
117
subprocess.check_output = profiled_check_output
118
subprocess.Popen = ProfiledPopen
119
120
class ToolchainProfiler:
121
# Provide a running counter towards negative numbers for PIDs for which we
122
# don't know what the actual process ID is
123
imaginary_pid_ = 0
124
profiler_logs_path = None # Log file not opened yet
125
126
block_stack = []
127
128
# Track if record_process_exit and record_process_start have been called
129
# so we can assert they are only ever called once.
130
process_start_recorded = False
131
process_exit_recorded = False
132
133
@staticmethod
134
def timestamp():
135
return '{0:.3f}'.format(time.time())
136
137
@staticmethod
138
def log_access():
139
# Note: This function is called in two importantly different contexts: in
140
# "main" process and in python subprocesses invoked via
141
# subprocessing.Pool.map(). The subprocesses have their own PIDs, and
142
# hence record their own data JSON files, but since the process pool is
143
# maintained internally by python, the toolchain profiler does not track
144
# the parent->child process spawns for the subprocessing pools. Therefore
145
# any profiling events that the subprocess children generate are virtually
146
# treated as if they were performed by the parent PID.
147
return open(os.path.join(ToolchainProfiler.profiler_logs_path, 'toolchain_profiler.pid_' + str(os.getpid()) + '.json'), 'a')
148
149
@staticmethod
150
def escape_string(arg):
151
return arg.replace('\\', '\\\\').replace('"', '\\"')
152
153
@staticmethod
154
def escape_args(args):
155
return [ToolchainProfiler.escape_string(arg) for arg in args]
156
157
@staticmethod
158
def record_process_start(write_log_entry=True):
159
assert not ToolchainProfiler.process_start_recorded
160
ToolchainProfiler.process_start_recorded = True
161
atexit.register(ToolchainProfiler.record_process_exit)
162
163
# For subprocessing.Pool.map() child processes, this points to the PID of
164
# the parent process that spawned the subprocesses. This makes the
165
# subprocesses look as if the parent had called the functions.
166
ToolchainProfiler.mypid_str = str(os.getpid())
167
ToolchainProfiler.profiler_logs_path = os.path.join(tempfile.gettempdir(), 'emscripten_toolchain_profiler_logs')
168
os.makedirs(ToolchainProfiler.profiler_logs_path, exist_ok=True)
169
170
ToolchainProfiler.block_stack = []
171
172
if write_log_entry:
173
with ToolchainProfiler.log_access() as f:
174
f.write('[\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"start","time":' + ToolchainProfiler.timestamp() + ',"cmdLine":["' + '","'.join(ToolchainProfiler.escape_args(sys.argv)) + '"]}')
175
176
@staticmethod
177
def record_process_exit():
178
assert not ToolchainProfiler.process_exit_recorded
179
assert ToolchainProfiler.process_start_recorded
180
ToolchainProfiler.process_exit_recorded = True
181
182
ToolchainProfiler.exit_all_blocks()
183
with ToolchainProfiler.log_access() as f:
184
returncode = process_returncode
185
if returncode is None:
186
returncode = '"MISSING EXIT CODE"'
187
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"exit","time":' + ToolchainProfiler.timestamp() + ',"returncode":' + str(returncode) + '}\n]\n')
188
189
@staticmethod
190
def record_subprocess_spawn(process_pid, process_cmdline):
191
try:
192
expanded_cmdline = response_file.substitute_response_files(process_cmdline)
193
except OSError:
194
# If user is passing a malformed input command line, then the toolchain profiler should not
195
# throw an exception, but profile the bad input command line as-is
196
expanded_cmdline = process_cmdline
197
198
with ToolchainProfiler.log_access() as f:
199
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"spawn","targetPid":' + str(process_pid) + ',"time":' + ToolchainProfiler.timestamp() + ',"cmdLine":["' + '","'.join(ToolchainProfiler.escape_args(expanded_cmdline)) + '"]}')
200
201
@staticmethod
202
def record_subprocess_wait(process_pid):
203
with ToolchainProfiler.log_access() as f:
204
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"wait","targetPid":' + str(process_pid) + ',"time":' + ToolchainProfiler.timestamp() + '}')
205
206
@staticmethod
207
def record_subprocess_finish(process_pid, returncode):
208
with ToolchainProfiler.log_access() as f:
209
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"finish","targetPid":' + str(process_pid) + ',"time":' + ToolchainProfiler.timestamp() + ',"returncode":' + str(returncode) + '}')
210
211
@staticmethod
212
def enter_block(block_name):
213
with ToolchainProfiler.log_access() as f:
214
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"enterBlock","name":"' + block_name + '","time":' + ToolchainProfiler.timestamp() + '}')
215
216
ToolchainProfiler.block_stack.append(block_name)
217
218
@staticmethod
219
def remove_last_occurrence_if_exists(lst, item):
220
for i in range(len(lst)):
221
if lst[i] == item:
222
lst.pop(i)
223
return True
224
return False
225
226
@staticmethod
227
def exit_block(block_name):
228
if ToolchainProfiler.remove_last_occurrence_if_exists(ToolchainProfiler.block_stack, block_name):
229
with ToolchainProfiler.log_access() as f:
230
f.write(',\n{"pid":' + ToolchainProfiler.mypid_str + ',"subprocessPid":' + str(os.getpid()) + ',"op":"exitBlock","name":"' + block_name + '","time":' + ToolchainProfiler.timestamp() + '}')
231
232
@staticmethod
233
def exit_all_blocks():
234
for b in ToolchainProfiler.block_stack[::-1]:
235
ToolchainProfiler.exit_block(b)
236
237
class ProfileBlock(Logger):
238
def __init__(self, block_name):
239
super().__init__(block_name)
240
self.name = block_name
241
242
def __enter__(self):
243
ToolchainProfiler.enter_block(self.name)
244
245
def __exit__(self, type, value, traceback):
246
ToolchainProfiler.exit_block(self.name)
247
248
@staticmethod
249
def profile_block(block_name):
250
return ToolchainProfiler.ProfileBlock(ToolchainProfiler.escape_string(block_name))
251
252
@staticmethod
253
def profile():
254
return ToolchainProfiler.ProfileBlock(None)
255
256
@staticmethod
257
def imaginary_pid():
258
ToolchainProfiler.imaginary_pid_ -= 1
259
return ToolchainProfiler.imaginary_pid_
260
261
ToolchainProfiler.record_process_start()
262
else:
263
class ToolchainProfiler:
264
@staticmethod
265
def enter_block(block_name):
266
pass
267
268
@staticmethod
269
def exit_block(block_name):
270
pass
271
272
@staticmethod
273
def profile_block(block_name):
274
return Logger(block_name)
275
276
@staticmethod
277
def profile():
278
return Logger(None)
279
280