fune/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
Chris H-C 280af19463 Bug 1688281 - Add IPC support to FOG Labeled Counters r=janerik
Since the submetric deals with IPCPayload, the submetric needs to know its
label. We could add that to CounterMetric::Child, but adding an optional label
to the child variant of CounterMetric looked strange.

So instead, I introduce the Labeled*Metric types.
Well, LabeledStringMetric and LabeledBooleanMetric are just re-exports.
But LabeledCounterMetric knows its label in non-parent processes.
And in parent processes it just acts like a normal CounterMetric, thanks to the
metric traits.

To figure out how to handle all these types it encouraged me to remove lambdas
from the MLA FFI and solve it with hygienic identifier capture instead. Bonus.

Here's hoping I don't regret this additional level of abstraction later.

Differential Revision: https://phabricator.services.mozilla.com/D107667
2021-03-10 17:51:30 +00:00

228 lines
7.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 Rust code for metrics.
"""
import enum
import json
import jinja2
from util import generate_metric_ids, generate_ping_ids
from glean_parser import util
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()
"""
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 "vec!["
first = True
for subvalue in sorted(list(value)):
if not first:
yield ", "
yield from self.iterencode(subvalue)
first = False
yield "]"
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"
elif isinstance(value, str):
yield '"' + value + '".into()'
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):
return "LabeledMetric<Labeled{}>".format(class_name(obj.type))
generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
if len(generate_enums):
for name, suffix in generate_enums:
if not len(getattr(obj, name)) and suffix == "Keys":
return class_name(obj.type) + "<NoExtraKeys>"
else:
return "{}<{}>".format(
class_name(obj.type), util.Camelize(obj.name) + suffix
)
return class_name(obj.type)
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, 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 options: options dictionary, presently unused.
"""
# Monkeypatch a util.snake_case function 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 = {}
if len(objs) == 1 and "pings" in objs:
template_filename = "rust_pings.jinja2"
else:
template_filename = "rust.jinja2"
for category_name, metrics in objs.items():
for metric in metrics.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_name = util.snake_case(category_name)
full_path = f"{category_name}::{metric_name}"
if metric.type == "event":
events_by_id[get_metric_id(metric)] = full_path
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),
("ctor", ctor),
("extra_keys", extra_keys),
("metric_id", get_metric_id),
("ping_id", get_ping_id),
),
)
# The list of all args to CommonMetricData (besides category and name).
# 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",
]
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,
min_submetric_id=2 ** 27 + 1, # One more than 2**ID_BITS from js.py
)
)
output_fd.write("\n")