Test freshness page should improve the ability to correlating issues from same builder.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Mar 2019 03:07:40 +0000 (03:07 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 14 Mar 2019 03:07:40 +0000 (03:07 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195242

Reviewed by Ryosuke Niwa.

Added the ability to highlight indicators with same builder when mouse is hovering over one indicator.
This is a very useful visualization for correlating issues specific to a builder.
Added tooltip with latest build link when hovering over an indicator.

* public/v3/components/freshness-indicator.js:
(FreshnessIndicator): Removed 'summary' field as it's no longer needed.
Added 'highlighted' field.
(FreshnessIndicator.prototype.update): Added 'highlighted' argument.
(FreshnessIndicator.prototype.didConstructShadowTree): Make indicator to dispatch mouse enter and leave
messages so that UI can highlight corresponding cells.
(FreshnessIndicator.prototype.render):
(FreshnessIndicator.cssTemplate):
* public/v3/pages/test-freshness-page.js: Added tooltip to show latest build time and build link.
Added logic to manually compute table body height.
(TestFreshnessPage):
(TestFreshnessPage.prototype.didConstructShadowTree):
(TestFreshnessPage.prototype._fetchTestResults):
(TestFreshnessPage.prototype.render):
(TestFreshnessPage.prototype._renderTooltip):
(TestFreshnessPage.prototype._constructTableCell):
(TestFreshnessPage.cssTemplate):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@242933 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js

index 5ab03d3..c33ed1e 100644 (file)
@@ -1,3 +1,32 @@
+2019-03-13  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Test freshness page should improve the ability to correlating issues from same builder.
+        https://bugs.webkit.org/show_bug.cgi?id=195242
+
+        Reviewed by Ryosuke Niwa.
+
+        Added the ability to highlight indicators with same builder when mouse is hovering over one indicator.
+        This is a very useful visualization for correlating issues specific to a builder.
+        Added tooltip with latest build link when hovering over an indicator.
+
+        * public/v3/components/freshness-indicator.js:
+        (FreshnessIndicator): Removed 'summary' field as it's no longer needed.
+        Added 'highlighted' field.
+        (FreshnessIndicator.prototype.update): Added 'highlighted' argument.
+        (FreshnessIndicator.prototype.didConstructShadowTree): Make indicator to dispatch mouse enter and leave
+        messages so that UI can highlight corresponding cells.
+        (FreshnessIndicator.prototype.render):
+        (FreshnessIndicator.cssTemplate):
+        * public/v3/pages/test-freshness-page.js: Added tooltip to show latest build time and build link.
+        Added logic to manually compute table body height.
+        (TestFreshnessPage):
+        (TestFreshnessPage.prototype.didConstructShadowTree):
+        (TestFreshnessPage.prototype._fetchTestResults):
+        (TestFreshnessPage.prototype.render):
+        (TestFreshnessPage.prototype._renderTooltip):
+        (TestFreshnessPage.prototype._constructTableCell):
+        (TestFreshnessPage.cssTemplate):
+
 2019-02-26  Dewei Zhu  <dewei_zhu@apple.com>
 
         The table head of test freshness page should not scroll with the page.
index 40dc264..0126feb 100644 (file)
@@ -3,30 +3,37 @@ class FreshnessIndicator extends ComponentBase {
     {
         super('freshness-indicator');
         this._lastDataPointDuration = lastDataPointDuration;
-        this._summary = summary;
         this._testAgeTolerance = testAgeTolerance;
         this._url = url;
+        this._highlighted = false;
 
         this._renderIndicatorLazily = new LazilyEvaluatedFunction(this._renderIndicator.bind(this));
     }
 
-    update(lastDataPointDuration, testAgeTolerance, summary, url)
+    update(lastDataPointDuration, testAgeTolerance, url, highlighted)
     {
         this._lastDataPointDuration = lastDataPointDuration;
-        this._summary = summary;
         this._testAgeTolerance = testAgeTolerance;
         this._url = url;
+        this._highlighted = highlighted;
         this.enqueueToRender();
     }
 
+    didConstructShadowTree()
+    {
+        const container = this.content('container');
+        container.addEventListener('mouseenter', () => this.dispatchAction('select', this));
+        container.addEventListener('mouseleave', () => this.dispatchAction('unselect'));
+    }
+
     render()
     {
         super.render();
-        this._renderIndicatorLazily.evaluate(this._lastDataPointDuration, this._testAgeTolerance, this._summary, this._url);
+        this._renderIndicatorLazily.evaluate(this._lastDataPointDuration, this._testAgeTolerance, this._url, this._highlighted);
 
     }
 
-    _renderIndicator(lastDataPointDuration, testAgeTolerance, summary, url)
+    _renderIndicator(lastDataPointDuration, testAgeTolerance, url, highlighted)
     {
         const element = ComponentBase.createElement;
         if (!lastDataPointDuration) {
@@ -39,7 +46,7 @@ class FreshnessIndicator extends ComponentBase {
         const rating = 1 / (1 + Math.exp(Math.log(1.2) * (hoursSinceLastDataPoint - testAgeToleranceInHours)));
         const hue = Math.round(120 * rating);
         const brightness = Math.round(30 + 50 * rating);
-        const indicator = element('a', {id: 'cell', title: summary, href: url});
+        const indicator = element('a', {id: 'cell', href: url, class: highlighted ? 'highlight' : ''});
 
         indicator.style.backgroundColor = `hsl(${hue}, 100%, ${brightness}%)`;
         this.renderReplace(this.content('container'), indicator);
@@ -54,23 +61,26 @@ class FreshnessIndicator extends ComponentBase {
     {
         return `
             div {
+                margin-left: 0;
                 height: 1.8rem;
                 width: 1.8rem;
-                padding-top: 0.1rem;
             }
             a {
                 display: block;
                 height:1.6rem;
                 width:1.6rem;
-                margin: 0.1rem;
+                border: 0.1rem;
+                border-color: white;
+                border-style: solid;
                 padding: 0;
             }
 
-            a:hover {
-                height: 1.8rem;
-                width: 1.8rem;
-                margin: 0rem;
-                padding: 0;
+            a.highlight {
+                height: 1.4rem;
+                width: 1.4rem;
+                border: 0.2rem;
+                border-style: solid;
+                border-color: #0099ff;
             }`;
     }
 }
index 4ab32ea..2023348 100644 (file)
@@ -9,10 +9,27 @@ class TestFreshnessPage extends PageWithHeading {
         this._lastDataPointByConfiguration = null;
         this._indicatorByConfiguration = null;
         this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
+        this._currentlyHighlightedIndicator = null;
+        this._hoveringTooltip = false;
+        this._builderByIndicator = null;
+        this._renderTooltipLazily = new LazilyEvaluatedFunction(this._renderTooltip.bind(this));
 
         this._loadConfig(summaryPageConfiguration);
     }
 
+    didConstructShadowTree()
+    {
+        const tooltipContainer = this.content('tooltip-container');
+        tooltipContainer.addEventListener('mouseenter', () => {
+            this._hoveringTooltip = true;
+            this.enqueueToRender();
+        });
+        tooltipContainer.addEventListener('mouseleave', () => {
+            this._hoveringTooltip = false;
+            this.enqueueToRender();
+        });
+    }
+
     name() { return 'Test-Freshness'; }
 
     _loadConfig(summaryPageConfiguration)
@@ -55,6 +72,7 @@ class TestFreshnessPage extends PageWithHeading {
     {
         this._measurementSetFetchTime = Date.now();
         this._lastDataPointByConfiguration = new Map;
+        this._builderByIndicator = new Map;
 
         const startTime = this._measurementSetFetchTime - this._timeDuration;
 
@@ -71,10 +89,17 @@ class TestFreshnessPage extends PageWithHeading {
                     const currentTimeSeries = measurementSet.fetchedTimeSeries('current', false, false);
 
                     let timeForLastDataPoint = startTime;
-                    if (currentTimeSeries.lastPoint())
-                        timeForLastDataPoint = currentTimeSeries.lastPoint().build().buildTime();
+                    let lastBuildLink = null;
+                    let builder = null;
+                    const lastPoint = currentTimeSeries.lastPoint();
+                    if (lastPoint) {
+                        timeForLastDataPoint = lastPoint.build().buildTime();
+                        lastBuildLink = lastPoint.build().url();
+                        builder = lastPoint.build().builder();
+                    }
 
-                    lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint()});
+                    lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint(),
+                        lastBuildLink, builder});
                     this.enqueueToRender();
                 });
             }
@@ -87,20 +112,51 @@ class TestFreshnessPage extends PageWithHeading {
 
         this._renderTableLazily.evaluate(this._platforms, this._metrics);
 
+        let buildSummaryForCurrentlyHighlightedIndicator = null;
+        let buildLinkForCurrentlyHighlightedIndicator = null;
+        const builderForCurrentlyHighlightedIndicator = this._currentlyHighlightedIndicator ? this._builderByIndicator.get(this._currentlyHighlightedIndicator) : null;
         for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
             for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
                 const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
                 const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
                 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
-                const testLabel = `"${metric.test().fullName()}" for "${platform.name()}"`;
-                const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since last data point on ${testLabel}`;
+                const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since latest data point.`;
                 const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
                     this._measurementSetFetchTime - this._timeDuration));
 
                 const indicator = this._indicatorByConfiguration.get(platform).get(metric);
-                indicator.update(timeDuration, this._testAgeTolerance, summary, url);
+                if (this._currentlyHighlightedIndicator && this._currentlyHighlightedIndicator === indicator) {
+                    buildSummaryForCurrentlyHighlightedIndicator = summary;
+                    buildLinkForCurrentlyHighlightedIndicator = lastDataPoint.lastBuildLink;
+                }
+                this._builderByIndicator.set(indicator, lastDataPoint.builder);
+                indicator.update(timeDuration, this._testAgeTolerance, url, builderForCurrentlyHighlightedIndicator && builderForCurrentlyHighlightedIndicator === lastDataPoint.builder);
             }
         }
+        this._renderTooltipLazily.evaluate(this._currentlyHighlightedIndicator, this._hoveringTooltip, buildSummaryForCurrentlyHighlightedIndicator, buildLinkForCurrentlyHighlightedIndicator);
+    }
+
+    _renderTooltip(indicator, hoveringTooltip, buildSummary, buildLink)
+    {
+        if (!indicator || !buildSummary) {
+            this.content('tooltip-container').style.display = hoveringTooltip ? null : 'none';
+            return;
+        }
+        const element = ComponentBase.createElement;
+
+        const rect = indicator.element().getBoundingClientRect();
+        const tooltipContainer = this.content('tooltip-container');
+        tooltipContainer.style.display = null;
+
+        const tooltipContainerComputedStyle = getComputedStyle(tooltipContainer);
+        const containerMarginTop = parseFloat(tooltipContainerComputedStyle.paddingTop);
+        const containerMarginLeft = parseFloat(tooltipContainerComputedStyle.marginLeft);
+
+        tooltipContainer.style.position = 'absolute';
+        tooltipContainer.style.top = rect.top - (tooltipContainer.offsetHeight + containerMarginTop)  + 'px';
+        tooltipContainer.style.left = rect.left + rect.width / 2 - tooltipContainer.offsetWidth / 2 + containerMarginLeft + 'px';
+
+        this.renderReplace(tooltipContainer, [element('p', buildSummary), buildLink ? element('a', {href: buildLink}, 'Latest Build') : []]);
     }
 
     _renderTable(platforms, metrics)
@@ -138,13 +194,21 @@ class TestFreshnessPage extends PageWithHeading {
             return element('td', {class: 'blank-cell'}, element('div'));
 
         const indicator = new FreshnessIndicator;
+        indicator.listenToAction('select', (originator) => {
+            this._currentlyHighlightedIndicator = originator;
+            this.enqueueToRender();
+        });
+        indicator.listenToAction('unselect', () => {
+            this._currentlyHighlightedIndicator = null;
+            this.enqueueToRender();
+        });
         indicatorByMetric.set(metric, indicator);
         return element('td', {class: 'status-cell'}, indicator);
     }
 
     static htmlTemplate()
     {
-        return `<section class="page-with-heading"><table id="test-health"></table></section>`;
+        return `<section class="page-with-heading"><div id="tooltip-container"></div><table id="test-health"></table></section>`;
     }
 
     static cssTemplate()
@@ -186,7 +250,7 @@ class TestFreshnessPage extends PageWithHeading {
             #test-health tbody {
                 display: block;
                 overflow: auto;
-                height: 75vh;
+                height: calc(100vh - 24rem);
             }
             #test-health td.status-cell {
                 margin: 0;
@@ -214,26 +278,55 @@ class TestFreshnessPage extends PageWithHeading {
                 overflow: hidden;
             }
             #test-health td.blank-cell > div::before {
-              content: "";
-              position: absolute;
-              top: -1px;
-              left: -1px;
-              display: block;
-              width: 0px;
-              height: 0px;
-              border-right: calc(1.6rem + 1px) solid #ddd;
-              border-top: calc(1.6rem + 1px) solid transparent;
+                content: "";
+                position: absolute;
+                top: -1px;
+                left: -1px;
+                display: block;
+                width: 0px;
+                height: 0px;
+                border-right: calc(1.6rem + 1px) solid #ddd;
+                border-top: calc(1.6rem + 1px) solid transparent;
             }
             #test-health td.blank-cell > div::after {
-              content: "";
-              display: block;
-              position: absolute;
-              top: 1px;
-              left: 1px;
-              width: 0px;
-              height: 0px;
-              border-right: calc(1.6rem - 1px) solid #F9F9F9;
-              border-top: calc(1.6rem - 1px) solid transparent;
+                content: "";
+                display: block;
+                position: absolute;
+                top: 1px;
+                left: 1px;
+                width: 0px;
+                height: 0px;
+                border-right: calc(1.6rem - 1px) solid #F9F9F9;
+                border-top: calc(1.6rem - 1px) solid transparent;
+            }
+            #tooltip-container {
+                width: 22rem;
+                height: 2rem;
+                background-color: #34495E;
+                opacity: 0.9;
+                margin: 0.3rem;
+                padding: 0.3rem;
+                border-radius: 0.4rem;
+                z-index: 1;
+                text-align: center;
+            }
+            #tooltip-container::after {
+                content: " ";
+                position: absolute;
+                top: 100%;
+                left: 50%;
+                margin-left: -1rem;
+                border-width: 0.2rem;
+                border-style: solid;
+                border-color: #34495E transparent transparent transparent;
+            }
+            #tooltip-container p {
+                color: white;
+                margin: 0;
+            }
+            #tooltip-container a {
+                color: #B03A2E;
+                font-weight: bold;
             }
         `;
     }