[ci skip] Add note that this change may break SetOption() + ninja usage with fix
[scons.git] / SCons / Debug.py
blob9a6433ee895873b80d663e3f22336d0260fabe43
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """Code for debugging SCons internal things.
26 Shouldn't be needed by most users. Quick shortcuts::
28 from SCons.Debug import caller_trace
29 caller_trace()
30 """
32 import atexit
33 import os
34 import sys
35 import time
36 import weakref
37 import inspect
39 # Global variable that gets set to 'True' by the Main script,
40 # when the creation of class instances should get tracked.
41 track_instances = False
42 # List of currently tracked classes
43 tracked_classes = {}
44 # Global variable that gets set to 'True' by the Main script
45 # when SConscript call tracing should be enabled.
46 sconscript_trace = False
48 def logInstanceCreation(instance, name=None) -> None:
49 if name is None:
50 name = instance.__class__.__name__
51 if name not in tracked_classes:
52 tracked_classes[name] = []
53 if hasattr(instance, '__dict__'):
54 tracked_classes[name].append(weakref.ref(instance))
55 else:
56 # weakref doesn't seem to work when the instance
57 # contains only slots...
58 tracked_classes[name].append(instance)
60 def string_to_classes(s):
61 if s == '*':
62 return sorted(tracked_classes.keys())
63 else:
64 return s.split()
66 def fetchLoggedInstances(classes: str="*"):
67 classnames = string_to_classes(classes)
68 return [(cn, len(tracked_classes[cn])) for cn in classnames]
70 def countLoggedInstances(classes, file=sys.stdout) -> None:
71 for classname in string_to_classes(classes):
72 file.write("%s: %d\n" % (classname, len(tracked_classes[classname])))
74 def listLoggedInstances(classes, file=sys.stdout) -> None:
75 for classname in string_to_classes(classes):
76 file.write('\n%s:\n' % classname)
77 for ref in tracked_classes[classname]:
78 if inspect.isclass(ref):
79 obj = ref()
80 else:
81 obj = ref
82 if obj is not None:
83 file.write(' %s\n' % repr(obj))
85 def dumpLoggedInstances(classes, file=sys.stdout) -> None:
86 for classname in string_to_classes(classes):
87 file.write('\n%s:\n' % classname)
88 for ref in tracked_classes[classname]:
89 obj = ref()
90 if obj is not None:
91 file.write(' %s:\n' % obj)
92 for key, value in obj.__dict__.items():
93 file.write(' %20s : %s\n' % (key, value))
96 if sys.platform[:5] == "linux":
97 # Linux doesn't actually support memory usage stats from getrusage().
98 def memory() -> int:
99 with open('/proc/self/stat') as f:
100 mstr = f.read()
101 mstr = mstr.split()[22]
102 return int(mstr)
103 elif sys.platform[:6] == 'darwin':
104 #TODO really get memory stats for OS X
105 def memory() -> int:
106 return 0
107 elif sys.platform == 'win32':
108 from SCons.compat.win32 import get_peak_memory_usage
109 memory = get_peak_memory_usage
110 else:
111 try:
112 import resource
113 except ImportError:
114 def memory() -> int:
115 return 0
116 else:
117 def memory() -> int:
118 res = resource.getrusage(resource.RUSAGE_SELF)
119 return res[4]
122 def caller_stack():
123 """return caller's stack"""
124 import traceback
125 tb = traceback.extract_stack()
126 # strip itself and the caller from the output
127 tb = tb[:-2]
128 result = []
129 for back in tb:
130 # (filename, line number, function name, text)
131 key = back[:3]
132 result.append('%s:%d(%s)' % func_shorten(key))
133 return result
135 caller_bases = {}
136 caller_dicts = {}
138 def caller_trace(back: int=0) -> None:
140 Trace caller stack and save info into global dicts, which
141 are printed automatically at the end of SCons execution.
143 global caller_bases, caller_dicts
144 import traceback
145 tb = traceback.extract_stack(limit=3+back)
146 tb.reverse()
147 callee = tb[1][:3]
148 caller_bases[callee] = caller_bases.get(callee, 0) + 1
149 for caller in tb[2:]:
150 caller = callee + caller[:3]
151 try:
152 entry = caller_dicts[callee]
153 except KeyError:
154 caller_dicts[callee] = entry = {}
155 entry[caller] = entry.get(caller, 0) + 1
156 callee = caller
158 # print a single caller and its callers, if any
159 def _dump_one_caller(key, file, level: int=0) -> None:
160 leader = ' '*level
161 for v,c in sorted([(-v,c) for c,v in caller_dicts[key].items()]):
162 file.write("%s %6d %s:%d(%s)\n" % ((leader,-v) + func_shorten(c[-3:])))
163 if c in caller_dicts:
164 _dump_one_caller(c, file, level+1)
166 # print each call tree
167 def dump_caller_counts(file=sys.stdout) -> None:
168 for k in sorted(caller_bases.keys()):
169 file.write("Callers of %s:%d(%s), %d calls:\n"
170 % (func_shorten(k) + (caller_bases[k],)))
171 _dump_one_caller(k, file)
173 shorten_list = [
174 ( '/scons/SCons/', 1),
175 ( '/src/engine/SCons/', 1),
176 ( '/usr/lib/python', 0),
179 if os.sep != '/':
180 shorten_list = [(t[0].replace('/', os.sep), t[1]) for t in shorten_list]
182 def func_shorten(func_tuple):
183 f = func_tuple[0]
184 for t in shorten_list:
185 i = f.find(t[0])
186 if i >= 0:
187 if t[1]:
188 i = i + len(t[0])
189 return (f[i:],)+func_tuple[1:]
190 return func_tuple
193 TraceFP = {}
194 if sys.platform == 'win32':
195 TraceDefault = 'con'
196 else:
197 TraceDefault = '/dev/tty'
198 TimeStampDefault = False
199 StartTime = time.perf_counter()
200 PreviousTime = StartTime
202 def Trace(msg, tracefile=None, mode: str='w', tstamp: bool=False) -> None:
203 """Write a trace message.
205 Write messages when debugging which do not interfere with stdout.
206 Useful in tests, which monitor stdout and would break with
207 unexpected output. Trace messages can go to the console (which is
208 opened as a file), or to a disk file; the tracefile argument persists
209 across calls unless overridden.
211 Args:
212 tracefile: file to write trace message to. If omitted,
213 write to the previous trace file (default: console).
214 mode: file open mode (default: 'w')
215 tstamp: write relative timestamps with trace. Outputs time since
216 scons was started, and time since last trace (default: False)
219 global TraceDefault
220 global TimeStampDefault
221 global PreviousTime
223 def trace_cleanup(traceFP) -> None:
224 traceFP.close()
226 if tracefile is None:
227 tracefile = TraceDefault
228 else:
229 TraceDefault = tracefile
230 if not tstamp:
231 tstamp = TimeStampDefault
232 else:
233 TimeStampDefault = tstamp
234 try:
235 fp = TraceFP[tracefile]
236 except KeyError:
237 try:
238 fp = TraceFP[tracefile] = open(tracefile, mode)
239 atexit.register(trace_cleanup, fp)
240 except TypeError:
241 # Assume we were passed an open file pointer.
242 fp = tracefile
243 if tstamp:
244 now = time.perf_counter()
245 fp.write('%8.4f %8.4f: ' % (now - StartTime, now - PreviousTime))
246 PreviousTime = now
247 fp.write(msg)
248 fp.flush()
250 # Local Variables:
251 # tab-width:4
252 # indent-tabs-mode:nil
253 # End:
254 # vim: set expandtab tabstop=4 shiftwidth=4: