1# MIT License
2#
3# Copyright The SCons Foundation
4#
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:
12#
13# The above copyright notice and this permission notice shall be included
14# in all copies or substantial portions of the Software.
15#
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.
23
24"""Code for debugging SCons internal things.
25
26Shouldn't be needed by most users. Quick shortcuts:
27
28from SCons.Debug import caller_trace
29caller_trace()
30"""
31
32import atexit
33import os
34import sys
35import time
36import weakref
37import inspect
38
39# Global variable that gets set to 'True' by the Main script,
40# when the creation of class instances should get tracked.
41track_instances = False
42# List of currently tracked classes
43tracked_classes = {}
44
45def logInstanceCreation(instance, name=None):
46    if name is None:
47        name = instance.__class__.__name__
48    if name not in tracked_classes:
49        tracked_classes[name] = []
50    if hasattr(instance, '__dict__'):
51        tracked_classes[name].append(weakref.ref(instance))
52    else:
53        # weakref doesn't seem to work when the instance
54        # contains only slots...
55        tracked_classes[name].append(instance)
56
57def string_to_classes(s):
58    if s == '*':
59        return sorted(tracked_classes.keys())
60    else:
61        return s.split()
62
63def fetchLoggedInstances(classes="*"):
64    classnames = string_to_classes(classes)
65    return [(cn, len(tracked_classes[cn])) for cn in classnames]
66
67def countLoggedInstances(classes, file=sys.stdout):
68    for classname in string_to_classes(classes):
69        file.write("%s: %d\n" % (classname, len(tracked_classes[classname])))
70
71def listLoggedInstances(classes, file=sys.stdout):
72    for classname in string_to_classes(classes):
73        file.write('\n%s:\n' % classname)
74        for ref in tracked_classes[classname]:
75            if inspect.isclass(ref):
76                obj = ref()
77            else:
78                obj = ref
79            if obj is not None:
80                file.write('    %s\n' % repr(obj))
81
82def dumpLoggedInstances(classes, file=sys.stdout):
83    for classname in string_to_classes(classes):
84        file.write('\n%s:\n' % classname)
85        for ref in tracked_classes[classname]:
86            obj = ref()
87            if obj is not None:
88                file.write('    %s:\n' % obj)
89                for key, value in obj.__dict__.items():
90                    file.write('        %20s : %s\n' % (key, value))
91
92
93if sys.platform[:5] == "linux":
94    # Linux doesn't actually support memory usage stats from getrusage().
95    def memory():
96        with open('/proc/self/stat') as f:
97            mstr = f.read()
98        mstr = mstr.split()[22]
99        return int(mstr)
100elif sys.platform[:6] == 'darwin':
101    #TODO really get memory stats for OS X
102    def memory():
103        return 0
104elif sys.platform == 'win32':
105    from SCons.compat.win32 import get_peak_memory_usage
106    memory = get_peak_memory_usage
107else:
108    try:
109        import resource
110    except ImportError:
111        def memory():
112            return 0
113    else:
114        def memory():
115            res = resource.getrusage(resource.RUSAGE_SELF)
116            return res[4]
117
118
119def caller_stack():
120    """return caller's stack"""
121    import traceback
122    tb = traceback.extract_stack()
123    # strip itself and the caller from the output
124    tb = tb[:-2]
125    result = []
126    for back in tb:
127        # (filename, line number, function name, text)
128        key = back[:3]
129        result.append('%s:%d(%s)' % func_shorten(key))
130    return result
131
132caller_bases = {}
133caller_dicts = {}
134
135def caller_trace(back=0):
136    """
137    Trace caller stack and save info into global dicts, which
138    are printed automatically at the end of SCons execution.
139    """
140    global caller_bases, caller_dicts
141    import traceback
142    tb = traceback.extract_stack(limit=3+back)
143    tb.reverse()
144    callee = tb[1][:3]
145    caller_bases[callee] = caller_bases.get(callee, 0) + 1
146    for caller in tb[2:]:
147        caller = callee + caller[:3]
148        try:
149            entry = caller_dicts[callee]
150        except KeyError:
151            caller_dicts[callee] = entry = {}
152        entry[caller] = entry.get(caller, 0) + 1
153        callee = caller
154
155# print a single caller and its callers, if any
156def _dump_one_caller(key, file, level=0):
157    leader = '      '*level
158    for v,c in sorted([(-v,c) for c,v in caller_dicts[key].items()]):
159        file.write("%s  %6d %s:%d(%s)\n" % ((leader,-v) + func_shorten(c[-3:])))
160        if c in caller_dicts:
161            _dump_one_caller(c, file, level+1)
162
163# print each call tree
164def dump_caller_counts(file=sys.stdout):
165    for k in sorted(caller_bases.keys()):
166        file.write("Callers of %s:%d(%s), %d calls:\n"
167                    % (func_shorten(k) + (caller_bases[k],)))
168        _dump_one_caller(k, file)
169
170shorten_list = [
171    ( '/scons/SCons/',          1),
172    ( '/src/engine/SCons/',     1),
173    ( '/usr/lib/python',        0),
174]
175
176if os.sep != '/':
177    shorten_list = [(t[0].replace('/', os.sep), t[1]) for t in shorten_list]
178
179def func_shorten(func_tuple):
180    f = func_tuple[0]
181    for t in shorten_list:
182        i = f.find(t[0])
183        if i >= 0:
184            if t[1]:
185                i = i + len(t[0])
186            return (f[i:],)+func_tuple[1:]
187    return func_tuple
188
189
190TraceFP = {}
191if sys.platform == 'win32':
192    TraceDefault = 'con'
193else:
194    TraceDefault = '/dev/tty'
195TimeStampDefault = False
196StartTime = time.perf_counter()
197PreviousTime = StartTime
198
199def Trace(msg, tracefile=None, mode='w', tstamp=False):
200    """Write a trace message.
201
202    Write messages when debugging which do not interfere with stdout.
203    Useful in tests, which monitor stdout and would break with
204    unexpected output. Trace messages can go to the console (which is
205    opened as a file), or to a disk file; the tracefile argument persists
206    across calls unless overridden.
207
208    Args:
209        tracefile: file to write trace message to. If omitted,
210          write to the previous trace file (default: console).
211        mode: file open mode (default: 'w')
212        tstamp: write relative timestamps with trace. Outputs time since
213          scons was started, and time since last trace (default: False)
214
215    """
216    global TraceDefault
217    global TimeStampDefault
218    global PreviousTime
219
220    def trace_cleanup(traceFP):
221        traceFP.close()
222
223    if tracefile is None:
224        tracefile = TraceDefault
225    else:
226        TraceDefault = tracefile
227    if not tstamp:
228        tstamp = TimeStampDefault
229    else:
230        TimeStampDefault = tstamp
231    try:
232        fp = TraceFP[tracefile]
233    except KeyError:
234        try:
235            fp = TraceFP[tracefile] = open(tracefile, mode)
236            atexit.register(trace_cleanup, fp)
237        except TypeError:
238            # Assume we were passed an open file pointer.
239            fp = tracefile
240    if tstamp:
241        now = time.perf_counter()
242        fp.write('%8.4f %8.4f:  ' % (now - StartTime, now - PreviousTime))
243        PreviousTime = now
244    fp.write(msg)
245    fp.flush()
246
247# Local Variables:
248# tab-width:4
249# indent-tabs-mode:nil
250# End:
251# vim: set expandtab tabstop=4 shiftwidth=4:
252