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
29var defaultDashboardSpecificStateValues = {
30    builder: null,
31    treemapfocus: '',
32};
33
34var DB_SPECIFIC_INVALIDATING_PARAMETERS = {
35    'testType': 'builder',
36    'group': 'builder'
37};
38
39var g_haveEverGeneratedPage = false;
40
41function generatePage(historyInstance)
42{
43    g_haveEverGeneratedPage = true;
44    $('header-container').innerHTML = ui.html.testTypeSwitcher();
45
46    g_isGeneratingPage = true;
47
48    var rawTree = g_resultsByBuilder[historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder()];
49    g_webTree = convertToWebTreemapFormat('AllTests', rawTree);
50    appendTreemap($('map'), g_webTree);
51
52    if (historyInstance.dashboardSpecificState.treemapfocus)
53        focusPath(g_webTree, historyInstance.dashboardSpecificState.treemapfocus)
54
55    g_isGeneratingPage = false;
56}
57
58function handleValidHashParameter(historyInstance, key, value)
59{
60    switch(key) {
61    case 'builder':
62        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
63            function() { return value in currentBuilders(); });
64        return true;
65
66    case 'treemapfocus':
67        history.validateParameter(historyInstance.dashboardSpecificState, key, value,
68            function() {
69                return value.match(/^[\w./]+$/);
70            });
71        return true;
72
73    default:
74        return false;
75    }
76}
77
78function handleQueryParameterChange(historyInstance, params)
79{
80    for (var param in params) {
81        // When we're first loading the page, if there is a treemapfocus parameter,
82        // it will show up here. After we've generated the page, treemapfocus parameter
83        // changes should just be handled by the treemap code instead of calling through
84        // to generatePage.
85        if (!g_haveEverGeneratedPage || param != 'treemapfocus') {
86            $('map').innerHTML = 'Loading...';
87            return true;
88        }
89    }
90    return false;
91}
92
93var treemapConfig = {
94    defaultStateValues: defaultDashboardSpecificStateValues,
95    generatePage: generatePage,
96    handleValidHashParameter: handleValidHashParameter,
97    handleQueryParameterChange: handleQueryParameterChange,
98    invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS
99};
100
101// FIXME(jparent): Eventually remove all usage of global history object.
102var g_history = new history.History(treemapConfig);
103g_history.parseCrossDashboardParameters();
104
105var TEST_URL_BASE_PATH = "http://src.chromium.org/blink/trunk/";
106
107function humanReadableTime(milliseconds)
108{
109    if (milliseconds < 1000)
110        return Math.floor(milliseconds) + 'ms';
111    else if (milliseconds < 60000)
112        return (milliseconds / 1000).toPrecision(2) + 's';
113
114    var minutes = Math.floor(milliseconds / 60000);
115    var seconds = Math.floor((milliseconds - minutes * 60000) / 1000);
116    return minutes + 'm' + seconds + 's';
117}
118
119// This looks like:
120// { "data": {"$area": (sum of all timings)},
121//   "name": (name of this node),
122//   "children": [ (child nodes, in the same format as this) ] }
123// childCount is added just to be includes in the node's name
124function convertToWebTreemapFormat(treename, tree, path)
125{
126    var total = 0;
127    var childCount = 0;
128    var children = [];
129    for (var name in tree) {
130        var treeNode = tree[name];
131        if (typeof treeNode == "number") {
132            var time = treeNode;
133            var node = {
134                "data": {"$area": time},
135                "name": name + " (" + humanReadableTime(time) + ")"
136            };
137            children.push(node);
138            total += time;
139            childCount++;
140        } else {
141            var newPath = path ? path + '/' + name : name;
142            var subtree = convertToWebTreemapFormat(name, treeNode, newPath);
143            children.push(subtree);
144            total += subtree["data"]["$area"];
145            childCount += subtree["childCount"];
146        }
147    }
148
149    children.sort(function(a, b) {
150        aTime = a.data["$area"]
151        bTime = b.data["$area"]
152        return bTime - aTime;
153    });
154
155    return {
156        "data": {"$area": total},
157        "name": treename + " (" + humanReadableTime(total) + " - " + childCount + " tests)",
158        "children": children,
159        "childCount": childCount,
160        "path": path
161    };
162}
163
164function listOfAllNonLeafNodes(tree, list)
165{
166    if (!tree.children)
167        return;
168
169    if (!list)
170        list = [];
171    list.push(tree);
172
173    tree.children.forEach(function(child) {
174        listOfAllNonLeafNodes(child, list);
175    });
176    return list;
177}
178
179function reverseSortByAverage(list)
180{
181    list.sort(function(a, b) {
182        var avgA = a.data['$area'] / a.childCount;
183        var avgB = b.data['$area'] / b.childCount;
184        return avgB - avgA;
185    });
186}
187
188function showAverages()
189{
190    if (!document.getElementById('map'))
191        return;
192
193    var table = document.createElement('table');
194    table.innerHTML = '<th>directory</th><th># tests</th><th>avg time / test</th>';
195
196    var allNodes = listOfAllNonLeafNodes(g_webTree);
197    reverseSortByAverage(allNodes);
198    allNodes.forEach(function(node) {
199        var average = node.data['$area'] / node.childCount;
200        if (average > 100 && node.childCount != 1) {
201            var tr = document.createElement('tr');
202            tr.innerHTML = '<td></td><td>' + node.childCount + '</td><td>' + humanReadableTime(average) + '</td>';
203            tr.querySelector('td').innerText = node.path;
204            table.appendChild(tr);
205        }
206    });
207
208    var map = document.getElementById('map');
209    map.parentNode.replaceChild(table, map);
210}
211
212var g_isGeneratingPage = false;
213var g_webTree;
214
215function focusPath(tree, path)
216{
217    var parts = decodeURIComponent(path).split('/');
218    if (extractName(tree) != parts[0]) {
219        console.error('Could not focus tree rooted at ' + parts[0]);
220        return;
221    }
222
223    for (var i = 1; i < parts.length; i++) {
224        var children = tree.children;
225        for (var j = 0; j < children.length; j++) {
226            var child = children[j];
227            if (extractName(child) == parts[i]) {
228                tree = child;
229                focus(tree);
230                break;
231            }
232        }
233        if (j == children.length) {
234            console.error('Could not find tree at ' + parts[i]);
235            break;
236        }
237    }
238
239}
240
241function extractName(node)
242{
243    return node.name.split(' ')[0];
244}
245
246function fullName(node)
247{
248    var buffer = [extractName(node)];
249    while (node.parent) {
250        node = node.parent;
251        buffer.unshift(extractName(node));
252    }
253    return buffer.join('/');
254}
255
256function handleFocus(tree)
257{
258    var currentlyFocusedNode = $('focused-leaf');
259    if (currentlyFocusedNode)
260        currentlyFocusedNode.id = '';
261
262    if (!tree.children)
263        tree.dom.id = 'focused-leaf';
264
265    var name = fullName(tree);
266
267    if (!tree.children && !tree.extraDom && g_history.isLayoutTestResults()) {
268        tree.extraDom = document.createElement('pre');
269        tree.extraDom.className = 'extra-dom';
270        tree.dom.appendChild(tree.extraDom);
271
272        loader.request(TEST_URL_BASE_PATH + name,
273            function(xhr) {
274                tree.extraDom.onmousedown = function(e) {
275                    e.stopPropagation();
276                };
277                tree.extraDom.textContent = xhr.responseText;
278            },
279            function (xhr) {
280                tree.extraDom.textContent = "Could not load test."
281        });
282    }
283
284    // We don't want the focus calls during generatePage to try to modify the query state.
285    if (!g_isGeneratingPage)
286        g_history.setQueryParameter('treemapfocus', name);
287}
288
289window.addEventListener('load', function() {
290    var resourceLoader = new loader.Loader();
291    resourceLoader.load();
292}, false);
293