1# Copyright (C) 2013 Google Inc. All rights reserved.
2#
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7#     * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following disclaimer
11# in the documentation and/or other materials provided with the
12# distribution.
13#     * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived from
15# this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import json
30import optparse
31
32from webkitpy.layout_tests.port import Port
33
34
35def main(host, argv):
36    parser = optparse.OptionParser(usage='%prog [times_ms.json]')
37    parser.add_option('-f', '--forward', action='store', type='int',
38                      help='group times by first N directories of test')
39    parser.add_option('-b', '--backward', action='store', type='int',
40                     help='group times by last N directories of test')
41    parser.add_option('--fastest', action='store', type='float',
42                      help='print a list of tests that will take N % of the time')
43
44    epilog = """
45       You can print out aggregate times per directory using the -f and -b
46       flags. The value passed to each flag indicates the "depth" of the flag,
47       similar to positive and negative arguments to python arrays.
48
49       For example, given fast/forms/week/week-input-type.html, -f 1
50       truncates to 'fast', -f 2 and -b 2 truncates to 'fast/forms', and -b 1
51       truncates to fast/forms/week . -f 0 truncates to '', which can be used
52       to produce a single total time for the run."""
53    parser.epilog = '\n'.join(s.lstrip() for s in epilog.splitlines())
54
55    options, args = parser.parse_args(argv)
56
57    port = host.port_factory.get()
58    if args and args[0]:
59        times_ms_path = args[0]
60    else:
61        times_ms_path = host.filesystem.join(port.results_directory(), 'times_ms.json')
62
63    times_trie = json.loads(host.filesystem.read_text_file(times_ms_path))
64
65    times = convert_trie_to_flat_paths(times_trie)
66
67    if options.fastest:
68        if options.forward is None and options.backward is None:
69            options.forward = 0
70        print_fastest(host, port, options, times)
71    else:
72        print_times(host, options, times)
73
74
75def print_times(host, options, times):
76    by_key = times_by_key(times, options.forward, options.backward)
77    for key in sorted(by_key):
78        if key:
79            host.print_("%s %d" % (key, by_key[key]))
80        else:
81            host.print_("%d" % by_key[key])
82
83
84def print_fastest(host, port, options, times):
85    total = times_by_key(times, 0, None)['']
86    by_key = times_by_key(times, options.forward, options.backward)
87    keys_by_time = sorted(by_key, key=lambda k: (by_key[k], k))
88
89    tests_by_key = {}
90    for test_name in sorted(times):
91        key = key_for(test_name, options.forward, options.backward)
92        if key in sorted(tests_by_key):
93            tests_by_key[key].append(test_name)
94        else:
95            tests_by_key[key] = [test_name]
96
97    fast_tests_by_key = {}
98    total_so_far = 0
99    per_key = total * options.fastest / (len(keys_by_time) * 100.0)
100    budget = 0
101    while keys_by_time:
102        budget += per_key
103        key = keys_by_time.pop(0)
104        tests_by_time = sorted(tests_by_key[key], key=lambda t: (times[t], t))
105        fast_tests_by_key[key] = []
106        while tests_by_time and total_so_far <= budget:
107            test = tests_by_time.pop(0)
108            test_time = times[test]
109             # Make sure test time > 0 so we don't include tests that are skipped.
110            if test_time and total_so_far + test_time <= budget:
111                fast_tests_by_key[key].append(test)
112                total_so_far += test_time
113
114    for k in sorted(fast_tests_by_key):
115        for t in fast_tests_by_key[k]:
116            host.print_("%s %d" % (t, times[t]))
117    return
118
119
120def key_for(path, forward, backward):
121    sep = Port.TEST_PATH_SEPARATOR
122    if forward is not None:
123        return sep.join(path.split(sep)[:-1][:forward])
124    if backward is not None:
125        return sep.join(path.split(sep)[:-backward])
126    return path
127
128
129def times_by_key(times, forward, backward):
130    by_key = {}
131    for test_name in times:
132        key = key_for(test_name, forward, backward)
133        if key in by_key:
134            by_key[key] += times[test_name]
135        else:
136            by_key[key] = times[test_name]
137    return by_key
138
139
140def convert_trie_to_flat_paths(trie, prefix=None):
141    result = {}
142    for name, data in trie.iteritems():
143        if prefix:
144            name = prefix + "/" + name
145        if isinstance(data, int):
146            result[name] = data
147        else:
148            result.update(convert_trie_to_flat_paths(data, name))
149
150    return result
151