Title: [266727] trunk/LayoutTests
Revision
266727
Author
[email protected]
Date
2020-09-08 09:46:23 -0700 (Tue, 08 Sep 2020)

Log Message

Add a standalone version of the test runtime tree map viewer, that can accept stats.json from a local test run
https://bugs.webkit.org/show_bug.cgi?id=189396

Reviewed by Darin Adler.

Add the viewer and a sample stats.json file. Devs can drag a stats.json file into test-duration-treemap.html to
see the map (or specify it as a url parameter).

A future patch will hook this up to results.html.

* fast/harness/stats.json: Added.
* fast/harness/test-duration-treemap-expected.txt: Added.
* fast/harness/test-duration-treemap.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (266726 => 266727)


--- trunk/LayoutTests/ChangeLog	2020-09-08 15:25:06 UTC (rev 266726)
+++ trunk/LayoutTests/ChangeLog	2020-09-08 16:46:23 UTC (rev 266727)
@@ -1,3 +1,19 @@
+2020-09-08  Simon Fraser  <[email protected]>
+
+        Add a standalone version of the test runtime tree map viewer, that can accept stats.json from a local test run
+        https://bugs.webkit.org/show_bug.cgi?id=189396
+
+        Reviewed by Darin Adler.
+        
+        Add the viewer and a sample stats.json file. Devs can drag a stats.json file into test-duration-treemap.html to
+        see the map (or specify it as a url parameter).
+
+        A future patch will hook this up to results.html.
+
+        * fast/harness/stats.json: Added.
+        * fast/harness/test-duration-treemap-expected.txt: Added.
+        * fast/harness/test-duration-treemap.html: Added.
+
 2020-09-08  Chris Lord  <[email protected]>
 
         [GStreamer] Some convolvernode tests are flaky crashing

Added: trunk/LayoutTests/fast/harness/stats.json (0 => 266727)


--- trunk/LayoutTests/fast/harness/stats.json	                        (rev 0)
+++ trunk/LayoutTests/fast/harness/stats.json	2020-09-08 16:46:23 UTC (rev 266727)
@@ -0,0 +1,217 @@
+{
+    "fast": {
+        "harness": {
+            "sample-mismatch-reftest.html": {
+                "results": [
+                    0,
+                    11,
+                    42597,
+                    393,
+                    197
+                ]
+            },
+            "fastmallocstatistics-object.html": {
+                "results": [
+                    0,
+                    0,
+                    42597,
+                    12985,
+                    13009
+                ]
+            },
+            "override-preferences-2.html": {
+                "results": [
+                    0,
+                    5,
+                    42597,
+                    103,
+                    302
+                ]
+            },
+            "snapshot-captures-compositing.html": {
+                "results": [
+                    0,
+                    12,
+                    42597,
+                    199,
+                    200
+                ]
+            },
+            "testrunner-object.html": {
+                "results": [
+                    0,
+                    14,
+                    42597,
+                    86,
+                    87
+                ]
+            },
+            "results.html": {
+                "results": [
+                    0,
+                    9,
+                    42597,
+                    591,
+                    593
+                ]
+            },
+            "render-tree-as-text-options.html": {
+                "results": [
+                    0,
+                    8,
+                    42597,
+                    96,
+                    97
+                ]
+            },
+            "uiscriptcontroller": {
+                "concurrent-ui-side-scripts.html": {
+                    "results": [
+                        2,
+                        0,
+                        42603,
+                        12830,
+                        12848
+                    ]
+                },
+                "ui-side-script-with-callback.html": {
+                    "results": [
+                        2,
+                        2,
+                        42603,
+                        148,
+                        150
+                    ]
+                },
+                "ui-side-scripts.html": {
+                    "results": [
+                        2,
+                        3,
+                        42603,
+                        134,
+                        135
+                    ]
+                },
+                "ui-side-script-unregister-callback.html": {
+                    "results": [
+                        2,
+                        1,
+                        42603,
+                        198,
+                        200
+                    ]
+                }
+            },
+            "override-zzz-reset.html": {
+                "results": [
+                    0,
+                    7,
+                    42597,
+                    97,
+                    98
+                ]
+            },
+            "memoryinfo-object.html": {
+                "results": [
+                    0,
+                    4,
+                    42597,
+                    94,
+                    96
+                ]
+            },
+            "image-diff-template.html": {
+                "results": [
+                    0,
+                    2,
+                    42597,
+                    276,
+                    277
+                ]
+            },
+            "internals-object.html": {
+                "results": [
+                    0,
+                    3,
+                    42597,
+                    99,
+                    101
+                ]
+            },
+            "font-weight-bold.html": {
+                "results": [
+                    0,
+                    1,
+                    42597,
+                    258,
+                    259
+                ]
+            },
+            "user-preferred-language.html": {
+                "results": [
+                    0,
+                    15,
+                    42597,
+                    89,
+                    91
+                ]
+            },
+            "test-duration-treemap.html": {
+                "results": [
+                    0,
+                    13,
+                    42597,
+                    125,
+                    127
+                ]
+            },
+            "sample-fail-mismatch-reftest.html": {
+                "results": [
+                    0,
+                    10,
+                    42597,
+                    222,
+                    224
+                ]
+            },
+            "perftests": {
+                "runs-per-second-iterations.html": {
+                    "results": [
+                        1,
+                        1,
+                        42600,
+                        172,
+                        174
+                    ]
+                },
+                "runs-per-second-log.html": {
+                    "results": [
+                        1,
+                        2,
+                        42600,
+                        168,
+                        169
+                    ]
+                },
+                "perf-runner-compute-statistics.html": {
+                    "results": [
+                        1,
+                        0,
+                        42600,
+                        13008,
+                        13025
+                    ]
+                }
+            },
+            "override-preferences.html": {
+                "results": [
+                    0,
+                    6,
+                    42597,
+                    37,
+                    38
+                ]
+            }
+        }
+    }
+}
\ No newline at end of file

Added: trunk/LayoutTests/fast/harness/test-duration-treemap-expected.txt (0 => 266727)


--- trunk/LayoutTests/fast/harness/test-duration-treemap-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/harness/test-duration-treemap-expected.txt	2020-09-08 16:46:23 UTC (rev 266727)
@@ -0,0 +1,11 @@
+Drop stats.json file here to load.
+Click on a box to zoom in. Click on the outermost box to zoom out. Reset
+
+LayoutTests (42s - 23 tests)
+fast (42s - 23 tests)
+harness (42s - 23 tests)
+perftests (13s - 3 tests)
+perf-runner-compute-statistics.html (13s)
+uiscriptcontroller (13s - 4 tests)
+concurrent-ui-side-scripts.html (13s)
+fastmallocstatistics-object.html (13s)

Added: trunk/LayoutTests/fast/harness/test-duration-treemap.html (0 => 266727)


--- trunk/LayoutTests/fast/harness/test-duration-treemap.html	                        (rev 0)
+++ trunk/LayoutTests/fast/harness/test-duration-treemap.html	2020-09-08 16:46:23 UTC (rev 266727)
@@ -0,0 +1,565 @@
+<!-- 
+Copyright (C) 2020 Apple Inc. All rights reserved.
+Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+-->
+<!DOCTYPE html>
+<html>
+<title>Test Runtimes</title>
+<style>
+    html {
+        height: 100%;
+    }
+
+    body {
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        font-family: "Helvetica Neue";
+    }
+
+    td:first-child {
+        text-align: left;
+    }
+
+    td {
+        text-align: right;
+    }
+    
+    header {
+        position: relative;
+        height: 4rem;
+    }
+    
+    header a {
+        margin-left: 0.25em
+    }
+
+    header p {
+        font-size: smaller;
+    }
+
+    #map {
+        position: relative;
+        flex-grow: 1;
+        cursor: pointer;
+        -webkit-user-select: none;
+    }
+
+    .extra-dom {
+        display: none;
+        border: none;
+        border-top: 1px dashed;
+        padding: 4px;
+        margin: 0;
+        overflow: auto;
+        cursor: auto;
+        -webkit-user-select: text;
+    }
+
+    .error {
+        color: red;
+        font-style: italic;
+    }
+
+    #dropTarget {
+        font-size: 10pt;
+        font-weight: bold;
+        color: #888;
+        position: absolute;
+        top: 4px;
+        right: 4px;
+        border: 2px solid rgba(0, 0, 0, 0.3);
+        background-color: rgba(0, 0, 0, 0.1);
+        padding: 10px;
+        border-radius: 10px;
+    }
+
+    #dropTarget.dragOver {
+        border: 2px solid rgba(0, 0, 0, 0.1);
+        background-color: rgba(0, 0, 0, 0.5);
+        color: #ddd;
+    }
+
+    .webtreemap-node {
+        /* Required attributes. */
+        position: absolute;
+        overflow: hidden;   /* To hide overlong captions. */
+        background: white;  /* Nodes must be opaque for zIndex layering. */
+        border: solid 1px black;  /* Calculations assume 1px border. */
+
+        transition: top    0.3s,
+        left   0.3s,
+        width  0.3s,
+        height 0.3s;
+    }
+
+    /* Optional: highlight nodes on mouseover. */
+    .webtreemap-node:hover {
+        background: #eee;
+    }
+
+    /* Optional: Different borders depending on level. */
+    .webtreemap-level0 {
+        border: solid 1px #444;
+    }
+    .webtreemap-level1 {
+        border: solid 1px #666;
+    }
+    .webtreemap-level2 {
+        border: solid 1px #888;
+    }
+    .webtreemap-level3 {
+        border: solid 1px #aaa;
+    }
+    .webtreemap-level4 {
+        border: solid 1px #ccc;
+    }
+
+    /* Optional: styling on node captions. */
+    .webtreemap-caption {
+        font-family: sans-serif;
+        font-size: 11px;
+        padding: 2px;
+        text-align: center;
+    }
+</style>
+
+<script>
+
+class WebTreeMap {
+    static get borderWidth()
+    {
+        return 1;
+    }
+
+    static get padding()
+    {
+        return 1;
+    }
+
+    constructor(treeData, containerElement)
+    {
+        this._focusedNode = null;
+
+        let style = getComputedStyle(containerElement, null);
+        let width = parseInt(style.width);
+        let height = parseInt(style.height);
+
+        this.makeDom(treeData, 0);
+        containerElement.appendChild(treeData.dom);
+        this.position(treeData.dom, 0, 0, width, height);
+        this.layout(treeData, 0, width, height);
+    }
+
+    focus(tree)
+    {
+        this._focusedNode = tree;
+
+        // Hide all visible siblings of all our ancestors by lowering them.
+        let level = 0;
+        let root = tree;
+        while (root.parent) {
+            root = root.parent;
+            level += 1;
+            for (let i = 0, sibling; sibling = root.children[i]; ++i) {
+                if (sibling.dom)
+                    sibling.dom.style.zIndex = 0;
+            }
+        }
+        let width = root.dom.offsetWidth;
+        let height = root.dom.offsetHeight;
+        // Unhide (raise) and maximize us and our ancestors.
+        for (let t = tree; t.parent; t = t.parent) {
+            // Shift off by border so we don't get nested borders.
+            // TODO: actually make nested borders work (need to adjust width/height).
+            this.position(t.dom, -WebTreeMap.borderWidth, -WebTreeMap.borderWidth, width, height);
+            t.dom.style.zIndex = 1;
+        }
+        // And layout into the topmost box.
+        this.layout(tree, level, width, height);
+        this.handleFocus(tree);
+    }
+    
+    handleFocus(tree)
+    {
+        // For delegation.
+    }
+
+    makeDom(tree, level)
+    {
+        let dom = document.createElement('div');
+        dom.style.zIndex = 1;
+        dom.className = 'webtreemap-node webtreemap-level' + Math.min(level, 4);
+
+        dom.addEventListener('mousedown', e => {
+            if (e.button == 0) {
+                if (this._focusedNode && tree == this._focusedNode && this._focusedNode.parent)
+                    this.focus(this._focusedNode.parent);
+                else
+                    this.focus(tree);
+            }
+            e.stopPropagation();
+            return true;
+        }, false);
+
+        let caption = document.createElement('div');
+        caption.className = 'webtreemap-caption';
+        caption.innerHTML = tree.name;
+        dom.appendChild(caption);
+
+        tree.dom = dom;
+        return dom;
+    }
+
+    position(dom, x, y, width, height)
+    {
+        // CSS width/height does not include border.
+        width -= WebTreeMap.borderWidth * 2;
+        height -= WebTreeMap.borderWidth * 2;
+
+        dom.style.left   = x + 'px';
+        dom.style.top    = y + 'px';
+        dom.style.width  = Math.max(width, 0) + 'px';
+        dom.style.height = Math.max(height, 0) + 'px';
+    }
+
+    // Given a list of rectangles |nodes|, the 1-d space available
+    // |space|, and a starting rectangle index |start|, compute an span of
+    // rectangles that optimizes a pleasant aspect ratio.
+    //
+    // Returns [end, sum], where end is one past the last rectangle and sum is the
+    // 2-d sum of the rectangles' areas.
+    selectSpan(nodes, space, start)
+    {
+        // Add rectangle one by one, stopping when aspect ratios begin to go
+        // bad. Result is [start,end) covering the best run for this span.
+        // http://scholar.google.com/scholar?cluster=5972512107845615474
+        let node = nodes[start];
+        let rmin = node.data['$area'];  // Smallest seen child so far.
+        let rmax = rmin;                // Largest child.
+        let rsum = 0;                   // Sum of children in this span.
+        let last_score = 0;             // Best score yet found.
+        let end;
+        for (end = start; node = nodes[end]; ++end) {
+            let size = node.data['$area'];
+            if (size < rmin)
+                rmin = size;
+            if (size > rmax)
+                rmax = size;
+            rsum += size;
+
+            // This formula is from the paper, but you can easily prove to
+            // yourself it's taking the larger of the x/y aspect ratio or the
+            // y/x aspect ratio. The additional magic fudge constant of 5
+            // makes us prefer wider rectangles to taller ones.
+            let score = Math.max(5 * space * space * rmax / (rsum * rsum), 1 * rsum * rsum / (space * space * rmin));
+            if (last_score && score > last_score) {
+                rsum -= size;    // Undo size addition from just above.
+                break;
+            }
+            last_score = score;
+        }
+        return [end, rsum];
+    }
+
+    layout(tree, level, width, height)
+    {
+        if (!('children' in tree))
+            return;
+
+        let total = tree.data['$area'];
+
+        // XXX why do I need an extra -1/-2 here for width/height to look right?
+        let x1 = 0, y1 = 0, x2 = width - 1, y2 = height - 2;
+        x1 += WebTreeMap.padding; y1 += WebTreeMap.padding;
+        x2 -= WebTreeMap.padding; y2 -= WebTreeMap.padding;
+        y1 += 14; // XXX get first child height for caption spacing
+
+        let pixels_to_units = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
+
+        for (let start = 0, child; child = tree.children[start]; ++start) {
+            if (x2 - x1 < 60 || y2 - y1 < 40) {
+                if (child.dom) {
+                    child.dom.style.zIndex = 0;
+                    this.position(child.dom, -2, -2, 0, 0);
+                }
+                continue;
+            }
+
+            // In theory we can dynamically decide whether to split in x or y based
+            // on aspect ratio. In practice, changing split direction with this
+            // layout doesn't look very good.
+            //   var ysplit = (y2 - y1) > (x2 - x1);
+            let ysplit = true;
+
+            let space;  // Space available along layout axis.
+            if (ysplit)
+                space = (y2 - y1) * pixels_to_units;
+            else
+                space = (x2 - x1) * pixels_to_units;
+
+            let span = this.selectSpan(tree.children, space, start);
+            let end = span[0];
+            let rsum = span[1];
+
+            // Now that we've selected a span, lay out rectangles [start,end) in our
+            // available space.
+            let x = x1;
+            let y = y1;
+            for (let i = start; i < end; ++i) {
+                child = tree.children[i];
+                if (!child.dom) {
+                    child.parent = tree;
+                    child.dom = this.makeDom(child, level + 1);
+                    tree.dom.appendChild(child.dom);
+                } else {
+                    child.dom.style.zIndex = 1;
+                }
+                let size = child.data['$area'];
+                let frac = size / rsum;
+                if (ysplit) {
+                    width = rsum / space;
+                    height = size / width;
+                } else {
+                    height = rsum / space;
+                    width = size / height;
+                }
+                width /= pixels_to_units;
+                height /= pixels_to_units;
+                width = Math.round(width);
+                height = Math.round(height);
+                this.position(child.dom, x, y, width, height);
+                if ('children' in child) {
+                    this.layout(child, level + 1, width, height);
+                }
+                if (ysplit)
+                    y += height;
+                else
+                    x += width;
+            }
+
+            // Shrink our available space based on the amount we used.
+            if (ysplit)
+                x1 += Math.round((rsum / space) / pixels_to_units);
+            else
+                y1 += Math.round((rsum / space) / pixels_to_units);
+
+            // end points one past where we ended, which is where we want to
+            // begin the next iteration, but subtract one to balance the ++ in
+            // the loop.
+            start = end - 1;
+        }
+    }
+};
+
+class Utils {
+  
+    static humanReadableTime(milliseconds)
+    {
+        if (milliseconds < 1000)
+            return Math.floor(milliseconds) + 'ms';
+        else if (milliseconds < 60000)
+            return (milliseconds / 1000).toPrecision(2) + 's';
+
+        let minutes = Math.floor(milliseconds / 60000);
+        let seconds = Math.floor((milliseconds - minutes * 60000) / 1000);
+        return minutes + 'm' + seconds + 's';
+    }
+};
+
+class DataConverter {
+
+    static convertToWebTreemapFormat(rootNodeName, tree)
+    {
+        return DataConverter._recursiveConvertNode(rootNodeName, tree);
+    }
+  
+    /*
+        stats.json looks like:
+        {
+            "imported": {
+                "w3c": {
+                    "web-platform-tests": {
+                        "IndexedDB": {
+                            "idbobjectstore_get4.htm": {
+                                "results": [
+                                    12,         // worker number
+                                    260,        // test number
+                                    41632,      // worker pid
+                                    50,         // test runtime (ms)
+                                    54          // total runtime (ms; includes time to run ref test, do pixel comparison etc.)
+                                ],
+                                ...
+                            }
+                        }
+                    }
+                }
+            }
+        }    
+    */
+  
+    static _recursiveConvertNode(treename, tree, path)
+    {
+        let total = 0;
+        let childCount = 0;
+        let children = [];
+        for (let name in tree) {
+            let treeNode = tree[name];
+            if ('results' in treeNode) {
+                let times = treeNode.results;
+                if (!times.hasOwnProperty('length'))
+                    continue;
+
+                let test_total_time = treeNode.results[4];
+                let node = {
+                    'data': { '$area': test_total_time },
+                    'name': name + " (" + Utils.humanReadableTime(test_total_time) + ")"
+                };
+                children.push(node);
+                total += test_total_time;
+                childCount++;
+            } else {
+                let newPath = path ? path + '/' + name : name;
+                let subtree = DataConverter._recursiveConvertNode(name, treeNode, newPath);
+                children.push(subtree);
+                total += subtree['data']['$area'];
+                childCount += subtree['childCount'];
+            }
+        }
+
+        children.sort(function(a, b) {
+            let aTime = a.data['$area']
+            let bTime = b.data['$area']
+            return bTime - aTime;
+        });
+
+        return {
+            'data': { '$area': total },
+            'name': treename + ' (' + Utils.humanReadableTime(total) + ' - ' + childCount + ' tests)',
+            'children': children,
+            'childCount': childCount,
+            'path': path
+        };
+    }
+};
+
+let treeMapController;
+class TreeMapController {
+  
+    constructor(statsJSONString, containerElement)
+    {
+        let jsonData = JSON.parse(statsJSONString);
+        this.treeMapData = DataConverter.convertToWebTreemapFormat('LayoutTests', jsonData);
+        this.treeMap = new WebTreeMap(this.treeMapData, containerElement);
+    }
+    
+    resetZoom()
+    {
+        focus(this.webTreeMap);
+    }
+};
+
+if (window.testRunner) {
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+}
+
+function setupInterface()
+{
+    // See if we have a file to load specified in the query string.
+    let query_parameters = {};
+    let pairs = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
+    let filename = 'stats.json';
+
+    for (let i = 0; i < pairs.length; i++) {
+        let pair = pairs[i].split('=');
+        query_parameters[pair[0]] = decodeURIComponent(pair[1]);
+    }
+
+    if ('filename' in query_parameters)
+        filename = query_parameters['filename'];
+
+    // This is used for local files, so we can't use fetch().
+    var request = new XMLHttpRequest();
+    request.open("GET", filename, true);
+    request.addEventListener('load', () => {
+        treeMapController = new TreeMapController(request.responseText, document.getElementById('map'));
+        if (window.testRunner)
+            testRunner.notifyDone();
+    });
+    request.addEventListener('error', () => {
+        console.log('Failed to load stats.json');
+    });
+    request.send();
+
+    let drop_target = document.getElementById('dropTarget');
+
+    drop_target.addEventListener('dragenter', function (e) {
+        drop_target.className = 'dragOver';
+        e.stopPropagation();
+        e.preventDefault();
+    }, false);
+
+    drop_target.addEventListener('dragover', function (e) {
+        e.stopPropagation();
+        e.preventDefault();
+    }, false);
+
+    drop_target.addEventListener('dragleave', function (e) {
+        drop_target.className = '';
+        e.stopPropagation();
+        e.preventDefault();
+    }, false);
+
+    drop_target.addEventListener('drop', function (e) {
+        drop_target.className = '';
+        e.stopPropagation();
+        e.preventDefault();
+
+        for (let i = 0; i < e.dataTransfer.files.length; ++i) {
+            let file = e.dataTransfer.files[i];
+
+            let reader = new FileReader();
+            reader.filename = file.name;
+            reader._onload_ = function(e) {
+                treeMapController = new TreeMapController(reader.result, document.getElementById('map'));
+            };
+
+            reader.readAsText(file);
+            document.title = 'Test result times: ' + reader.filename;
+        }
+    }, false);
+}
+
+window.addEventListener('load', setupInterface, false);
+</script>
+<body>
+<header>
+    <div id="dropTarget">Drop stats.json file here to load.</div>
+    <p>Click on a box to zoom in. Click on the outermost box to zoom out. <a href="" _onclick_="treeMapController.resetZoom()">Reset</a></p>
+    </div>
+</header>
+<section id='map'></section>
+</body>
+</html>
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to