fa04d3725aa5227db1f345b783ab7a42e37bc6c3
[scons.git] / src / script / scons-time.py
1 #!/usr/bin/env python
2 #
3 # scons-time - run SCons timings and collect statistics
4 #
5 # A script for running a configuration through SCons with a standard
6 # set of invocations to collect timing and memory statistics and to
7 # capture the results in a consistent set of output files for display
8 # and analysis.
9 #
10
11 #
12 # __COPYRIGHT__
13 #
14 # Permission is hereby granted, free of charge, to any person obtaining
15 # a copy of this software and associated documentation files (the
16 # "Software"), to deal in the Software without restriction, including
17 # without limitation the rights to use, copy, modify, merge, publish,
18 # distribute, sublicense, and/or sell copies of the Software, and to
19 # permit persons to whom the Software is furnished to do so, subject to
20 # the following conditions:
21 #
22 # The above copyright notice and this permission notice shall be included
23 # in all copies or substantial portions of the Software.
24 #
25 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
27 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 #
33 from __future__ import nested_scopes
34 from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
35
36 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
37
38 import getopt
39 import glob
40 import os
41 import os.path
42 import re
43 import shutil
44 import sys
45 import tempfile
46 import time
47
48 try:
49     False
50 except NameError:
51     # Pre-2.2 Python has no False keyword.
52     False = not 1
53
54 try:
55     True
56 except NameError:
57     # Pre-2.2 Python has no True keyword.
58     True = not 0
59
60 try:
61     sorted
62 except NameError:
63     # Pre-2.4 Python has no sorted() function.
64     #
65     # The pre-2.4 Python list.sort() method does not support
66     # list.sort(key=) nor list.sort(reverse=) keyword arguments, so
67     # we must implement the functionality of those keyword arguments
68     # by hand instead of passing them to list.sort().
69     def sorted(iterable, cmp=None, key=None, reverse=False):
70         if key is not None:
71             result = [(key(x), x) for x in iterable]
72         else:
73             result = iterable[:]
74         if cmp is None:
75             # Pre-2.3 Python does not support list.sort(None).
76             result.sort()
77         else:
78             result.sort(cmp)
79         if key is not None:
80             result = [t1 for t0,t1 in result]
81         if reverse:
82             result.reverse()
83         return result
84
85 def make_temp_file(**kw):
86     try:
87         result = tempfile.mktemp(**kw)
88         try:
89             result = os.path.realpath(result)
90         except AttributeError:
91             # Python 2.1 has no os.path.realpath() method.
92             pass
93     except TypeError:
94         try:
95             save_template = tempfile.template
96             prefix = kw['prefix']
97             del kw['prefix']
98             tempfile.template = prefix
99             result = tempfile.mktemp(**kw)
100         finally:
101             tempfile.template = save_template
102     return result
103
104 def HACK_for_exec(cmd, *args):
105     '''
106     For some reason, Python won't allow an exec() within a function
107     that also declares an internal function (including lambda functions).
108     This function is a hack that calls exec() in a function with no
109     internal functions.
110     '''
111     if not args:          exec(cmd)
112     elif len(args) == 1:  exec cmd in args[0]
113     else:                 exec cmd in args[0], args[1]
114
115 class Plotter:
116     def increment_size(self, largest):
117         """
118         Return the size of each horizontal increment line for a specified
119         maximum value.  This returns a value that will provide somewhere
120         between 5 and 9 horizontal lines on the graph, on some set of
121         boundaries that are multiples of 10/100/1000/etc.
122         """
123         i = largest / 5
124         if not i:
125             return largest
126         multiplier = 1
127         while i >= 10:
128             i = i / 10
129             multiplier = multiplier * 10
130         return i * multiplier
131
132     def max_graph_value(self, largest):
133         # Round up to next integer.
134         largest = int(largest) + 1
135         increment = self.increment_size(largest)
136         return ((largest + increment - 1) / increment) * increment
137
138 class Line:
139     def __init__(self, points, type, title, label, comment, fmt="%s %s"):
140         self.points = points
141         self.type = type
142         self.title = title
143         self.label = label
144         self.comment = comment
145         self.fmt = fmt
146
147     def print_label(self, inx, x, y):
148         if self.label:
149             print 'set label %s "%s" at %s,%s right' % (inx, self.label, x, y)
150
151     def plot_string(self):
152         if self.title:
153             title_string = 'title "%s"' % self.title
154         else:
155             title_string = 'notitle'
156         return "'-' %s with lines lt %s" % (title_string, self.type)
157
158     def print_points(self, fmt=None):
159         if fmt is None:
160             fmt = self.fmt
161         if self.comment:
162             print '# %s' % self.comment
163         for x, y in self.points:
164             # If y is None, it usually represents some kind of break
165             # in the line's index number.  We might want to represent
166             # this some way rather than just drawing the line straight
167             # between the two points on either side.
168             if not y is None:
169                 print fmt % (x, y)
170         print 'e'
171
172     def get_x_values(self):
173         return [ p[0] for p in self.points ]
174
175     def get_y_values(self):
176         return [ p[1] for p in self.points ]
177
178 class Gnuplotter(Plotter):
179
180     def __init__(self, title, key_location):
181         self.lines = []
182         self.title = title
183         self.key_location = key_location
184
185     def line(self, points, type, title=None, label=None, comment=None, fmt='%s %s'):
186         if points:
187             line = Line(points, type, title, label, comment, fmt)
188             self.lines.append(line)
189
190     def plot_string(self, line):
191         return line.plot_string()
192
193     def vertical_bar(self, x, type, label, comment):
194         if self.get_min_x() <= x and x <= self.get_max_x():
195             points = [(x, 0), (x, self.max_graph_value(self.get_max_y()))]
196             self.line(points, type, label, comment)
197
198     def get_all_x_values(self):
199         result = []
200         for line in self.lines:
201             result.extend(line.get_x_values())
202         return [r for r in result if not r is None]
203
204     def get_all_y_values(self):
205         result = []
206         for line in self.lines:
207             result.extend(line.get_y_values())
208         return [r for r in result if not r is None]
209
210     def get_min_x(self):
211         try:
212             return self.min_x
213         except AttributeError:
214             try:
215                 self.min_x = min(self.get_all_x_values())
216             except ValueError:
217                 self.min_x = 0
218             return self.min_x
219
220     def get_max_x(self):
221         try:
222             return self.max_x
223         except AttributeError:
224             try:
225                 self.max_x = max(self.get_all_x_values())
226             except ValueError:
227                 self.max_x = 0
228             return self.max_x
229
230     def get_min_y(self):
231         try:
232             return self.min_y
233         except AttributeError:
234             try:
235                 self.min_y = min(self.get_all_y_values())
236             except ValueError:
237                 self.min_y = 0
238             return self.min_y
239
240     def get_max_y(self):
241         try:
242             return self.max_y
243         except AttributeError:
244             try:
245                 self.max_y = max(self.get_all_y_values())
246             except ValueError:
247                 self.max_y = 0
248             return self.max_y
249
250     def draw(self):
251
252         if not self.lines:
253             return
254
255         if self.title:
256             print 'set title "%s"' % self.title
257         print 'set key %s' % self.key_location
258
259         min_y = self.get_min_y()
260         max_y = self.max_graph_value(self.get_max_y())
261         incr = (max_y - min_y) / 10.0
262         start = min_y + (max_y / 2.0) + (2.0 * incr)
263         position = [ start - (i * incr) for i in range(5) ]
264
265         inx = 1
266         for line in self.lines:
267             line.print_label(inx, line.points[0][0]-1,
268                              position[(inx-1) % len(position)])
269             inx += 1
270
271         plot_strings = [ self.plot_string(l) for l in self.lines ]
272         print 'plot ' + ', \\\n     '.join(plot_strings)
273
274         for line in self.lines:
275             line.print_points()
276
277
278
279 def untar(fname):
280     import tarfile
281     tar = tarfile.open(name=fname, mode='r')
282     for tarinfo in tar:
283         tar.extract(tarinfo)
284     tar.close()
285
286 def unzip(fname):
287     import zipfile
288     zf = zipfile.ZipFile(fname, 'r')
289     for name in zf.namelist():
290         dir = os.path.dirname(name)
291         try:
292             os.makedirs(dir)
293         except:
294             pass
295         open(name, 'w').write(zf.read(name))
296
297 def read_tree(dir):
298     def read_files(arg, dirname, fnames):
299         for fn in fnames:
300             fn = os.path.join(dirname, fn)
301             if os.path.isfile(fn):
302                 open(fn, 'rb').read()
303     os.path.walk('.', read_files, None)
304
305 def redirect_to_file(command, log):
306     return '%s > %s 2>&1' % (command, log)
307
308 def tee_to_file(command, log):
309     return '%s 2>&1 | tee %s' % (command, log)
310
311
312     
313 class SConsTimer:
314     """
315     Usage: scons-time SUBCOMMAND [ARGUMENTS]
316     Type "scons-time help SUBCOMMAND" for help on a specific subcommand.
317
318     Available subcommands:
319         func            Extract test-run data for a function
320         help            Provides help
321         mem             Extract --debug=memory data from test runs
322         obj             Extract --debug=count data from test runs
323         time            Extract --debug=time data from test runs
324         run             Runs a test configuration
325     """
326
327     name = 'scons-time'
328     name_spaces = ' '*len(name)
329
330     def makedict(**kw):
331         return kw
332
333     default_settings = makedict(
334         aegis               = 'aegis',
335         aegis_project       = None,
336         chdir               = None,
337         config_file         = None,
338         initial_commands    = [],
339         key_location        = 'bottom left',
340         orig_cwd            = os.getcwd(),
341         outdir              = None,
342         prefix              = '',
343         python              = '"%s"' % sys.executable,
344         redirect            = redirect_to_file,
345         scons               = None,
346         scons_flags         = '--debug=count --debug=memory --debug=time --debug=memoizer',
347         scons_lib_dir       = None,
348         scons_wrapper       = None,
349         startup_targets     = '--help',
350         subdir              = None,
351         subversion_url      = None,
352         svn                 = 'svn',
353         svn_co_flag         = '-q',
354         tar                 = 'tar',
355         targets             = '',
356         targets0            = None,
357         targets1            = None,
358         targets2            = None,
359         title               = None,
360         unzip               = 'unzip',
361         verbose             = False,
362         vertical_bars       = [],
363
364         unpack_map = {
365             '.tar.gz'       : (untar,   '%(tar)s xzf %%s'),
366             '.tgz'          : (untar,   '%(tar)s xzf %%s'),
367             '.tar'          : (untar,   '%(tar)s xf %%s'),
368             '.zip'          : (unzip,   '%(unzip)s %%s'),
369         },
370     )
371
372     run_titles = [
373         'Startup',
374         'Full build',
375         'Up-to-date build',
376     ]
377
378     run_commands = [
379         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof0)s %(targets0)s',
380         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof1)s %(targets1)s',
381         '%(python)s %(scons_wrapper)s %(scons_flags)s --profile=%(prof2)s %(targets2)s',
382     ]
383
384     stages = [
385         'pre-read',
386         'post-read',
387         'pre-build',
388         'post-build',
389     ]
390
391     stage_strings = {
392         'pre-read'      : 'Memory before reading SConscript files:',
393         'post-read'     : 'Memory after reading SConscript files:',
394         'pre-build'     : 'Memory before building targets:',
395         'post-build'    : 'Memory after building targets:',
396     }
397
398     memory_string_all = 'Memory '
399
400     default_stage = stages[-1]
401
402     time_strings = {
403         'total'         : 'Total build time',
404         'SConscripts'   : 'Total SConscript file execution time',
405         'SCons'         : 'Total SCons execution time',
406         'commands'      : 'Total command execution time',
407     }
408     
409     time_string_all = 'Total .* time'
410
411     #
412
413     def __init__(self):
414         self.__dict__.update(self.default_settings)
415
416     # Functions for displaying and executing commands.
417
418     def subst(self, x, dictionary):
419         try:
420             return x % dictionary
421         except TypeError:
422             # x isn't a string (it's probably a Python function),
423             # so just return it.
424             return x
425
426     def subst_variables(self, command, dictionary):
427         """
428         Substitutes (via the format operator) the values in the specified
429         dictionary into the specified command.
430
431         The command can be an (action, string) tuple.  In all cases, we
432         perform substitution on strings and don't worry if something isn't
433         a string.  (It's probably a Python function to be executed.)
434         """
435         try:
436             command + ''
437         except TypeError:
438             action = command[0]
439             string = command[1]
440             args = command[2:]
441         else:
442             action = command
443             string = action
444             args = (())
445         action = self.subst(action, dictionary)
446         string = self.subst(string, dictionary)
447         return (action, string, args)
448
449     def _do_not_display(self, msg, *args):
450         pass
451
452     def display(self, msg, *args):
453         """
454         Displays the specified message.
455
456         Each message is prepended with a standard prefix of our name
457         plus the time.
458         """
459         if callable(msg):
460             msg = msg(*args)
461         else:
462             msg = msg % args
463         if msg is None:
464             return
465         fmt = '%s[%s]: %s\n'
466         sys.stdout.write(fmt % (self.name, time.strftime('%H:%M:%S'), msg))
467
468     def _do_not_execute(self, action, *args):
469         pass
470
471     def execute(self, action, *args):
472         """
473         Executes the specified action.
474
475         The action is called if it's a callable Python function, and
476         otherwise passed to os.system().
477         """
478         if callable(action):
479             action(*args)
480         else:
481             os.system(action % args)
482
483     def run_command_list(self, commands, dict):
484         """
485         Executes a list of commands, substituting values from the
486         specified dictionary.
487         """
488         commands = [ self.subst_variables(c, dict) for c in commands ]
489         for action, string, args in commands:
490             self.display(string, *args)
491             sys.stdout.flush()
492             status = self.execute(action, *args)
493             if status:
494                 sys.exit(status)
495
496     def log_display(self, command, log):
497         command = self.subst(command, self.__dict__)
498         if log:
499             command = self.redirect(command, log)
500         return command
501
502     def log_execute(self, command, log):
503         command = self.subst(command, self.__dict__)
504         output = os.popen(command).read()
505         if self.verbose:
506             sys.stdout.write(output)
507         open(log, 'wb').write(output)
508
509     #
510
511     def archive_splitext(self, path):
512         """
513         Splits an archive name into a filename base and extension.
514
515         This is like os.path.splitext() (which it calls) except that it
516         also looks for '.tar.gz' and treats it as an atomic extensions.
517         """
518         if path.endswith('.tar.gz'):
519             return path[:-7], path[-7:]
520         else:
521             return os.path.splitext(path)
522
523     def args_to_files(self, args, tail=None):
524         """
525         Takes a list of arguments, expands any glob patterns, and
526         returns the last "tail" files from the list.
527         """
528         files = []
529         for a in args:
530             files.extend(sorted(glob.glob(a)))
531
532         if tail:
533             files = files[-tail:]
534
535         return files
536
537     def ascii_table(self, files, columns,
538                     line_function, file_function=lambda x: x,
539                     *args, **kw):
540
541         header_fmt = ' '.join(['%12s'] * len(columns))
542         line_fmt = header_fmt + '    %s'
543
544         print header_fmt % columns
545
546         for file in files:
547             t = line_function(file, *args, **kw)
548             if t is None:
549                 t = []
550             diff = len(columns) - len(t)
551             if diff > 0:
552                 t += [''] * diff
553             t.append(file_function(file))
554             print line_fmt % tuple(t)
555
556     def collect_results(self, files, function, *args, **kw):
557         results = {}
558
559         for file in files:
560             base = os.path.splitext(file)[0]
561             run, index = base.split('-')[-2:]
562
563             run = int(run)
564             index = int(index)
565
566             value = function(file, *args, **kw)
567
568             try:
569                 r = results[index]
570             except KeyError:
571                 r = []
572                 results[index] = r
573             r.append((run, value))
574
575         return results
576
577     def doc_to_help(self, obj):
578         """
579         Translates an object's __doc__ string into help text.
580
581         This strips a consistent number of spaces from each line in the
582         help text, essentially "outdenting" the text to the left-most
583         column.
584         """
585         doc = obj.__doc__
586         if doc is None:
587             return ''
588         return self.outdent(doc)
589
590     def find_next_run_number(self, dir, prefix):
591         """
592         Returns the next run number in a directory for the specified prefix.
593
594         Examines the contents the specified directory for files with the
595         specified prefix, extracts the run numbers from each file name,
596         and returns the next run number after the largest it finds.
597         """
598         x = re.compile(re.escape(prefix) + '-([0-9]+).*')
599         matches = [x.match(e) for e in os.listdir(dir)]
600         matches = [_f for _f in matches if _f]
601         if not matches:
602             return 0
603         run_numbers = [int(m.group(1)) for m in matches]
604         return int(max(run_numbers)) + 1
605
606     def gnuplot_results(self, results, fmt='%s %.3f'):
607         """
608         Prints out a set of results in Gnuplot format.
609         """
610         gp = Gnuplotter(self.title, self.key_location)
611
612         for i in sorted(results.keys()):
613             try:
614                 t = self.run_titles[i]
615             except IndexError:
616                 t = '??? %s ???' % i
617             results[i].sort()
618             gp.line(results[i], i+1, t, None, t, fmt=fmt)
619
620         for bar_tuple in self.vertical_bars:
621             try:
622                 x, type, label, comment = bar_tuple
623             except ValueError:
624                 x, type, label = bar_tuple
625                 comment = label
626             gp.vertical_bar(x, type, label, comment)
627
628         gp.draw()
629
630     def logfile_name(self, invocation):
631         """
632         Returns the absolute path of a log file for the specificed
633         invocation number.
634         """
635         name = self.prefix_run + '-%d.log' % invocation
636         return os.path.join(self.outdir, name)
637
638     def outdent(self, s):
639         """
640         Strip as many spaces from each line as are found at the beginning
641         of the first line in the list.
642         """
643         lines = s.split('\n')
644         if lines[0] == '':
645             lines = lines[1:]
646         spaces = re.match(' *', lines[0]).group(0)
647         def strip_initial_spaces(l, s=spaces):
648             if l.startswith(spaces):
649                 l = l[len(spaces):]
650             return l
651         return '\n'.join([ strip_initial_spaces(l) for l in lines ]) + '\n'
652
653     def profile_name(self, invocation):
654         """
655         Returns the absolute path of a profile file for the specified
656         invocation number.
657         """
658         name = self.prefix_run + '-%d.prof' % invocation
659         return os.path.join(self.outdir, name)
660
661     def set_env(self, key, value):
662         os.environ[key] = value
663
664     #
665
666     def get_debug_times(self, file, time_string=None):
667         """
668         Fetch times from the --debug=time strings in the specified file.
669         """
670         if time_string is None:
671             search_string = self.time_string_all
672         else:
673             search_string = time_string
674         contents = open(file).read()
675         if not contents:
676             sys.stderr.write('file %s has no contents!\n' % repr(file))
677             return None
678         result = re.findall(r'%s: ([\d\.]*)' % search_string, contents)[-4:]
679         result = [ float(r) for r in result ]
680         if not time_string is None:
681             try:
682                 result = result[0]
683             except IndexError:
684                 sys.stderr.write('file %s has no results!\n' % repr(file))
685                 return None
686         return result
687
688     def get_function_profile(self, file, function):
689         """
690         Returns the file, line number, function name, and cumulative time.
691         """
692         try:
693             import pstats
694         except ImportError, e:
695             sys.stderr.write('%s: func: %s\n' % (self.name, e))
696             sys.stderr.write('%s  This version of Python is missing the profiler.\n' % self.name_spaces)
697             sys.stderr.write('%s  Cannot use the "func" subcommand.\n' % self.name_spaces)
698             sys.exit(1)
699         statistics = pstats.Stats(file).stats
700         matches = [ e for e in statistics.items() if e[0][2] == function ]
701         r = matches[0]
702         return r[0][0], r[0][1], r[0][2], r[1][3]
703
704     def get_function_time(self, file, function):
705         """
706         Returns just the cumulative time for the specified function.
707         """
708         return self.get_function_profile(file, function)[3]
709
710     def get_memory(self, file, memory_string=None):
711         """
712         Returns a list of integers of the amount of memory used.  The
713         default behavior is to return all the stages.
714         """
715         if memory_string is None:
716             search_string = self.memory_string_all
717         else:
718             search_string = memory_string
719         lines = open(file).readlines()
720         lines = [ l for l in lines if l.startswith(search_string) ][-4:]
721         result = [ int(l.split()[-1]) for l in lines[-4:] ]
722         if len(result) == 1:
723             result = result[0]
724         return result
725
726     def get_object_counts(self, file, object_name, index=None):
727         """
728         Returns the counts of the specified object_name.
729         """
730         object_string = ' ' + object_name + '\n'
731         lines = open(file).readlines()
732         line = [ l for l in lines if l.endswith(object_string) ][0]
733         result = [ int(field) for field in line.split()[:4] ]
734         if index is not None:
735             result = result[index]
736         return result
737
738     #
739
740     command_alias = {}
741
742     def execute_subcommand(self, argv):
743         """
744         Executes the do_*() function for the specified subcommand (argv[0]).
745         """
746         if not argv:
747             return
748         cmdName = self.command_alias.get(argv[0], argv[0])
749         try:
750             func = getattr(self, 'do_' + cmdName)
751         except AttributeError:
752             return self.default(argv)
753         try:
754             return func(argv)
755         except TypeError, e:
756             sys.stderr.write("%s %s: %s\n" % (self.name, cmdName, e))
757             import traceback
758             traceback.print_exc(file=sys.stderr)
759             sys.stderr.write("Try '%s help %s'\n" % (self.name, cmdName))
760
761     def default(self, argv):
762         """
763         The default behavior for an unknown subcommand.  Prints an
764         error message and exits.
765         """
766         sys.stderr.write('%s: Unknown subcommand "%s".\n' % (self.name, argv[0]))
767         sys.stderr.write('Type "%s help" for usage.\n' % self.name)
768         sys.exit(1)
769
770     #
771
772     def do_help(self, argv):
773         """
774         """
775         if argv[1:]:
776             for arg in argv[1:]:
777                 try:
778                     func = getattr(self, 'do_' + arg)
779                 except AttributeError:
780                     sys.stderr.write('%s: No help for "%s"\n' % (self.name, arg))
781                 else:
782                     try:
783                         help = getattr(self, 'help_' + arg)
784                     except AttributeError:
785                         sys.stdout.write(self.doc_to_help(func))
786                         sys.stdout.flush()
787                     else:
788                         help()
789         else:
790             doc = self.doc_to_help(self.__class__)
791             if doc:
792                 sys.stdout.write(doc)
793             sys.stdout.flush()
794             return None
795
796     #
797
798     def help_func(self):
799         help = """\
800         Usage: scons-time func [OPTIONS] FILE [...]
801
802           -C DIR, --chdir=DIR           Change to DIR before looking for files
803           -f FILE, --file=FILE          Read configuration from specified FILE
804           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
805           --func=NAME, --function=NAME  Report time for function NAME
806           -h, --help                    Print this help and exit
807           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
808           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
809           --title=TITLE                 Specify the output plot TITLE
810         """
811         sys.stdout.write(self.outdent(help))
812         sys.stdout.flush()
813
814     def do_func(self, argv):
815         """
816         """
817         format = 'ascii'
818         function_name = '_main'
819         tail = None
820
821         short_opts = '?C:f:hp:t:'
822
823         long_opts = [
824             'chdir=',
825             'file=',
826             'fmt=',
827             'format=',
828             'func=',
829             'function=',
830             'help',
831             'prefix=',
832             'tail=',
833             'title=',
834         ]
835
836         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
837
838         for o, a in opts:
839             if o in ('-C', '--chdir'):
840                 self.chdir = a
841             elif o in ('-f', '--file'):
842                 self.config_file = a
843             elif o in ('--fmt', '--format'):
844                 format = a
845             elif o in ('--func', '--function'):
846                 function_name = a
847             elif o in ('-?', '-h', '--help'):
848                 self.do_help(['help', 'func'])
849                 sys.exit(0)
850             elif o in ('--max',):
851                 max_time = int(a)
852             elif o in ('-p', '--prefix'):
853                 self.prefix = a
854             elif o in ('-t', '--tail'):
855                 tail = int(a)
856             elif o in ('--title',):
857                 self.title = a
858
859         if self.config_file:
860             exec open(self.config_file, 'rU').read() in self.__dict__
861
862         if self.chdir:
863             os.chdir(self.chdir)
864
865         if not args:
866
867             pattern = '%s*.prof' % self.prefix
868             args = self.args_to_files([pattern], tail)
869
870             if not args:
871                 if self.chdir:
872                     directory = self.chdir
873                 else:
874                     directory = os.getcwd()
875
876                 sys.stderr.write('%s: func: No arguments specified.\n' % self.name)
877                 sys.stderr.write('%s  No %s*.prof files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
878                 sys.stderr.write('%s  Type "%s help func" for help.\n' % (self.name_spaces, self.name))
879                 sys.exit(1)
880
881         else:
882
883             args = self.args_to_files(args, tail)
884
885         cwd_ = os.getcwd() + os.sep
886
887         if format == 'ascii':
888
889             for file in args:
890                 try:
891                     f, line, func, time = \
892                             self.get_function_profile(file, function_name)
893                 except ValueError, e:
894                     sys.stderr.write("%s: func: %s: %s\n" %
895                                      (self.name, file, e))
896                 else:
897                     if f.startswith(cwd_):
898                         f = f[len(cwd_):]
899                     print "%.3f %s:%d(%s)" % (time, f, line, func)
900
901         elif format == 'gnuplot':
902
903             results = self.collect_results(args, self.get_function_time,
904                                            function_name)
905
906             self.gnuplot_results(results)
907
908         else:
909
910             sys.stderr.write('%s: func: Unknown format "%s".\n' % (self.name, format))
911             sys.exit(1)
912
913     #
914
915     def help_mem(self):
916         help = """\
917         Usage: scons-time mem [OPTIONS] FILE [...]
918
919           -C DIR, --chdir=DIR           Change to DIR before looking for files
920           -f FILE, --file=FILE          Read configuration from specified FILE
921           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
922           -h, --help                    Print this help and exit
923           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
924           --stage=STAGE                 Plot memory at the specified stage:
925                                           pre-read, post-read, pre-build,
926                                           post-build (default: post-build)
927           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
928           --title=TITLE                 Specify the output plot TITLE
929         """
930         sys.stdout.write(self.outdent(help))
931         sys.stdout.flush()
932
933     def do_mem(self, argv):
934
935         format = 'ascii'
936         logfile_path = lambda x: x
937         stage = self.default_stage
938         tail = None
939
940         short_opts = '?C:f:hp:t:'
941
942         long_opts = [
943             'chdir=',
944             'file=',
945             'fmt=',
946             'format=',
947             'help',
948             'prefix=',
949             'stage=',
950             'tail=',
951             'title=',
952         ]
953
954         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
955
956         for o, a in opts:
957             if o in ('-C', '--chdir'):
958                 self.chdir = a
959             elif o in ('-f', '--file'):
960                 self.config_file = a
961             elif o in ('--fmt', '--format'):
962                 format = a
963             elif o in ('-?', '-h', '--help'):
964                 self.do_help(['help', 'mem'])
965                 sys.exit(0)
966             elif o in ('-p', '--prefix'):
967                 self.prefix = a
968             elif o in ('--stage',):
969                 if not a in self.stages:
970                     sys.stderr.write('%s: mem: Unrecognized stage "%s".\n' % (self.name, a))
971                     sys.exit(1)
972                 stage = a
973             elif o in ('-t', '--tail'):
974                 tail = int(a)
975             elif o in ('--title',):
976                 self.title = a
977
978         if self.config_file:
979             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
980
981         if self.chdir:
982             os.chdir(self.chdir)
983             logfile_path = lambda x: os.path.join(self.chdir, x)
984
985         if not args:
986
987             pattern = '%s*.log' % self.prefix
988             args = self.args_to_files([pattern], tail)
989
990             if not args:
991                 if self.chdir:
992                     directory = self.chdir
993                 else:
994                     directory = os.getcwd()
995
996                 sys.stderr.write('%s: mem: No arguments specified.\n' % self.name)
997                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
998                 sys.stderr.write('%s  Type "%s help mem" for help.\n' % (self.name_spaces, self.name))
999                 sys.exit(1)
1000
1001         else:
1002
1003             args = self.args_to_files(args, tail)
1004
1005         cwd_ = os.getcwd() + os.sep
1006
1007         if format == 'ascii':
1008
1009             self.ascii_table(args, tuple(self.stages), self.get_memory, logfile_path)
1010
1011         elif format == 'gnuplot':
1012
1013             results = self.collect_results(args, self.get_memory,
1014                                            self.stage_strings[stage])
1015
1016             self.gnuplot_results(results)
1017
1018         else:
1019
1020             sys.stderr.write('%s: mem: Unknown format "%s".\n' % (self.name, format))
1021             sys.exit(1)
1022
1023         return 0
1024
1025     #
1026
1027     def help_obj(self):
1028         help = """\
1029         Usage: scons-time obj [OPTIONS] OBJECT FILE [...]
1030
1031           -C DIR, --chdir=DIR           Change to DIR before looking for files
1032           -f FILE, --file=FILE          Read configuration from specified FILE
1033           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
1034           -h, --help                    Print this help and exit
1035           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1036           --stage=STAGE                 Plot memory at the specified stage:
1037                                           pre-read, post-read, pre-build,
1038                                           post-build (default: post-build)
1039           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
1040           --title=TITLE                 Specify the output plot TITLE
1041         """
1042         sys.stdout.write(self.outdent(help))
1043         sys.stdout.flush()
1044
1045     def do_obj(self, argv):
1046
1047         format = 'ascii'
1048         logfile_path = lambda x: x
1049         stage = self.default_stage
1050         tail = None
1051
1052         short_opts = '?C:f:hp:t:'
1053
1054         long_opts = [
1055             'chdir=',
1056             'file=',
1057             'fmt=',
1058             'format=',
1059             'help',
1060             'prefix=',
1061             'stage=',
1062             'tail=',
1063             'title=',
1064         ]
1065
1066         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1067
1068         for o, a in opts:
1069             if o in ('-C', '--chdir'):
1070                 self.chdir = a
1071             elif o in ('-f', '--file'):
1072                 self.config_file = a
1073             elif o in ('--fmt', '--format'):
1074                 format = a
1075             elif o in ('-?', '-h', '--help'):
1076                 self.do_help(['help', 'obj'])
1077                 sys.exit(0)
1078             elif o in ('-p', '--prefix'):
1079                 self.prefix = a
1080             elif o in ('--stage',):
1081                 if not a in self.stages:
1082                     sys.stderr.write('%s: obj: Unrecognized stage "%s".\n' % (self.name, a))
1083                     sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1084                     sys.exit(1)
1085                 stage = a
1086             elif o in ('-t', '--tail'):
1087                 tail = int(a)
1088             elif o in ('--title',):
1089                 self.title = a
1090
1091         if not args:
1092             sys.stderr.write('%s: obj: Must specify an object name.\n' % self.name)
1093             sys.stderr.write('%s       Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1094             sys.exit(1)
1095
1096         object_name = args.pop(0)
1097
1098         if self.config_file:
1099             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
1100
1101         if self.chdir:
1102             os.chdir(self.chdir)
1103             logfile_path = lambda x: os.path.join(self.chdir, x)
1104
1105         if not args:
1106
1107             pattern = '%s*.log' % self.prefix
1108             args = self.args_to_files([pattern], tail)
1109
1110             if not args:
1111                 if self.chdir:
1112                     directory = self.chdir
1113                 else:
1114                     directory = os.getcwd()
1115
1116                 sys.stderr.write('%s: obj: No arguments specified.\n' % self.name)
1117                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
1118                 sys.stderr.write('%s  Type "%s help obj" for help.\n' % (self.name_spaces, self.name))
1119                 sys.exit(1)
1120
1121         else:
1122
1123             args = self.args_to_files(args, tail)
1124
1125         cwd_ = os.getcwd() + os.sep
1126
1127         if format == 'ascii':
1128
1129             self.ascii_table(args, tuple(self.stages), self.get_object_counts, logfile_path, object_name)
1130
1131         elif format == 'gnuplot':
1132
1133             stage_index = 0
1134             for s in self.stages:
1135                 if stage == s:
1136                     break
1137                 stage_index = stage_index + 1
1138
1139             results = self.collect_results(args, self.get_object_counts,
1140                                            object_name, stage_index)
1141
1142             self.gnuplot_results(results)
1143
1144         else:
1145
1146             sys.stderr.write('%s: obj: Unknown format "%s".\n' % (self.name, format))
1147             sys.exit(1)
1148
1149         return 0
1150
1151     #
1152
1153     def help_run(self):
1154         help = """\
1155         Usage: scons-time run [OPTIONS] [FILE ...]
1156
1157           --aegis=PROJECT               Use SCons from the Aegis PROJECT
1158           --chdir=DIR                   Name of unpacked directory for chdir
1159           -f FILE, --file=FILE          Read configuration from specified FILE
1160           -h, --help                    Print this help and exit
1161           -n, --no-exec                 No execute, just print command lines
1162           --number=NUMBER               Put output in files for run NUMBER
1163           --outdir=OUTDIR               Put output files in OUTDIR
1164           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1165           --python=PYTHON               Time using the specified PYTHON
1166           -q, --quiet                   Don't print command lines
1167           --scons=SCONS                 Time using the specified SCONS
1168           --svn=URL, --subversion=URL   Use SCons from Subversion URL
1169           -v, --verbose                 Display output of commands
1170         """
1171         sys.stdout.write(self.outdent(help))
1172         sys.stdout.flush()
1173
1174     def do_run(self, argv):
1175         """
1176         """
1177         run_number_list = [None]
1178
1179         short_opts = '?f:hnp:qs:v'
1180
1181         long_opts = [
1182             'aegis=',
1183             'file=',
1184             'help',
1185             'no-exec',
1186             'number=',
1187             'outdir=',
1188             'prefix=',
1189             'python=',
1190             'quiet',
1191             'scons=',
1192             'svn=',
1193             'subdir=',
1194             'subversion=',
1195             'verbose',
1196         ]
1197
1198         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1199
1200         for o, a in opts:
1201             if o in ('--aegis',):
1202                 self.aegis_project = a
1203             elif o in ('-f', '--file'):
1204                 self.config_file = a
1205             elif o in ('-?', '-h', '--help'):
1206                 self.do_help(['help', 'run'])
1207                 sys.exit(0)
1208             elif o in ('-n', '--no-exec'):
1209                 self.execute = self._do_not_execute
1210             elif o in ('--number',):
1211                 run_number_list = self.split_run_numbers(a)
1212             elif o in ('--outdir',):
1213                 self.outdir = a
1214             elif o in ('-p', '--prefix'):
1215                 self.prefix = a
1216             elif o in ('--python',):
1217                 self.python = a
1218             elif o in ('-q', '--quiet'):
1219                 self.display = self._do_not_display
1220             elif o in ('-s', '--subdir'):
1221                 self.subdir = a
1222             elif o in ('--scons',):
1223                 self.scons = a
1224             elif o in ('--svn', '--subversion'):
1225                 self.subversion_url = a
1226             elif o in ('-v', '--verbose'):
1227                 self.redirect = tee_to_file
1228                 self.verbose = True
1229                 self.svn_co_flag = ''
1230
1231         if not args and not self.config_file:
1232             sys.stderr.write('%s: run: No arguments or -f config file specified.\n' % self.name)
1233             sys.stderr.write('%s  Type "%s help run" for help.\n' % (self.name_spaces, self.name))
1234             sys.exit(1)
1235
1236         if self.config_file:
1237             exec open(self.config_file, 'rU').read() in self.__dict__
1238
1239         if args:
1240             self.archive_list = args
1241
1242         archive_file_name = os.path.split(self.archive_list[0])[1]
1243
1244         if not self.subdir:
1245             self.subdir = self.archive_splitext(archive_file_name)[0]
1246
1247         if not self.prefix:
1248             self.prefix = self.archive_splitext(archive_file_name)[0]
1249
1250         prepare = None
1251         if self.subversion_url:
1252             prepare = self.prep_subversion_run
1253         elif self.aegis_project:
1254             prepare = self.prep_aegis_run
1255
1256         for run_number in run_number_list:
1257             self.individual_run(run_number, self.archive_list, prepare)
1258
1259     def split_run_numbers(self, s):
1260         result = []
1261         for n in s.split(','):
1262             try:
1263                 x, y = n.split('-')
1264             except ValueError:
1265                 result.append(int(n))
1266             else:
1267                 result.extend(list(range(int(x), int(y)+1)))
1268         return result
1269
1270     def scons_path(self, dir):
1271         return os.path.join(dir, 'src', 'script', 'scons.py')
1272
1273     def scons_lib_dir_path(self, dir):
1274         return os.path.join(dir, 'src', 'engine')
1275
1276     def prep_aegis_run(self, commands, removals):
1277         self.aegis_tmpdir = make_temp_file(prefix = self.name + '-aegis-')
1278         removals.append((shutil.rmtree, 'rm -rf %%s', self.aegis_tmpdir))
1279
1280         self.aegis_parent_project = os.path.splitext(self.aegis_project)[0]
1281         self.scons = self.scons_path(self.aegis_tmpdir)
1282         self.scons_lib_dir = self.scons_lib_dir_path(self.aegis_tmpdir)
1283
1284         commands.extend([
1285             'mkdir %(aegis_tmpdir)s',
1286             (lambda: os.chdir(self.aegis_tmpdir), 'cd %(aegis_tmpdir)s'),
1287             '%(aegis)s -cp -ind -p %(aegis_parent_project)s .',
1288             '%(aegis)s -cp -ind -p %(aegis_project)s -delta %(run_number)s .',
1289         ])
1290
1291     def prep_subversion_run(self, commands, removals):
1292         self.svn_tmpdir = make_temp_file(prefix = self.name + '-svn-')
1293         removals.append((shutil.rmtree, 'rm -rf %%s', self.svn_tmpdir))
1294
1295         self.scons = self.scons_path(self.svn_tmpdir)
1296         self.scons_lib_dir = self.scons_lib_dir_path(self.svn_tmpdir)
1297
1298         commands.extend([
1299             'mkdir %(svn_tmpdir)s',
1300             '%(svn)s co %(svn_co_flag)s -r %(run_number)s %(subversion_url)s %(svn_tmpdir)s',
1301         ])
1302
1303     def individual_run(self, run_number, archive_list, prepare=None):
1304         """
1305         Performs an individual run of the default SCons invocations.
1306         """
1307
1308         commands = []
1309         removals = []
1310
1311         if prepare:
1312             prepare(commands, removals)
1313
1314         save_scons              = self.scons
1315         save_scons_wrapper      = self.scons_wrapper
1316         save_scons_lib_dir      = self.scons_lib_dir
1317
1318         if self.outdir is None:
1319             self.outdir = self.orig_cwd
1320         elif not os.path.isabs(self.outdir):
1321             self.outdir = os.path.join(self.orig_cwd, self.outdir)
1322
1323         if self.scons is None:
1324             self.scons = self.scons_path(self.orig_cwd)
1325
1326         if self.scons_lib_dir is None:
1327             self.scons_lib_dir = self.scons_lib_dir_path(self.orig_cwd)
1328
1329         if self.scons_wrapper is None:
1330             self.scons_wrapper = self.scons
1331
1332         if not run_number:
1333             run_number = self.find_next_run_number(self.outdir, self.prefix)
1334
1335         self.run_number = str(run_number)
1336
1337         self.prefix_run = self.prefix + '-%03d' % run_number
1338
1339         if self.targets0 is None:
1340             self.targets0 = self.startup_targets
1341         if self.targets1 is None:
1342             self.targets1 = self.targets
1343         if self.targets2 is None:
1344             self.targets2 = self.targets
1345
1346         self.tmpdir = make_temp_file(prefix = self.name + '-')
1347
1348         commands.extend([
1349             'mkdir %(tmpdir)s',
1350
1351             (os.chdir, 'cd %%s', self.tmpdir),
1352         ])
1353
1354         for archive in archive_list:
1355             if not os.path.isabs(archive):
1356                 archive = os.path.join(self.orig_cwd, archive)
1357             if os.path.isdir(archive):
1358                 dest = os.path.split(archive)[1]
1359                 commands.append((shutil.copytree, 'cp -r %%s %%s', archive, dest))
1360             else:
1361                 suffix = self.archive_splitext(archive)[1]
1362                 unpack_command = self.unpack_map.get(suffix)
1363                 if not unpack_command:
1364                     dest = os.path.split(archive)[1]
1365                     commands.append((shutil.copyfile, 'cp %%s %%s', archive, dest))
1366                 else:
1367                     commands.append(unpack_command + (archive,))
1368
1369         commands.extend([
1370             (os.chdir, 'cd %%s', self.subdir),
1371         ])
1372
1373         commands.extend(self.initial_commands)
1374
1375         commands.extend([
1376             (lambda: read_tree('.'),
1377             'find * -type f | xargs cat > /dev/null'),
1378
1379             (self.set_env, 'export %%s=%%s',
1380              'SCONS_LIB_DIR', self.scons_lib_dir),
1381
1382             '%(python)s %(scons_wrapper)s --version',
1383         ])
1384
1385         index = 0
1386         for run_command in self.run_commands:
1387             setattr(self, 'prof%d' % index, self.profile_name(index))
1388             c = (
1389                 self.log_execute,
1390                 self.log_display,
1391                 run_command,
1392                 self.logfile_name(index),
1393             )
1394             commands.append(c)
1395             index = index + 1
1396
1397         commands.extend([
1398             (os.chdir, 'cd %%s', self.orig_cwd),
1399         ])
1400
1401         if not os.environ.get('PRESERVE'):
1402             commands.extend(removals)
1403
1404             commands.append((shutil.rmtree, 'rm -rf %%s', self.tmpdir))
1405
1406         self.run_command_list(commands, self.__dict__)
1407
1408         self.scons              = save_scons
1409         self.scons_lib_dir      = save_scons_lib_dir
1410         self.scons_wrapper      = save_scons_wrapper
1411
1412     #
1413
1414     def help_time(self):
1415         help = """\
1416         Usage: scons-time time [OPTIONS] FILE [...]
1417
1418           -C DIR, --chdir=DIR           Change to DIR before looking for files
1419           -f FILE, --file=FILE          Read configuration from specified FILE
1420           --fmt=FORMAT, --format=FORMAT Print data in specified FORMAT
1421           -h, --help                    Print this help and exit
1422           -p STRING, --prefix=STRING    Use STRING as log file/profile prefix
1423           -t NUMBER, --tail=NUMBER      Only report the last NUMBER files
1424           --which=TIMER                 Plot timings for TIMER:  total,
1425                                           SConscripts, SCons, commands.
1426         """
1427         sys.stdout.write(self.outdent(help))
1428         sys.stdout.flush()
1429
1430     def do_time(self, argv):
1431
1432         format = 'ascii'
1433         logfile_path = lambda x: x
1434         tail = None
1435         which = 'total'
1436
1437         short_opts = '?C:f:hp:t:'
1438
1439         long_opts = [
1440             'chdir=',
1441             'file=',
1442             'fmt=',
1443             'format=',
1444             'help',
1445             'prefix=',
1446             'tail=',
1447             'title=',
1448             'which=',
1449         ]
1450
1451         opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
1452
1453         for o, a in opts:
1454             if o in ('-C', '--chdir'):
1455                 self.chdir = a
1456             elif o in ('-f', '--file'):
1457                 self.config_file = a
1458             elif o in ('--fmt', '--format'):
1459                 format = a
1460             elif o in ('-?', '-h', '--help'):
1461                 self.do_help(['help', 'time'])
1462                 sys.exit(0)
1463             elif o in ('-p', '--prefix'):
1464                 self.prefix = a
1465             elif o in ('-t', '--tail'):
1466                 tail = int(a)
1467             elif o in ('--title',):
1468                 self.title = a
1469             elif o in ('--which',):
1470                 if not a in self.time_strings.keys():
1471                     sys.stderr.write('%s: time: Unrecognized timer "%s".\n' % (self.name, a))
1472                     sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
1473                     sys.exit(1)
1474                 which = a
1475
1476         if self.config_file:
1477             HACK_for_exec(open(self.config_file, 'rU').read(), self.__dict__)
1478
1479         if self.chdir:
1480             os.chdir(self.chdir)
1481             logfile_path = lambda x: os.path.join(self.chdir, x)
1482
1483         if not args:
1484
1485             pattern = '%s*.log' % self.prefix
1486             args = self.args_to_files([pattern], tail)
1487
1488             if not args:
1489                 if self.chdir:
1490                     directory = self.chdir
1491                 else:
1492                     directory = os.getcwd()
1493
1494                 sys.stderr.write('%s: time: No arguments specified.\n' % self.name)
1495                 sys.stderr.write('%s  No %s*.log files found in "%s".\n' % (self.name_spaces, self.prefix, directory))
1496                 sys.stderr.write('%s  Type "%s help time" for help.\n' % (self.name_spaces, self.name))
1497                 sys.exit(1)
1498
1499         else:
1500
1501             args = self.args_to_files(args, tail)
1502
1503         cwd_ = os.getcwd() + os.sep
1504
1505         if format == 'ascii':
1506
1507             columns = ("Total", "SConscripts", "SCons", "commands")
1508             self.ascii_table(args, columns, self.get_debug_times, logfile_path)
1509
1510         elif format == 'gnuplot':
1511
1512             results = self.collect_results(args, self.get_debug_times,
1513                                            self.time_strings[which])
1514
1515             self.gnuplot_results(results, fmt='%s %.6f')
1516
1517         else:
1518
1519             sys.stderr.write('%s: time: Unknown format "%s".\n' % (self.name, format))
1520             sys.exit(1)
1521
1522 if __name__ == '__main__':
1523     opts, args = getopt.getopt(sys.argv[1:], 'h?V', ['help', 'version'])
1524
1525     ST = SConsTimer()
1526
1527     for o, a in opts:
1528         if o in ('-?', '-h', '--help'):
1529             ST.do_help(['help'])
1530             sys.exit(0)
1531         elif o in ('-V', '--version'):
1532             sys.stdout.write('scons-time version\n')
1533             sys.exit(0)
1534
1535     if not args:
1536         sys.stderr.write('Type "%s help" for usage.\n' % ST.name)
1537         sys.exit(1)
1538
1539     ST.execute_subcommand(args)
1540
1541 # Local Variables:
1542 # tab-width:4
1543 # indent-tabs-mode:nil
1544 # End:
1545 # vim: set expandtab tabstop=4 shiftwidth=4: