blob: 064826a8fc966dd6529c1e795ebc09152162c427 [file] [log] [blame] [edit]
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Objects for describing template code to be generated from structured.xml."""
import hashlib
import os
import re
import struct
class Util:
"""Helpers for generating C++."""
@staticmethod
def sanitize_name(name):
return re.sub("[^0-9a-zA-Z_]", "_", name)
@staticmethod
def camel_to_snake(name):
pat = "((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))"
return re.sub(pat, r"_\1", name).lower()
@staticmethod
def hash_name(name):
# This must match the hash function in chromium's
# //metrics/uploader/metric_hashes.cc. >Q means 8 bytes, big endian.
name = name.encode("utf-8")
md5 = hashlib.md5(name)
return struct.unpack(">Q", md5.digest()[:8])[0]
@staticmethod
def event_name_hash(project_name, event_name):
"""Make the name hash for an event.
This gets uploaded in the StructuredEventProto.event_name_hash field. It
is the sole means of recording which event from structured.xml a
StructuredEventProto instance represents.
To avoid naming collisions, it must contain three pieces of information:
- the name of the event itself
- the name of the event's project, to avoid collisions with events of
the same name in other projects
- an identifier that this comes from chromeos, to avoid collisions with
events and projects of the same name defined in chromiumos's
structured.xml
This must use sanitized names for the project and event.
"""
event_name = Util.sanitize_name(event_name)
project_name = Util.sanitize_name(project_name)
return Util.hash_name(f"cros::{project_name}::{event_name}")
class FileInfo:
"""Codegen-related info about a file."""
def __init__(self, dirname, basename):
self.dirname = dirname
self.basename = basename
self.filepath = os.path.join(dirname, basename)
# This takes the last three components of the filepath for use in the
# header guard, ie. METRICS_STRUCTURED_STRUCTURED_EVENTS_H_
relative_path = os.sep.join(self.filepath.split(os.sep)[-3:])
self.guard_path = Util.sanitize_name(relative_path).upper()
class ProjectInfo:
"""Codegen-related info about a project."""
def __init__(self, project):
self.raw_name = project.name
self.name = Util.sanitize_name(project.name)
self.namespace = Util.camel_to_snake(self.name)
self.name_hash = Util.hash_name(self.name)
self.is_event_sequence = project.is_event_sequence_project
# Set ID Type.
if project.id == "uma":
self.id_type = "kUmaId"
elif project.id == "per-project":
self.id_type = "kProjectId"
elif project.id == "none":
self.id_type = "kUnidentified"
# Set the event type.
if self.is_event_sequence:
self.event_type = "SEQUENCE"
else:
# Set event type. This is inferred by checking all metrics within
# the project. If any of a project's metrics is a raw string, then
# it's events are considered raw string events, even if they also
# contain non-strings.
self.event_type = "REGULAR"
for event in project.events:
for metric in event.metrics:
if metric.type == "raw-string":
self.event_type = "RAW_STRING"
break
class EventInfo:
"""Codegen-related info about an event."""
def __init__(self, event, project_info):
self.raw_name = event.name
self.name = Util.sanitize_name(event.name)
self.name_hash = Util.event_name_hash(project_info.name, self.name)
class MetricInfo:
"""Codegen-related info about a metric."""
def __init__(self, metric):
self.raw_name = metric.name
self.name = Util.sanitize_name(metric.name)
self.hash = Util.hash_name(metric.name)
self.type_name = metric.type
if metric.type == "hmac-string":
self.setter_type = "std::string&"
self.setter = "AddHmacMetric"
self.getter_type = "std::string"
self.getter = "GetHmacMetricForTest"
self.arg_parser = "ParseStringStructuredMetricsArg"
elif metric.type == "int":
self.setter_type = "int64_t"
self.setter = "AddIntMetric"
self.getter_type = "int64_t"
self.getter = "GetIntMetricForTest"
self.arg_parser = "ParseIntStructuredMetricsArg"
elif metric.type == "raw-string":
self.setter_type = "std::string&"
self.setter = "AddRawStringMetric"
self.getter_type = "std::string"
self.getter = "GetRawStringMetricForTest"
self.arg_parser = "ParseStringStructuredMetricsArg"
elif metric.type == "double":
self.setter_type = "double"
self.setter = "AddDoubleMetric"
self.getter_type = "double"
self.getter = "GetDoubleMetricForTest"
self.arg_parser = "ParseDoubleStructuredMetricsArg"
elif metric.type == "int-array":
self.setter_type = "std::vector<int64_t>&"
self.max_size = metric.max_size
self.setter = "AddIntArrayMetric"
self.getter_type = "std::vector<int64_t>"
self.getter = "GetIntArrayMetricForTest"
self.arg_parser = "ParseIntArrayStructuredMetricsArg"
else:
raise ValueError("Invalid metric type.")
class Template:
"""Template for producing code from structured.xml."""
def __init__(
self,
model,
dirname,
basename,
file_template,
project_template,
event_template,
pre_metric_template,
metric_template,
array_template,
is_header,
):
self.model = model
self.dirname = dirname
self.basename = basename
self.file_template = file_template
self.project_template = project_template
self.event_template = event_template
self.pre_metric_template = pre_metric_template
self.metric_template = metric_template
self.array_template = array_template
self.is_header = is_header
def write_file(self):
file_info = FileInfo(self.dirname, self.basename)
with open(file_info.filepath, "w", encoding="utf-8") as f:
f.write(self._stamp_file(file_info))
def _stamp_file(self, file_info):
project_code = "".join(
self._stamp_project(file_info, p) for p in self.model.projects
)
project_names = sorted([p.name for p in self.model.projects])
project_hashes_list = [
f"UINT64_C({Util.hash_name(n)})" for n in project_names
]
project_hashes_literal = "{" + ", ".join(project_hashes_list) + "}"
return self.file_template.format(
file=file_info,
project_code=project_code,
project_hashes=project_hashes_literal,
)
def _stamp_project(self, file_info, project):
project_info = ProjectInfo(project)
event_code = "".join(
self._stamp_event(file_info, project_info, event)
for event in project.events
)
return self.project_template.format(
file=file_info, project=project_info, event_code=event_code
)
def _stamp_event(self, file_info, project_info, event):
event_info = EventInfo(event, project_info)
if self.is_header:
metric_code = "".join(
self._stamp_metric(file_info, event_info, metric)
for metric in event.metrics
)
metric_code += "".join(
self._stamp_array_metric(file_info, event_info, metric)
for metric in event.metrics
if metric.is_array()
)
else:
metric_code = "".join(
self._stamp_metric(file_info, event_info, metric)
for metric in event.metrics
if not metric.is_array()
)
metric_code += "".join(
self._stamp_array_metric(file_info, event_info, metric)
for metric in event.metrics
if metric.is_array()
)
pre_metric_code = "".join(
self._stamp_pre_metric(file_info, event_info, metric)
for metric in event.metrics
)
return self.event_template.format(
file=file_info,
project=project_info,
event=event_info,
pre_metric_code=pre_metric_code,
metric_code=metric_code,
)
def _stamp_pre_metric(self, file_info, event_info, metric):
return self.pre_metric_template.format(
file=file_info, event=event_info, metric=MetricInfo(metric)
)
def _stamp_metric(self, file_info, event_info, metric):
return self.metric_template.format(
file=file_info, event=event_info, metric=MetricInfo(metric)
)
def _stamp_array_metric(self, file_info, event_info, metric):
return self.array_template.format(
file=file_info, event=event_info, metric=MetricInfo(metric)
)