diff --git a/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts b/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts index 1dc8e480f3..c451a2b3dd 100644 --- a/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts +++ b/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts @@ -39,13 +39,34 @@ namespace tf_tensorboard { _autoReloadId: { type: Number, }, + // Tracks whethere an auto reload was missed because the document was not visible. + _missedAutoReload: { + type: Boolean, + value: false, + }, + _boundHandleVisibilityChange: { + type: Object, + }, autoReloadIntervalSecs: { type: Number, value: 30, }, }, + attached: function() { + this._boundHandleVisibilityChange_ = this._handleVisibilityChange.bind( + this + ); + document.addEventListener( + 'visibilitychange', + this._boundHandleVisibilityChange + ); + }, detached: function() { window.clearTimeout(this._autoReloadId); + document.removeEventListener( + 'visibilitychange', + this._boundHandleVisibilityChange + ); }, _autoReloadObserver: function(autoReload) { window.localStorage.setItem(AUTORELOAD_LOCALSTORAGE_KEY, autoReload); @@ -59,14 +80,34 @@ namespace tf_tensorboard { } }, _doAutoReload: function() { - if (this.reload == null) { - throw new Error('AutoReloadBehavior requires a reload method'); + if (this._isDocumentVisible()) { + this._doReload(); + } else { + this._missedAutoReload = true; } - this.reload(); this._autoReloadId = window.setTimeout( () => this._doAutoReload(), this.autoReloadIntervalSecs * 1000 ); }, + _doReload: function() { + if (this.reload == null) { + throw new Error('AutoReloadBehavior requires a reload method'); + } + this.reload(); + }, + _handleVisibilityChange: function() { + if (this._isDocumentVisible() && this._missedAutoReload) { + this._missedAutoReload = false; + this._doReload(); + } + }, + /** + * Wraps Page Visibility API call to determine if document is visible. + * Can be overriden for testing purposes. + */ + _isDocumentVisible: function() { + return document.visibilityState === 'visible'; + }, }; } // namespace tf_tensorboard diff --git a/tensorboard/components/tf_tensorboard/test/autoReloadTests.ts b/tensorboard/components/tf_tensorboard/test/autoReloadTests.ts index 5e23b69ccd..9cf0e07b71 100644 --- a/tensorboard/components/tf_tensorboard/test/autoReloadTests.ts +++ b/tensorboard/components/tf_tensorboard/test/autoReloadTests.ts @@ -27,6 +27,8 @@ namespace tf_tensorboard { const key = AUTORELOAD_LOCALSTORAGE_KEY; let clock; let callCount: number; + let sandbox: any; + let isDocumentVisible = true; beforeEach(function() { ls.setItem(key, 'false'); // start it turned off so we can mutate fns @@ -35,6 +37,16 @@ namespace tf_tensorboard { testElement.reload = function() { callCount++; }; + + sandbox = sinon.sandbox.create(); + sandbox.stub(testElement, '_isDocumentVisible', function() { + return isDocumentVisible; + }); + }); + + afterEach(function() { + isDocumentVisible = true; + sandbox.restore(); }); before(function() { @@ -45,6 +57,11 @@ namespace tf_tensorboard { clock.restore(); }); + function simulateVisibilityChange(visibility) { + isDocumentVisible = visibility; + testElement._handleVisibilityChange(); + } + it('reads and writes autoReload state from localStorage', function() { ls.removeItem(key); testElement = fixture('autoReloadFixture'); @@ -99,6 +116,118 @@ namespace tf_tensorboard { clock.tick(5000); }); }); + + it('does not reload if document is not visible', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = true; + + clock.tick(1000); + chai.assert.equal(callCount, 1, 'ticking clock triggered call'); + + simulateVisibilityChange(false); + clock.tick(1000); + chai.assert.equal( + callCount, + 1, + 'ticking clock while not visible did not trigger call' + ); + }); + + it('reloads when document becomes visible if missed reload', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = true; + + simulateVisibilityChange(false); + clock.tick(1000); + chai.assert.equal( + callCount, + 0, + 'ticking clock while not visible did not trigger call' + ); + + simulateVisibilityChange(true); + chai.assert.equal(callCount, 1, 'visibility change triggered call'); + }); + + it('reloads when document becomes visible if missed reload, regardless of how long not visible', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = true; + clock.tick(1000); + chai.assert.equal(callCount, 1, 'ticking clock triggered call'); + + // Document is not visible during time period that includes missed auto reload but is less than + // autoReloadIntervalSecs + clock.tick(300); + simulateVisibilityChange(false); + clock.tick(800); + chai.assert.equal( + callCount, + 1, + 'ticking clock while not visible did not trigger call' + ); + + simulateVisibilityChange(true); + chai.assert.equal(callCount, 2, 'visibility change triggered call'); + }); + + it('does not reload when document becomes visible if there was not a missed reload', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = true; + + clock.tick(1000); + chai.assert.equal(callCount, 1, 'ticking clock triggered call'); + + // Document is not visible during time period that does not include missed auto reload. + simulateVisibilityChange(false); + clock.tick(500); + simulateVisibilityChange(true); + chai.assert.equal( + callCount, + 1, + 'visibility change did not trigger call' + ); + }); + + it('does not reload when document becomes visible if missed reload was already handled', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = true; + + simulateVisibilityChange(false); + clock.tick(1200); + chai.assert.equal( + callCount, + 0, + 'ticking clock while not visible did not trigger call' + ); + + simulateVisibilityChange(true); + chai.assert.equal(callCount, 1, 'visibility change triggered call'); + + // Document is not visible during time period that does not include another missed reload. + simulateVisibilityChange(false); + clock.tick(200); + simulateVisibilityChange(true); + chai.assert.equal( + callCount, + 1, + 'visibility change did not trigger call' + ); + }); + + it('does not reload when document becomes visible if auto reload is off', function() { + testElement.autoReloadIntervalSecs = 1; + testElement.autoReloadEnabled = false; + + simulateVisibilityChange(false); + clock.tick(5000); + + simulateVisibilityChange(true); + chai.assert.equal( + callCount, + 0, + 'visibility change did not trigger call' + ); + }); }); }); } // namespace tf_tensorboard