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