Web Inspector: TabActivity diagnostic event should sample the active tab uniformly
https://bugs.webkit.org/show_bug.cgi?id=204531

Reviewed by Devin Rousso.

Rewrite this class to use a uniform sampling approach. Every n seconds, a timer fires and
samples what the current tab is. If the last user interaction happened up to n seconds ago,
report a TabActivity diagnostic event. Keeping with the previous implementation, samples
are taken every n=60 seconds.

To account for bias in the initial sample when Web Inspector is open, wait m seconds for
the first sample. This accounts for the time between opening Web Inspector and choosing the
desired tab. In my testing, m=10 is enough time to load Web Inspector and switch
immediately to a different tab. In that case, the initial tab would not be sampled as the
active tab even if the last user interaction (clicking tab bar) happened while the initial
tab was displayed. If the recorder's setup() method is called some time after Web Inspector is
opened, then the initial delay will shrink to ensure at least 10s has elapsed since the frontend
finished loading.

* UserInterface/Base/Main.js:
(WI.contentLoaded): Keep a timestamp of when the frontend finished loading.

* UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js:
(WI.TabActivityDiagnosticEventRecorder):
(WI.TabActivityDiagnosticEventRecorder.prototype.setup):
(WI.TabActivityDiagnosticEventRecorder.prototype.teardown):
(WI.TabActivityDiagnosticEventRecorder.prototype._startInitialDelayBeforeSamplingTimer):
(WI.TabActivityDiagnosticEventRecorder.prototype._stopInitialDelayBeforeSamplingTimer):
(WI.TabActivityDiagnosticEventRecorder.prototype._startEventSamplingTimer):
(WI.TabActivityDiagnosticEventRecorder.prototype._stopEventSamplingTimer):
(WI.TabActivityDiagnosticEventRecorder.prototype._sampleCurrentTabActivity):
(WI.TabActivityDiagnosticEventRecorder.prototype._didObserveUserInteraction):
(WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowFocus):
(WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowBlur):
(WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowKeyDown):
(WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowMouseDown):
(WI.TabActivityDiagnosticEventRecorder.prototype._didInteractWithTabContent): Deleted.
(WI.TabActivityDiagnosticEventRecorder.prototype._clearTabActivityTimeout): Deleted.
(WI.TabActivityDiagnosticEventRecorder.prototype._beginTabActivityTimeout): Deleted.
(WI.TabActivityDiagnosticEventRecorder.prototype._stopTrackingTabActivity): Deleted.
(WI.TabActivityDiagnosticEventRecorder.prototype._handleTabBrowserSelectedTabContentViewDidChange): Deleted.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@253211 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog
index e88906f..cf925f8 100644
--- a/Source/WebInspectorUI/ChangeLog
+++ b/Source/WebInspectorUI/ChangeLog
@@ -1,3 +1,47 @@
+2019-11-25  Brian Burg  <bburg@apple.com>
+
+        Web Inspector: TabActivity diagnostic event should sample the active tab uniformly
+        https://bugs.webkit.org/show_bug.cgi?id=204531
+
+        Reviewed by Devin Rousso.
+
+        Rewrite this class to use a uniform sampling approach. Every n seconds, a timer fires and
+        samples what the current tab is. If the last user interaction happened up to n seconds ago,
+        report a TabActivity diagnostic event. Keeping with the previous implementation, samples
+        are taken every n=60 seconds.
+
+        To account for bias in the initial sample when Web Inspector is open, wait m seconds for
+        the first sample. This accounts for the time between opening Web Inspector and choosing the
+        desired tab. In my testing, m=10 is enough time to load Web Inspector and switch
+        immediately to a different tab. In that case, the initial tab would not be sampled as the
+        active tab even if the last user interaction (clicking tab bar) happened while the initial
+        tab was displayed. If the recorder's setup() method is called some time after Web Inspector is
+        opened, then the initial delay will shrink to ensure at least 10s has elapsed since the frontend
+        finished loading.
+
+        * UserInterface/Base/Main.js:
+        (WI.contentLoaded): Keep a timestamp of when the frontend finished loading.
+
+        * UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js:
+        (WI.TabActivityDiagnosticEventRecorder):
+        (WI.TabActivityDiagnosticEventRecorder.prototype.setup):
+        (WI.TabActivityDiagnosticEventRecorder.prototype.teardown):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._startInitialDelayBeforeSamplingTimer):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._stopInitialDelayBeforeSamplingTimer):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._startEventSamplingTimer):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._stopEventSamplingTimer):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._sampleCurrentTabActivity):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._didObserveUserInteraction):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowFocus):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowBlur):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowKeyDown):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._handleWindowMouseDown):
+        (WI.TabActivityDiagnosticEventRecorder.prototype._didInteractWithTabContent): Deleted.
+        (WI.TabActivityDiagnosticEventRecorder.prototype._clearTabActivityTimeout): Deleted.
+        (WI.TabActivityDiagnosticEventRecorder.prototype._beginTabActivityTimeout): Deleted.
+        (WI.TabActivityDiagnosticEventRecorder.prototype._stopTrackingTabActivity): Deleted.
+        (WI.TabActivityDiagnosticEventRecorder.prototype._handleTabBrowserSelectedTabContentViewDidChange): Deleted.
+
 2019-12-05  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: REGRESSION(r242604): Console: unread indicator overlaps selection background of previous scope bar item
diff --git a/Source/WebInspectorUI/UserInterface/Base/Main.js b/Source/WebInspectorUI/UserInterface/Base/Main.js
index 6d81cfc..78910fa 100644
--- a/Source/WebInspectorUI/UserInterface/Base/Main.js
+++ b/Source/WebInspectorUI/UserInterface/Base/Main.js
@@ -545,6 +545,8 @@
     // Store WI on the window in case the WebInspector global gets corrupted.
     window.__frontendCompletedLoad = true;
 
+    WI.frontendCompletedLoadTimestamp = performance.now();
+
     if (WI.runBootstrapOperations)
         WI.runBootstrapOperations();
 
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js b/Source/WebInspectorUI/UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js
index b8ee945..3d7e987 100644
--- a/Source/WebInspectorUI/UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js
+++ b/Source/WebInspectorUI/UserInterface/Controllers/TabActivityDiagnosticEventRecorder.js
@@ -29,16 +29,23 @@
     {
         super("TabActivity", controller);
 
-        this._shouldLogTabContentActivity = false;
-        this._tabActivityTimeoutIdentifier = 0;
+        this._inspectorHasFocus = true;
+        this._lastUserInteractionTimestamp = undefined;
+
+        this._eventSamplingTimerIdentifier = undefined;
+        this._initialDelayBeforeSamplingTimerIdentifier = undefined;
     }
 
+    // Static
+
+    // In milliseconds.
+    static get eventSamplingInterval() { return 60 * 1000; }
+    static get initialDelayBeforeSamplingInterval() { return 10 * 1000; }
+
     // Protected
 
     setup()
     {
-        WI.TabBrowser.addEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._handleTabBrowserSelectedTabContentViewDidChange, this);
-
         const options = {
             capture: true,
         };
@@ -47,13 +54,16 @@
         window.addEventListener("keydown", this, options);
         window.addEventListener("mousedown", this, options);
 
-        this._shouldLogTabContentActivity = true;
+        // If it's been less than 10 seconds since the frontend loaded, wait a bit.
+        if (performance.now() - WI.frontendCompletedLoadTimestamp < TabActivityDiagnosticEventRecorder.initialDelayBeforeSamplingInterval)
+            this._startInitialDelayBeforeSamplingTimer();
+        else
+            this._startEventSamplingTimer();
+
     }
 
     teardown()
     {
-        WI.TabBrowser.removeEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._handleTabBrowserSelectedTabContentViewDidChange, this);
-
         const options = {
             capture: true,
         };
@@ -62,7 +72,9 @@
         window.removeEventListener("keydown", this, options);
         window.removeEventListener("mousedown", this, options);
 
-        this._stopTrackingTabActivity();
+        this._stopInitialDelayBeforeSamplingTimer();
+        this._stopEventSamplingTimer();
+
     }
 
     // Public
@@ -87,10 +99,60 @@
 
     // Private
 
-    _didInteractWithTabContent()
+    _startInitialDelayBeforeSamplingTimer()
     {
-        if (!this._shouldLogTabContentActivity)
+        if (this._initialDelayBeforeSamplingTimerIdentifier) {
+            clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier);
+            this._initialDelayBeforeSamplingTimerIdentifier = undefined;
+        }
+
+        // All intervals are in milliseconds.
+        let maximumInitialDelay = TabActivityDiagnosticEventRecorder.initialDelayBeforeSamplingInterval;
+        let elapsedTime = performance.now() - WI.frontendCompletedLoadTimestamp;
+        let remainingTime = maximumInitialDelay - elapsedTime;
+        let initialDelay = Number.constrain(remainingTime, 0, maximumInitialDelay);
+        this._initialDelayBeforeSamplingTimerIdentifier = setTimeout(this._sampleCurrentTabActivity.bind(this), initialDelay);
+    }
+
+    _stopInitialDelayBeforeSamplingTimer()
+    {
+        if (this._initialDelayBeforeSamplingTimerIdentifier) {
+            clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier);
+            this._initialDelayBeforeSamplingTimerIdentifier = undefined;
+        }
+    }
+
+    _startEventSamplingTimer()
+    {
+        if (this._eventSamplingTimerIdentifier) {
+            clearTimeout(this._eventSamplingTimerIdentifier);
+            this._eventSamplingTimerIdentifier = undefined;
+        }
+
+        this._eventSamplingTimerIdentifier = setTimeout(this._sampleCurrentTabActivity.bind(this), TabActivityDiagnosticEventRecorder.eventSamplingInterval);
+    }
+
+    _stopEventSamplingTimer()
+    {
+        if (this._eventSamplingTimerIdentifier) {
+            clearTimeout(this._eventSamplingTimerIdentifier);
+            this._eventSamplingTimerIdentifier = undefined;
+        }
+    }
+
+    _sampleCurrentTabActivity()
+    {
+        // Set up the next timer first so later code can bail out if there's nothing to do.
+        this._stopEventSamplingTimer();
+        this._stopInitialDelayBeforeSamplingTimer();
+        this._startEventSamplingTimer();
+
+        let intervalSinceLastUserInteraction = performance.now() - this._lastUserInteractionTimestamp;
+        if (intervalSinceLastUserInteraction > TabActivityDiagnosticEventRecorder.eventSamplingInterval) {
+            if (WI.settings.debugAutoLogDiagnosticEvents.valueRespectingDebugUIAvailability)
+                console.log("TabActivity: sample not reported, last user interaction was %.1f seconds ago.".format(intervalSinceLastUserInteraction / 1000));
             return;
+        }
 
         let selectedTabContentView = WI.tabBrowser.selectedTabContentView;
         console.assert(selectedTabContentView);
@@ -98,34 +160,16 @@
             return;
 
         let tabType = selectedTabContentView.type;
-        let interval = TabActivityDiagnosticEventRecorder.ActivityTimingResolution;
+        let interval = TabActivityDiagnosticEventRecorder.eventSamplingInterval / 1000;
         this.logDiagnosticEvent(this.name, {tabType, interval});
-
-        this._beginTabActivityTimeout();
     }
 
-    _clearTabActivityTimeout()
+    _didObserveUserInteraction()
     {
-        if (this._tabActivityTimeoutIdentifier) {
-            clearTimeout(this._tabActivityTimeoutIdentifier);
-            this._tabActivityTimeoutIdentifier = 0;
-        }
-    }
+        if (!this._inspectorHasFocus)
+            return;
 
-    _beginTabActivityTimeout()
-    {
-        this._stopTrackingTabActivity();
-
-        this._tabActivityTimeoutIdentifier = setTimeout(() => {
-            this._shouldLogTabContentActivity = true;
-            this._tabActivityTimeoutIdentifier = 0;
-        }, TabActivityDiagnosticEventRecorder.ActivityTimingResolution);
-    }
-
-    _stopTrackingTabActivity()
-    {
-        this._clearTabActivityTimeout();
-        this._shouldLogTabContentActivity = false;
+        this._lastUserInteractionTimestamp = performance.now();
     }
 
     _handleWindowFocus(event)
@@ -133,7 +177,7 @@
         if (event.target !== window)
             return;
 
-        this._shouldLogTabContentActivity = true;
+        this._inspectorHasFocus = true;
     }
 
     _handleWindowBlur(event)
@@ -141,24 +185,17 @@
         if (event.target !== window)
             return;
 
-        this._stopTrackingTabActivity();
+        this._inspectorHasFocus = false;
     }
 
     _handleWindowKeyDown(event)
     {
-        this._didInteractWithTabContent();
+        this._didObserveUserInteraction();
     }
 
     _handleWindowMouseDown(event)
     {
-        this._didInteractWithTabContent();
-    }
-
-    _handleTabBrowserSelectedTabContentViewDidChange(event)
-    {
-        this._clearTabActivityTimeout();
-        this._shouldLogTabContentActivity = true;
+        this._didObserveUserInteraction();
     }
 };
 
-WI.TabActivityDiagnosticEventRecorder.ActivityTimingResolution = 60 * 1000;