Add a summary page to v3 UI
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 13 Apr 2016 09:26:28 +0000 (09:26 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 13 Apr 2016 09:26:28 +0000 (09:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=156531

Reviewed by Stephanie Lewis.

Add new "Summary" page, which shows the average difference (better or worse) from the baseline across
multiple platforms and tests by a single number.

* public/include/manifest.php:
(ManifestGenerator::generate): Include "summary" in manifest.json.
* public/shared/statistics.js:
(Statistics.mean): Added.
(Statistics.median): Added.
* public/v3/components/ratio-bar-graph.js: Added.
(RatioBarGraph): Shows a horizontal bar graph that visualizes the relative difference (e.g. 3% better).
(RatioBarGraph.prototype.update):
(RatioBarGraph.prototype.render):
(RatioBarGraph.cssTemplate):
(RatioBarGraph.htmlTemplate):
* public/v3/index.html:
* public/v3/main.js:
(main): Instantiate SummaryPage and add it to the navigation bar and the router.
* public/v3/models/manifest.js:
(Manifest._didFetchManifest): Let "summary" pass through from manifest.json to main().
* public/v3/models/measurement-set.js:
(MeasurementSet.prototype._failedToFetchJSON): Invoke the callback with an error or true in order for
the callback can detect a failure.
(MeasurementSet.prototype._invokeCallbacks): Ditto.
* public/v3/pages/charts-page.js:
(ChartsPage.createStateForConfigurationList): Added to add a hyperlink from summary page to charts page.
* public/v3/pages/summary-page.js: Added.
(SummaryPage): Added.
(SummaryPage.prototype.routeName): Added.
(SummaryPage.prototype.open): Added.
(SummaryPage.prototype.render): Added.
(SummaryPage.prototype._createConfigurationGroupAndStartFetchingData): Added.
(SummaryPage.prototype._constructTable): Added.
(SummaryPage.prototype._constructRatioGraph): Added.
(SummaryPage.htmlTemplate): Added.
(SummaryPage.cssTemplate): Added.
(SummaryPageConfigurationGroup): Added. Represents a set of platforms and tests shown in a single cell.
(SummaryPageConfigurationGroup.prototype.ratio): Added.
(SummaryPageConfigurationGroup.prototype.label): Added.
(SummaryPageConfigurationGroup.prototype.changeType): Added.
(SummaryPageConfigurationGroup.prototype.configurationList): Added.
(SummaryPageConfigurationGroup.prototype.fetchAndComputeSummary): Added.
(SummaryPageConfigurationGroup.prototype._computeSummary): Added.
(SummaryPageConfigurationGroup.prototype._fetchAndComputeRatio): Added. Invoked for each time series in
the set, and stores the computed ratio of the current values to the baseline in this._setToRatio.
The results are aggregated by _computeSummary as a single number later.
(SummaryPageConfigurationGroup._medianForTimeRange): Added.
(SummaryPageConfigurationGroup._fetchData): A thin wrapper to make MeasurementSet.fetchBetween promise
friendly since MeasurementSet doesn't support Promise at the moment (but it should!).
* server-tests/api-manifest.js: Updated a test case.

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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/include/manifest.php
Websites/perf.webkit.org/public/shared/statistics.js
Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/index.html
Websites/perf.webkit.org/public/v3/main.js
Websites/perf.webkit.org/public/v3/models/manifest.js
Websites/perf.webkit.org/public/v3/models/measurement-set.js
Websites/perf.webkit.org/public/v3/pages/charts-page.js
Websites/perf.webkit.org/public/v3/pages/summary-page.js [new file with mode: 0644]
Websites/perf.webkit.org/server-tests/api-manifest.js

index 5f71758..a806cc5 100644 (file)
@@ -1,5 +1,62 @@
 2016-04-12  Ryosuke Niwa  <rniwa@webkit.org>
 
+        Add a summary page to v3 UI
+        https://bugs.webkit.org/show_bug.cgi?id=156531
+
+        Reviewed by Stephanie Lewis.
+
+        Add new "Summary" page, which shows the average difference (better or worse) from the baseline across
+        multiple platforms and tests by a single number.
+
+        * public/include/manifest.php:
+        (ManifestGenerator::generate): Include "summary" in manifest.json.
+        * public/shared/statistics.js:
+        (Statistics.mean): Added.
+        (Statistics.median): Added.
+        * public/v3/components/ratio-bar-graph.js: Added.
+        (RatioBarGraph): Shows a horizontal bar graph that visualizes the relative difference (e.g. 3% better).
+        (RatioBarGraph.prototype.update):
+        (RatioBarGraph.prototype.render):
+        (RatioBarGraph.cssTemplate):
+        (RatioBarGraph.htmlTemplate):
+        * public/v3/index.html:
+        * public/v3/main.js:
+        (main): Instantiate SummaryPage and add it to the navigation bar and the router.
+        * public/v3/models/manifest.js:
+        (Manifest._didFetchManifest): Let "summary" pass through from manifest.json to main().
+        * public/v3/models/measurement-set.js:
+        (MeasurementSet.prototype._failedToFetchJSON): Invoke the callback with an error or true in order for
+        the callback can detect a failure.
+        (MeasurementSet.prototype._invokeCallbacks): Ditto.
+        * public/v3/pages/charts-page.js:
+        (ChartsPage.createStateForConfigurationList): Added to add a hyperlink from summary page to charts page.
+        * public/v3/pages/summary-page.js: Added.
+        (SummaryPage): Added.
+        (SummaryPage.prototype.routeName): Added.
+        (SummaryPage.prototype.open): Added.
+        (SummaryPage.prototype.render): Added.
+        (SummaryPage.prototype._createConfigurationGroupAndStartFetchingData): Added.
+        (SummaryPage.prototype._constructTable): Added.
+        (SummaryPage.prototype._constructRatioGraph): Added.
+        (SummaryPage.htmlTemplate): Added.
+        (SummaryPage.cssTemplate): Added.
+        (SummaryPageConfigurationGroup): Added. Represents a set of platforms and tests shown in a single cell.
+        (SummaryPageConfigurationGroup.prototype.ratio): Added.
+        (SummaryPageConfigurationGroup.prototype.label): Added.
+        (SummaryPageConfigurationGroup.prototype.changeType): Added.
+        (SummaryPageConfigurationGroup.prototype.configurationList): Added.
+        (SummaryPageConfigurationGroup.prototype.fetchAndComputeSummary): Added.
+        (SummaryPageConfigurationGroup.prototype._computeSummary): Added.
+        (SummaryPageConfigurationGroup.prototype._fetchAndComputeRatio): Added. Invoked for each time series in
+        the set, and stores the computed ratio of the current values to the baseline in this._setToRatio.
+        The results are aggregated by _computeSummary as a single number later.
+        (SummaryPageConfigurationGroup._medianForTimeRange): Added.
+        (SummaryPageConfigurationGroup._fetchData): A thin wrapper to make MeasurementSet.fetchBetween promise
+        friendly since MeasurementSet doesn't support Promise at the moment (but it should!).
+        * server-tests/api-manifest.js: Updated a test case.
+
+2016-04-12  Ryosuke Niwa  <rniwa@webkit.org>
+
         Make sync-buildbot.js fault safe
         https://bugs.webkit.org/show_bug.cgi?id=156498
 
index 080f9a6..bf10cd5 100644 (file)
@@ -36,6 +36,7 @@ class ManifestGenerator {
             'builders' => (object)$this->builders(),
             'bugTrackers' => (object)$this->bug_trackers($repositories_table),
             'dashboards' => (object)config('dashboards'),
+            'summary' => (object)config('summary'),
         );
 
         $this->manifest['elapsedTime'] = (microtime(true) - $start_time) * 1000;
index 36b188f..72215c5 100644 (file)
@@ -12,6 +12,14 @@ var Statistics = new (function () {
         return values.length ? values.reduce(function (a, b) { return a + b; }) : 0;
     }
 
+    this.mean = function (values) {
+        return this.sum(values) / values.length;
+    }
+
+    this.median = function (values) {
+        return values.sort(function (a, b) { return a - b; })[Math.floor(values.length / 2)];
+    }
+
     this.squareSum = function (values) {
         return values.length ? values.reduce(function (sum, value) { return sum + value * value;}, 0) : 0;
     }
diff --git a/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js b/Websites/perf.webkit.org/public/v3/components/ratio-bar-graph.js
new file mode 100644 (file)
index 0000000..70f9238
--- /dev/null
@@ -0,0 +1,89 @@
+class RatioBarGraph extends ComponentBase {
+
+    constructor()
+    {
+        super('ratio-bar-graph');
+        this._ratio = 0;
+        this._label = null;
+        this._shouldRender = true;
+        this._bar = this.content().querySelector('.bar');
+        this._labelContainer = this.content().querySelector('.label');
+    }
+
+    update(ratio, label)
+    {
+        console.assert(ratio >= -1 && ratio <= 1);
+        this._ratio = ratio;
+        this._label = label;
+        this._shouldRender = true;
+    }
+
+    render()
+    {
+        if (!this._shouldRender)
+            return;
+
+        var percent = Math.abs(this._ratio * 100);
+        this._labelContainer.textContent = this._label;
+        this._bar.style.width = Math.min(percent, 50) + '%';
+        this._bar.parentNode.className = 'ratio-bar-graph ' + (this._ratio > 0 ? 'better' : 'worse');
+
+        this._shouldRender = false;
+    }
+
+    static htmlTemplate()
+    {
+        return `<div class="ratio-bar-graph"><div class="seperator"></div><div class="bar"></div><div class="label"></div></div>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            .ratio-bar-graph {
+                position: relative;
+                display: block;
+                margin-left: auto;
+                margin-right: auto;
+                width: 10rem;
+                height: 2.5rem;
+                overflow: hidden;
+                text-decoration: none;
+                color: black;
+            }
+            .ratio-bar-graph .seperator {
+                position: absolute;
+                left: 50%;
+                width: 0px;
+                height: 100%;
+                border-left: solid 1px #ccc;
+            }
+            .ratio-bar-graph .bar {
+                position: absolute;
+                left: 50%;
+                top: 0.5rem;
+                height: calc(100% - 1rem);
+                background: #ccc;
+            }
+            .ratio-bar-graph.worse .bar {
+                transform: translateX(-100%);
+                background: #c33;
+            }
+            .ratio-bar-graph.better .bar {
+                background: #3c3;
+            }
+            .ratio-bar-graph .label {
+                position: absolute;
+                line-height: 2.5rem;
+            }
+            .ratio-bar-graph.worse .label {
+                text-align: left;
+                left: calc(50% + 0.2rem);
+            }
+            .ratio-bar-graph.better .label {
+                text-align: right;
+                right: calc(50% + 0.2rem);
+            }
+        `;
+    }
+
+}
\ No newline at end of file
index 8f80bed..3db657b 100644 (file)
@@ -82,6 +82,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="components/chart-styles.js"></script>
         <script src="components/chart-pane-base.js"></script>
         <script src="components/mutable-list-view.js"></script>
+        <script src="components/ratio-bar-graph.js"></script>
         <script src="pages/page.js"></script>
         <script src="pages/page-router.js"></script>
         <script src="pages/heading.js"></script>
@@ -98,6 +99,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`);
         <script src="pages/analysis-category-page.js"></script>
         <script src="pages/analysis-task-page.js"></script>
         <script src="pages/create-analysis-task-page.js"></script>
+        <script src="pages/summary-page.js"></script>
 
         <script src="main.js"></script>
     </template>
index 6705625..a21b735 100644 (file)
@@ -18,10 +18,10 @@ function main() {
         }
 
         var router = new PageRouter();
-
         var chartsToolbar = new ChartsToolbar;
-        var chartsPage = new ChartsPage(chartsToolbar);
 
+        var summaryPage = new SummaryPage(manifest.summary);
+        var chartsPage = new ChartsPage(chartsToolbar);
         var analysisCategoryPage = new AnalysisCategoryPage();
 
         var createAnalysisTaskPage = new CreateAnalysisTaskPage();
@@ -31,12 +31,13 @@ function main() {
         analysisTaskPage.setParentPage(analysisCategoryPage);
 
         var heading = new Heading(manifest.siteTitle);
-        heading.addPageGroup([chartsPage, analysisCategoryPage]);
+        heading.addPageGroup([summaryPage, chartsPage, analysisCategoryPage]);
 
         heading.setTitle(manifest.siteTitle);
         heading.addPageGroup(dashboardPages);
 
         var router = new PageRouter();
+        router.addPage(summaryPage);
         router.addPage(chartsPage);
         router.addPage(createAnalysisTaskPage);
         router.addPage(analysisTaskPage);
index ad52374..b7ac86c 100644 (file)
@@ -48,6 +48,7 @@ class Manifest {
         return {
             siteTitle: rawResponse.siteTitle,
             dashboards: rawResponse.dashboards, // FIXME: Add an abstraction around dashboards.
+            summary: rawResponse.summary,
         }
     }
 }
index 45d1dec..97e9206 100644 (file)
@@ -166,22 +166,22 @@ class MeasurementSet {
     _failedToFetchJSON(clusterEndTime, error)
     {
         if (clusterEndTime) {
-            this._invokeCallbacks(clusterEndTime);
+            this._invokeCallbacks(clusterEndTime, error || true);
             return;
         }
 
         console.assert(!this._fetchedPrimary);
         console.assert(this._waitingForPrimaryCluster instanceof Array);
         for (var entry of this._waitingForPrimaryCluster)
-            entry.callback();
+            entry.callback(error || true);
         this._waitingForPrimaryCluster = false;
     }
 
-    _invokeCallbacks(clusterEndTime)
+    _invokeCallbacks(clusterEndTime, error)
     {
         var callbackList = this._endTimeToCallback[clusterEndTime];
         for (var callback of callbackList)
-            callback();
+            callback(error);
         this._endTimeToCallback[clusterEndTime] = true;
     }
 
index b95fd98..f9ceca5 100644 (file)
@@ -37,6 +37,13 @@ class ChartsPage extends PageWithHeading {
         return state;
     }
 
+    static createStateForConfigurationList(configurationList, startTime)
+    {
+        console.assert(configurationList instanceof Array);
+        var state = {paneList: configurationList};
+        return state;
+    }
+
     open(state)
     {
         this.toolbar().setNumberOfDaysCallback(this.setNumberOfDaysFromToolbar.bind(this));
diff --git a/Websites/perf.webkit.org/public/v3/pages/summary-page.js b/Websites/perf.webkit.org/public/v3/pages/summary-page.js
new file mode 100644 (file)
index 0000000..4877a93
--- /dev/null
@@ -0,0 +1,286 @@
+
+class SummaryPage extends PageWithHeading {
+
+    constructor(summarySettings)
+    {
+        super('Summary', null);
+
+        this._table = {
+            heading: summarySettings.platformGroups.map(function (platformGroup) { return platformGroup.name; }),
+            groups: [],
+        };
+        this._shouldConstructTable = true;
+        this._renderQueue = [];
+
+        var current = Date.now();
+        var timeRange = [current - 7 * 24 * 3600 * 1000, current];
+        for (var metricGroup of summarySettings.metricGroups) {
+            var group = {name: metricGroup.name, rows: []};
+            this._table.groups.push(group);
+            for (var subMetricGroup of metricGroup.subgroups) {
+                var row = {name: subMetricGroup.name, cells: []};
+                group.rows.push(row);
+                for (var platformGroup of summarySettings.platformGroups)
+                    row.cells.push(this._createConfigurationGroupAndStartFetchingData(platformGroup.platforms, subMetricGroup.metrics, timeRange));
+            }
+        }
+    }
+
+    routeName() { return 'summary'; }
+
+    open(state)
+    {
+        super.open(state);
+    }
+    
+    render()
+    {
+        super.render();
+
+        if (this._shouldConstructTable)
+            this.renderReplace(this.content().querySelector('.summary-table'), this._constructTable());
+
+        for (var render of this._renderQueue)
+            render();
+    }
+
+    _createConfigurationGroupAndStartFetchingData(platformIdList, metricIdList, timeRange)
+    {
+        var platforms = platformIdList.map(function (id) { return Platform.findById(id); }).filter(function (obj) { return !!obj; });
+        var metrics = metricIdList.map(function (id) { return Metric.findById(id); }).filter(function (obj) { return !!obj; });
+        var configGroup = new SummaryPageConfigurationGroup(platforms, metrics);
+        configGroup.fetchAndComputeSummary(timeRange).then(this.render.bind(this));
+        return configGroup;
+    }
+
+    _constructTable()
+    {
+        var element = ComponentBase.createElement;
+
+        var self = this;
+
+        this._shouldConstructTable = false;
+        this._renderQueue = [];
+
+        return [
+            element('thead',
+                element('tr', [
+                    element('td', {colspan: 2}),
+                    this._table.heading.map(function (label) { return element('td', label); }),
+                ])),
+            this._table.groups.map(function (rowGroup) {
+                return rowGroup.rows.map(function (row, rowIndex) {
+                    var headings;
+                    if (rowGroup.rows.length == 1)
+                        headings = [element('th', {class: 'unifiedHeader', colspan: 2}, row.name)];
+                    else {
+                        headings = [element('th', {class: 'minorHeader'}, row.name)];
+                        if (!rowIndex)
+                            headings.unshift(element('th', {class: 'majorHeader', rowspan: rowGroup.rows.length}, rowGroup.name));
+                    }
+                    return element('tr', [headings, row.cells.map(self._constructRatioGraph.bind(self))]);
+                });
+            }),
+        ];
+    }
+
+    _constructRatioGraph(configurationGroup)
+    {
+        var element = ComponentBase.createElement;
+        var link = ComponentBase.createLink;
+
+        var ratioGraph = new RatioBarGraph();
+
+        this._renderQueue.push(function () {
+            ratioGraph.update(configurationGroup.ratio(), configurationGroup.label());
+            ratioGraph.render();
+        });
+
+        var state = ChartsPage.createStateForConfigurationList(configurationGroup.configurationList());
+        return element('td', link(ratioGraph, 'Open charts', this.router().url('charts', state)));
+    }
+
+    static htmlTemplate()
+    {
+        return `<section class="page-with-heading"><table class="summary-table"></table></section>`;
+    }
+
+    static cssTemplate()
+    {
+        return `
+            .summary-table {
+                border-collapse: collapse;
+                border: none;
+                margin: 0 1rem;
+                width: calc(100% - 2rem - 2px);
+            }
+
+            .summary-table td,
+            .summary-table th {
+                text-align: center;
+                padding: 0px;
+            }
+
+            .summary-table .majorHeader {
+                width: 5rem;
+            }
+
+            .summary-table .minorHeader {
+                width: 7rem;
+            }
+
+            .summary-table .unifiedHeader {
+                padding-left: 5rem;
+            }
+
+            .summary-table > tr:nth-child(even) > *:not(.majorHeader) {
+                background: #f9f9f9;
+            }
+
+            .summary-table th,
+            .summary-table thead td {
+                color: #333;
+                font-weight: inherit;
+                font-size: 1rem;
+                padding: 0.2rem 0.4rem;
+            }
+
+            .summary-table thead td {
+                font-size: 1.2rem;
+            }
+
+            .summary-table tbody td {
+                font-weight: inherit;
+                font-size: 0.9rem;
+                padding: 0;
+            }
+
+            .summary-table td > * {
+                height: 100%;
+            }
+        `;
+    }
+}
+
+class SummaryPageConfigurationGroup {
+    constructor(platforms, metrics)
+    {
+        this._measurementSets = [];
+        this._configurationList = [];
+        this._setToRatio = new Map;
+        this._ratio = null;
+        this._label = null;
+        this._changeType = null;
+        this._smallerIsBetter = metrics.length ? metrics[0].isSmallerBetter() : null;
+
+        for (var platform of platforms) {
+            console.assert(platform instanceof Platform);
+            for (var metric of metrics) {
+                console.assert(metric instanceof Metric);
+                console.assert(this._smallerIsBetter == metric.isSmallerBetter());
+                metric.isSmallerBetter();
+                if (platform.hasMetric(metric)) {
+                    this._measurementSets.push(MeasurementSet.findSet(platform.id(), metric.id(), platform.lastModified(metric)));
+                    this._configurationList.push([platform.id(), metric.id()]);
+                }
+            }
+        }
+    }
+
+    ratio() { return this._ratio; }
+    label() { return this._label; }
+    changeType() { return this._changeType; }
+    configurationList() { return this._configurationList; }
+
+    fetchAndComputeSummary(timeRange)
+    {
+        console.assert(timeRange instanceof Array);
+        console.assert(typeof(timeRange[0]) == 'number');
+        console.assert(typeof(timeRange[1]) == 'number');
+
+        var promises = [];
+        for (var set of this._measurementSets)
+            promises.push(this._fetchAndComputeRatio(set, timeRange));
+
+        return Promise.all(promises).then(this._computeSummary.bind(this));
+    }
+
+    _computeSummary()
+    {
+        var ratios = [];
+        for (var set of this._measurementSets) {
+            var ratio = this._setToRatio.get(set);
+            if (!isNaN(ratio))
+                ratios.push(ratio);
+        }
+
+        var averageRatio = Statistics.mean(ratios);
+        if (isNaN(averageRatio)) {
+            this._summary = '-';
+            this._changeType = null;
+            return;
+        }
+
+        if (Math.abs(averageRatio - 1) < 0.001) { // Less than 0.1% difference.
+            this._summary = 'No change';
+            this._changeType = null;
+            return;
+        }
+
+        var currentIsSmallerThanBaseline = averageRatio < 1;
+        var changeType = this._smallerIsBetter == currentIsSmallerThanBaseline ? 'better' : 'worse';
+        if (currentIsSmallerThanBaseline)
+            averageRatio = 1 / averageRatio;
+
+        this._ratio = (averageRatio - 1) * (changeType == 'better' ? 1 : -1);
+        this._label = ((averageRatio - 1) * 100).toFixed(1) + '%';
+        this._changeType = changeType;
+    }
+
+    _fetchAndComputeRatio(set, timeRange)
+    {
+        var setToRatio = this._setToRatio;
+        return SummaryPageConfigurationGroup._fetchData(set, timeRange).then(function () {
+            var baselineTimeSeries = set.fetchedTimeSeries('baseline', false, false);
+            var currentTimeSeries = set.fetchedTimeSeries('current', false, false);
+
+            var baselineMedian = SummaryPageConfigurationGroup._medianForTimeRange(baselineTimeSeries, timeRange);
+            var currentMedian = SummaryPageConfigurationGroup._medianForTimeRange(currentTimeSeries, timeRange);
+            setToRatio.set(set, currentMedian / baselineMedian);
+        }).catch(function () {
+            setToRatio.set(set, NaN);
+        });
+    }
+
+    static _medianForTimeRange(timeSeries, timeRange)
+    {
+        if (!timeSeries.firstPoint())
+            return NaN;
+
+        var startPoint = timeSeries.findPointAfterTime(timeRange[0]) || timeSeries.lastPoint();
+        var afterEndPoint = timeSeries.findPointAfterTime(timeRange[1]) || timeSeries.lastPoint();
+        var endPoint = timeSeries.previousPoint(afterEndPoint);
+        if (!endPoint || startPoint == afterEndPoint)
+            endPoint = afterEndPoint;
+
+        var points = timeSeries.dataBetweenPoints(startPoint, endPoint).map(function (point) { return point.value; });
+        return Statistics.median(points);
+    }
+
+    static _fetchData(set, timeRange)
+    {
+        // FIXME: Make fetchBetween return a promise.
+        var done = false;
+        return new Promise(function (resolve, reject) {
+            set.fetchBetween(timeRange[0], timeRange[1], function (error) {
+                if (done)
+                    return;
+                if (error)
+                    reject(null);
+                else if (set.hasFetchedRange(timeRange[0], timeRange[1]))
+                    resolve();
+                done = true;
+            });
+        });
+    }
+}
index eacffdf..2f1b03a 100644 (file)
@@ -17,7 +17,7 @@ describe('/api/manifest', function () {
     it("should generate an empty manifest when database is empty", function (done) {
         TestServer.remoteAPI().getJSON('/api/manifest').then(function (manifest) {
             assert.deepEqual(Object.keys(manifest).sort(), ['all', 'bugTrackers', 'builders', 'dashboard', 'dashboards',
-                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'tests']);
+                'elapsedTime', 'metrics', 'repositories', 'siteTitle', 'status', 'summary', 'tests']);
 
             assert.equal(typeof(manifest.elapsedTime), 'number');
             delete manifest.elapsedTime;
@@ -32,6 +32,7 @@ describe('/api/manifest', function () {
                 metrics: {},
                 repositories: {},
                 tests: {},
+                summary: {},
                 status: 'OK'
             });
             done();