fune/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.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

139 lines
4.4 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 C++ code for metrics.
"""
import json
import jinja2
from glean_parser import metrics, util
from util import generate_metric_ids, generate_ping_ids, get_metrics
def cpp_datatypes_filter(value):
"""
A Jinja2 filter that renders C++ literals.
Based on Python's JSONEncoder, but overrides:
- lists to array literals {}
- strings to "value"
"""
class CppEncoder(json.JSONEncoder):
def iterencode(self, value):
if isinstance(value, list):
yield "{"
first = True
for subvalue in list(value):
if not first:
yield ", "
yield from self.iterencode(subvalue)
first = False
yield "}"
elif isinstance(value, str):
yield '"' + value + '"'
else:
yield from super().iterencode(value)
return "".join(CppEncoder().iterencode(value))
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.
label_enum = "DynamicLabel"
if obj.labels and len(obj.labels):
label_enum = f"{util.Camelize(obj.name)}Label"
return f"Labeled<impl::{class_name}Metric, {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, metrics.Event):
return util.Camelize(obj.type) + "Metric<NoExtraKeys>"
else:
# we always use the `extra` suffix,
# because we only expose the new event API
suffix = "Extra"
return "{}Metric<{}>".format(
util.Camelize(obj.type), util.Camelize(obj.name) + suffix
)
return util.Camelize(obj.type) + "Metric"
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 "nsCString"
elif typ == "quantity":
return "uint32_t"
else:
return "UNSUPPORTED"
def output_cpp(objs, output_fd, options={}):
"""
Given a tree of objects, output C++ 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 options: options dictionary.
"""
# 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("cpp", "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)
if "pings" in objs:
template_filename = "cpp_pings.jinja2"
if objs.get("tags"):
del objs["tags"]
else:
template_filename = "cpp.jinja2"
objs = get_metrics(objs)
template = util.get_jinja2_template(
template_filename,
filters=(
("cpp", cpp_datatypes_filter),
("snake_case", util.snake_case),
("type_name", type_name),
("extra_type_name", extra_type_name),
("metric_id", get_metric_id),
("ping_id", get_ping_id),
("Camelize", util.Camelize),
),
)
output_fd.write(template.render(all_objs=objs))
output_fd.write("\n")