Bug 1498173 - Allow snapshotting from non-main stores for plain histograms r=chutten
authorJan-Erik Rediger <jrediger@mozilla.com>
Thu, 22 Nov 2018 23:35:32 +0000
changeset 504240 5edb94ce0260d72ab9fd67a88842ab63811d5f73
parent 504239 5f6b602fb590ccbaab9a33474d72a0aacdbd5529
child 504241 cf109d30e7efb96d5afbb2f018da189de2198931
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerschutten
bugs1498173
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1498173 - Allow snapshotting from non-main stores for plain histograms r=chutten This changes the API of the object returned from `Telemetry.getHistogramById` * `add` will continue to record into all registered stores for the histogram. * `snapshot` now takes an object `{store: "storeName"}`, if not given it defaults to "main" * `clear` now takes an object `{store: "storeName"}`, if not given it defaults to "main" Differential Revision: https://phabricator.services.mozilla.com/D12555
toolkit/components/telemetry/core/Telemetry.cpp
toolkit/components/telemetry/core/TelemetryHistogram.cpp
toolkit/components/telemetry/core/TelemetryHistogram.h
toolkit/components/telemetry/core/nsITelemetry.idl
toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
--- a/toolkit/components/telemetry/core/Telemetry.cpp
+++ b/toolkit/components/telemetry/core/Telemetry.cpp
@@ -625,17 +625,17 @@ TelemetryImpl::GetSnapshotForHistograms(
                                         bool aClearStore,
                                         bool aFilterTest,
                                         JSContext* aCx,
                                         JS::MutableHandleValue aResult)
 {
   unsigned int dataset = mCanRecordExtended ?
     nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN :
     nsITelemetry::DATASET_RELEASE_CHANNEL_OPTOUT;
-  return TelemetryHistogram::CreateHistogramSnapshots(aCx, aResult, dataset, aClearStore, aFilterTest);
+  return TelemetryHistogram::CreateHistogramSnapshots(aCx, aResult, aStoreName, dataset, aClearStore, aFilterTest);
 }
 
 NS_IMETHODIMP
 TelemetryImpl::GetSnapshotForKeyedHistograms(const nsACString& aStoreName,
                                              bool aClearStore,
                                              bool aFilterTest,
                                              JSContext* aCx,
                                              JS::MutableHandleValue aResult)
@@ -674,17 +674,17 @@ TelemetryImpl::GetSnapshotForKeyedScalar
                                                aResult, aFilterTest, aStoreName);
 }
 
 NS_IMETHODIMP
 TelemetryImpl::SnapshotHistograms(unsigned int aDataset,
                                   bool aClearHistograms, JSContext* aCx,
                                   JS::MutableHandleValue aResult)
 {
-  return TelemetryHistogram::CreateHistogramSnapshots(aCx, aResult, aDataset,
+  return TelemetryHistogram::CreateHistogramSnapshots(aCx, aResult, NS_LITERAL_CSTRING("main"), aDataset,
                                                       aClearHistograms);
 }
 
 NS_IMETHODIMP
 TelemetryImpl::SnapshotKeyedHistograms(unsigned int aDataset,
                                        bool aClearHistograms, JSContext* aCx,
                                        JS::MutableHandleValue aResult)
 {
--- a/toolkit/components/telemetry/core/TelemetryHistogram.cpp
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.cpp
@@ -196,20 +196,19 @@ public:
   ~Histogram();
 
   /**
    * Add a sample to this histogram in all registered stores.
    */
   void Add(uint32_t sample);
 
   /**
-   * Clear the main store for this histogram.
-   * TODO(bug 1498173): Add clearing of specific store.
+   * Clear the named store for this histogram.
    */
-  void Clear();
+  void Clear(const nsACString& store);
 
   /**
    * Get the histogram instance from the named store.
    */
   bool GetHistogram(const nsACString& store, base::Histogram** h);
 
   bool IsExpired() const { return mIsExpired; }
 
@@ -901,26 +900,28 @@ internal_ShouldReflectHistogram(const St
 
   return true;
 }
 
 /**
  * Helper function to get a snapshot of the histograms.
  *
  * @param {aLock} the lock proof.
+ * @param {aStore} the name of the store to snapshot.
  * @param {aDataset} the dataset for which the snapshot is being requested.
  * @param {aClearSubsession} whether or not to clear the data after
  *        taking the snapshot.
  * @param {aIncludeGPU} whether or not to include data for the GPU.
  * @param {aOutSnapshot} the container in which the snapshot data will be stored.
  * @return {nsresult} NS_OK if the snapshot was successfully taken or
  *         NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory.
  */
 nsresult
 internal_GetHistogramsSnapshot(const StaticMutexAutoLock& aLock,
+                               const nsACString& aStore,
                                unsigned int aDataset,
                                bool aClearSubsession,
                                bool aIncludeGPU,
                                bool aFilterTest,
                                HistogramProcessSnapshotsArray& aOutSnapshot)
 {
   if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) {
     return NS_ERROR_OUT_OF_MEMORY;
@@ -950,18 +951,17 @@ internal_GetHistogramsSnapshot(const Sta
         info.histogramType == nsITelemetry::HISTOGRAM_FLAG;
       Histogram* w = internal_GetHistogramById(aLock, id, ProcessID(process),
                                                shouldInstantiate);
       if (!w || w->IsExpired()) {
         continue;
       }
 
       base::Histogram *h = nullptr;
-      NS_NAMED_LITERAL_CSTRING(store, "main");
-      if (!w->GetHistogram(store, &h)) {
+      if (!w->GetHistogram(aStore, &h)) {
         continue;
       }
 
       if (!internal_ShouldReflectHistogram(aLock, h, id)) {
         continue;
       }
 
       const char* name = info.name();
@@ -1045,48 +1045,54 @@ Histogram::Add(uint32_t sample)
     for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) {
       auto& h = iter.Data();
       h->Add(sample);
     }
   }
 }
 
 void
-Histogram::Clear()
+Histogram::Clear(const nsACString& store)
 {
   MOZ_ASSERT(XRE_IsParentProcess(), "Only clear histograms in the parent process");
   if (!XRE_IsParentProcess()) {
     return;
   }
 
   if (mSingleStore != nullptr) {
-    mSingleStore->Clear();
+    if (store.EqualsASCII("main")) {
+      mSingleStore->Clear();
+    }
   } else {
     base::Histogram* h = nullptr;
-    bool found = GetHistogram(NS_LITERAL_CSTRING("main"), &h);
+    bool found = GetHistogram(store, &h);
     if (!found) {
       return;
     }
-    MOZ_ASSERT(h, "Should have found a valid histogram in the main store");
+    MOZ_ASSERT(h, "Should have found a valid histogram in the named store");
 
     h->Clear();
   }
 }
 
 bool
 Histogram::GetHistogram(const nsACString& store, base::Histogram** h)
 {
   MOZ_ASSERT(!IsExpired());
   if (IsExpired()) {
     return false;
   }
 
   if (mSingleStore != nullptr){
-    *h = mSingleStore;
-    return true;
+    if (store.EqualsASCII("main")) {
+      *h = mSingleStore;
+      return true;
+    }
+
+    return false;
   }
 
   return mStorage.Get(store, h);
 }
 
 size_t
 Histogram::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
 {
@@ -1614,17 +1620,17 @@ internal_AccumulateChildKeyed(const Stat
   }
 
   KeyedHistogram* keyed = internal_GetKeyedHistogramById(aId, aProcessType);
   MOZ_ASSERT(keyed);
   keyed->Add(aKey, aSample, aProcessType);
 }
 
 void
-internal_ClearHistogram(const StaticMutexAutoLock& aLock, HistogramID id)
+internal_ClearHistogram(const StaticMutexAutoLock& aLock, HistogramID id, const nsACString& aStore)
 {
   MOZ_ASSERT(XRE_IsParentProcess());
   if (!XRE_IsParentProcess()) {
     return;
   }
 
   // Handle keyed histograms.
   if (gHistogramInfos[id].keyed) {
@@ -1634,17 +1640,17 @@ internal_ClearHistogram(const StaticMute
         kh->Clear();
       }
     }
   } else {
     // Reset the histograms instances for all processes.
     for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count); ++process) {
       Histogram* h = internal_GetHistogramById(aLock, id, static_cast<ProcessID>(process), /* instantiate = */ false);
       if (h) {
-        h->Clear();
+        h->Clear(aStore);
       }
     }
   }
 }
 
 } // namespace
 
 
@@ -1842,45 +1848,88 @@ internal_JSHistogram_Add(JSContext *cx, 
     StaticMutexAutoLock locker(gTelemetryHistogramMutex);
     for (uint32_t aValue: values) {
       internal_Accumulate(locker, id, aValue);
     }
   }
   return true;
 }
 
+/**
+ * Extract the store name from JavaScript function arguments.
+ * The first and only argument needs to be an object with a "store" property.
+ * If no arguments are given it defaults to "main".
+ */
+nsresult
+internal_JS_StoreFromObjectArgument(JSContext *cx, const JS::CallArgs& args, nsAutoString& aStoreName)
+{
+  if (args.length() == 0) {
+    aStoreName.AssignLiteral("main");
+  } else if (args.length() == 1) {
+    if (!args[0].isObject()) {
+      JS_ReportErrorASCII(cx, "Expected object argument.");
+      return NS_ERROR_FAILURE;
+    }
+
+    JS::RootedValue storeValue(cx);
+    JS::RootedObject argsObject(cx, &args[0].toObject());
+    if (!JS_GetProperty(cx, argsObject, "store", &storeValue)) {
+      JS_ReportErrorASCII(cx, "Expected object argument to have property 'store'.");
+      return NS_ERROR_FAILURE;
+    }
+
+    nsAutoJSString store;
+    if (!storeValue.isString() || !store.init(cx, storeValue)) {
+      JS_ReportErrorASCII(cx, "Expected object argument's 'store' property to be a string.");
+      return NS_ERROR_FAILURE;
+    }
+
+    aStoreName.Assign(store);
+  } else {
+    JS_ReportErrorASCII(cx, "Expected at most one argument.");
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
 bool
 internal_JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
 
   if (!args.thisv().isObject() ||
       JS_GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
     JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
     return false;
   }
 
   JSObject* obj = &args.thisv().toObject();
   JSHistogramData* data = static_cast<JSHistogramData*>(JS_GetPrivate(obj));
   MOZ_ASSERT(data);
   HistogramID id = data->histogramId;
 
+  nsAutoString storeName;
+  nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
   HistogramSnapshotData dataSnapshot;
   {
     StaticMutexAutoLock locker(gTelemetryHistogramMutex);
     MOZ_ASSERT(internal_IsHistogramEnumId(id));
 
     // This is not good standard behavior given that we have histogram instances
     // covering multiple processes.
     // However, changing this requires some broader changes to callers.
     Histogram* w = internal_GetHistogramById(locker, id, ProcessID::Parent);
     base::Histogram *h = nullptr;
-    NS_NAMED_LITERAL_CSTRING(store, "main");
-    if (!w->GetHistogram(store, &h)) {
-      // When it's not in the 'main' store, let's skip the snapshot completely,
+    if (!w->GetHistogram(NS_ConvertUTF16toUTF8(storeName), &h)) {
+      // When it's not in the named store, let's skip the snapshot completely,
       // but don't fail
       args.rval().setUndefined();
       return true;
     }
     // Take a snapshot of the data here, protected by the lock, and then,
     // outside of the lock protection, mirror it to a JS structure
     if (NS_FAILED(internal_GetHistogramAndSamples(locker, h, dataSnapshot))) {
       return false;
@@ -1913,26 +1962,32 @@ internal_JSHistogram_Clear(JSContext *cx
     JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
     return false;
   }
 
   JSObject* obj = &args.thisv().toObject();
   JSHistogramData* data = static_cast<JSHistogramData*>(JS_GetPrivate(obj));
   MOZ_ASSERT(data);
 
+  nsAutoString storeName;
+  nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
   // This function should always return |undefined| and never fail but
   // rather report failures using the console.
   args.rval().setUndefined();
 
   HistogramID id = data->histogramId;
   {
     StaticMutexAutoLock locker(gTelemetryHistogramMutex);
 
     MOZ_ASSERT(internal_IsHistogramEnumId(id));
-    internal_ClearHistogram(locker, id);
+    internal_ClearHistogram(locker, id, NS_ConvertUTF16toUTF8(storeName));
   }
 
   return true;
 }
 
 // NOTE: Runs without protection from |gTelemetryHistogramMutex|.
 // See comment at the top of this section.
 nsresult
@@ -1943,18 +1998,18 @@ internal_WrapAndReturnHistogram(Histogra
   if (!obj) {
     return NS_ERROR_FAILURE;
   }
 
   // The 3 functions that are wrapped up here are eventually called
   // by the same thread that runs this function.
   if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0)
         && JS_DefineFunction(cx, obj, "snapshot",
-                             internal_JSHistogram_Snapshot, 0, 0)
-        && JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 0, 0))) {
+                             internal_JSHistogram_Snapshot, 1, 0)
+        && JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 1, 0))) {
     return NS_ERROR_FAILURE;
   }
 
   JSHistogramData* data = new JSHistogramData{id};
   JS_SetPrivate(obj, data);
   ret.setObject(*obj);
 
   return NS_OK;
@@ -2784,16 +2839,17 @@ TelemetryHistogram::GetHistogramName(His
   StaticMutexAutoLock locker(gTelemetryHistogramMutex);
   const HistogramInfo& h = gHistogramInfos[id];
   return h.name();
 }
 
 nsresult
 TelemetryHistogram::CreateHistogramSnapshots(JSContext* aCx,
                                              JS::MutableHandleValue aResult,
+                                             const nsACString& aStore,
                                              unsigned int aDataset,
                                              bool aClearSubsession,
                                              bool aFilterTest)
 {
   // Runs without protection from |gTelemetryHistogramMutex|
   JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
   if (!root_obj) {
     return NS_ERROR_FAILURE;
@@ -2803,16 +2859,17 @@ TelemetryHistogram::CreateHistogramSnaps
   // Include the GPU process in histogram snapshots only if we actually tried
   // to launch a process for it.
   bool includeGPUProcess = internal_AttemptedGPUProcess();
 
   HistogramProcessSnapshotsArray processHistArray;
   {
     StaticMutexAutoLock locker(gTelemetryHistogramMutex);
     nsresult rv = internal_GetHistogramsSnapshot(locker,
+                                                 aStore,
                                                  aDataset,
                                                  aClearSubsession,
                                                  includeGPUProcess,
                                                  aFilterTest,
                                                  processHistArray);
     if (NS_FAILED(rv)) {
       return rv;
     }
@@ -3140,16 +3197,17 @@ TelemetryHistogram::SerializeHistograms(
   // Take a snapshot of the histograms.
   HistogramProcessSnapshotsArray processHistArray;
   {
     StaticMutexAutoLock locker(gTelemetryHistogramMutex);
     // We always request the "opt-in"/"prerelease" dataset: we internally
     // record the right subset, so this will only return "prerelease" if
     // it was recorded.
     if (NS_FAILED(internal_GetHistogramsSnapshot(locker,
+                                                 NS_LITERAL_CSTRING("main"),
                                                  nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN,
                                                  false /* aClearSubsession */,
                                                  includeGPUProcess,
                                                  false /* aFilterTest */,
                                                  processHistArray))) {
       return NS_ERROR_FAILURE;
     }
   }
--- a/toolkit/components/telemetry/core/TelemetryHistogram.h
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.h
@@ -63,17 +63,19 @@ GetHistogramById(const nsACString &name,
 nsresult
 GetKeyedHistogramById(const nsACString &name, JSContext *cx,
                       JS::MutableHandle<JS::Value> ret);
 
 const char*
 GetHistogramName(mozilla::Telemetry::HistogramID id);
 
 nsresult
-CreateHistogramSnapshots(JSContext* aCx, JS::MutableHandleValue aResult, unsigned int aDataset,
+CreateHistogramSnapshots(JSContext* aCx, JS::MutableHandleValue aResult,
+                         const nsACString& aStore,
+                         unsigned int aDataset,
                          bool aClearSubsession, bool aFilterTest=false);
 
 nsresult
 GetKeyedHistogramSnapshots(JSContext *aCx, JS::MutableHandleValue aResult, unsigned int aDataset,
                            bool aClearSubsession, bool aFilterTest=false);
 
 size_t
 GetHistogramSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
--- a/toolkit/components/telemetry/core/nsITelemetry.idl
+++ b/toolkit/components/telemetry/core/nsITelemetry.idl
@@ -59,17 +59,17 @@ interface nsITelemetry : nsISupports
    * Each histogram is represented in a packed format and has the following properties:
    *   bucket_count - Number of buckets of this histogram
    *   histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, HISTOGRAM_BOOLEAN,
    *                    HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL
    *   sum - sum of the bucket contents
    *   range - A 2-item array of minimum and maximum bucket size
    *   values - Map from bucket to the bucket's count
    *
-   * @param aStoreName The name of the store to snapshot. Ignored at the moment.
+   * @param aStoreName The name of the store to snapshot. Something like "main".
    * @param aClearStore Whether to clear out the histograms in the named store after snapshotting.
    * @param aFilterTest If true, `TELEMETRY_TEST_` histograms will be filtered out.
                         Filtered histograms are still cleared if `aClearStore` is true.
    *                    Defaults to false.
    */
   [implicit_jscontext]
   jsval getSnapshotForHistograms(in ACString aStoreName, in boolean aClearStore, [optional] in boolean aFilterTest);
 
@@ -276,19 +276,23 @@ interface nsITelemetry : nsISupports
   [implicit_jscontext]
   readonly attribute jsval lateWrites;
 
   /**
    * Create and return a histogram registered in TelemetryHistograms.h.
    *
    * @param id - unique identifier from TelemetryHistograms.h
    * The returned object has the following functions:
-   *   add(int) - Adds an int value to the appropriate bucket
-   *   snapshot() - Returns a snapshot of the histogram with the same data fields as in histogramSnapshots()
-   *   clear() - Zeros out the histogram's buckets and sum. This is intended to be only used in tests.
+   *   add(int) - Adds an int value to the appropriate bucket.
+   *   snapshot([optional] {store}) - Returns a snapshot of the histogram with the same data fields
+                                      as in getSnapshotForHistograms().
+                                      Defaults to the "main" store.
+   *   clear([optional] {store}) - Zeros out the histogram's buckets and sum.
+                                   Defaults to the "main" store.
+                                   Note: This is intended to be only used in tests.
    */
   [implicit_jscontext]
   jsval getHistogramById(in ACString id);
 
   /**
    * Serializes the keyed histograms from the given dataset to a JSON-style object.
    * The returned structure looks like:
    *   { process1: {name1: {histogramData1}, name2:{histogramData2}...}}
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
@@ -1312,8 +1312,154 @@ add_task(async function test_multistore_
 
   // Should be empty after clearing
   snapshot = Telemetry.getSnapshotForKeyedHistograms("main", /* clear */ false).parent;
   id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
   Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
   id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
   Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
 });
+
+add_task(async function test_multistore_argument_handling() {
+  Telemetry.canRecordExtended = true;
+  // Clear histograms
+  Telemetry.getSnapshotForHistograms("main", true);
+  Telemetry.getSnapshotForHistograms("sync", true);
+
+  let id;
+  let hist;
+  let snapshot;
+
+  id = "TELEMETRY_TEST_MULTIPLE_STORES";
+  hist = Telemetry.getHistogramById(id);
+  hist.add(37);
+
+  // No argument
+  snapshot = hist.snapshot();
+  Assert.equal(37, snapshot.sum, `${id} should be in a default store snapshot`);
+
+  hist.clear();
+  snapshot = hist.snapshot();
+  Assert.equal(0, snapshot.sum, `${id} should be cleared in the default store`);
+
+  snapshot = hist.snapshot({store: "sync"});
+  Assert.equal(37, snapshot.sum, `${id} should not have been cleared in the sync store`);
+
+  Assert.throws(() => hist.snapshot(2, "or", "more", "arguments"),
+    /one argument/, "snapshot should check argument count");
+  Assert.throws(() => hist.snapshot(2),
+    /object argument/, "snapshot should check argument type");
+  Assert.throws(() => hist.snapshot({}),
+    /property/, "snapshot should check for object property");
+  Assert.throws(() => hist.snapshot({store: 1}),
+    /string/, "snapshot should check object property's type");
+
+  Assert.throws(() => hist.clear(2, "or", "more", "arguments"),
+    /one argument/, "clear should check argument count");
+  Assert.throws(() => hist.clear(2),
+    /object argument/, "clear should check argument type");
+  Assert.throws(() => hist.clear({}),
+    /property/, "clear should check for object property");
+  Assert.throws(() => hist.clear({store: 1}),
+    /string/, "clear should check object property's type");
+});
+
+add_task(async function test_multistore_sync_snapshot() {
+  Telemetry.canRecordExtended = true;
+  // Clear histograms
+  Telemetry.getSnapshotForHistograms("main", true);
+  Telemetry.getSnapshotForHistograms("sync", true);
+
+  let id;
+  let hist;
+  let snapshot;
+
+  // Plain histograms
+
+  // Fill with data
+  id = "TELEMETRY_TEST_MAIN_ONLY";
+  hist = Telemetry.getHistogramById(id);
+  hist.add(1);
+
+  id = "TELEMETRY_TEST_MULTIPLE_STORES";
+  hist = Telemetry.getHistogramById(id);
+  hist.add(1);
+
+  id = "TELEMETRY_TEST_SYNC_ONLY";
+  hist = Telemetry.getHistogramById(id);
+  hist.add(1);
+
+  // Getting snapshot and clearing
+  snapshot = Telemetry.getSnapshotForHistograms("main", /* clear */ true).parent;
+  id = "TELEMETRY_TEST_MAIN_ONLY";
+  Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+  id = "TELEMETRY_TEST_MULTIPLE_STORES";
+  Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+  id = "TELEMETRY_TEST_SYNC_ONLY";
+  Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+  snapshot = Telemetry.getSnapshotForHistograms("sync", /* clear */ true).parent;
+  id = "TELEMETRY_TEST_MAIN_ONLY";
+  Assert.ok(!(id in snapshot), `${id} should not be in a sync store snapshot`);
+  id = "TELEMETRY_TEST_MULTIPLE_STORES";
+  Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+  id = "TELEMETRY_TEST_SYNC_ONLY";
+  Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+});
+
+add_task(async function test_multistore_plain_individual_snapshot() {
+  Telemetry.canRecordExtended = true;
+  // Clear histograms
+  Telemetry.getSnapshotForHistograms("main", true);
+  Telemetry.getSnapshotForHistograms("sync", true);
+
+  let id;
+  let hist;
+
+  id = "TELEMETRY_TEST_MAIN_ONLY";
+  hist = Telemetry.getHistogramById(id);
+
+  hist.add(37);
+  Assert.deepEqual(37, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(undefined, hist.snapshot({store: "sync"}));
+
+  hist.clear({store: "main"});
+  Assert.deepEqual(0, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(undefined, hist.snapshot({store: "sync"}));
+
+  id = "TELEMETRY_TEST_MULTIPLE_STORES";
+  hist = Telemetry.getHistogramById(id);
+
+  hist.add(37);
+  Assert.deepEqual(37, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(37, hist.snapshot({store: "sync"}).sum);
+
+  hist.clear({store: "main"});
+  Assert.deepEqual(0, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(37, hist.snapshot({store: "sync"}).sum);
+
+  hist.add(3);
+  Assert.deepEqual(3, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(40, hist.snapshot({store: "sync"}).sum);
+
+  hist.clear({store: "sync"});
+  Assert.deepEqual(3, hist.snapshot({store: "main"}).sum);
+  Assert.deepEqual(0, hist.snapshot({store: "sync"}).sum);
+
+  id = "TELEMETRY_TEST_SYNC_ONLY";
+  hist = Telemetry.getHistogramById(id);
+
+  hist.add(37);
+  Assert.deepEqual(undefined, hist.snapshot({store: "main"}));
+  Assert.deepEqual(37, hist.snapshot({store: "sync"}).sum);
+
+  hist.clear({store: "main"});
+  Assert.deepEqual(undefined, hist.snapshot({store: "main"}));
+  Assert.deepEqual(37, hist.snapshot({store: "sync"}).sum);
+
+  hist.add(3);
+  Assert.deepEqual(undefined, hist.snapshot({store: "main"}));
+  Assert.deepEqual(40, hist.snapshot({store: "sync"}).sum);
+
+  hist.clear({store: "sync"});
+  Assert.deepEqual(undefined, hist.snapshot({store: "main"}));
+  Assert.deepEqual(0, hist.snapshot({store: "sync"}).sum);
+});