Make calls to render() functions async
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Jan 2017 21:22:39 +0000 (21:22 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 18 Jan 2017 21:22:39 +0000 (21:22 +0000)
https://bugs.webkit.org/show_bug.cgi?id=167151

Reviewed by Andreas Kling.

Make calls to render() async by coalescing calls inside enqueueToRender(), which has been renamed from
updateRendering(). We now queue up all the components and wait until the next animation frame to invoke
render() on all those components.

This reduces render() calls in the summary page of our internal dashboard by 15x from ~9400 to ~600 by
eliminating pathological O(n^2) behavior between render() calls.

Consolidated TimeSeriesChart's enqueueRender into this newly added feature of ComponentBase along with
the support to call render() on a resize event. New implementation makes use of connectedCallback and
disconnectedCallback to avoid the work when the component is not in a document tree.

The rest of the patch concerns with renaming updateRendering to enqueueToRender and fixing a few minor bugs
that I encountered while working on this patch.

* browser-tests/component-base-tests.js: Added tests for ComponentBase.enqueueToRender().
* browser-tests/index.html:
(BrowserContext.prototype.importScripts): Renamed from importScript; Now supports loading multiple scripts.
(BrowserContext.prototype.importScript): Added.
(BrowserContext): Removed the unused createWithScripts.

* public/v3/components/analysis-results-viewer.js:
(AnalysisResultsViewer.prototype._expandBetween):
* public/v3/components/bar-graph-group.js:
(BarGraphGroup.prototype.updateGroupRendering):

* public/v3/components/base.js:
(ComponentBase): When the browser doesn't support custom elements and
(ComponentBase.prototype.enqueueToRender): Renamed from updateRendering. Queues up the component to call
render() instead of immediately invoking it.
(ComponentBase.renderingTimerDidFire): Call render(). Since render() function often calls enqueueToRender
on child components, go ahead and invoke render() on any components enqueued during render() calls.
(ComponentBase._connectedComponentToRenderOnResize): Added.
(ComponentBase._disconnectedComponentToRenderOnResize): Added.
(ComponentBase.defineElement.elementClass.prototype.connectedCallback): Added. This is an optimization to
avoid the work when the component is not in the document; e.g. because the entire page component has been
detached from the document. The old implementation in TimeSeriesChart was not doing this.
(ComponentBase.defineElement.elementClass.prototype.disconnectedCallback): Added.
(ComponentBase): Replaced unused static variables with _componentsToRender and _componentsToRenderOnResize.

* public/v3/components/chart-pane-base.js:
(ChartPaneBase.prototype.fetchAnalysisTasks):
(ChartPaneBase.prototype.didUpdateAnnotations): Added. Addresses the bug that the annotation bars in the
charts shown an an analysis task doesn't update its color when the state is updated in the UI.
(ChartPaneBase.prototype._mainSelectionDidZoom):
(ChartPaneBase.prototype._updateStatus):
(ChartPaneBase.prototype._requestOpeningCommitViewer):
(ChartPaneBase.prototype._keyup):
(ChartPaneBase.prototype.render):
* public/v3/components/commit-log-viewer.js:
* public/v3/components/customizable-test-group-form.js:
(CustomizableTestGroupForm):
(CustomizableTestGroupForm.prototype._customize):
* public/v3/components/editable-text.js:
(EditableText.prototype._didUpdate):
* public/v3/components/interactive-time-series-chart.js:
* public/v3/components/pane-selector.js:
(PaneSelector.prototype._selectedItem):
* public/v3/components/time-series-chart.js:
(TimeSeriesChart): Removed the logic to update upon resize. See _connectedComponentToRenderOnResize above.
(TimeSeriesChart.prototype.get enqueueToRenderOnResize): Added. Returns true.
(TimeSeriesChart.prototype.enqueueToRender): Deleted.
(TimeSeriesChart._renderEnqueuedCharts): Deleted.
(TimeSeriesChart): Call ComponentBase.defineElement to make this a proper component so that the logic in
connectedCallback to update upon resize event would work.
* public/v3/instrumentation.js:
(Instrumentation.dumpStatistics): Sort results by the key names.
* public/v3/models/time-series.js:
(TimeSeries.prototype.values): Added. This method was never ported to v3 in r198462, and broke the feature
to show moving averages, etc... on the charts page.
* public/v3/pages/analysis-category-page.js:
(AnalysisCategoryPage.prototype.open):
(AnalysisCategoryPage.prototype.updateFromSerializedState):
(AnalysisCategoryPage.prototype.filterDidChange):
(AnalysisCategoryPage.prototype.render):
* public/v3/pages/analysis-task-page.js:
(AnalysisTaskChartPane.prototype._updateStatus):
(AnalysisTaskPage.prototype.updateFromSerializedState):
(AnalysisTaskPage.prototype._didFetchTask):
(AnalysisTaskPage.prototype._didFetchRelatedAnalysisTasks):
(AnalysisTaskPage.prototype._didFetchMeasurement):
(AnalysisTaskPage.prototype._didFetchTestGroups):
(AnalysisTaskPage.prototype._showAllTestGroups):
(AnalysisTaskPage.prototype._didFetchAnalysisResults):
(AnalysisTaskPage.prototype.render):
(AnalysisTaskPage.prototype._renderTestGroupList.):
(AnalysisTaskPage.prototype._renderTestGroupList):
(AnalysisTaskPage.prototype._createTestGroupListItem):
(AnalysisTaskPage.prototype._showTestGroup):
(AnalysisTaskPage.prototype._didStartEditingTaskName):
(AnalysisTaskPage.prototype._updateTaskName):
(AnalysisTaskPage.prototype._updateTestGroupName):
(AnalysisTaskPage.prototype._hideCurrentTestGroup):
(AnalysisTaskPage.prototype._updateChangeType): Fixed the bug that we were never updating annotation bars
in the main chart by calling didUpdateAnnotations.
(AnalysisTaskPage.prototype._associateBug):
(AnalysisTaskPage.prototype._dissociateBug):
(AnalysisTaskPage.prototype._associateCommit):
(AnalysisTaskPage.prototype._dissociateCommit):
(AnalysisTaskPage.prototype._chartSelectionDidChange):
(AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer):
* public/v3/pages/build-request-queue-page.js:
(BuildRequestQueuePage.prototype.open.):
(BuildRequestQueuePage.prototype.open):
* public/v3/pages/chart-pane.js:
(ChartPane.prototype.setOpenRepository):
(ChartPane.prototype._renderTrendLinePopover): Fixed a race condition. Insert a select element as needed
before trying to assign the current value on it.
(ChartPane.prototype._trendLineTypeDidChange):
(ChartPane.prototype._updateTrendLine):
* public/v3/pages/charts-page.js:
(ChartsPage.prototype.updateFromSerializedState):
(ChartsPage.prototype._updateDomainsFromSerializedState):
(ChartsPage.prototype.setNumberOfDaysFromToolbar):
(ChartsPage.prototype._didMutatePaneList):
(ChartsPage.prototype.render):
* public/v3/pages/charts-toolbar.js:
(ChartsToolbar.prototype.render):
* public/v3/pages/create-analysis-task-page.js:
(CreateAnalysisTaskPage.prototype.updateFromSerializedState):
* public/v3/pages/dashboard-page.js:
(DashboardPage.prototype.updateFromSerializedState):
(DashboardPage.prototype._fetchedData):
* public/v3/pages/heading.js:
(Heading.prototype.render):
* public/v3/pages/page-with-heading.js:
(PageWithHeading.prototype.render):
* public/v3/pages/page.js:
(Page.prototype.open):
* public/v3/pages/summary-page.js:
(SummaryPage.prototype.open):
(SummaryPage.prototype.this._renderQueue.push):
(SummaryPage):
(SummaryPage.prototype._renderCell):

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

27 files changed:
Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/browser-tests/component-base-tests.js
Websites/perf.webkit.org/browser-tests/index.html
Websites/perf.webkit.org/public/v3/components/analysis-results-viewer.js
Websites/perf.webkit.org/public/v3/components/bar-graph-group.js
Websites/perf.webkit.org/public/v3/components/base.js
Websites/perf.webkit.org/public/v3/components/chart-pane-base.js
Websites/perf.webkit.org/public/v3/components/commit-log-viewer.js
Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js
Websites/perf.webkit.org/public/v3/components/editable-text.js
Websites/perf.webkit.org/public/v3/components/interactive-time-series-chart.js
Websites/perf.webkit.org/public/v3/components/pane-selector.js
Websites/perf.webkit.org/public/v3/components/time-series-chart.js
Websites/perf.webkit.org/public/v3/instrumentation.js
Websites/perf.webkit.org/public/v3/models/time-series.js
Websites/perf.webkit.org/public/v3/pages/analysis-category-page.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js
Websites/perf.webkit.org/public/v3/pages/build-request-queue-page.js
Websites/perf.webkit.org/public/v3/pages/chart-pane.js
Websites/perf.webkit.org/public/v3/pages/charts-page.js
Websites/perf.webkit.org/public/v3/pages/charts-toolbar.js
Websites/perf.webkit.org/public/v3/pages/create-analysis-task-page.js
Websites/perf.webkit.org/public/v3/pages/dashboard-page.js
Websites/perf.webkit.org/public/v3/pages/heading.js
Websites/perf.webkit.org/public/v3/pages/page-with-heading.js
Websites/perf.webkit.org/public/v3/pages/page.js
Websites/perf.webkit.org/public/v3/pages/summary-page.js

index 1013075..4ca0c09 100644 (file)
@@ -1,3 +1,144 @@
+2017-01-17  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Make calls to render() functions async
+        https://bugs.webkit.org/show_bug.cgi?id=167151
+
+        Reviewed by Andreas Kling.
+
+        Make calls to render() async by coalescing calls inside enqueueToRender(), which has been renamed from
+        updateRendering(). We now queue up all the components and wait until the next animation frame to invoke
+        render() on all those components.
+
+        This reduces render() calls in the summary page of our internal dashboard by 15x from ~9400 to ~600 by
+        eliminating pathological O(n^2) behavior between render() calls.
+
+        Consolidated TimeSeriesChart's enqueueRender into this newly added feature of ComponentBase along with
+        the support to call render() on a resize event. New implementation makes use of connectedCallback and
+        disconnectedCallback to avoid the work when the component is not in a document tree.
+
+        The rest of the patch concerns with renaming updateRendering to enqueueToRender and fixing a few minor bugs
+        that I encountered while working on this patch.
+
+        * browser-tests/component-base-tests.js: Added tests for ComponentBase.enqueueToRender().
+        * browser-tests/index.html:
+        (BrowserContext.prototype.importScripts): Renamed from importScript; Now supports loading multiple scripts.
+        (BrowserContext.prototype.importScript): Added.
+        (BrowserContext): Removed the unused createWithScripts.
+
+        * public/v3/components/analysis-results-viewer.js:
+        (AnalysisResultsViewer.prototype._expandBetween):
+        * public/v3/components/bar-graph-group.js:
+        (BarGraphGroup.prototype.updateGroupRendering):
+
+        * public/v3/components/base.js:
+        (ComponentBase): When the browser doesn't support custom elements and 
+        (ComponentBase.prototype.enqueueToRender): Renamed from updateRendering. Queues up the component to call
+        render() instead of immediately invoking it.
+        (ComponentBase.renderingTimerDidFire): Call render(). Since render() function often calls enqueueToRender
+        on child components, go ahead and invoke render() on any components enqueued during render() calls.
+        (ComponentBase._connectedComponentToRenderOnResize): Added.
+        (ComponentBase._disconnectedComponentToRenderOnResize): Added.
+        (ComponentBase.defineElement.elementClass.prototype.connectedCallback): Added. This is an optimization to
+        avoid the work when the component is not in the document; e.g. because the entire page component has been
+        detached from the document. The old implementation in TimeSeriesChart was not doing this.
+        (ComponentBase.defineElement.elementClass.prototype.disconnectedCallback): Added.
+        (ComponentBase): Replaced unused static variables with _componentsToRender and _componentsToRenderOnResize.
+
+        * public/v3/components/chart-pane-base.js:
+        (ChartPaneBase.prototype.fetchAnalysisTasks):
+        (ChartPaneBase.prototype.didUpdateAnnotations): Added. Addresses the bug that the annotation bars in the
+        charts shown an an analysis task doesn't update its color when the state is updated in the UI. 
+        (ChartPaneBase.prototype._mainSelectionDidZoom):
+        (ChartPaneBase.prototype._updateStatus):
+        (ChartPaneBase.prototype._requestOpeningCommitViewer):
+        (ChartPaneBase.prototype._keyup):
+        (ChartPaneBase.prototype.render):
+        * public/v3/components/commit-log-viewer.js:
+        * public/v3/components/customizable-test-group-form.js:
+        (CustomizableTestGroupForm):
+        (CustomizableTestGroupForm.prototype._customize):
+        * public/v3/components/editable-text.js:
+        (EditableText.prototype._didUpdate):
+        * public/v3/components/interactive-time-series-chart.js:
+        * public/v3/components/pane-selector.js:
+        (PaneSelector.prototype._selectedItem):
+        * public/v3/components/time-series-chart.js:
+        (TimeSeriesChart): Removed the logic to update upon resize. See _connectedComponentToRenderOnResize above.
+        (TimeSeriesChart.prototype.get enqueueToRenderOnResize): Added. Returns true.
+        (TimeSeriesChart.prototype.enqueueToRender): Deleted.
+        (TimeSeriesChart._renderEnqueuedCharts): Deleted.
+        (TimeSeriesChart): Call ComponentBase.defineElement to make this a proper component so that the logic in
+        connectedCallback to update upon resize event would work.
+        * public/v3/instrumentation.js:
+        (Instrumentation.dumpStatistics): Sort results by the key names.
+        * public/v3/models/time-series.js:
+        (TimeSeries.prototype.values): Added. This method was never ported to v3 in r198462, and broke the feature
+        to show moving averages, etc... on the charts page.
+        * public/v3/pages/analysis-category-page.js:
+        (AnalysisCategoryPage.prototype.open):
+        (AnalysisCategoryPage.prototype.updateFromSerializedState):
+        (AnalysisCategoryPage.prototype.filterDidChange):
+        (AnalysisCategoryPage.prototype.render):
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskChartPane.prototype._updateStatus):
+        (AnalysisTaskPage.prototype.updateFromSerializedState):
+        (AnalysisTaskPage.prototype._didFetchTask):
+        (AnalysisTaskPage.prototype._didFetchRelatedAnalysisTasks):
+        (AnalysisTaskPage.prototype._didFetchMeasurement):
+        (AnalysisTaskPage.prototype._didFetchTestGroups):
+        (AnalysisTaskPage.prototype._showAllTestGroups):
+        (AnalysisTaskPage.prototype._didFetchAnalysisResults):
+        (AnalysisTaskPage.prototype.render):
+        (AnalysisTaskPage.prototype._renderTestGroupList.):
+        (AnalysisTaskPage.prototype._renderTestGroupList):
+        (AnalysisTaskPage.prototype._createTestGroupListItem):
+        (AnalysisTaskPage.prototype._showTestGroup):
+        (AnalysisTaskPage.prototype._didStartEditingTaskName):
+        (AnalysisTaskPage.prototype._updateTaskName):
+        (AnalysisTaskPage.prototype._updateTestGroupName):
+        (AnalysisTaskPage.prototype._hideCurrentTestGroup):
+        (AnalysisTaskPage.prototype._updateChangeType): Fixed the bug that we were never updating annotation bars
+        in the main chart by calling didUpdateAnnotations.
+        (AnalysisTaskPage.prototype._associateBug):
+        (AnalysisTaskPage.prototype._dissociateBug):
+        (AnalysisTaskPage.prototype._associateCommit):
+        (AnalysisTaskPage.prototype._dissociateCommit):
+        (AnalysisTaskPage.prototype._chartSelectionDidChange):
+        (AnalysisTaskPage.prototype._selectedRowInAnalysisResultsViewer):
+        * public/v3/pages/build-request-queue-page.js:
+        (BuildRequestQueuePage.prototype.open.):
+        (BuildRequestQueuePage.prototype.open):
+        * public/v3/pages/chart-pane.js:
+        (ChartPane.prototype.setOpenRepository):
+        (ChartPane.prototype._renderTrendLinePopover): Fixed a race condition. Insert a select element as needed
+        before trying to assign the current value on it.
+        (ChartPane.prototype._trendLineTypeDidChange):
+        (ChartPane.prototype._updateTrendLine):
+        * public/v3/pages/charts-page.js:
+        (ChartsPage.prototype.updateFromSerializedState):
+        (ChartsPage.prototype._updateDomainsFromSerializedState):
+        (ChartsPage.prototype.setNumberOfDaysFromToolbar):
+        (ChartsPage.prototype._didMutatePaneList):
+        (ChartsPage.prototype.render):
+        * public/v3/pages/charts-toolbar.js:
+        (ChartsToolbar.prototype.render):
+        * public/v3/pages/create-analysis-task-page.js:
+        (CreateAnalysisTaskPage.prototype.updateFromSerializedState):
+        * public/v3/pages/dashboard-page.js:
+        (DashboardPage.prototype.updateFromSerializedState):
+        (DashboardPage.prototype._fetchedData):
+        * public/v3/pages/heading.js:
+        (Heading.prototype.render):
+        * public/v3/pages/page-with-heading.js:
+        (PageWithHeading.prototype.render):
+        * public/v3/pages/page.js:
+        (Page.prototype.open):
+        * public/v3/pages/summary-page.js:
+        (SummaryPage.prototype.open):
+        (SummaryPage.prototype.this._renderQueue.push):
+        (SummaryPage):
+        (SummaryPage.prototype._renderCell):
+
 2017-01-15  Ryosuke Niwa  <rniwa@webkit.org>
 
         Add the build fix for browsers that don't yet support custom elements SPI.
index ef50ce3..eb72875 100644 (file)
@@ -4,7 +4,7 @@ describe('ComponentBase', function() {
     function createTestToCheckExistenceOfShadowTree(callback, options = {htmlTemplate: false, cssTemplate: true})
     {
         const context = new BrowsingContext();
-        return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+        return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
             class SomeComponent extends ComponentBase { }
             if (options.htmlTemplate)
                 SomeComponent.htmlTemplate = () => { return '<div style="height: 10px;"></div>'; };
@@ -20,13 +20,13 @@ describe('ComponentBase', function() {
 
     describe('constructor', () => {
         it('is a function', () => {
-            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 expect(ComponentBase).toBeA('function');
             });
         });
 
         it('can be instantiated', () => {
-            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 let callCount = 0;
                 class SomeComponent extends ComponentBase {
                     constructor() {
@@ -51,7 +51,7 @@ describe('ComponentBase', function() {
     describe('element()', () => {
         it('must return an element', () => {
             const context = new BrowsingContext();
-            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 class SomeComponent extends ComponentBase { }
                 let instance = new SomeComponent('some-component');
                 expect(instance.element()).toBeA(context.global.HTMLElement);
@@ -59,7 +59,7 @@ describe('ComponentBase', function() {
         });
 
         it('must return an element whose component() matches the component', () => {
-            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 class SomeComponent extends ComponentBase { }
                 let instance = new SomeComponent('some-component');
                 expect(instance.element().component()).toBe(instance);
@@ -89,6 +89,187 @@ describe('ComponentBase', function() {
         });
     });
 
+    describe('enqueueToRender()', () => {
+        it('must not immediately call render()', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                context.global.requestAnimationFrame = () => {}
+
+                let renderCallCount = 0;
+                const SomeComponent = class extends ComponentBase {
+                    render() { renderCallCount++; }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                (new SomeComponent).enqueueToRender();
+                expect(renderCallCount).toBe(0);
+
+                (new SomeComponent).enqueueToRender();
+                expect(renderCallCount).toBe(0);
+            });
+        });
+
+        it('must request an animation frame exactly once', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                let requestAnimationFrameCount = 0;
+                context.global.requestAnimationFrame = () => { requestAnimationFrameCount++; }
+
+                const SomeComponent = class extends ComponentBase { }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                expect(requestAnimationFrameCount).toBe(0);
+                let instance = new SomeComponent;
+                instance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+
+                instance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+
+                (new SomeComponent).enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+
+                const AnotherComponent = class extends ComponentBase { }
+                ComponentBase.defineElement('another-component', AnotherComponent);
+                (new AnotherComponent).enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+            });
+        });
+
+        it('must invoke render() when the callback to requestAnimationFrame is called', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                let callback = null;
+                context.global.requestAnimationFrame = (newCallback) => {
+                    expect(callback).toBe(null);
+                    expect(newCallback).toNotBe(null);
+                    callback = newCallback;
+                }
+
+                let renderCalls = [];
+                const SomeComponent = class extends ComponentBase {
+                    render() {
+                        renderCalls.push(this);
+                    }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                expect(renderCalls.length).toBe(0);
+                const instance = new SomeComponent;
+                instance.enqueueToRender();
+                instance.enqueueToRender();
+
+                const anotherInstance = new SomeComponent;
+                anotherInstance.enqueueToRender();
+                expect(renderCalls.length).toBe(0);
+
+                callback();
+
+                expect(renderCalls.length).toBe(2);
+                expect(renderCalls[0]).toBe(instance);
+                expect(renderCalls[1]).toBe(anotherInstance);
+            });
+        });
+
+        it('must immediately invoke render() on a component enqueued inside another render() call', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                let callback = null;
+                context.global.requestAnimationFrame = (newCallback) => {
+                    expect(callback).toBe(null);
+                    expect(newCallback).toNotBe(null);
+                    callback = newCallback;
+                }
+
+                let renderCalls = [];
+                let instanceToEnqueue = null;
+                const SomeComponent = class extends ComponentBase {
+                    render() {
+                        renderCalls.push(this);
+                        if (instanceToEnqueue)
+                            instanceToEnqueue.enqueueToRender();
+                        instanceToEnqueue = null;
+                    }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                expect(renderCalls.length).toBe(0);
+                const instance = new SomeComponent;
+                const anotherInstance = new SomeComponent;
+                instance.enqueueToRender();
+                instanceToEnqueue = anotherInstance;
+                callback();
+                callback = null;
+                expect(renderCalls.length).toBe(2);
+                expect(renderCalls[0]).toBe(instance);
+                expect(renderCalls[1]).toBe(anotherInstance);
+                renderCalls = [];
+
+                instance.enqueueToRender();
+                anotherInstance.enqueueToRender();
+                instanceToEnqueue = instance;
+                callback();
+                expect(renderCalls.length).toBe(3);
+                expect(renderCalls[0]).toBe(instance);
+                expect(renderCalls[1]).toBe(anotherInstance);
+                expect(renderCalls[2]).toBe(instance);
+            });
+        });
+
+        it('must request a new animation frame once it exited the callback from requestAnimationFrame', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                let requestAnimationFrameCount = 0;
+                let callback = null;
+                context.global.requestAnimationFrame = (newCallback) => {
+                    expect(callback).toBe(null);
+                    expect(newCallback).toNotBe(null);
+                    callback = newCallback;
+                    requestAnimationFrameCount++;
+                }
+
+                let renderCalls = [];
+                const SomeComponent = class extends ComponentBase {
+                    render() { renderCalls.push(this); }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                const instance = new SomeComponent;
+                const anotherInstance = new SomeComponent;
+                expect(requestAnimationFrameCount).toBe(0);
+
+                instance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+                anotherInstance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(1);
+
+                expect(renderCalls.length).toBe(0);
+                callback();
+                callback = null;
+                expect(renderCalls.length).toBe(2);
+                expect(renderCalls[0]).toBe(instance);
+                expect(renderCalls[1]).toBe(anotherInstance);
+                expect(requestAnimationFrameCount).toBe(1);
+
+                anotherInstance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(2);
+                instance.enqueueToRender();
+                expect(requestAnimationFrameCount).toBe(2);
+
+                expect(renderCalls.length).toBe(2);
+                callback();
+                callback = null;
+                expect(renderCalls.length).toBe(4);
+                expect(renderCalls[0]).toBe(instance);
+                expect(renderCalls[1]).toBe(anotherInstance);
+                expect(renderCalls[2]).toBe(anotherInstance);
+                expect(renderCalls[3]).toBe(instance);
+                expect(requestAnimationFrameCount).toBe(2);
+            });
+        });
+
+    });
+
     describe('render()', () => {
         it('must create shadow tree', () => {
             return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
@@ -123,7 +304,7 @@ describe('ComponentBase', function() {
 
         it('must define a custom element with a class of an appropriate name', () => {
             const context = new BrowsingContext();
-            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 class SomeComponent extends ComponentBase { }
                 ComponentBase.defineElement('some-component', SomeComponent);
 
@@ -135,7 +316,7 @@ describe('ComponentBase', function() {
 
         it('must define a custom element that can be instantiated via document.createElement', () => {
             const context = new BrowsingContext();
-            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 let instances = [];
                 class SomeComponent extends ComponentBase {
                     constructor() {
@@ -158,7 +339,7 @@ describe('ComponentBase', function() {
 
         it('must define a custom element that can be instantiated via new', () => {
             const context = new BrowsingContext();
-            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
                 let instances = [];
                 class SomeComponent extends ComponentBase {
                     constructor() {
@@ -179,6 +360,52 @@ describe('ComponentBase', function() {
             });
         });
 
+        it('must enqueue a connected component to render upon a resize event if enqueueToRenderOnResize is true', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                class SomeComponent extends ComponentBase {
+                    static get enqueueToRenderOnResize() { return true; }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                let requestAnimationFrameCount = 0;
+                let callback = null;
+                context.global.requestAnimationFrame = (newCallback) => {
+                    callback = newCallback;
+                    requestAnimationFrameCount++;
+                }
+
+                expect(requestAnimationFrameCount).toBe(0);
+                const instance = new SomeComponent;
+                context.global.dispatchEvent(new Event('resize'));
+                context.document.body.appendChild(instance.element());
+                context.global.dispatchEvent(new Event('resize'));
+                expect(requestAnimationFrameCount).toBe(1);
+            });
+        });
+
+        it('must not enqueue a disconnected component to render upon a resize event if enqueueToRenderOnResize is true', () => {
+            const context = new BrowsingContext();
+            return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+                class SomeComponent extends ComponentBase {
+                    static get enqueueToRenderOnResize() { return true; }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                let requestAnimationFrameCount = 0;
+                let callback = null;
+                context.global.requestAnimationFrame = (newCallback) => {
+                    callback = newCallback;
+                    requestAnimationFrameCount++;
+                }
+
+                const instance = new SomeComponent;
+                expect(requestAnimationFrameCount).toBe(0);
+                context.global.dispatchEvent(new Event('resize'));
+                expect(requestAnimationFrameCount).toBe(0);
+            });
+        });
+
     });
 
 });
index e40dd46..3611737 100644 (file)
@@ -36,17 +36,20 @@ class BrowsingContext {
         this.document = this._iframe.contentDocument;
     }
 
-    importScript(path, ...symbolList)
+    importScripts(pathList, ...symbolList)
     {
         const doc = this._iframe.contentDocument;
         const global = this._iframe.contentWindow;
-        return new Promise((resolve, reject) => {
-            let script = doc.createElement('script');
-            script.addEventListener('load', resolve);
-            script.addEventListener('error', reject);
-            script.src = path;
-            doc.body.appendChild(script);
-        }).then(() => {
+
+        return Promise.all(pathList.map((path) => {
+            return new Promise((resolve, reject) => {
+                let script = doc.createElement('script');
+                script.addEventListener('load', resolve);
+                script.addEventListener('error', reject);
+                script.src = '../public/v3/' + path;
+                doc.body.appendChild(script);
+            });
+        })).then(() => {
             const script = doc.createElement('script');
             script.textContent = `window.importedSymbols = [${symbolList.join(', ')}];`;
             doc.body.appendChild(script);
@@ -59,35 +62,15 @@ class BrowsingContext {
         });
     }
 
-    static cleanup()
+    importScript(path, ...symbols)
     {
-        BrowsingContext._iframes.forEach((iframe) => { iframe.remove(); });
-        BrowsingContext._iframes = [];
+        return this.importScripts([path], ...symbols);
     }
 
-    static createWithScripts(scriptList)
+    static cleanup()
     {
-        let iframe = document.createElement('iframe');
-        document.body.appendChild(iframe);
-        const doc = iframe.contentDocument;
-
-        let symbolList = [];
-        return Promise.all(scriptList.map((entry) => {
-            let [path, ...symbols] = entry;
-            symbolList = symbolList.concat(symbols);
-            return new Promise((resolve, reject) => {
-                let script = doc.createElement('script');
-                script.addEventListener('load', resolve);
-                script.addEventListener('error', reject);
-                script.src = path;
-                doc.body.appendChild(script);
-            });
-        })).then(() => {
-            const script = doc.createElement('script');
-            script.textContent = `var symbols = { ${symbolList.join(', ')} };`;
-            doc.body.appendChild(script);
-            return iframe.contentWindow;
-        });
+        BrowsingContext._iframes.forEach((iframe) => { iframe.remove(); });
+        BrowsingContext._iframes = [];
     }
 }
 BrowsingContext._iframes = [];
index 0a24d8a..5e25403 100644 (file)
@@ -260,7 +260,7 @@ class AnalysisResultsViewer extends ResultsTable {
         for (var i = indexBeforeStart + 1; i < indexAfterEnd; i += increment)
             this._expandedPoints.add(series.findPointByIndex(i));
         this._shouldRenderTable = true;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     static htmlTemplate()
index 76597a3..92d264c 100644 (file)
@@ -35,7 +35,7 @@ class BarGraphGroup {
             var start = min - (range - diff) / 2;
 
             entry.bar.update((value - start) / range, formattedValue);
-            entry.bar.updateRendering();
+            entry.bar.enqueueToRender();
         }
 
         Instrumentation.endMeasuringTime('BarGraphGroup', 'updateGroupRendering');
index ef986f4..6ddedf8 100644 (file)
@@ -15,6 +15,9 @@ class ComponentBase {
 
         this._element = element;
         this._shadow = null;
+
+        if (!window.customElements && new.target.enqueueToRenderOnResize)
+            ComponentBase._connectedComponentToRenderOnResize(this);
     }
 
     element() { return this._element; }
@@ -26,13 +29,54 @@ class ComponentBase {
 
     render() { this._ensureShadowTree(); }
 
-    updateRendering()
+    enqueueToRender()
     {
         Instrumentation.startMeasuringTime('ComponentBase', 'updateRendering');
-        this.render();
+
+        if (!ComponentBase._componentsToRender) {
+            ComponentBase._componentsToRender = new Set;
+            requestAnimationFrame(() => ComponentBase.renderingTimerDidFire());
+        }
+        ComponentBase._componentsToRender.add(this);
+
         Instrumentation.endMeasuringTime('ComponentBase', 'updateRendering');
     }
 
+    static renderingTimerDidFire()
+    {
+        Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+
+        do {
+            const currentSet = [...ComponentBase._componentsToRender];
+            ComponentBase._componentsToRender.clear();
+            for (let component of currentSet) {
+                Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
+                component.render();
+                Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
+            }
+        } while (ComponentBase._componentsToRender.size);
+        ComponentBase._componentsToRender = null;
+
+        Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+    }
+
+    static _connectedComponentToRenderOnResize(component)
+    {
+        if (!ComponentBase._componentsToRenderOnResize) {
+            ComponentBase._componentsToRenderOnResize = new Set;
+            window.addEventListener('resize', () => {
+                for (let component of ComponentBase._componentsToRenderOnResize)
+                    component.enqueueToRender();
+            });
+        }
+        ComponentBase._componentsToRenderOnResize.add(component);
+    }
+
+    static _disconnectedComponentToRenderOnResize(component)
+    {
+        ComponentBase._componentsToRenderOnResize.delete(component);
+    }
+
     renderReplace(element, content) { ComponentBase.renderReplace(element, content); }
 
     static renderReplace(element, content)
@@ -94,6 +138,8 @@ class ComponentBase {
         ComponentBase._componentByName.set(name, elementInterface);
         ComponentBase._componentByClass.set(elementInterface, name);
 
+        const enqueueToRenderOnResize = elementInterface.enqueueToRenderOnResize;
+
         if (!window.customElements)
             return;
 
@@ -111,6 +157,18 @@ class ComponentBase {
                 new elementInterface();
                 currentlyConstructed.delete(elementInterface);
             }
+
+            connectedCallback()
+            {
+                if (enqueueToRenderOnResize)
+                    ComponentBase._connectedComponentToRenderOnResize(this.component());
+            }
+
+            disconnectedCallback()
+            {
+                if (enqueueToRenderOnResize)
+                    ComponentBase._disconnectedComponentToRenderOnResize(this.component());
+            }
         }
 
         const nameDescriptor = Object.getOwnPropertyDescriptor(elementClass, 'name');
@@ -193,7 +251,5 @@ class ComponentBase {
 ComponentBase._componentByName = new Map;
 ComponentBase._componentByClass = new Map;
 ComponentBase._currentlyConstructedByInterface = new Map;
-
-ComponentBase.css = Symbol();
-ComponentBase.html = Symbol();
-ComponentBase.map = {};
+ComponentBase._componentsToRender = null;
+ComponentBase._componentsToRenderOnResize = null;
index e10b11a..baca9bb 100644 (file)
@@ -96,10 +96,17 @@ class ChartPaneBase extends ComponentBase {
         AnalysisTask.fetchByPlatformAndMetric(this._platformId, this._metricId, noCache).then(function (tasks) {
             self._tasksForAnnotations = tasks;
             self._renderedAnnotations = false;
-            self.updateRendering();
+            self.enqueueToRender();
         });
     }
 
+    // FIXME: We should have a mechanism to get notified whenever the set of annotations change.
+    didUpdateAnnotations()
+    {
+        this._renderedAnnotations = false;
+        this.enqueueToRender();
+    }
+
     platformId() { return this._platformId; }
     metricId() { return this._metricId; }
 
@@ -132,7 +139,7 @@ class ChartPaneBase extends ComponentBase {
     {
         this._overviewChart.setSelection(selection, this);
         this._mainChart.setSelection(null);
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _indicatorDidChange(indicatorID, isLocked)
@@ -148,7 +155,7 @@ class ChartPaneBase extends ComponentBase {
     _updateStatus()
     {
         var range = this._mainChartStatus.updateRevisionList();
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         this._commitLogViewer.view(range.repository, range.from, range.to).then(updateRendering);
         updateRendering();
     }
@@ -165,7 +172,7 @@ class ChartPaneBase extends ComponentBase {
     _requestOpeningCommitViewer(repository, from, to)
     {
         this._mainChartStatus.setCurrentRepository(repository);
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         this._commitLogViewer.view(repository, from, to).then(updateRendering);
         updateRendering();
     }
@@ -191,7 +198,7 @@ class ChartPaneBase extends ComponentBase {
             return;
         }
 
-        this.updateRendering();
+        this.enqueueToRender();
 
         event.preventDefault();
         event.stopPropagation();
@@ -217,12 +224,12 @@ class ChartPaneBase extends ComponentBase {
         this._renderAnnotations();
 
         if (this._mainChartStatus)
-            this._mainChartStatus.updateRendering();
+            this._mainChartStatus.enqueueToRender();
 
         var body = this.content().querySelector('.chart-pane-body');
         if (this._commitLogViewer && this._commitLogViewer.currentRepository()) {
             body.classList.add('has-second-sidebar');
-            this._commitLogViewer.updateRendering();
+            this._commitLogViewer.enqueueToRender();
         } else
             body.classList.remove('has-second-sidebar');
 
index f9159b5..4ecc326 100644 (file)
@@ -31,7 +31,7 @@ class CommitLogViewer extends ComponentBase {
 
         if (!to) {
             this._fetchingPromise = null;
-            this.updateRendering();
+            this.enqueueToRender();
             return Promise.resolve(null);
         }
 
@@ -41,7 +41,7 @@ class CommitLogViewer extends ComponentBase {
 
         var self = this;
         var spinnerTimer = setTimeout(function () {
-            self.updateRendering();
+            self.enqueueToRender();
         }, 300);
 
         this._fetchingPromise.then(function (commits) {
index a6d9ba2..b03d6bd 100644 (file)
@@ -8,7 +8,7 @@ class CustomizableTestGroupForm extends TestGroupForm {
         this._renderedRepositorylist = null;
         this._customized = false;
         this._nameControl = this.content().querySelector('.name');
-        this._nameControl.oninput = () => { this.updateRendering(); }
+        this._nameControl.oninput = () => { this.enqueueToRender(); }
         this.content().querySelector('a').onclick = this._customize.bind(this);
     }
 
@@ -28,7 +28,7 @@ class CustomizableTestGroupForm extends TestGroupForm {
     {
         event.preventDefault();
         this._customized = true;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _computeRootSetMap()
index 520711d..6431352 100644 (file)
@@ -73,7 +73,7 @@ class EditableText extends ComponentBase {
     {
         this._inEditingMode = false;
         this._updatingPromise = null;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     static htmlTemplate()
index a71bc8d..08871a8 100644 (file)
@@ -481,3 +481,5 @@ class InteractiveTimeSeriesChart extends TimeSeriesChart {
         Instrumentation.endMeasuringTime('InteractiveTimeSeriesChart', 'renderChartContent');
     }
 }
+
+ComponentBase.defineElement('interactive-time-series-chart', InteractiveTimeSeriesChart);
index fd1b3f6..998af8e 100644 (file)
@@ -167,7 +167,7 @@ class PaneSelector extends ComponentBase {
             if (data instanceof Metric && data.test().onlyContainsSingleMetric())
                 this._currentPath.splice(this._currentPath.length - 2, 1);
         }
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     setCallback(callback)
index 5b69e2b..e3f7037 100644 (file)
@@ -2,7 +2,7 @@
 class TimeSeriesChart extends ComponentBase {
     constructor(sourceList, options)
     {
-        super('time-series-chart');
+        super();
         this.element().style.display = 'block';
         this.element().style.position = 'relative';
         this._canvas = null;
@@ -22,12 +22,6 @@ class TimeSeriesChart extends ComponentBase {
         this._contextScaleX = 1;
         this._contextScaleY = 1;
         this._rem = null;
-
-        if (!TimeSeriesChart._chartList) {
-            TimeSeriesChart._chartList = [];
-            window.addEventListener('resize', TimeSeriesChart._updateAllCharts.bind(TimeSeriesChart));
-        }
-        TimeSeriesChart._chartList.push(this);
     }
 
     _ensureCanvas()
@@ -46,6 +40,7 @@ class TimeSeriesChart extends ComponentBase {
     }
 
     static cssTemplate() { return ''; }
+    static get enqueueToRenderOnResize() { return true; }
 
     _createCanvas()
     {
@@ -57,22 +52,6 @@ class TimeSeriesChart extends ComponentBase {
         TimeSeriesChart._chartList.map(function (chart) { chart.render(); });
     }
 
-    enqueueToRender()
-    {
-        if (!TimeSeriesChart._chartQueue) {
-            TimeSeriesChart._chartQueue = new Set;
-            window.requestAnimationFrame(TimeSeriesChart._renderEnqueuedCharts.bind(TimeSeriesChart));
-        }
-        TimeSeriesChart._chartQueue.add(this);
-    }
-
-    static _renderEnqueuedCharts()
-    {
-        for (var chart of TimeSeriesChart._chartQueue)
-            chart.updateRendering();
-        TimeSeriesChart._chartQueue = null;
-    }
-
     setDomain(startTime, endTime)
     {
         console.assert(startTime < endTime, 'startTime must be before endTime');
@@ -804,3 +783,5 @@ class TimeSeriesChart extends ComponentBase {
         return gridValues;
     }
 }
+
+ComponentBase.defineElement('time-series-chart', TimeSeriesChart);
index 0e8341d..2aeb9ae 100644 (file)
@@ -37,13 +37,13 @@ class Instrumentation {
         if (!this._statistics)
             return;
         var maxKeyLength = 0;
-        for (var key in this._statistics)
+        for (let key in this._statistics)
             maxKeyLength = Math.max(key.length, maxKeyLength);
 
-        for (var key in this._statistics) {
-            var item = this._statistics[key];
-            var keySuffix = ' '.repeat(maxKeyLength - key.length);
-            var log = `${key}${keySuffix}: `;
+        for (let key of Object.keys(this._statistics).sort()) {
+            const item = this._statistics[key];
+            const keySuffix = ' '.repeat(maxKeyLength - key.length);
+            let log = `${key}${keySuffix}: `;
             log += ` mean = ${item.mean.toFixed(2)} ${item.unit}`;
             if (item.unit == 'ms')
                 log += ` total = ${item.value.toFixed(2)} ${item.unit}`;
index 67cca13..8591696 100644 (file)
@@ -8,6 +8,7 @@ var TimeSeries = class {
         this._data = [];
     }
 
+    values() { return this._data.map((point) => point.value); }
     length() { return this._data.length; }
 
     append(item)
index dda0d07..9bfb555 100644 (file)
@@ -23,10 +23,10 @@ class AnalysisCategoryPage extends PageWithHeading {
         var self = this;
         AnalysisTask.fetchAll().then(function () {
             self._fetched = true;
-            self.updateRendering();
+            self.enqueueToRender();
         }, function (error) {
             self._errorMessage = 'Failed to fetch the list of analysis tasks: ' + error;
-            self.updateRendering();
+            self.enqueueToRender();
         });
         super.open(state);
     }
@@ -59,12 +59,12 @@ class AnalysisCategoryPage extends PageWithHeading {
             this._categoryToolbar.setFilter(state.filter);
 
         if (!isOpen)
-            this.updateRendering();
+            this.enqueueToRender();
     }
 
     filterDidChange(shouldUpdateState)
     {
-        this.updateRendering();
+        this.enqueueToRender();
         if (shouldUpdateState)
             this.scheduleUrlStateUpdate();
     }
@@ -74,7 +74,7 @@ class AnalysisCategoryPage extends PageWithHeading {
         Instrumentation.startMeasuringTime('AnalysisCategoryPage', 'render');
 
         super.render();
-        this._categoryToolbar.updateRendering();
+        this._categoryToolbar.enqueueToRender();
 
         if (this._errorMessage) {
             console.assert(!this._fetched);
index 7e41c8b..a186f7b 100644 (file)
@@ -19,7 +19,7 @@ class AnalysisTaskChartPane extends ChartPaneBase {
     _updateStatus()
     {
         super._updateStatus();
-        this._page.updateRendering();
+        this._page.enqueueToRender();
     }
 
     selectedPoints()
@@ -101,7 +101,7 @@ class AnalysisTaskPage extends PageWithHeading {
             var taskId = parseInt(state.remainingRoute);
             AnalysisTask.fetchById(taskId).then(this._didFetchTask.bind(this), function (error) {
                 self._errorMessage = `Failed to fetch the analysis task ${state.remainingRoute}: ${error}`;
-                self.updateRendering();
+                self.enqueueToRender();
             });
             this._fetchRelatedInfoForTaskId(taskId);
         } else if (state.buildRequest) {
@@ -110,7 +110,7 @@ class AnalysisTaskPage extends PageWithHeading {
                 self._fetchRelatedInfoForTaskId(task.id());
             }, function (error) {
                 self._errorMessage = `Failed to fetch the analysis task for the build request ${buildRequestId}: ${error}`;
-                self.updateRendering();
+                self.enqueueToRender();
             });
         }
     }
@@ -146,7 +146,7 @@ class AnalysisTaskPage extends PageWithHeading {
         this._chartPane.setOverviewDomain(domain[0], domain[1]);
         this._chartPane.setMainDomain(domain[0], domain[1]);
 
-        this.updateRendering();
+        this.enqueueToRender();
 
         return task;
     }
@@ -154,7 +154,7 @@ class AnalysisTaskPage extends PageWithHeading {
     _didFetchRelatedAnalysisTasks(relatedTasks)
     {
         this._relatedTasks = relatedTasks;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _didFetchMeasurement()
@@ -171,7 +171,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
         this._startPoint = startPoint;
         this._endPoint = endPoint;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _didFetchTestGroups(testGroups)
@@ -179,14 +179,14 @@ class AnalysisTaskPage extends PageWithHeading {
         this._testGroups = testGroups.sort(function (a, b) { return +a.createdAt() - b.createdAt(); });
         this._didUpdateTestGroupHiddenState();
         this._assignTestResultsIfPossible();
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _showAllTestGroups()
     {
         this._showHiddenTestGroups = true;
         this._didUpdateTestGroupHiddenState();
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _didUpdateTestGroupHiddenState()
@@ -206,7 +206,7 @@ class AnalysisTaskPage extends PageWithHeading {
     {
         this._analysisResults = results;
         if (this._assignTestResultsIfPossible())
-            this.updateRendering();
+            this.enqueueToRender();
     }
 
     _assignTestResultsIfPossible()
@@ -233,7 +233,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
         this.content().querySelector('.error-message').textContent = this._errorMessage || '';
 
-        this._chartPane.updateRendering();
+        this._chartPane.enqueueToRender();
 
         var element = ComponentBase.createElement;
         var link = ComponentBase.createLink;
@@ -264,18 +264,18 @@ class AnalysisTaskPage extends PageWithHeading {
         } else
             repositoryList = Repository.sortByNamePreferringOnesWithURL(Repository.all());
 
-        this._bugList.updateRendering();
+        this._bugList.enqueueToRender();
 
         this._causeList.setKindList(repositoryList);
-        this._causeList.updateRendering();
+        this._causeList.enqueueToRender();
 
         this._fixList.setKindList(repositoryList);
-        this._fixList.updateRendering();
+        this._fixList.enqueueToRender();
 
         this.content().querySelector('.analysis-task-status').style.display = this._task ? null : 'none';
         this.content().querySelector('.overview-chart').style.display = this._task ? null : 'none';
         this.content().querySelector('.test-group-view').style.display = this._task && this._testGroups && this._testGroups.length ? null : 'none';
-        this._taskNameLabel.updateRendering();
+        this._taskNameLabel.enqueueToRender();
 
         if (this._relatedTasks && this._task) {
             var router = this.router();
@@ -296,7 +296,7 @@ class AnalysisTaskPage extends PageWithHeading {
         var a = selectedRange['A'];
         var b = selectedRange['B'];
         this._newTestGroupFormForViewer.setRootSetMap(a && b ? {'A': a.rootSet(), 'B': b.rootSet()} : null);
-        this._newTestGroupFormForViewer.updateRendering();
+        this._newTestGroupFormForViewer.enqueueToRender();
         this._newTestGroupFormForViewer.element().style.display = this._triggerable ? null : 'none';
 
         this._renderTestGroupList();
@@ -308,13 +308,13 @@ class AnalysisTaskPage extends PageWithHeading {
         var points = this._chartPane.selectedPoints();
         this._newTestGroupFormForChart.setRootSetMap(points && points.length >= 2 ?
                 {'A': points[0].rootSet(), 'B': points[points.length - 1].rootSet()} : null);
-        this._newTestGroupFormForChart.updateRendering();
+        this._newTestGroupFormForChart.enqueueToRender();
         this._newTestGroupFormForChart.element().style.display = this._triggerable ? null : 'none';
 
         this._analysisResultsViewer.setCurrentTestGroup(this._currentTestGroup);
-        this._analysisResultsViewer.updateRendering();
+        this._analysisResultsViewer.enqueueToRender();
 
-        this._testGroupResultsTable.updateRendering();
+        this._testGroupResultsTable.enqueueToRender();
 
         Instrumentation.endMeasuringTime('AnalysisTaskPage', 'render');
     }
@@ -356,7 +356,7 @@ class AnalysisTaskPage extends PageWithHeading {
             for (var testGroup of this._filteredTestGroups) {
                 var label = this._testGroupLabelMap.get(testGroup);
                 label.setText(testGroup.label());
-                label.updateRendering();
+                label.enqueueToRender();
             }
         }
     }
@@ -364,7 +364,7 @@ class AnalysisTaskPage extends PageWithHeading {
     _createTestGroupListItem(group)
     {
         var text = new EditableText(group.label());
-        text.setStartedEditingCallback(() => { return text.updateRendering(); });
+        text.setStartedEditingCallback(() => { return text.enqueueToRender(); });
         text.setUpdateCallback(this._updateTestGroupName.bind(this, group));
 
         this._testGroupLabelMap.set(group, text);
@@ -408,30 +408,30 @@ class AnalysisTaskPage extends PageWithHeading {
 
             this._renderedCurrentTestGroup = this._currentTestGroup;
         }
-        this._retryForm.updateRendering();
+        this._retryForm.enqueueToRender();
     }
 
     _showTestGroup(testGroup)
     {
         this._currentTestGroup = testGroup;        
         this._testGroupResultsTable.setTestGroup(this._currentTestGroup);
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _didStartEditingTaskName()
     {
-        this._taskNameLabel.updateRendering();
+        this._taskNameLabel.enqueueToRender();
     }
 
     _updateTaskName()
     {
         console.assert(this._task);
-        this._taskNameLabel.updateRendering();
+        this._taskNameLabel.enqueueToRender();
 
         return this._task.updateName(this._taskNameLabel.editedText()).then(() => {
-            this.updateRendering();
+            this.enqueueToRender();
         }, (error) => {
-            this.updateRendering();
+            this.enqueueToRender();
             alert('Failed to update the name: ' + error);
         });
     }
@@ -439,12 +439,12 @@ class AnalysisTaskPage extends PageWithHeading {
     _updateTestGroupName(testGroup)
     {
         var label = this._testGroupLabelMap.get(testGroup);
-        label.updateRendering();
+        label.enqueueToRender();
 
         return testGroup.updateName(label.editedText()).then(() => {
-            this.updateRendering();
+            this.enqueueToRender();
         }, (error) => {
-            this.updateRendering();
+            this.enqueueToRender();
             alert('Failed to hide the test name: ' + error);
         });
     }
@@ -454,10 +454,10 @@ class AnalysisTaskPage extends PageWithHeading {
         console.assert(this._currentTestGroup);
         return this._currentTestGroup.updateHiddenFlag(!this._currentTestGroup.isHidden()).then(() => {
             this._didUpdateTestGroupHiddenState();
-            this.updateRendering();
+            this.enqueueToRender();
         }, function (error) {
             this._mayHaveMutatedTestGroupHiddenState();
-            this.updateRendering();
+            this.enqueueToRender();
             alert('Failed to update the group: ' + error);
         });
     }
@@ -471,7 +471,10 @@ class AnalysisTaskPage extends PageWithHeading {
         if (newChangeType == 'unconfirmed')
             newChangeType = null;
 
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => {
+            this._chartPane.didUpdateAnnotations();
+            this.enqueueToRender();
+        };
         return this._task.updateChangeType(newChangeType).then(updateRendering, (error) => {
             updateRendering();
             alert('Failed to update the status: ' + error);
@@ -483,7 +486,7 @@ class AnalysisTaskPage extends PageWithHeading {
         console.assert(tracker instanceof BugTracker);
         bugNumber = parseInt(bugNumber);
 
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         return this._task.associateBug(tracker, bugNumber).then(updateRendering, (error) => {
             updateRendering();
             alert('Failed to associate the bug: ' + error);
@@ -492,7 +495,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _dissociateBug(bug)
     {
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         return this._task.dissociateBug(bug).then(updateRendering, (error) => {
             updateRendering();
             alert('Failed to dissociate the bug: ' + error);
@@ -501,7 +504,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _associateCommit(kind, repository, revision)
     {
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         return this._task.associateCommit(kind, repository, revision).then(updateRendering, (error) => {
             updateRendering();
             if (error == 'AmbiguousRevision')
@@ -515,7 +518,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _dissociateCommit(commit)
     {
-        const updateRendering = () => { this.updateRendering(); };
+        const updateRendering = () => { this.enqueueToRender(); };
         return this._task.dissociateCommit(commit).then(updateRendering, (error) => {
             updateRendering();
             alert('Failed to dissociate the commit: ' + error);
@@ -539,7 +542,7 @@ class AnalysisTaskPage extends PageWithHeading {
     _chartSelectionDidChange()
     {
         this._selectionWasModifiedByUser = true;
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _createNewTestGroupFromChart(name, repetitionCount, rootSetMap)
@@ -549,7 +552,7 @@ class AnalysisTaskPage extends PageWithHeading {
 
     _selectedRowInAnalysisResultsViewer()
     {
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _createNewTestGroupFromViewer(name, repetitionCount, rootSetMap)
index e718a91..bdb092b 100644 (file)
@@ -18,16 +18,16 @@ class BuildRequestQueuePage extends PageWithHeading {
 
                 BuildRequest.fetchForTriggerable(entry.name).then(function (requests) {
                     triggerable.buildRequests = requests;
-                    self.updateRendering();
+                    self.enqueueToRender();
                 });
 
                 return triggerable;
             });
-            self.updateRendering();
+            self.enqueueToRender();
         });
 
         AnalysisTask.fetchAll().then(function () {
-            self.updateRendering();
+            self.enqueueToRender();
         });
 
         super.open(state);
index 50a37e3..511565d 100644 (file)
@@ -6,7 +6,7 @@ function createTrendLineExecutableFromAveragingFunction(callback) {
         if (!values.length)
             return Promise.resolve(null);
 
-        var averageValues = callback.call(null, values, parameters[0], parameters[1]);
+        var averageValues = callback.call(null, values, ...parameters);
         if (!averageValues)
             return Promise.resolve(null);
 
@@ -186,8 +186,8 @@ class ChartPane extends ChartPaneBase {
     {
         if (repository != this._commitLogViewer.currentRepository()) {
             var range = this._mainChartStatus.setCurrentRepository(repository);
-            this._commitLogViewer.view(repository, range.from, range.to).then(() => { this.updateRendering(); });
-            this.updateRendering();
+            this._commitLogViewer.view(repository, range.from, range.to).then(() => { this.enqueueToRender(); });
+            this.enqueueToRender();
         }
     }
 
@@ -402,15 +402,15 @@ class ChartPane extends ChartPaneBase {
         var link = ComponentBase.createLink;
         var self = this;
 
-        if (this._trendLineType == null) {
-            this.renderReplace(this.content().querySelector('.trend-line-types'), [
+        const trendLineTypesContainer = this.content().querySelector('.trend-line-types');
+        if (!trendLineTypesContainer.querySelector('select')) {
+            this.renderReplace(trendLineTypesContainer, [
                 element('select', {onchange: this._trendLineTypeDidChange.bind(this)},
-                    ChartTrendLineTypes.map(function (type) {
-                        return element('option', type == self._trendLineType ? {value: type.id, selected: true} : {value: type.id}, type.label);
-                    }))
+                    ChartTrendLineTypes.map((type) => { return element('option', {value: type.id}, type.label); }))
             ]);
-        } else
-            this.content().querySelector('.trend-line-types select').value = this._trendLineType.id;
+        }
+        if (this._trendLineType)
+            trendLineTypesContainer.querySelector('select').value = this._trendLineType.id;
 
         if (this._renderedTrendLineOptions)
             return;
@@ -448,7 +448,7 @@ class ChartPane extends ChartPaneBase {
 
         this._updateTrendLine();
         this._chartsPage.graphOptionsDidChange();
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     _defaultParametersForTrendLine(type)
@@ -493,7 +493,7 @@ class ChartPane extends ChartPaneBase {
 
         if (!currentTrendLineType.execute) {
             this._mainChart.clearTrendLines();
-            this.updateRendering();
+            this.enqueueToRender();
         } else {
             // Wait for all trendlines to be ready. Otherwise we might see FOC when the domain is expanded.
             Promise.all(sourceList.map(function (source, sourceIndex) {
@@ -502,7 +502,7 @@ class ChartPane extends ChartPaneBase {
                         self._mainChart.setTrendLine(sourceIndex, trendlineSeries);
                 });
             })).then(function () {
-                self.updateRendering();
+                self.enqueueToRender();
             });
         }
     }
index a19013e..e4e1997 100644 (file)
@@ -79,7 +79,7 @@ class ChartsPage extends PageWithHeading {
         if (newPaneList) {
             this._paneList = newPaneList;
             this._paneListChanged = true;
-            this.updateRendering();
+            this.enqueueToRender();
         }
 
         this._updateDomainsFromSerializedState(state);
@@ -109,7 +109,7 @@ class ChartsPage extends PageWithHeading {
             since = zoom[0] - (zoom[1] - zoom[0]) / 2;
 
         this.toolbar().setStartTime(since);
-        this.toolbar().updateRendering();
+        this.toolbar().enqueueToRender();
 
         this._mainDomain = zoom || null;
 
@@ -151,7 +151,7 @@ class ChartsPage extends PageWithHeading {
     setNumberOfDaysFromToolbar(numberOfDays, shouldUpdateState)
     {
         this.toolbar().setNumberOfDays(numberOfDays, true);
-        this.toolbar().updateRendering();
+        this.toolbar().enqueueToRender();
         this._updateOverviewDomain();
         this._updateMainDomain();
         if (shouldUpdateState)
@@ -298,7 +298,7 @@ class ChartsPage extends PageWithHeading {
             this._updateOverviewDomain();
             this._updateMainDomain();
         }
-        this.updateRendering();
+        this.enqueueToRender();
         this.scheduleUrlStateUpdate();
     }
 
@@ -310,7 +310,7 @@ class ChartsPage extends PageWithHeading {
             this.renderReplace(this.content().querySelector('.pane-list'), this._paneList);
 
         for (var pane of this._paneList)
-            pane.updateRendering();
+            pane.enqueueToRender();
 
         this._paneListChanged = false;
     }
index 9c6323a..a097e34 100644 (file)
@@ -34,7 +34,7 @@ class ChartsToolbar extends DomainControlToolbar {
     render()
     {
         super.render();
-        this._paneSelector.updateRendering();
+        this._paneSelector.enqueueToRender();
         this._labelSpan.textContent = this._numberOfDays;
         this._setInputElementValue(this._numberOfDays);
     }
index 49224d2..8b802d2 100644 (file)
@@ -16,7 +16,7 @@ class CreateAnalysisTaskPage extends PageWithHeading {
 
         this._errorMessage = state.error;
         if (!isOpen)
-            this.updateRendering();
+            this.enqueueToRender();
     }
 
     render()
index ca3d05c..8aef82a 100644 (file)
@@ -35,7 +35,7 @@ class DashboardPage extends PageWithHeading {
 
         this._needsTableConstruction = true;
         if (!isOpen)
-            this.updateRendering();
+            this.enqueueToRender();
     }
 
     open(state)
@@ -114,7 +114,7 @@ class DashboardPage extends PageWithHeading {
 
         if (this._needsStatusUpdate) {
             for (var statusView of this._statusViews)
-                statusView.updateRendering();
+                statusView.enqueueToRender();
             this._needsStatusUpdate = false;
         }
     }
@@ -155,7 +155,7 @@ class DashboardPage extends PageWithHeading {
             return;
 
         this._needsStatusUpdate = true;
-        setTimeout(() => { this.updateRendering(); }, 10);
+        setTimeout(() => { this.enqueueToRender(); }, 10);
     }
 
     static htmlTemplate()
index d8e0d96..e3dde77 100644 (file)
@@ -50,7 +50,7 @@ class Heading extends ComponentBase {
         }
 
         if (this._toolbar)
-            this._toolbar.updateRendering();
+            this._toolbar.enqueueToRender();
 
         var currentPage = this._router.currentPage();
         if (this._renderedCurrentPage == currentPage)
index 55c55d9..a236775 100644 (file)
@@ -32,7 +32,7 @@ class PageWithHeading extends Page {
             document.body.insertBefore(this.heading().element(), document.body.firstChild);
 
         super.render();
-        this.heading().updateRendering();
+        this.heading().enqueueToRender();
     }
 
     static htmlTemplate()
index 1351da2..f887c03 100644 (file)
@@ -19,7 +19,7 @@ class Page extends ComponentBase {
         if (this._router)
             this._router.pageDidOpen(this);
         this.updateFromSerializedState(state, true);
-        this.updateRendering();
+        this.enqueueToRender();
     }
 
     render()
index 5397732..ee45cc4 100644 (file)
@@ -36,7 +36,7 @@ class SummaryPage extends PageWithHeading {
         var current = Date.now();
         var timeRange = [current - 24 * 3600 * 1000, current];
         for (var group of this._configGroups)
-            group.fetchAndComputeSummary(timeRange).then(() => { this.updateRendering(); });
+            group.fetchAndComputeSummary(timeRange).then(() => { this.enqueueToRender(); });
     }
 
     render()
@@ -106,7 +106,7 @@ class SummaryPage extends PageWithHeading {
         var ratioGraph = new RatioBarGraph();
 
         if (configurationList.length == 0) {
-            this._renderQueue.push(() => { ratioGraph.updateRendering(); });
+            this._renderQueue.push(() => { ratioGraph.enqueueToRender(); });
             return element('td', ratioGraph);
         }
 
@@ -121,7 +121,7 @@ class SummaryPage extends PageWithHeading {
 
     _renderCell(cell, spinner, anchor, ratioGraph, configurationGroup)
     {
-        spinner.updateRendering();
+        spinner.enqueueToRender();
 
         if (configurationGroup.isFetching())
             cell.classList.add('fetching');
@@ -131,7 +131,7 @@ class SummaryPage extends PageWithHeading {
         var warningText = this._warningTextForGroup(configurationGroup);
         anchor.title = warningText || 'Open charts';
         ratioGraph.update(configurationGroup.ratio(), configurationGroup.label(), !!warningText);
-        ratioGraph.updateRendering();
+        ratioGraph.enqueueToRender();
     }
 
     _warningTextForGroup(configurationGroup)