Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (225897 => 225898)
--- trunk/Websites/perf.webkit.org/ChangeLog 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2017-12-14 09:49:37 UTC (rev 225898)
@@ -1,3 +1,49 @@
+2017-12-13 Dewei Zhu <dewei_...@apple.com>
+
+ Add a test freshness page.
+ https://bugs.webkit.org/show_bug.cgi?id=180126
+
+ Reviewed by Ryosuke Niwa.
+
+ Added a page to show freshness of a test.
+ The test freshness page reports on the same set of tests as the one shown in the summary page.
+ Use a logistic function to evaluate the freshness of the data points.
+ This function has the desired property which increase dramatically when it close to the center of the graph.
+ 'acceptableLastDataPointDurationInHour' configs the center of the graph.
+
+ * public/include/manifest-generator.php:
+ * public/v3/components/freshness-indicator.js: Added.
+ (FreshnessIndicator): A cell of the test freshness table, color will transit from green to red.
+ (FreshnessIndicator.prototype.update): Update the the data point information and triggers
+ the cell to re-render if anything changes.
+ (FreshnessIndicator.prototype._renderIndicator): Re-render the indicator.
+ (FreshnessIndicator.prototype.render): Render the box color base on a logistic function.
+ (FreshnessIndicator.prototype._createIndicator):
+ (FreshnessIndicator.htmlTemplate):
+ (FreshnessIndicator.cssTemplate):
+ * public/v3/index.html:
+ * public/v3/main.js: Added test freshness page.
+ (main):
+ * public/v3/models/build-request.js: Refactored waitingTime function to make it reusable.
+ (BuildRequest.formatTimeInterval): Format time interval in million seconds to more user friendly text.
+ (BuildRequest.prototype.waitingTime):
+ * public/v3/pages/test-freshness-page.js: Added.
+ (TestFreshnessPage):
+ (TestFreshnessPage.prototype.name):
+ (TestFreshnessPage.prototype._loadConfig): Load config from summary page configurations.
+ (TestFreshnessPage.prototype.open):
+ (TestFreshnessPage.prototype._fetchTestResults):
+ (TestFreshnessPage.prototype.render):
+ (TestFreshnessPage.prototype._renderTable):
+ (TestFreshnessPage.prototype._isValidPlatformMetricCombination): Return whether a platform
+ and metric combination is valid.
+ (TestFreshnessPage.prototype._constructTableCell):
+ (TestFreshnessPage.cssTemplate):
+ (TestFreshnessPage.prototype.routeName):
+ * server-tests/api-manifest-tests.js: Added 'warningHourBaseline' so that we can config the
+ parameter of logistic funciton.
+ * unit-tests/build-request-tests.js: Added unit tests for formatTimeInterval.
+
2017-11-02 Dewei Zhu <dewei_...@apple.com>
Add platform argument for syncing script.
Modified: trunk/Websites/perf.webkit.org/public/include/manifest-generator.php (225897 => 225898)
--- trunk/Websites/perf.webkit.org/public/include/manifest-generator.php 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/public/include/manifest-generator.php 2017-12-14 09:49:37 UTC (rev 225898)
@@ -44,6 +44,7 @@
'dashboards' => (object)config('dashboards'),
'summaryPages' => config('summaryPages'),
'fileUploadSizeLimit' => config('uploadFileLimitInMB', 0) * 1024 * 1024,
+ 'testAgeToleranceInHours' => config('testAgeToleranceInHours'),
);
$this->manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
Added: trunk/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js (0 => 225898)
--- trunk/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -0,0 +1,79 @@
+class FreshnessIndicator extends ComponentBase {
+ constructor(lastDataPointDuration, testAgeTolerance, summary, url)
+ {
+ super('freshness-indicator');
+ this._lastDataPointDuration = lastDataPointDuration;
+ this._summary = summary;
+ this._testAgeTolerance = testAgeTolerance;
+ this._url = url;
+
+ this._renderIndicatorLazily = new LazilyEvaluatedFunction(this._renderIndicator.bind(this));
+ }
+
+ update(lastDataPointDuration, testAgeTolerance, summary, url)
+ {
+ this._lastDataPointDuration = lastDataPointDuration;
+ this._summary = summary;
+ this._testAgeTolerance = testAgeTolerance;
+ this._url = url;
+ this.enqueueToRender();
+ }
+
+ render()
+ {
+ super.render();
+ this._renderIndicatorLazily.evaluate(this._lastDataPointDuration, this._testAgeTolerance, this._summary, this._url);
+
+ }
+
+ _renderIndicator(lastDataPointDuration, testAgeTolerance, summary, url)
+ {
+ const element = ComponentBase.createElement;
+ if (!lastDataPointDuration) {
+ this.renderReplace(this.content('container'), new SpinnerIcon);
+ return;
+ }
+
+ const hoursSinceLastDataPoint = this._lastDataPointDuration / 3600 / 1000;
+ const testAgeToleranceInHours = testAgeTolerance / 3600 / 1000;
+ 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});
+
+ indicator.style.backgroundColor = `hsl(${hue}, 100%, ${brightness}%)`;
+ this.renderReplace(this.content('container'), indicator);
+ }
+
+ static htmlTemplate()
+ {
+ return `<div id='container'></div>`;
+ }
+
+ static cssTemplate()
+ {
+ return `
+ div {
+ height: 1.8rem;
+ width: 1.8rem;
+ padding-top: 0.1rem;
+ }
+ a {
+ display: block;
+ height:1.6rem;
+ width:1.6rem;
+ margin: 0.1rem;
+ padding: 0;
+ }
+
+ a:hover {
+ height: 1.8rem;
+ width: 1.8rem;
+ margin: 0rem;
+ padding: 0;
+ }`;
+ }
+}
+
+
+ComponentBase.defineElement('freshness-indicator', FreshnessIndicator);
\ No newline at end of file
Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (225897 => 225898)
--- trunk/Websites/perf.webkit.org/public/v3/index.html 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html 2017-12-14 09:49:37 UTC (rev 225898)
@@ -98,6 +98,7 @@
<script src=""
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
@@ -117,6 +118,7 @@
<script src=""
<script src=""
<script src=""
+ <script src=""
<script src=""
</template>
Modified: trunk/Websites/perf.webkit.org/public/v3/main.js (225897 => 225898)
--- trunk/Websites/perf.webkit.org/public/v3/main.js 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/public/v3/main.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -47,8 +47,10 @@
var buildRequestQueuePage = new BuildRequestQueuePage();
buildRequestQueuePage.setParentPage(analysisCategoryPage);
+ const testHealthPage = new TestFreshnessPage(manifest.summaryPages, manifest.testAgeToleranceInHours);
+
var heading = new Heading(manifest.siteTitle);
- heading.addPageGroup(summaryPages.concat([chartsPage, analysisCategoryPage]));
+ heading.addPageGroup(summaryPages.concat([chartsPage, analysisCategoryPage, testHealthPage]));
heading.setTitle(manifest.siteTitle);
heading.addPageGroup(dashboardPages);
@@ -61,6 +63,7 @@
router.addPage(analysisTaskPage);
router.addPage(buildRequestQueuePage);
router.addPage(analysisCategoryPage);
+ router.addPage(testHealthPage);
for (var page of dashboardPages)
router.addPage(page);
Modified: trunk/Websites/perf.webkit.org/public/v3/models/build-request.js (225897 => 225898)
--- trunk/Websites/perf.webkit.org/public/v3/models/build-request.js 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/public/v3/models/build-request.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -84,9 +84,9 @@
buildId() { return this._buildId; }
createdAt() { return this._createdAt; }
- waitingTime(referenceTime)
- {
- var units = [
+ static formatTimeInterval(intervalInMillionSeconds) {
+ let intervalInSeconds = intervalInMillionSeconds / 1000;
+ const units = [
{unit: 'week', length: 7 * 24 * 3600},
{unit: 'day', length: 24 * 3600},
{unit: 'hour', length: 3600},
@@ -93,34 +93,38 @@
{unit: 'minute', length: 60},
];
- var diff = (referenceTime - this.createdAt()) / 1000;
- var indexOfFirstSmallEnoughUnit = units.length - 1;
- for (var i = 0; i < units.length; i++) {
- if (diff > 1.5 * units[i].length) {
+ let indexOfFirstSmallEnoughUnit = units.length - 1;
+ for (let i = 0; i < units.length; i++) {
+ if (intervalInSeconds > 1.5 * units[i].length) {
indexOfFirstSmallEnoughUnit = i;
break;
}
}
- var label = '';
- var lastUnit = false;
- for (var i = indexOfFirstSmallEnoughUnit; !lastUnit; i++) {
+ let label = '';
+ let lastUnit = false;
+ for (let i = indexOfFirstSmallEnoughUnit; !lastUnit; i++) {
lastUnit = i == indexOfFirstSmallEnoughUnit + 1 || i == units.length - 1;
- var length = units[i].length;
- var valueForUnit = lastUnit ? Math.round(diff / length) : Math.floor(diff / length);
+ const length = units[i].length;
+ const valueForUnit = lastUnit ? Math.round(intervalInSeconds / length) : Math.floor(intervalInSeconds / length);
- var unit = units[i].unit + (valueForUnit == 1 ? '' : 's');
+ const unit = units[i].unit + (valueForUnit == 1 ? '' : 's');
if (label)
label += ' ';
label += `${valueForUnit} ${unit}`;
- diff = diff - valueForUnit * length;
+ intervalInSeconds = intervalInSeconds - valueForUnit * length;
}
return label;
}
+ waitingTime(referenceTime)
+ {
+ return BuildRequest.formatTimeInterval(referenceTime - this.createdAt());
+ }
+
static fetchForTriggerable(triggerable)
{
return RemoteAPI.getJSONWithStatus('/api/build-requests/' + triggerable).then(function (data) {
Added: trunk/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js (0 => 225898)
--- trunk/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -0,0 +1,230 @@
+
+class TestFreshnessPage extends PageWithHeading {
+ constructor(summaryPageConfiguration, testAgeToleranceInHours)
+ {
+ super('test-freshness', null);
+ this._testAgeTolerance = (testAgeToleranceInHours || 24) * 3600 * 1000;
+ this._timeDuration = this._testAgeTolerance * 2;
+ this._excludedConfigurations = {};
+ this._lastDataPointByConfiguration = null;
+ this._indicatorByConfiguration = null;
+ this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
+
+ this._loadConfig(summaryPageConfiguration);
+ }
+
+ name() { return 'Test-Freshness'; }
+
+ _loadConfig(summaryPageConfiguration)
+ {
+ const platformIdSet = new Set;
+ const metricIdSet = new Set;
+
+ for (const config of summaryPageConfiguration) {
+ for (const platformGroup of config.platformGroups) {
+ for (const platformId of platformGroup.platforms)
+ platformIdSet.add(platformId);
+ }
+
+ for (const metricGroup of config.metricGroups) {
+ for (const subgroup of metricGroup.subgroups) {
+ for (const metricId of subgroup.metrics)
+ metricIdSet.add(metricId);
+ }
+ }
+
+ const excludedConfigs = config.excludedConfigurations;
+ for (const platform in excludedConfigs) {
+ if (platform in this._excludedConfigurations)
+ this._excludedConfigurations[platform] = this._excludedConfigurations[platform].concat(excludedConfigs[platform]);
+ else
+ this._excludedConfigurations[platform] = excludedConfigs[platform];
+ }
+ }
+ this._platforms = [...platformIdSet].map((platformId) => Platform.findById(platformId));
+ this._metrics = [...metricIdSet].map((metricId) => Metric.findById(metricId));
+ }
+
+ open(state)
+ {
+ this._fetchTestResults();
+ super.open(state);
+ }
+
+ _fetchTestResults()
+ {
+ this._measurementSetFetchTime = Date.now();
+ this._lastDataPointByConfiguration = new Map;
+
+ const startTime = this._measurementSetFetchTime - this._timeDuration;
+
+ for (const platform of this._platforms) {
+ const lastDataPointByMetric = new Map;
+ this._lastDataPointByConfiguration.set(platform, lastDataPointByMetric);
+
+ for (const metric of this._metrics) {
+ if (!this._isValidPlatformMetricCombination(platform, metric, this._excludedConfigurations))
+ continue;
+
+ const measurementSet = MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric));
+ measurementSet.fetchBetween(startTime, this._measurementSetFetchTime).then(() => {
+ const currentTimeSeries = measurementSet.fetchedTimeSeries('current', false, false);
+
+ let timeForLastDataPoint = startTime;
+ if (currentTimeSeries.lastPoint())
+ timeForLastDataPoint = currentTimeSeries.lastPoint().time;
+
+ lastDataPointByMetric.set(metric, {time: timeForLastDataPoint, hasCurrentDataPoint: !!currentTimeSeries.lastPoint()});
+ this.enqueueToRender();
+ });
+ }
+ }
+ }
+
+ render()
+ {
+ super.render();
+
+ this._renderTableLazily.evaluate(this._platforms, this._metrics);
+
+ 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 url = "" 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);
+ }
+ }
+ }
+
+ _renderTable(platforms, metrics)
+ {
+ const element = ComponentBase.createElement;
+ const tableBodyElement = [];
+ const tableHeadElements = [element('th', {class: 'table-corner'}, 'Platform \\ Test')];
+
+ for (const metric of metrics)
+ tableHeadElements.push(element('th', {class: 'diagonal-header'}, element('div', metric.test().fullName())));
+
+ this._indicatorByConfiguration = new Map;
+ for (const platform of platforms) {
+ const indicatorByMetric = new Map;
+ this._indicatorByConfiguration.set(platform, indicatorByMetric);
+ tableBodyElement.push(element('tr',
+ [element('th', platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
+ }
+
+ this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
+ }
+
+ _isValidPlatformMetricCombination(platform, metric)
+ {
+ return !(this._excludedConfigurations && this._excludedConfigurations[platform.id()]
+ && this._excludedConfigurations[platform.id()].some((metricId) => metricId == metric.id()))
+ && platform.hasMetric(metric);
+ }
+
+ _constructTableCell(platform, metric, indicatorByMetric)
+ {
+ const element = ComponentBase.createElement;
+
+ if (!this._isValidPlatformMetricCombination(platform, metric))
+ return element('td', {class: 'blank-cell'}, element('div'));
+
+ const indicator = new FreshnessIndicator;
+ 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>`;
+ }
+
+ static cssTemplate()
+ {
+ return `
+ .page-with-heading {
+ display: flex;
+ justify-content: center;
+ }
+ #test-health {
+ font-size: 1rem;
+ }
+ #test-health th.table-corner {
+ text-align: right;
+ vertical-align: bottom;
+ }
+ #test-health th {
+ text-align: left;
+ border-bottom: 0.1rem solid #ccc;
+ font-weight: normal;
+ }
+ #test-health th.diagonal-header {
+ white-space: nowrap;
+ height: 16rem;
+ border-bottom: 0rem;
+ }
+ #test-health th.diagonal-header > div {
+ transform: translate(1rem, 7rem) rotate(315deg);
+ width: 2rem;
+ border: 0rem;
+ }
+ #test-health td.status-cell {
+ margin: 0;
+ padding: 0;
+ max-width: 2.2rem;
+ max-height: 2.2rem;
+ min-width: 2.2rem;
+ min-height: 2.2rem;
+ }
+ #test-health td.blank-cell {
+ margin: 0;
+ padding: 0;
+ max-width: 2.2rem;
+ max-height: 2.2rem;
+ min-width: 2.2rem;
+ min-height: 2.2rem;
+ }
+ #test-health td.blank-cell > div {
+ background-color: #F9F9F9;
+ height: 1.6rem;
+ width: 1.6rem;
+ margin: 0.1rem;
+ padding: 0;
+ position: relative;
+ 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;
+ }
+ #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;
+ }
+ `;
+ }
+
+ routeName() { return 'test-freshness'; }
+}
\ No newline at end of file
Modified: trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js (225897 => 225898)
--- trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/server-tests/api-manifest-tests.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -14,7 +14,7 @@
it("should generate an empty manifest when database is empty", () => {
return TestServer.remoteAPI().getJSON('/api/manifest').then((manifest) => {
assert.deepEqual(Object.keys(manifest).sort(), ['all', 'bugTrackers', 'builders', 'dashboard', 'dashboards',
- 'elapsedTime', 'fileUploadSizeLimit', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'tests', 'triggerables']);
+ 'elapsedTime', 'fileUploadSizeLimit', 'metrics', 'repositories', 'siteTitle', 'status', 'summaryPages', 'testAgeToleranceInHours', 'tests', 'triggerables']);
assert.equal(typeof(manifest.elapsedTime), 'number');
delete manifest.elapsedTime;
@@ -29,6 +29,7 @@
fileUploadSizeLimit: 2097152, // 2MB during testing.
metrics: {},
repositories: {},
+ testAgeToleranceInHours: null,
tests: {},
triggerables: {},
summaryPages: [],
Modified: trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js (225897 => 225898)
--- trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js 2017-12-14 09:39:59 UTC (rev 225897)
+++ trunk/Websites/perf.webkit.org/unit-tests/build-request-tests.js 2017-12-14 09:49:37 UTC (rev 225898)
@@ -158,4 +158,54 @@
});
+ describe('formatTimeInterval', () => {
+ it('should return "0 minutes" when formatting for 0 second in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(0), '0 minutes');
+ });
+
+ it('should return "1 minute" when formatting for 60 seconds in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(60 * 1000), '1 minute');
+ });
+
+ it('should return "1 minute" when formatting for 75 seconds in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(75 * 1000), '1 minute');
+ });
+
+ it('should return "2 minutes" when formatting for 118 seconds in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(118 * 1000), '2 minutes');
+ });
+
+ it('should return "75 minutes" when formatting for 75 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(75 * 60 * 1000), '75 minutes');
+ });
+
+ it('should return "1 hour 58 minutes" when formatting for 118 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(118 * 60 * 1000), '1 hour 58 minutes');
+ });
+
+ it('should return "3 hours 2 minutes" when formatting for 182 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(182 * 60 * 1000), '3 hours 2 minutes');
+ });
+
+ it('should return "27 hours 14 minutes" when formatting for 27 hours 14 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval((27 * 3600 + 14 * 60) * 1000), '27 hours 14 minutes');
+ });
+
+ it('should return "2 days 3 hours" when formatting for 51 hours 14 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval((51 * 3600 + 14 * 60) * 1000), '2 days 3 hours');
+ });
+
+ it('should return "2 days 0 hours" when formatting for 48 hours 1 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval((48 * 3600 + 1 * 60) * 1000), '2 days 0 hours');
+ });
+
+ it('should return "2 days 2 hours" when formatting for 49 hours 59 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval((49 * 3600 + 59 * 60) * 1000), '2 days 2 hours');
+ });
+
+ it('should return "2 weeks 6 days" when formatting for 20 days 5 hours 21 minutes in million seconds', () => {
+ assert.equal(BuildRequest.formatTimeInterval(((20 * 24 + 5) * 3600 + 21 * 60) * 1000), '2 weeks 6 days');
+ });
+ });
+
});
\ No newline at end of file