Bug 1673648 - Implement C++ and JS API for Memory Distribution r=janerik
authorChris H-C <chutten@mozilla.com>
Mon, 14 Dec 2020 17:17:45 +0000
changeset 560675 cb2974d10cd2e3cdb2d872217264db9b36d67e2a
parent 560674 0c03e3ad5e56201e35437734b0ae1d34faa52005
child 560676 ab3cb32e5be762943ce7b9ca8292cf1c8487ee09
push id132750
push userchutten@mozilla.com
push dateMon, 14 Dec 2020 19:47:51 +0000
treeherderautoland@ab3cb32e5be7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanerik
bugs1673648
milestone85.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 1673648 - Implement C++ and JS API for Memory Distribution r=janerik ...and do it atop the RLB impl. Differential Revision: https://phabricator.services.mozilla.com/D99497
toolkit/components/glean/api/src/ffi/mod.rs
toolkit/components/glean/api/src/private/memory_distribution.rs
toolkit/components/glean/bindings/MetricTypes.h
toolkit/components/glean/bindings/private/MemoryDistribution.cpp
toolkit/components/glean/bindings/private/MemoryDistribution.h
toolkit/components/glean/build_scripts/glean_parser_ext/util.py
toolkit/components/glean/docs/new_metric_types.md
toolkit/components/glean/gtest/TestFog.cpp
toolkit/components/glean/moz.build
toolkit/components/glean/pytest/metrics_test_output_cpp
toolkit/components/glean/pytest/metrics_test_output_js
toolkit/components/glean/test_metrics.yaml
toolkit/components/glean/xpcom/nsIGleanMetrics.idl
toolkit/components/glean/xpcshell/test_Glean.js
--- a/toolkit/components/glean/api/src/ffi/mod.rs
+++ b/toolkit/components/glean/api/src/ffi/mod.rs
@@ -1,14 +1,15 @@
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
 
 #![cfg(feature = "with_gecko")]
 
+use thin_vec::ThinVec;
 use {nsstring::nsACString, uuid::Uuid};
 
 #[macro_use]
 mod macros;
 mod event;
 
 #[no_mangle]
 pub unsafe extern "C" fn fog_counter_add(id: u32, amount: i32) {
@@ -147,8 +148,38 @@ pub extern "C" fn fog_datetime_set(
     minute: u32,
     second: u32,
     nano: u32,
     offset_seconds: i32,
 ) {
     let metric = metric_get!(DATETIME_MAP, id);
     metric.set_with_details(year, month, day, hour, minute, second, nano, offset_seconds);
 }
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_test_has_value(
+    id: u32,
+    storage_name: &nsACString,
+) -> bool {
+    test_has!(MEMORY_DISTRIBUTION_MAP, id, storage_name)
+}
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_test_get_value(
+    id: u32,
+    storage_name: &nsACString,
+    sum: &mut u64,
+    buckets: &mut ThinVec<u64>,
+    counts: &mut ThinVec<u64>,
+) {
+    let val = test_get!(MEMORY_DISTRIBUTION_MAP, id, storage_name);
+    *sum = val.sum;
+    for (&bucket, &count) in val.values.iter() {
+        buckets.push(bucket);
+        counts.push(count)
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) {
+    let metric = metric_get!(MEMORY_DISTRIBUTION_MAP, id);
+    metric.accumulate(sample);
+}
--- a/toolkit/components/glean/api/src/private/memory_distribution.rs
+++ b/toolkit/components/glean/api/src/private/memory_distribution.rs
@@ -1,79 +1,78 @@
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
 
-use std::sync::Arc;
+use inherent::inherent;
 
 use super::{CommonMetricData, DistributionData, MemoryUnit, MetricId};
 
-use crate::dispatcher;
+use glean_core::traits::MemoryDistribution;
+
 use crate::ipc::{need_ipc, with_ipc_payload};
 
 /// A memory distribution metric.
 ///
 /// Memory distributions are used to accumulate and store memory measurements for analyzing distributions of the memory data.
-#[derive(Debug)]
+#[derive(Clone)]
 pub enum MemoryDistributionMetric {
     Parent {
         /// The metric's ID.
         ///
         /// **TEST-ONLY** - Do not use unless gated with `#[cfg(test)]`.
         id: MetricId,
-        inner: Arc<MemoryDistributionMetricImpl>,
+        inner: glean::private::MemoryDistributionMetric,
     },
     Child(MemoryDistributionMetricIpc),
 }
-#[derive(Debug)]
-pub struct MemoryDistributionMetricImpl {
-    inner: glean_core::metrics::MemoryDistributionMetric,
-}
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct MemoryDistributionMetricIpc(MetricId);
 
 impl MemoryDistributionMetric {
     /// Create a new memory distribution metric.
     pub fn new(id: MetricId, meta: CommonMetricData, memory_unit: MemoryUnit) -> Self {
         if need_ipc() {
             MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(id))
         } else {
-            let inner = Arc::new(MemoryDistributionMetricImpl::new(meta, memory_unit));
+            let inner = glean::private::MemoryDistributionMetric::new(meta, memory_unit);
             MemoryDistributionMetric::Parent { id, inner }
         }
     }
 
     #[cfg(test)]
     pub(crate) fn child_metric(&self) -> Self {
         match self {
             MemoryDistributionMetric::Parent { id, .. } => {
                 MemoryDistributionMetric::Child(MemoryDistributionMetricIpc(*id))
             }
             MemoryDistributionMetric::Child(_) => {
                 panic!("Can't get a child metric from a child metric")
             }
         }
     }
+}
 
+#[inherent(pub)]
+impl MemoryDistribution for MemoryDistributionMetric {
     /// Accumulates the provided sample in the metric.
     ///
     /// ## Arguments
     ///
     /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the
     ///   configured memory unit of the metric.
     ///
     /// ## Notes
     ///
     /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
     /// and an `ErrorType::InvalidValue` error is recorded.
-    pub fn accumulate(&self, sample: u64) {
+    fn accumulate(&self, sample: u64) {
         match self {
             MemoryDistributionMetric::Parent { inner, .. } => {
-                let metric = Arc::clone(&inner);
-                dispatcher::launch(move || metric.accumulate(sample));
+                MemoryDistribution::accumulate(&*inner, sample);
             }
             MemoryDistributionMetric::Child(c) => {
                 with_ipc_payload(move |payload| {
                     if let Some(v) = payload.memory_samples.get_mut(&c.0) {
                         v.push(sample);
                     } else {
                         payload.memory_samples.insert(c.0, vec![sample]);
                     }
@@ -84,57 +83,69 @@ impl MemoryDistributionMetric {
 
     /// **Test-only API.**
     ///
     /// Get the currently-stored histogram as a DistributionData of the serialized value.
     /// This doesn't clear the stored value.
     ///
     /// ## Arguments
     ///
-    /// * `storage_name` - the storage name to look into.
+    /// * `ping_name` - the storage name to look into.
     ///
     /// ## Return value
     ///
     /// Returns the stored value or `None` if nothing stored.
-    pub fn test_get_value(&self, storage_name: &str) -> Option<DistributionData> {
+    fn test_get_value<'a, S: Into<Option<&'a str>>>(
+        &self,
+        ping_name: S,
+    ) -> Option<DistributionData> {
         match self {
-            MemoryDistributionMetric::Parent { inner, .. } => {
-                dispatcher::block_on_queue();
-                inner.test_get_value(storage_name)
+            MemoryDistributionMetric::Parent { inner, .. } => inner.test_get_value(ping_name),
+            MemoryDistributionMetric::Child(c) => {
+                panic!("Cannot get test value for {:?} in non-parent process!", c.0)
             }
-            MemoryDistributionMetric::Child(_c) => panic!(
-                "Cannot get test value for {:?} in non-parent process!",
-                self
-            ),
         }
     }
-}
 
-impl MemoryDistributionMetricImpl {
-    pub fn new(meta: CommonMetricData, memory_unit: MemoryUnit) -> Self {
-        let inner = glean_core::metrics::MemoryDistributionMetric::new(meta, memory_unit);
-        Self { inner }
-    }
-
-    pub fn accumulate(&self, sample: u64) {
-        crate::with_glean(|glean| self.inner.accumulate(glean, sample))
-    }
-
-    pub fn test_get_value(&self, storage_name: &str) -> Option<DistributionData> {
-        crate::with_glean(move |glean| self.inner.test_get_value(glean, storage_name))
+    /// **Exported for test purposes.**
+    ///
+    /// Gets the number of recorded errors for the given error type.
+    ///
+    /// # Arguments
+    ///
+    /// * `error` - The type of error
+    /// * `ping_name` - represents the optional name of the ping to retrieve the
+    ///   metric for. Defaults to the first value in `send_in_pings`.
+    ///
+    /// # Returns
+    ///
+    /// The number of errors recorded.
+    fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>(
+        &self,
+        error: glean::ErrorType,
+        ping_name: S,
+    ) -> i32 {
+        match self {
+            MemoryDistributionMetric::Parent { inner, .. } => {
+                inner.test_get_num_recorded_errors(error, ping_name)
+            }
+            MemoryDistributionMetric::Child(c) => panic!(
+                "Cannot get the number of recorded errors for {:?} in non-parent process!",
+                c.0
+            ),
+        }
     }
 }
 
 #[cfg(test)]
 mod test {
     use super::*;
     use crate::{common_test::*, ipc, metrics};
 
     #[test]
-    #[ignore] // TODO: Enable them back when bug 1677451 lands.
     fn smoke_test_memory_distribution() {
         let _lock = lock_test();
 
         let metric = MemoryDistributionMetric::new(
             0.into(),
             CommonMetricData {
                 name: "memory_distribution_metric".into(),
                 category: "telemetry".into(),
@@ -149,17 +160,16 @@ mod test {
 
         let metric_data = metric.test_get_value("store1").unwrap();
         assert_eq!(1, metric_data.values[&42494]);
         assert_eq!(0, metric_data.values[&44376]);
         assert_eq!(43008, metric_data.sum);
     }
 
     #[test]
-    #[ignore] // TODO: Enable them back when bug 1677451 lands.
     fn memory_distribution_child() {
         let _lock = lock_test();
 
         let parent_metric = &metrics::test_only_ipc::a_memory_dist;
         parent_metric.accumulate(42);
 
         {
             let child_metric = parent_metric.child_metric();
--- a/toolkit/components/glean/bindings/MetricTypes.h
+++ b/toolkit/components/glean/bindings/MetricTypes.h
@@ -4,13 +4,14 @@
 
 #ifndef mozilla_Glean_MetricTypes_h
 #define mozilla_Glean_MetricTypes_h
 
 #include "mozilla/glean/Boolean.h"
 #include "mozilla/glean/Counter.h"
 #include "mozilla/glean/Datetime.h"
 #include "mozilla/glean/Event.h"
+#include "mozilla/glean/MemoryDistribution.h"
 #include "mozilla/glean/Timespan.h"
 #include "mozilla/glean/String.h"
 #include "mozilla/glean/Uuid.h"
 
 #endif  // mozilla_Glean_MetricTypes_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/MemoryDistribution.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/glean/MemoryDistribution.h"
+
+#include "mozilla/Components.h"
+#include "nsIClassInfoImpl.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CLASSINFO(GleanMemoryDistribution, nullptr, 0, {0})
+NS_IMPL_ISUPPORTS_CI(GleanMemoryDistribution, nsIGleanMemoryDistribution)
+
+NS_IMETHODIMP
+GleanMemoryDistribution::Accumulate(uint64_t aSample) {
+  mMemoryDist.Accumulate(aSample);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+GleanMemoryDistribution::TestGetValue(const nsACString& aPingName,
+                                      JSContext* aCx,
+                                      JS::MutableHandleValue aResult) {
+  auto result = mMemoryDist.TestGetValue(aPingName);
+  if (result.isNothing()) {
+    aResult.set(JS::UndefinedValue());
+  } else {
+    // Build return value of the form: { sum: #, values: {bucket1: count1, ...}
+    JS::RootedObject root(aCx, JS_NewPlainObject(aCx));
+    if (!root) {
+      return NS_ERROR_FAILURE;
+    }
+    uint64_t sum = result.ref().sum;
+    if (!JS_DefineProperty(aCx, root, "sum", static_cast<double>(sum),
+                           JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+    JS::RootedObject valuesObj(aCx, JS_NewPlainObject(aCx));
+    if (!valuesObj ||
+        !JS_DefineProperty(aCx, root, "values", valuesObj, JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+    auto& data = result.ref().values;
+    for (auto iter = data.ConstIter(); !iter.Done(); iter.Next()) {
+      const uint64_t bucket = iter.Key();
+      const uint64_t count = iter.UserData();
+      if (!JS_DefineProperty(aCx, valuesObj,
+                             nsPrintfCString("%lu", bucket).get(),
+                             static_cast<double>(count), JSPROP_ENUMERATE)) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+    aResult.setObject(*root);
+  }
+  return NS_OK;
+}
+
+}  // namespace mozilla::glean
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/MemoryDistribution.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_glean_GleanMemoryDistribution_h
+#define mozilla_glean_GleanMemoryDistribution_h
+
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/Maybe.h"
+#include "nsDataHashtable.h"
+#include "nsIGleanMetrics.h"
+#include "nsTArray.h"
+
+namespace mozilla::glean {
+
+struct DistributionData final {
+  uint64_t sum;
+  nsDataHashtable<nsUint64HashKey, uint64_t> values;
+};
+
+namespace impl {
+
+class MemoryDistributionMetric {
+ public:
+  constexpr explicit MemoryDistributionMetric(uint32_t aId) : mId(aId) {}
+
+  /*
+   * Accumulates the provided sample in the metric.
+   *
+   * @param aSample The sample to be recorded by the metric. The sample is
+   *                assumed to be in the confgured memory unit of the metric.
+   *
+   * Notes: Values bigger than 1 Terabyte (2^40 bytes) are truncated and an
+   * InvalidValue error is recorded.
+   */
+  void Accumulate(uint64_t aSample) const {
+    fog_memory_distribution_accumulate(mId, aSample);
+  }
+
+  /**
+   * **Test-only API**
+   *
+   * Gets the currently stored value as a DistributionData.
+   *
+   * This function will attempt to await the last parent-process task (if any)
+   * writing to the the metric's storage engine before returning a value.
+   * This function will not wait for data from child processes.
+   *
+   * This doesn't clear the stored value.
+   * Parent process only. Panics in child processes.
+   *
+   * @return value of the stored metric, or Nothing() if there is no value.
+   */
+  Maybe<DistributionData> TestGetValue(const nsACString& aPingName) const {
+    if (!fog_memory_distribution_test_has_value(mId, &aPingName)) {
+      return Nothing();
+    }
+    nsTArray<uint64_t> buckets;
+    nsTArray<uint64_t> counts;
+    DistributionData ret;
+    fog_memory_distribution_test_get_value(mId, &aPingName, &ret.sum, &buckets,
+                                           &counts);
+    for (size_t i = 0; i < buckets.Length(); ++i) {
+      ret.values.Put(buckets[i], counts[i]);
+    }
+    return Some(std::move(ret));
+  }
+
+ private:
+  const uint32_t mId;
+};
+}  // namespace impl
+
+class GleanMemoryDistribution final : public nsIGleanMemoryDistribution {
+ public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIGLEANMEMORYDISTRIBUTION
+
+  explicit GleanMemoryDistribution(uint64_t aId) : mMemoryDist(aId){};
+
+ private:
+  virtual ~GleanMemoryDistribution() = default;
+
+  const impl::MemoryDistributionMetric mMemoryDist;
+};
+
+}  // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanMemoryDistribution_h */
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
@@ -29,16 +29,17 @@ def generate_metric_ids(objs):
     return lambda metric: metric_id_mapping[(metric.category, metric.name)]
 
 
 IMPLEMENTED_CPP_TYPES = [
     "boolean",
     "counter",
     "datetime",
     "event",
+    "memory_distribution",
     "string",
     "timespan",
     "uuid",
 ]
 
 
 def is_implemented_metric_type(typ):
     """
--- a/toolkit/components/glean/docs/new_metric_types.md
+++ b/toolkit/components/glean/docs/new_metric_types.md
@@ -87,39 +87,42 @@ Each metric type has six pieces you'll n
 - Using our convenient macros, define the Multi-Language Architecture's FFI layer above the Rust API in [`api/src/ffi/mod.rs`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/api/src/ffi/mod.rs).
 
 ### 2. C++ Impl
 
 - Implement a type called `XMetric` (e.g. `CounterMetric`) in `mozilla::glean::impl` in [`bindings/private/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/).
     - Its methods should be named the same as the ones in the Rust API, transformed to `CamelCase`.
     - They should all be public.
     - Multiplex the FFI's `test_have` and `test_get` functions into a single `TestGetValue` function that returns a `mozilla::Maybe` wrapping the C++ type that best fits the metric type.
-- Include the new metric type in [`bindings/MetricTypes.h`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h)
-- Include the new files in [`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h). The header file should be added to `EXPORTS.mozilla.glean` and the `.cpp` file should be added to `UNIFIED_SOURCES`.
+- Include the new metric type in
+  [`bindings/MetricTypes.h`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h)
+  and
+  [`build_scripts/glean_parser_ext/util.py`'s `IMPLEMENTED_CPP_TYPES`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/build_scripts/glean_parser_ext/util.py).
+- Include the new files in [`moz.build`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/MetricTypes.h). The header file should be added to `EXPORTS.mozilla.glean.bindings` and the `.cpp` file should be added to `UNIFIED_SOURCES`.
 
 ### 3. IDL
 
 - Duplicate the public API (including its docs) to [`xpcom/nsIGleanMetrics.idl`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/xpcom/nsIGleanMetrics.idl) with the name `nsIGleanX` (e.g. `nsIGleanCounter`).
     - Inherit from `nsISupports`.
     - The naming style for members here is `lowerCamelCase`. You'll need a [GUID](https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Generating_GUIDs) because this is XPCOM, but you'll only need the canonical form since we're only exposing to JS.
     - The `testGetValue` method will return a `jsval`.
 
 ### 4. JS Impl
 
 - Add an `nsIGleanX`-deriving, `XMetric`-owning type called `GleanX` (e.g. `GleanCounter`) in the same header and `.cpp` as `XMetric` in [`bindings/private/`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/bindings/private/).
     - Don't declare any methods beyond a ctor (takes a `uint32_t` metric id, init-constructs a `impl::XMetric` member) and dtor (`default`): the IDL will do the rest so long as you remember to add `NS_DECL_ISUPPORTS` and `NS_DECL_NSIGLEANX`.
     - In the definition of `GleanX`, member identifiers are back to `CamelCase` and need macros like `NS_IMETHODIMP`. Delegate operations to the owned `XMetric`, returning `NS_OK` no matter what in non-test methods.
     - Test-only methods can return `NS_ERROR` codes on failures, but mostly return `NS_OK` and use `undefined` in the `JS::MutableHandleValue` result to signal no value.
 
-### 5. Tests
+### 6. Tests
 
 Two languages means two test suites.
 
 - Add a never-expiring test-only metric of your type to [`test_metrics.yaml`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/test_metrics.yaml).
     - Feel free to be clever with the name, but be sure to make clear that it is test-only.
 - **C++ Tests (GTest)** - Add a small test case to [`gtest/TestFog.cpp`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/gtest/TestFog.cpp).
     - For more details, peruse the [testing docs](testing.md).
 - **JS Tests (xpcshell)** - Add a small test case to [`xpcshell/test_Glean.js`](https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/glean/xpcshell/test_Glean.js).
     - For more details, peruse the [testing docs](testing.md).
 
-### 6. API Documentation
+### 7. API Documentation
 
 TODO
--- a/toolkit/components/glean/gtest/TestFog.cpp
+++ b/toolkit/components/glean/gtest/TestFog.cpp
@@ -132,8 +132,27 @@ TEST(FOG, TestCppEventWorks)
   // Ugh, this API...
   nsTArray<Tuple<test_only_ipc::AnEventKeys, nsCString>> extra;
   nsCString val = "can set extras"_ns;
   extra.AppendElement(MakeTuple(AnEventKeys::Extra1, val));
 
   test_only_ipc::an_event.Record(std::move(extra));
   ASSERT_TRUE(test_only_ipc::an_event.TestGetValue("store1"_ns).isSome());
 }
+
+TEST(FOG, TestCppMemoryDistWorks)
+{
+  test_only::do_you_remember.Accumulate(7);
+  test_only::do_you_remember.Accumulate(17);
+
+  DistributionData data =
+      test_only::do_you_remember.TestGetValue("test-ping"_ns).ref();
+  // Sum is in bytes, test_only::do_you_remember is in megabytes. So
+  // multiplication ahoy!
+  ASSERT_EQ(data.sum, 24UL * 1024 * 1024);
+  for (auto iter = data.values.Iter(); !iter.Done(); iter.Next()) {
+    const uint64_t bucket = iter.Key();
+    const uint64_t count = iter.UserData();
+    ASSERT_TRUE(count == 0 ||
+                (count == 1 && (bucket == 17520006 || bucket == 7053950)))
+    << "Only two occupied buckets";
+  }
+}
--- a/toolkit/components/glean/moz.build
+++ b/toolkit/components/glean/moz.build
@@ -27,16 +27,17 @@ if CONFIG["MOZ_GLEAN"]:
         "!GleanMetrics.h",
         "bindings/Category.h",
         "bindings/Glean.h",
         "bindings/MetricTypes.h",
         "bindings/private/Boolean.h",
         "bindings/private/Counter.h",
         "bindings/private/Datetime.h",
         "bindings/private/Event.h",
+        "bindings/private/MemoryDistribution.h",
         "bindings/private/String.h",
         "bindings/private/Timespan.h",
         "bindings/private/Uuid.h",
     ]
 
     if CONFIG["COMPILE_ENVIRONMENT"]:
         EXPORTS.mozilla.glean += [
             "!fog_ffi_generated.h",
@@ -47,16 +48,17 @@ if CONFIG["MOZ_GLEAN"]:
     UNIFIED_SOURCES += [
         "bindings/Category.cpp",
         "bindings/Glean.cpp",
         "bindings/private/Boolean.cpp",
         "bindings/private/Common.cpp",
         "bindings/private/Counter.cpp",
         "bindings/private/Datetime.cpp",
         "bindings/private/Event.cpp",
+        "bindings/private/MemoryDistribution.cpp",
         "bindings/private/String.cpp",
         "bindings/private/Timespan.cpp",
         "bindings/private/Uuid.cpp",
         "ipc/FOGIPC.cpp",
     ]
 
     TEST_DIRS += [
         "gtest",
--- a/toolkit/components/glean/pytest/metrics_test_output_cpp
+++ b/toolkit/components/glean/pytest/metrics_test_output_cpp
@@ -48,16 +48,25 @@ namespace test {
      * generated from test.timespan_metric
      */
     /**
      * A multi-line
      * description
      */
     constexpr impl::TimespanMetric timespan_metric(11);
 
+    /**
+     * generated from test.memory_distribution_metric
+     */
+    /**
+     * A multi-line
+     * description
+     */
+    constexpr impl::MemoryDistributionMetric memory_distribution_metric(13);
+
 }
 namespace test_nested {
     /**
      * generated from test.nested.uuid_metric
      */
     /**
      * A multi-line
      * description
--- a/toolkit/components/glean/pytest/metrics_test_output_js
+++ b/toolkit/components/glean/pytest/metrics_test_output_js
@@ -48,16 +48,20 @@ static already_AddRefed<nsISupports> New
     case 5: /* String */
     {
       return MakeAndAddRef<GleanString>(metricId);
     }
     case 8: /* Timespan */
     {
       return MakeAndAddRef<GleanTimespan>(metricId);
     }
+    case 10: /* MemoryDistribution */
+    {
+      return MakeAndAddRef<GleanMemoryDistribution>(metricId);
+    }
     case 11: /* Uuid */
     {
       return MakeAndAddRef<GleanUuid>(metricId);
     }
     case 12: /* Datetime */
     {
       return MakeAndAddRef<GleanDatetime>(metricId);
     }
--- a/toolkit/components/glean/test_metrics.yaml
+++ b/toolkit/components/glean/test_metrics.yaml
@@ -106,16 +106,34 @@ test_only:
     data_sensitivity:
       - technical
     notification_emails:
       - glean-team@mozilla.com
     expires: never
     send_in_pings:
       - test-ping
 
+  do_you_remember:
+    type: memory_distribution
+    memory_unit: megabyte
+    description: |
+      They say it's the second thing to go.
+      This is a test-only metric.
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1673648#c1
+    data_sensitivity:
+      - technical
+    expires: never
+    notification_emails:
+      - glean-team@mozilla.com
+    send_in_pings:
+      - test-ping
+
 test_only.ipc:
   a_counter:
     type: counter
     description: |
       This is a test-only metric.
       Just counting things.
     bugs:
       - https://bugzilla.mozilla.org/show_bug.cgi?id=1646165
--- a/toolkit/components/glean/xpcom/nsIGleanMetrics.idl
+++ b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl
@@ -83,16 +83,48 @@ interface nsIGleanCounter : nsISupports
    * This doesn't clear the stored value.
    * Parent process only. Panics in child processes.
    *
    * @return value of the stored metric, or undefined if there is no value.
    */
   jsval testGetValue(in AUTF8String aStorageName);
 };
 
+[scriptable, uuid(eea5ed46-16ba-46cd-bb1f-504581987fe1)]
+interface nsIGleanMemoryDistribution : nsISupports
+{
+  /*
+   * Accumulates the provided sample in the metric.
+   *
+   * @param aSample The sample to be recorded by the metric. The sample is
+   *                assumed to be in the confgured memory unit of the metric.
+   *
+   * Notes: Values bigger than 1 Terabyte (2^40 bytes) are truncated and an
+   * InvalidValue error is recorded.
+   */
+	void accumulate(in uint64_t aSample);
+
+  /**
+   * **Test-only API**
+   *
+   * Gets the currently stored value as a DistributionData.
+   *
+   * This function will attempt to await the last parent-process task (if any)
+   * writing to the the metric's storage engine before returning a value.
+   * This function will not wait for data from child processes.
+   *
+   * This doesn't clear the stored value.
+   * Parent process only. Panics in child processes.
+   *
+   * @return value of the stored metric, or Nothing() if there is no value.
+   */
+  [implicit_jscontext]
+	jsval testGetValue(in ACString aStorageName);
+};
+
 [scriptable, uuid(d84a3555-46f1-48c1-9122-e8e88b069d2b)]
 interface nsIGleanString : nsISupports
 {
   /*
   * Set to the specified value.
   *
   * @param value The string to set the metric to.
   */
--- a/toolkit/components/glean/xpcshell/test_Glean.js
+++ b/toolkit/components/glean/xpcshell/test_Glean.js
@@ -142,8 +142,23 @@ add_task(async function test_fog_event_w
   // FIXME(bug 1678567): Check that the value was recorded when we can.
   // Assert.ok(Glean.test_only_ipc.no_extra_event.testGetValue("store1"));
 
   let extra = { extra1: "can set extras", extra2: "passing more data" };
   Glean.test_only_ipc.an_event.record(extra);
   // FIXME(bug 1678567): Check that the value was recorded when we can.
   // Assert.ok(Glean.test_only_ipc.an_event.testGetValue("store1"));
 });
+
+add_task(async function test_fog_memory_distribution_works() {
+  Glean.test_only.do_you_remember.accumulate(7);
+  Glean.test_only.do_you_remember.accumulate(17);
+
+  let data = Glean.test_only.do_you_remember.testGetValue("test-ping");
+  // `data.sum` is in bytes, but the metric is in MB.
+  Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
+  for (let [bucket, count] of Object.entries(data.values)) {
+    Assert.ok(
+      count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
+      "Only two buckets have a sample"
+    );
+  }
+});