Add histograms for Devtools Experiments

This CL introduces 3 histograms around devtools experiments:
ExperimentEnabled - fired when users turn on an experiment
ExperimentDisabled - fired when users disable an experiment
ExperimentEnabledAtLaunch - fires on Devtools launch for each
experiment that is enabled.

These will allow us to gain better understanding of how users
interact with devtools experiments.

Explainer:
https://docs.google.com/document/d/1pcvN11C4onSXqOE0dTjUUHodMj55bRVl_5_e3kMg_rs/edit?ts=5f2329dc#

Corresponding backend CL: https://crrev.com/c/2341215

Bug: 1107636
Change-Id: I4ae72881b6409d25204fea0335f6796161cc61c0
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2341950
Commit-Queue: Brandon Goddard <[email protected]>
Reviewed-by: Shane Clifford <[email protected]>
Reviewed-by: Kalon Hinds <[email protected]>
Reviewed-by: Patrick Brosset <[email protected]>
Reviewed-by: Peter Marshall <[email protected]>
Reviewed-by: Jack Lynch <[email protected]>
diff --git a/front_end/devtools_compatibility.js b/front_end/devtools_compatibility.js
index 815a5da..5a3205a 100644
--- a/front_end/devtools_compatibility.js
+++ b/front_end/devtools_compatibility.js
@@ -370,6 +370,9 @@
     KeybindSetSettingChanged: 'DevTools.KeybindSetSettingChanged',
     DualScreenDeviceEmulated: 'DevTools.DualScreenDeviceEmulated',
     CSSGridSettings: 'DevTools.CSSGridSettings',
+    ExperimentEnabledAtLaunch: 'DevTools.ExperimentEnabledAtLaunch',
+    ExperimentEnabled: 'DevTools.ExperimentEnabled',
+    ExperimentDisabled: 'DevTools.ExperimentDisabled',
   };
 
   /**
diff --git a/front_end/externs.js b/front_end/externs.js
index 39257ff..70a5ccc 100644
--- a/front_end/externs.js
+++ b/front_end/externs.js
@@ -1468,6 +1468,9 @@
   KeybindSetSettingChanged: 'DevTools.KeybindSetSettingChanged',
   DualScreenDeviceEmulated: 'DevTools.DualScreenDeviceEmulated',
   CSSGridSettings: 'DevTools.CSSGridSettings',
+  ExperimentEnabledAtLaunch: 'DevTools.ExperimentEnabledAtLaunch',
+  ExperimentEnabled: 'DevTools.ExperimentEnabled',
+  ExperimentDisabled: 'DevTools.ExperimentDisabled',
 };
 
 /**
diff --git a/front_end/host/InspectorFrontendHostAPI.js b/front_end/host/InspectorFrontendHostAPI.js
index e4102d2..405f2d9 100644
--- a/front_end/host/InspectorFrontendHostAPI.js
+++ b/front_end/host/InspectorFrontendHostAPI.js
@@ -416,4 +416,7 @@
   KeybindSetSettingChanged: 'DevTools.KeybindSetSettingChanged',
   DualScreenDeviceEmulated: 'DevTools.DualScreenDeviceEmulated',
   CSSGridSettings: 'DevTools.CSSGridSettings',
+  ExperimentEnabledAtLaunch: 'DevTools.ExperimentEnabledAtLaunch',
+  ExperimentEnabled: 'DevTools.ExperimentEnabled',
+  ExperimentDisabled: 'DevTools.ExperimentDisabled',
 };
diff --git a/front_end/host/UserMetrics.js b/front_end/host/UserMetrics.js
index d528df2..20a61c2 100644
--- a/front_end/host/UserMetrics.js
+++ b/front_end/host/UserMetrics.js
@@ -175,6 +175,35 @@
     InspectorFrontendHostInstance.recordEnumeratedHistogram(EnumeratedHistogram.CSSGridSettings, gridSetting, size);
     Common.EventTarget.fireEvent(EnumeratedHistogram.CSSGridSettings, {value: gridSetting});
   }
+
+  /**
+   * @param {string} experimentId
+   */
+  experimentEnabledAtLaunch(experimentId) {
+    const size = DevtoolsExperiments['__lastValidEnumPosition'] + 1;
+    const experiment = DevtoolsExperiments[experimentId];
+    if (experiment === undefined) {
+      return;
+    }
+    InspectorFrontendHostInstance.recordEnumeratedHistogram(
+        EnumeratedHistogram.ExperimentEnabledAtLaunch, experiment, size);
+    Common.EventTarget.fireEvent(EnumeratedHistogram.ExperimentEnabledAtLaunch, {value: experiment});
+  }
+
+  /**
+   * @param {string} experimentId
+   * @param {boolean} isEnabled
+   */
+  experimentChanged(experimentId, isEnabled) {
+    const size = DevtoolsExperiments['__lastValidEnumPosition'] + 1;
+    const experiment = DevtoolsExperiments[experimentId];
+    if (experiment === undefined) {
+      return;
+    }
+    const actionName = isEnabled ? EnumeratedHistogram.ExperimentEnabled : EnumeratedHistogram.ExperimentDisabled;
+    InspectorFrontendHostInstance.recordEnumeratedHistogram(actionName, experiment, size);
+    Common.EventTarget.fireEvent(actionName, {value: experiment});
+  }
 }
 
 // Codes below are used to collect UMA histograms in the Chromium port.
@@ -415,3 +444,53 @@
   'showGridTrackSizes.false': 19,
   'showGridTrackSizes.true': 20,
 };
+
+/**
+ * This list should contain the currently active Devtools Experiments.
+ * Therefore, it is possible that the id's will no longer be continuous
+ * as experiemnts are removed.
+ * When adding a new experiemnt:
+ * 1. Add an entry to the bottom of the list before '__lastValidEnumPosition'
+ * 2. Set the value of the new entry and '__lastValidEnumPosition' to
+ *    __lastValidEnumPosition + 1
+ * When removing an experiment, simply delete the line from the enum.
+ */
+/** @type {!Object<string, number>} */
+export const DevtoolsExperiments = {
+  'applyCustomStylesheet': 0,
+  'captureNodeCreationStacks': 1,
+  'sourcesPrettyPrint': 2,
+  'backgroundServices': 3,
+  'backgroundServicesNotifications': 4,
+  'backgroundServicesPaymentHandler': 5,
+  'backgroundServicesPushMessaging': 6,
+  'blackboxJSFramesOnTimeline': 7,
+  'cssOverview': 8,
+  'emptySourceMapAutoStepping': 9,
+  'inputEventsOnTimelineOverview': 10,
+  'liveHeapProfile': 11,
+  'nativeHeapProfiler': 12,
+  'protocolMonitor': 13,
+  'issuesPane': 14,
+  'developerResourcesView': 15,
+  'recordCoverageWithPerformanceTracing': 16,
+  'samplingHeapProfilerTimeline': 17,
+  'showOptionToNotTreatGlobalObjectsAsRoots': 18,
+  'sourceDiff': 19,
+  'sourceOrderViewer': 20,
+  'spotlight': 21,
+  'webauthnPane': 22,
+  'customKeyboardShortcuts': 23,
+  'timelineEventInitiators': 24,
+  'timelineFlowEvents': 25,
+  'timelineInvalidationTracking': 26,
+  'timelineShowAllEvents': 27,
+  'timelineV8RuntimeCallStats': 28,
+  'timelineWebGL': 29,
+  'timelineReplayEvent': 30,
+  'wasmDWARFDebugging': 31,
+  'dualScreenSupport': 32,
+  'cssGridFeatures': 33,
+  'movableTabs': 34,
+  '__lastValidEnumPosition': 34,
+};
diff --git a/front_end/main/MainImpl.js b/front_end/main/MainImpl.js
index 67c28cc..7f6a1f5 100644
--- a/front_end/main/MainImpl.js
+++ b/front_end/main/MainImpl.js
@@ -137,7 +137,6 @@
   }
 
   _initializeExperiments() {
-    // Keep this sorted alphabetically: both keys and values.
     Root.Runtime.experiments.register('applyCustomStylesheet', 'Allow custom UI themes');
     Root.Runtime.experiments.register('captureNodeCreationStacks', 'Capture node creation stacks');
     Root.Runtime.experiments.register('sourcesPrettyPrint', 'Automatically pretty print in the Sources Panel');
@@ -210,6 +209,10 @@
         Root.Runtime.queryParam('test').includes('live-line-level-heap-profile.js')) {
       Root.Runtime.experiments.enableForTest('liveHeapProfile');
     }
+
+    for (const experiment of Root.Runtime.experiments.enabledExperiments()) {
+      Host.userMetrics.experimentEnabledAtLaunch(experiment.name);
+    }
   }
 
   /**
diff --git a/front_end/root/Runtime.js b/front_end/root/Runtime.js
index 9021be2..898168a 100644
--- a/front_end/root/Runtime.js
+++ b/front_end/root/Runtime.js
@@ -890,6 +890,13 @@
   }
 
   /**
+  * @return {!Array.<!Experiment>}
+  */
+  enabledExperiments() {
+    return this._experiments.filter(experiment => experiment.isEnabled());
+  }
+
+  /**
   * @param {!Object} value
   */
   _setExperimentsSetting(value) {
diff --git a/front_end/settings/SettingsScreen.js b/front_end/settings/SettingsScreen.js
index 06fe058..400b4e0 100644
--- a/front_end/settings/SettingsScreen.js
+++ b/front_end/settings/SettingsScreen.js
@@ -366,6 +366,7 @@
     input.name = experiment.name;
     function listener() {
       experiment.setEnabled(input.checked);
+      Host.userMetrics.experimentChanged(experiment.name, experiment.isEnabled());
       UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning(
           ls`One or more settings have changed which requires a reload to take effect.`);
     }