diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 27f866fb21a8..519582a67b1f 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -1472,6 +1472,10 @@ DOMInterfaces = { 'nativeType': 'mozilla::glean::GleanPings', 'headerFile': 'mozilla/glean/bindings/GleanPings.h', }, +'GleanLabeled': { + 'nativeType': 'mozilla::glean::GleanLabeled', + 'headerFile': 'mozilla/glean/bindings/Labeled.h', +}, # WebRTC diff --git a/dom/chrome-webidl/Glean.webidl b/dom/chrome-webidl/Glean.webidl index a7c2d194c1f4..f8f55e241824 100644 --- a/dom/chrome-webidl/Glean.webidl +++ b/dom/chrome-webidl/Glean.webidl @@ -26,3 +26,20 @@ interface GleanImpl { */ getter GleanCategory (DOMString identifier); }; + +[ChromeOnly, Exposed=Window] +interface GleanLabeled { + /** + * Get a specific metric for a given label. + * + * If a set of acceptable labels were specified in the `metrics.yaml` file, + * and the given label is not in the set, it will be recorded under the + * special `OTHER_LABEL` label. + * + * If a set of acceptable labels was not specified in the `metrics.yaml` file, + * only the first 16 unique labels will be used. + * After that, any additional labels will be recorded under the special + * `OTHER_LABEL` label. + */ + getter nsISupports (DOMString identifier); +}; diff --git a/toolkit/components/glean/bindings/MetricTypes.h b/toolkit/components/glean/bindings/MetricTypes.h index a6de2e5f7e1a..c58d27fe25a4 100644 --- a/toolkit/components/glean/bindings/MetricTypes.h +++ b/toolkit/components/glean/bindings/MetricTypes.h @@ -9,6 +9,7 @@ #include "mozilla/glean/bindings/Counter.h" #include "mozilla/glean/bindings/Datetime.h" #include "mozilla/glean/bindings/Event.h" +#include "mozilla/glean/bindings/Labeled.h" #include "mozilla/glean/bindings/MemoryDistribution.h" #include "mozilla/glean/bindings/String.h" #include "mozilla/glean/bindings/StringList.h" diff --git a/toolkit/components/glean/bindings/private/Labeled.cpp b/toolkit/components/glean/bindings/private/Labeled.cpp new file mode 100644 index 000000000000..19e96fa5837a --- /dev/null +++ b/toolkit/components/glean/bindings/private/Labeled.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/bindings/Labeled.h" + +#include "mozilla/dom/GleanBinding.h" +#include "mozilla/glean/fog_ffi_generated.h" +#include "mozilla/glean/bindings/GleanJSMetricsLookup.h" +#include "mozilla/glean/bindings/MetricTypes.h" +#include "nsString.h" + +namespace mozilla::glean { + +namespace impl { +template <> +BooleanMetric Labeled::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_boolean_get(mId, &aLabel); + return BooleanMetric(submetricId); +} + +template <> +CounterMetric Labeled::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_counter_get(mId, &aLabel); + return CounterMetric(submetricId); +} + +template <> +StringMetric Labeled::Get(const nsACString& aLabel) const { + auto submetricId = fog_labeled_string_get(mId, &aLabel); + return StringMetric(submetricId); +} +} // namespace impl + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GleanLabeled) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanLabeled) +NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanLabeled) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanLabeled) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* GleanLabeled::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::GleanLabeled_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed GleanLabeled::NamedGetter(const nsAString& aName, + bool& aFound) { + auto label = NS_ConvertUTF16toUTF8(aName); + aFound = true; + return NewSubMetricFromIds(mTypeId, mId, label); +} + +bool GleanLabeled::NameIsEnumerable(const nsAString& aName) { return false; } + +void GleanLabeled::GetSupportedNames(nsTArray& aNames) { + // We really don't know, so don't do anything. +} + +} // namespace mozilla::glean diff --git a/toolkit/components/glean/bindings/private/Labeled.h b/toolkit/components/glean/bindings/private/Labeled.h new file mode 100644 index 000000000000..474c9978a21e --- /dev/null +++ b/toolkit/components/glean/bindings/private/Labeled.h @@ -0,0 +1,75 @@ +/* -*- 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_Labeled_h +#define mozilla_glean_Labeled_h + +#include "nsIGleanMetrics.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/glean/fog_ffi_generated.h" + +namespace mozilla::glean { + +namespace impl { + +template +class Labeled { + public: + constexpr explicit Labeled(uint32_t id) : mId(id) {} + + /** + * Gets a specific metric for a given label. + * + * If a set of acceptable labels were specified in the `metrics.yaml` file, + * and the given label is not in the set, it will be recorded under the + * special `OTHER_LABEL` label. + * + * If a set of acceptable labels was not specified in the `metrics.yaml` file, + * only the first 16 unique labels will be used. + * After that, any additional labels will be recorded under the special + * `OTHER_LABEL` label. + * + * @param aLabel - a snake_case string under 30 characters in length, + * otherwise the metric will be recorded under the special + * `OTHER_LABEL` label and an error will be recorded. + */ + T Get(const nsACString& aLabel) const; + + private: + const uint32_t mId; +}; + +} // namespace impl + +class GleanLabeled final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(GleanLabeled) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + nsISupports* GetParentObject() { return nullptr; } + + explicit GleanLabeled(uint32_t aId, uint32_t aTypeId) + : mId(aId), mTypeId(aTypeId){}; + + already_AddRefed NamedGetter(const nsAString& aName, + bool& aFound); + bool NameIsEnumerable(const nsAString& aName); + void GetSupportedNames(nsTArray& aNames); + + private: + virtual ~GleanLabeled() = default; + + const uint32_t mId; + const uint32_t mTypeId; +}; + +} // namespace mozilla::glean + +#endif /* mozilla_glean_Labeled_h */ diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py index a0ebe6dd96a1..6fb5444d13a9 100644 --- a/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py @@ -48,6 +48,9 @@ def type_name(obj): Returns the C++ type to use for a given metric object. """ + if getattr(obj, "labeled", False): + class_name = util.Camelize(obj.type[8:]) # strips "labeled_" off the front. + return "Labeled".format(class_name) generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons? if len(generate_enums): for name, suffix in generate_enums: diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/js.py b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py index 5607783d5efd..1de908672de3 100644 --- a/toolkit/components/glean/build_scripts/glean_parser_ext/js.py +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py @@ -71,12 +71,26 @@ def metric_identifier(category, metric_name): return f"{category}.{util.camelize(metric_name)}" -def type_name(type): +def type_name(obj): """ Returns the C++ type to use for a given metric object. """ - return "Glean" + util.Camelize(type) + if getattr(obj, "labeled", False): + return "GleanLabeled" + return "Glean" + util.Camelize(obj.type) + + +def subtype_name(obj): + """ + Returns the subtype name for labeled metrics. + (e.g. 'boolean' for 'labeled_boolean'). + Returns "" for non-labeled metrics. + """ + if getattr(obj, "labeled", False): + type = obj.type[8:] # strips "labeled_" off the front + return "Glean" + util.Camelize(type) + return "" def output_js(objs, output_fd, options={}): @@ -118,7 +132,6 @@ def write_metrics(objs, output_fd, template_filename): template = util.get_jinja2_template( template_filename, - filters=(("type_name", type_name),), ) assert ( @@ -142,11 +155,12 @@ def write_metrics(objs, output_fd, template_filename): for metric in objs.values(): identifier = metric_identifier(category_name, metric.name) - if metric.type in metric_type_ids: - type_id = metric_type_ids[metric.type] + metric_type_tuple = (type_name(metric), subtype_name(metric)) + if metric_type_tuple in metric_type_ids: + type_id, _ = metric_type_ids[metric_type_tuple] else: type_id = len(metric_type_ids) + 1 - metric_type_ids[metric.type] = type_id + metric_type_ids[metric_type_tuple] = (type_id, metric.type) idx = metric_string_table.stringIndex(identifier) metric_id = get_metric_id(metric) diff --git a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 index ba82b58ec9ba..f019a30d9af2 100644 --- a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 +++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js.jinja2 @@ -14,6 +14,7 @@ Jinja2 template is not. Please file bugs! #} #include "mozilla/PerfectHash.h" #include "mozilla/Maybe.h" #include "mozilla/glean/bindings/MetricTypes.h" +#include "mozilla/glean/fog_ffi_generated.h" #define GLEAN_INDEX_BITS ({{index_bits}}) #define GLEAN_ID_BITS ({{id_bits}}) @@ -39,10 +40,10 @@ static already_AddRefed NewMetricFromId(uint32_t id) { uint32_t metricId = GLEAN_METRIC_ID(id); switch (typeId) { - {% for type, type_id in metric_type_ids.items() %} - case {{ type_id }}: /* {{ type|Camelize }} */ + {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %} + case {{ type_id }}: /* {{ original_type }} */ { - return MakeAndAddRef<{{type | type_name}}>(metricId); + return MakeAndAddRef<{{type_name}}>(metricId{% if subtype_name|length > 0 %}, {{ type_id }}{% endif %}); } {% endfor %} default: @@ -51,6 +52,22 @@ static already_AddRefed NewMetricFromId(uint32_t id) { } } +static already_AddRefed NewSubMetricFromIds(uint32_t aParentTypeId, uint32_t aParentMetricId, const nsACString& aLabel) { + switch (aParentTypeId) { + {% for (type_name, subtype_name), (type_id, original_type) in metric_type_ids.items() %} + {% if subtype_name|length > 0 %} + case {{ type_id }}: { /* {{ original_type }} */ + return MakeAndAddRef<{{subtype_name}}>(impl::fog_{{original_type}}_get(aParentMetricId, &aLabel)); + } + {% endif %} + {% endfor %} + default: { + MOZ_ASSERT_UNREACHABLE("Invalid type ID for submetric."); + return nullptr; + } + } +} + static Maybe category_result_check(const nsACString& aKey, category_entry_t entry); static Maybe metric_result_check(const nsACString& aKey, metric_entry_t entry); diff --git a/toolkit/components/glean/gtest/TestFog.cpp b/toolkit/components/glean/gtest/TestFog.cpp index 1d96c07e5d72..75468570d9c3 100644 --- a/toolkit/components/glean/gtest/TestFog.cpp +++ b/toolkit/components/glean/gtest/TestFog.cpp @@ -214,3 +214,50 @@ TEST(FOG, TestCppTimingDistWorks) } ASSERT_EQ(sampleCount, (uint64_t)2); } + +TEST(FOG, TestLabeledBooleanWorks) +{ + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_like_balloons.Get("hot_air"_ns).TestGetValue()); + test_only::mabels_like_balloons.Get("hot_air"_ns).Set(true); + test_only::mabels_like_balloons.Get("helium"_ns).Set(false); + ASSERT_EQ( + true, + test_only::mabels_like_balloons.Get("hot_air"_ns).TestGetValue().ref()); + ASSERT_EQ( + false, + test_only::mabels_like_balloons.Get("helium"_ns).TestGetValue().ref()); +} + +TEST(FOG, TestLabeledCounterWorks) +{ + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_kitchen_counters.Get("marble"_ns).TestGetValue()); + test_only::mabels_kitchen_counters.Get("marble"_ns).Add(1); + test_only::mabels_kitchen_counters.Get("laminate"_ns).Add(2); + ASSERT_EQ( + 1, + test_only::mabels_kitchen_counters.Get("marble"_ns).TestGetValue().ref()); + ASSERT_EQ(2, test_only::mabels_kitchen_counters.Get("laminate"_ns) + .TestGetValue() + .ref()); +} + +TEST(FOG, TestLabeledStringWorks) +{ + ASSERT_EQ(mozilla::Nothing(), + test_only::mabels_balloon_strings.Get("twine"_ns).TestGetValue()); + test_only::mabels_balloon_strings.Get("twine"_ns).Set("seems acceptable"_ns); + test_only::mabels_balloon_strings.Get("parachute_cord"_ns) + .Set("preferred"_ns); + ASSERT_STREQ("seems acceptable", + test_only::mabels_balloon_strings.Get("twine"_ns) + .TestGetValue() + .ref() + .get()); + ASSERT_STREQ("preferred", + test_only::mabels_balloon_strings.Get("parachute_cord"_ns) + .TestGetValue() + .ref() + .get()); +} diff --git a/toolkit/components/glean/moz.build b/toolkit/components/glean/moz.build index 5fdbaeb95eb1..77ee864e728f 100644 --- a/toolkit/components/glean/moz.build +++ b/toolkit/components/glean/moz.build @@ -39,6 +39,7 @@ if CONFIG["MOZ_GLEAN"]: "bindings/private/Datetime.h", "bindings/private/DistributionData.h", "bindings/private/Event.h", + "bindings/private/Labeled.h", "bindings/private/MemoryDistribution.h", "bindings/private/Ping.h", "bindings/private/String.h", @@ -64,6 +65,7 @@ if CONFIG["MOZ_GLEAN"]: "bindings/private/Counter.cpp", "bindings/private/Datetime.cpp", "bindings/private/Event.cpp", + "bindings/private/Labeled.cpp", "bindings/private/MemoryDistribution.cpp", "bindings/private/Ping.cpp", "bindings/private/String.cpp", diff --git a/toolkit/components/glean/xpcshell/test_Glean.js b/toolkit/components/glean/xpcshell/test_Glean.js index de273634a32f..579123dddb1d 100644 --- a/toolkit/components/glean/xpcshell/test_Glean.js +++ b/toolkit/components/glean/xpcshell/test_Glean.js @@ -219,3 +219,90 @@ add_task(async function test_fog_timing_distribution_works() { "Only two buckets with samples" ); }); + +add_task(async function test_fog_labeled_boolean_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsLikeBalloons.at_parties.set(true); + Glean.testOnly.mabelsLikeBalloons.at_funerals.set(false); + Assert.equal( + true, + Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue() + ); + Assert.equal( + false, + Glean.testOnly.mabelsLikeBalloons.at_funerals.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue() + ); + Glean.testOnly.mabelsLikeBalloons.InvalidLabel.set(true); + Assert.equal( + true, + Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue() + ); + // TODO: Test that we have the right number and type of errors (bug 1683171) +}); + +add_task(async function test_fog_labeled_counter_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(1); + Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(2); + Assert.equal( + 1, + Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue() + ); + Assert.equal( + 2, + Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue() + ); + Glean.testOnly.mabelsKitchenCounters.InvalidLabel.add(1); + Assert.equal( + 1, + Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue() + ); + // TODO: Test that we have the right number and type of errors (bug 1683171) +}); + +add_task(async function test_fog_labeled_string_works() { + Assert.equal( + undefined, + Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue(), + "New labels with no values should return undefined" + ); + Glean.testOnly.mabelsBalloonStrings.colour_of_99.set("crimson"); + Glean.testOnly.mabelsBalloonStrings.string_lengths.set("various"); + Assert.equal( + "crimson", + Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue() + ); + Assert.equal( + "various", + Glean.testOnly.mabelsBalloonStrings.string_lengths.testGetValue() + ); + // What about invalid/__other__? + Assert.equal( + undefined, + Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue() + ); + Glean.testOnly.mabelsBalloonStrings.InvalidLabel.set("valid"); + Assert.equal( + "valid", + Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue() + ); + // TODO: Test that we have the right number and type of errors (bug 1683171) +});