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