fune/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
Chris H-C 62cd1559ba Bug 1672273 - Provide enums in Rust and C++ for labeled_* metrics with static labels r=janerik
First we have to replace the venerable metric hashmaps with metric `match`
statements (like events). This is because, by adding the enum to the Labeled<>
type, we've made it impossible to use HashMap (since its type requires a known
Value type `V`, and Labeled<Counter, $SomeEnumType> is incomplete).

Then we had to generate meaningful enums. The order of the values is important,
as I foresee a future in which we may wish to use the enum variant value
(its "discriminant") to index into the glean-core `labels` list.

The enums can't have naming collisions. In Rust this is easy, so long as we
stay away from reserved keywords (bug 1814767). In C++ we are using `enum
class` so the variants' identifiers are scoped. However, on my system at least,
this is still susceptible to collision with preprocessor defines. So we prefix
the variants with the letter `e`. So far so good.

C++ Labeled metric implementations is regrettably moved from Labeled.cpp to
Labeled.h, as the template specialization is no longer full.

No decision about JS is made or implied by its exclusion from this commit.
This includes support for runtime registration (via JOG).

As for Rust, this patch maintains the string-only Rust API advertised by the
SDK's `Labeled<U>` trait. No decision has been made about whether to support
the now-codegen'd Rust enum labels in FOG only through implementation against
FOG's `LabeledMetric<U, E>` or by extending support into the SDK itself.

The use of strings for labeled_* metrics is preserved mostly unchanged.

The transformation from enum back to string label is performed in Rust during
`enum_get`. This is an improvement over the current situation as the strings
in use are guaranteed to not be allocated. However, I suspect we can improve
even more by moving this translation lower (into the SDK, ideally).

GIFFT support requires getting the label string from the enum, which is exposed
on the FFI.

Depends on D170108

Differential Revision: https://phabricator.services.mozilla.com/D170109
2023-05-02 13:13:41 +00:00

283 lines
9.8 KiB
Python

# -*- coding: utf-8 -*-
# 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/.
"""
Outputter to generate Rust code for metrics.
"""
import enum
import json
import jinja2
from glean_parser import util
from glean_parser.metrics import CowString, Event, Rate
from util import generate_metric_ids, generate_ping_ids, get_metrics
from js import ID_BITS, ID_SIGNAL_BITS
# The list of all args to CommonMetricData.
# No particular order is required, but I have these in common_metric_data.rs
# order just to be organized.
common_metric_data_args = [
"name",
"category",
"send_in_pings",
"lifetime",
"disabled",
"dynamic_label",
]
def rust_datatypes_filter(value):
"""
A Jinja2 filter that renders Rust literals.
Based on Python's JSONEncoder, but overrides:
- dicts and sets to raise an error
- sets to vec![] (used in labels)
- enums to become Class::Value
- lists to vec![] (used in send_in_pings)
- null to None
- strings to "value".into()
- Rate objects to a CommonMetricData initializer
(for external Denominators' Numerators lists)
"""
class RustEncoder(json.JSONEncoder):
def iterencode(self, value):
if isinstance(value, dict):
raise ValueError("RustEncoder doesn't know dicts {}".format(str(value)))
elif isinstance(value, enum.Enum):
yield (value.__class__.__name__ + "::" + util.Camelize(value.name))
elif isinstance(value, set):
yield from self.iterencode(sorted(list(value)))
elif isinstance(value, list):
yield "vec!["
first = True
for subvalue in list(value):
if not first:
yield ", "
yield from self.iterencode(subvalue)
first = False
yield "]"
elif value is None:
yield "None"
# CowString is also a 'str' but is a special case.
# Ensure its case is handled before str's (below).
elif isinstance(value, CowString):
yield f'::std::borrow::Cow::from("{value.inner}")'
elif isinstance(value, str):
yield '"' + value + '".into()'
elif isinstance(value, Rate):
yield "CommonMetricData {"
for arg_name in common_metric_data_args:
if hasattr(value, arg_name):
yield f"{arg_name}: "
yield from self.iterencode(getattr(value, arg_name))
yield ", "
yield " ..Default::default()}"
else:
yield from super().iterencode(value)
return "".join(RustEncoder().iterencode(value))
def ctor(obj):
"""
Returns the scope and name of the constructor to use for a metric object.
Necessary because LabeledMetric<T> is constructed using LabeledMetric::new
not LabeledMetric<T>::new
"""
if getattr(obj, "labeled", False):
return "LabeledMetric::new"
return class_name(obj.type) + "::new"
def type_name(obj):
"""
Returns the Rust type to use for a given metric or ping object.
"""
if getattr(obj, "labeled", False):
label_enum = "super::DynamicLabel"
if obj.labels and len(obj.labels):
label_enum = f"{util.Camelize(obj.name)}Label"
return f"LabeledMetric<Labeled{class_name(obj.type)}, {label_enum}>"
generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
if len(generate_enums):
for name, _ in generate_enums:
if not len(getattr(obj, name)) and isinstance(obj, Event):
return class_name(obj.type) + "<NoExtraKeys>"
else:
# we always use the `extra` suffix,
# because we only expose the new event API
suffix = "Extra"
return "{}<{}>".format(
class_name(obj.type), util.Camelize(obj.name) + suffix
)
return class_name(obj.type)
def extra_type_name(typ: str) -> str:
"""
Returns the corresponding Rust type for event's extra key types.
"""
if typ == "boolean":
return "bool"
elif typ == "string":
return "String"
elif typ == "quantity":
return "u32"
else:
return "UNSUPPORTED"
def class_name(obj_type):
"""
Returns the Rust class name for a given metric or ping type.
"""
if obj_type == "ping":
return "Ping"
if obj_type.startswith("labeled_"):
obj_type = obj_type[8:]
return util.Camelize(obj_type) + "Metric"
def extra_keys(allowed_extra_keys):
"""
Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys
"""
return "&[" + ", ".join(map(lambda key: '"' + key + '"', allowed_extra_keys)) + "]"
def output_rust(objs, output_fd, ping_names_by_app_id, options={}):
"""
Given a tree of objects, output Rust code to the file-like object `output_fd`.
:param objs: A tree of objects (metrics and pings) as returned from
`parser.parse_objects`.
:param output_fd: Writeable file to write the output to.
:param ping_names_by_app_id: A map of app_ids to lists of ping names.
Used to determine which custom pings to register.
:param options: options dictionary, presently unused.
"""
# Monkeypatch util.snake_case for the templates to use
util.snake_case = lambda value: value.replace(".", "_").replace("-", "_")
# Monkeypatch util.get_jinja2_template to find templates nearby
def get_local_template(template_name, filters=()):
env = jinja2.Environment(
loader=jinja2.PackageLoader("rust", "templates"),
trim_blocks=True,
lstrip_blocks=True,
)
env.filters["camelize"] = util.camelize
env.filters["Camelize"] = util.Camelize
for filter_name, filter_func in filters:
env.filters[filter_name] = filter_func
return env.get_template(template_name)
util.get_jinja2_template = get_local_template
get_metric_id = generate_metric_ids(objs)
get_ping_id = generate_ping_ids(objs)
# Map from a tuple (const, typ) to an array of tuples (id, path)
# where:
# const: The Rust constant name to be used for the lookup map
# typ: The metric type to be stored in the lookup map
# id: The numeric metric ID
# path: The fully qualified path to the metric object in Rust
#
# This map is only filled for metrics, not for pings.
#
# Example:
#
# ("COUNTERS", "CounterMetric") -> [(1, "test_only::clicks"), ...]
objs_by_type = {}
# Map from a metric ID to the fully qualified path of the event object in Rust.
# Required for the special handling of event lookups.
#
# Example:
#
# 17 -> "test_only::an_event"
events_by_id = {}
# Map from a labeled type (e.g. "counter") to a map from metric ID to the
# fully qualified path of the labeled metric object in Rust paired with
# whether the labeled metric has an enum.
# Required for the special handling of labeled metric lookups.
#
# Example:
#
# "counter" -> 42 -> ("test_only::mabels_kitchen_counters", false)
labeleds_by_id_by_type = {}
if "pings" in objs:
template_filename = "rust_pings.jinja2"
objs = {"pings": objs["pings"]}
else:
template_filename = "rust.jinja2"
objs = get_metrics(objs)
for category_name, category_value in objs.items():
for metric in category_value.values():
# The constant is all uppercase and suffixed by `_MAP`
const_name = util.snake_case(metric.type).upper() + "_MAP"
typ = type_name(metric)
key = (const_name, typ)
metric_name = util.snake_case(metric.name)
category_snake = util.snake_case(category_name)
full_path = f"{category_snake}::{metric_name}"
if metric.type == "event":
events_by_id[get_metric_id(metric)] = full_path
continue
if getattr(metric, "labeled", False):
labeled_type = metric.type[8:]
if labeled_type not in labeleds_by_id_by_type:
labeleds_by_id_by_type[labeled_type] = {}
labeleds_by_id_by_type[labeled_type][get_metric_id(metric)] = (
full_path,
metric.labels and len(metric.labels),
)
continue
if key not in objs_by_type:
objs_by_type[key] = []
objs_by_type[key].append((get_metric_id(metric), full_path))
# Now for the modules for each category.
template = util.get_jinja2_template(
template_filename,
filters=(
("rust", rust_datatypes_filter),
("snake_case", util.snake_case),
("type_name", type_name),
("extra_type_name", extra_type_name),
("ctor", ctor),
("extra_keys", extra_keys),
("metric_id", get_metric_id),
("ping_id", get_ping_id),
),
)
output_fd.write(
template.render(
all_objs=objs,
common_metric_data_args=common_metric_data_args,
metric_by_type=objs_by_type,
extra_args=util.extra_args,
events_by_id=events_by_id,
labeleds_by_id_by_type=labeleds_by_id_by_type,
submetric_bit=ID_BITS - ID_SIGNAL_BITS,
ping_names_by_app_id=ping_names_by_app_id,
)
)
output_fd.write("\n")