fune/dom/base/usecounters.py

884 lines
28 KiB
Python

# 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/.
import collections
import re
def read_conf(conf_filename):
# Can't read/write from a single StringIO, so make a new one for reading.
stream = open(conf_filename, "r")
def parse_counters(stream):
for line_num, full_line in enumerate(stream):
line = full_line.rstrip("\n")
if not line or line.startswith("//"):
# empty line or comment
continue
m = re.match(r"method ([A-Za-z0-9]+)\.([A-Za-z0-9]+)$", line)
if m:
interface_name, method_name = m.groups()
yield {
"type": "method",
"interface_name": interface_name,
"method_name": method_name,
}
continue
m = re.match(r"attribute ([A-Za-z0-9]+)\.([A-Za-z0-9]+)$", line)
if m:
interface_name, attribute_name = m.groups()
yield {
"type": "attribute",
"interface_name": interface_name,
"attribute_name": attribute_name,
}
continue
m = re.match(r"custom ([A-Za-z0-9_]+) (.*)$", line)
if m:
name, desc = m.groups()
yield {"type": "custom", "name": name, "desc": desc}
continue
raise ValueError(
"error parsing %s at line %d" % (conf_filename, line_num + 1)
)
return parse_counters(stream)
def generate_histograms(filename, is_for_worker=False):
# The mapping for use counters to telemetry histograms depends on the
# ordering of items in the dictionary.
# The ordering of the ending for workers depends on the WorkerType defined
# in WorkerPrivate.h.
endings = (
["DEDICATED_WORKER", "SHARED_WORKER", "SERVICE_WORKER"]
if is_for_worker
else ["DOCUMENT", "PAGE"]
)
items = collections.OrderedDict()
for counter in read_conf(filename):
def append_counter(name, desc):
items[name] = {
"expires_in_version": "never",
"kind": "boolean",
"description": desc,
}
def append_counters(name, desc):
for ending in endings:
append_counter(
"USE_COUNTER2_%s_%s" % (name, ending),
"Whether a %s %s" % (ending.replace("_", " ").lower(), desc),
)
if counter["type"] == "method":
method = "%s.%s" % (counter["interface_name"], counter["method_name"])
append_counters(method.replace(".", "_").upper(), "called %s" % method)
elif counter["type"] == "attribute":
attr = "%s.%s" % (counter["interface_name"], counter["attribute_name"])
counter_name = attr.replace(".", "_").upper()
append_counters("%s_getter" % counter_name, "got %s" % attr)
append_counters("%s_setter" % counter_name, "set %s" % attr)
elif counter["type"] == "custom":
append_counters(counter["name"].upper(), counter["desc"])
return items
YAML_HEADER = """\
# This file is AUTOGENERATED by usecounters.py. DO NOT EDIT.
# (instead, re-run ./mach gen-use-counter-metrics)
# 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/.
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Core :: DOM: Core & HTML'
"""
BASE_METRICS = """\
use.counter:
content_documents_destroyed:
type: counter
description: >
A count of how many content documents were destroyed.
Used to turn document use counters' counts into rates.
Excludes documents for which we do not count use counters
(See `Document::ShouldIncludeInTelemetry`).
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1204994
- https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
- https://bugzilla.mozilla.org/show_bug.cgi?id=1845779
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
top_level_content_documents_destroyed:
type: counter
description: >
A count of how many "pages" were destroyed.
Used to turn page use counters' counts into rates.
Excludes pages that contain only documents for which we do not count use
counters (See `Document::ShouldIncludeInTelemetry`).
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1204994
- https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
- https://bugzilla.mozilla.org/show_bug.cgi?id=1845779
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
dedicated_workers_destroyed:
type: counter
description: >
A count of how many `Dedicated`-kind workers were destroyed.
Used to turn dedicated worker use counters' counts into rates.
Excludes chrome workers.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
shared_workers_destroyed:
type: counter
description: >
A count of how many `Shared`-kind workers were destroyed.
Used to turn shared worker use counters' counts into rates.
Excludes chrome workers.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
service_workers_destroyed:
type: counter
description: >
A count of how many `Service`-kind workers were destroyed.
Used to turn service worker use counters' counts into rates.
Excludes chrome workers.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
use.counter.error:
unknown_counter:
type: labeled_counter
description: >
How many times did we try to increment a use counter we couldn't find?
Labeled by what kind of use counter it is.
labels:
- page
- doc
- dedicated
- shared
- service
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
notification_emails:
- dom-core@mozilla.com
- chutten@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
"""
USE_COUNTER_TEMPLATE = """\
{name}:
type: counter
description: >
{desc}
Compare against `{denominator}`
to calculate the rate.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
notification_emails:
- dom-core@mozilla.com
- emilio@mozilla.com
expires: never
send_in_pings:
- use-counters
"""
def gen_use_counter_metrics():
"""
Finds use counters in:
* dom/base/UseCounters.conf
* dom/base/UseCountersWorker.conf
* dom/base/nsDeprecatedOperationsList.h
* !/layout/style/ServoCSSPropList.py
* servo/components/style/properties/counted_unknown_properties.py
and overwrites the Glean metrics definition file
`dom/base/use_counter_metrics.yaml` with definitions for each use counter found.
IF YOU CHANGE THIS FUNCTION:
* You should probably add your bug's number to USE_COUNTER_TEMPLATE, above.
Returns 0 on success.
"""
(
page,
doc,
dedicated,
shared,
service,
ops_page,
ops_doc,
css_page,
css_doc,
) = parse_use_counters()
import os
import buildconfig
from mozbuild.util import FileAvoidWrite
yaml_path = os.path.join(
buildconfig.topsrcdir, "dom", "base", "use_counter_metrics.yaml"
)
with FileAvoidWrite(yaml_path) as f:
f.write(YAML_HEADER)
f.write(BASE_METRICS)
total = (
len(page)
+ len(doc)
+ len(dedicated)
+ len(shared)
+ len(service)
+ len(ops_page)
+ len(ops_doc)
+ len(css_page)
+ len(css_doc)
)
f.write(f"# Total of {total} use counter metrics (excludes denominators).\n")
f.write(f"# Total of {len(page)} 'page' use counters.\n")
f.write("use.counter.page:\n")
for [_, name, desc] in page:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.top_level_content_documents_destroyed",
)
)
f.write(f"# Total of {len(doc)} 'document' use counters.\n")
f.write("use.counter.doc:\n")
for [_, name, desc] in doc:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.content_documents_destroyed",
)
)
f.write(f"# Total of {len(dedicated)} 'dedicated worker' use counters.\n")
f.write("use.counter.worker.dedicated:\n")
for [_, name, desc] in dedicated:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.dedicated_workers_destroyed",
)
)
f.write(f"# Total of {len(shared)} 'shared worker' use counters.\n")
f.write("use.counter.worker.shared:\n")
for [_, name, desc] in shared:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.shared_workers_destroyed",
)
)
f.write(f"# Total of {len(service)} 'service worker' use counters.\n")
f.write("use.counter.worker.service:\n")
for [_, name, desc] in service:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.service_workers_destroyed",
)
)
f.write(
f"# Total of {len(ops_page)} 'deprecated operations (page)' use counters.\n"
)
f.write("use.counter.deprecated_ops.page:\n")
for [_, name, desc] in ops_page:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.top_level_content_documents_destroyed",
)
)
f.write(
f"# Total of {len(ops_doc)} 'deprecated operations (document)' use counters.\n"
)
f.write("use.counter.deprecated_ops.doc:\n")
for [_, name, desc] in ops_doc:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.content_documents_destroyed",
)
)
f.write(f"# Total of {len(css_page)} 'CSS (page)' use counters.\n")
f.write("use.counter.css.page:\n")
for [_, name, desc] in css_page:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.top_level_content_documents_destroyed",
)
)
f.write(f"# Total of {len(css_doc)} 'CSS (document)' use counters.\n")
f.write("use.counter.css.doc:\n")
for [_, name, desc] in css_doc:
f.write(
USE_COUNTER_TEMPLATE.format(
name=name,
desc=desc,
denominator="use.counter.content_documents_destroyed",
)
)
return 0
def parse_use_counters():
"""
Finds use counters in:
* dom/base/UseCounters.conf
* dom/base/UseCountersWorker.conf
* dom/base/nsDeprecatedOperationsList.h
* !/layout/style/ServoCSSPropList.py
* servo/components/style/properties/counted_unknown_properties.py
and returns them as a tuple of lists of tuples of the form:
(page, doc, dedicated, shared, service, ops_page, ops_doc, css_page, css_doc)
where each of the items is a List<Tuple<enum_name, glean_name, description>>
where `enum_name` is the name of the enum variant from UseCounter.h
(like `eUseCounter_custom_CustomizedBuiltin`), and
where `glean_name` is the name conjugated to Glean metric name safety.
"""
# Note, this function contains a duplication of enum naming logic from UseCounter.h.
# If you change the enum name format, you'll need to do it twice.
# There are 3 kinds of Use Counters in conf files: method, attribute, custom.
# `method` and `attribute` are presumed label-safe and are taken as-is.
# `custom` can be any case, so are coerced to snake_case.
import os
import buildconfig
uc_path = os.path.join(buildconfig.topsrcdir, "dom", "base", "UseCounters.conf")
page = []
doc = []
for counter in read_conf(uc_path):
if counter["type"] == "method":
enum_name = (
f"eUseCounter_{counter['interface_name']}_{counter['method_name']}"
)
glean_name = f"{counter['interface_name']}_{counter['method_name']}".lower()
method = f"called {counter['interface_name']}.{counter['method_name']}"
page.append((enum_name, glean_name, f"Whether a page called {method}."))
doc.append((enum_name, glean_name, f"Whether a document called {method}."))
elif counter["type"] == "attribute":
enum_root = (
f"eUseCounter_{counter['interface_name']}_{counter['attribute_name']}"
)
name = f"{counter['interface_name']}_{counter['attribute_name']}".lower()
attr = f"{counter['interface_name']}.{counter['attribute_name']}"
page.append(
(f"{enum_root}_getter", f"{name}_getter", f"Whether a page got {attr}.")
)
page.append(
(f"{enum_root}_setter", f"{name}_setter", f"Whether a page set {attr}.")
)
doc.append(
(
f"{enum_root}_getter",
f"{name}_getter",
f"Whether a document got {attr}.",
)
)
doc.append(
(
f"{enum_root}_setter",
f"{name}_setter",
f"Whether a document set {attr}.",
)
)
elif counter["type"] == "custom":
enum_name = f"eUseCounter_custom_{counter['name']}"
page.append(
(
enum_name,
to_snake_case(counter["name"]),
f"Whether a page {counter['desc']}.",
)
)
doc.append(
(
enum_name,
to_snake_case(counter["name"]),
f"Whether a document {counter['desc']}.",
)
)
else:
print(f"Found unexpected use counter type {counter['type']}. Returning 1.")
return 1
worker_uc_path = os.path.join(
buildconfig.topsrcdir, "dom", "base", "UseCountersWorker.conf"
)
dedicated = []
shared = []
service = []
for counter in read_conf(worker_uc_path):
if counter["type"] == "method":
enum_name = f"{counter['interface_name']}_{counter['method_name']}"
name = f"{counter['interface_name']}_{counter['method_name']}".lower()
method = f"called {counter['interface_name']}.{counter['method_name']}"
dedicated.append(
(enum_name, name, f"Whether a dedicated worker called {method}.")
)
shared.append(
(enum_name, name, f"Whether a shared worker called {method}.")
)
service.append(
(enum_name, name, f"Whether a service worker called {method}.")
)
elif counter["type"] == "attribute":
enum_root = f"{counter['interface_name']}_{counter['attribute_name']}"
name = f"{counter['interface_name']}_{counter['attribute_name']}".lower()
attr = f"{counter['interface_name']}.{counter['attribute_name']}"
dedicated.append(
(
f"{enum_root}_getter",
f"{name}_getter",
f"Whether a dedicated worker got {attr}.",
)
)
dedicated.append(
(
f"{enum_root}_setter",
f"{name}_setter",
f"Whether a dedicated worker set {attr}.",
)
)
shared.append(
(
f"{enum_root}_getter",
f"{name}_getter",
f"Whether a shared worker got {attr}.",
)
)
shared.append(
(
f"{enum_root}_setter",
f"{name}_setter",
f"Whether a shared worker set {attr}.",
)
)
service.append(
(
f"{enum_root}_getter",
f"{name}_getter",
f"Whether a service worker got {attr}.",
)
)
service.append(
(
f"{enum_root}_setter",
f"{name}_setter",
f"Whether a service worker set {attr}.",
)
)
elif counter["type"] == "custom":
enum_name = f"Custom_{counter['name']}"
dedicated.append(
(
enum_name,
to_snake_case(counter["name"]),
f"Whether a dedicated worker {counter['desc']}.",
)
)
shared.append(
(
enum_name,
to_snake_case(counter["name"]),
f"Whether a shared worker {counter['desc']}.",
)
)
service.append(
(
enum_name,
to_snake_case(counter["name"]),
f"Whether a service worker {counter['desc']}.",
)
)
else:
print(
f"Found unexpected worker use counter type {counter['type']}. Returning 1."
)
return 1
# nsDeprecatedOperationsList.h parsing is adapted from parse_histograms.py.
operation_list_path = os.path.join(
buildconfig.topsrcdir, "dom", "base", "nsDeprecatedOperationList.h"
)
operation_regex = re.compile("^DEPRECATED_OPERATION\\(([^)]+)\\)")
ops_page = []
ops_doc = []
with open(operation_list_path) as f:
for line in f:
match = operation_regex.search(line)
if not match:
# No macro, probably whitespace or comment.
continue
op = match.group(1)
op_name = to_snake_case(op)
enum_name = f"eUseCounter_{op}"
ops_page.append((enum_name, op_name, f"Whether a page used {op}."))
ops_doc.append((enum_name, op_name, f"Whether a document used {op}."))
# Theoretically, we could do this without a completed build
# (ie, without the generated ServoCSSPropList.py) by sourcing direct from
# servo/components/style/properties/data.py:PropertiesData(engine=gecko).
#
# ...but parse_histograms.py doesn't do this the hard way. Should we?
import runpy
proplist_path = os.path.join(
buildconfig.topobjdir, "layout", "style", "ServoCSSPropList.py"
)
css_properties = runpy.run_path(proplist_path)["data"]
css_page = []
css_doc = []
for prop in css_properties.values():
# We prefix `prop_name` with `css_` to avoid colliding with C++ keywords
# like `float`.
prop_name = "css_" + to_snake_case(prop.name)
# Dependency keywords: CSS_PROP_PUBLIC_OR_PRIVATE, GenerateServoCSSPropList.py.
method = "Float" if prop.method == "CssFloat" else prop.method
# Dependency keywords: CSS_PROP_DOMPROP_PREFIXED, GenerateServoCSSPropList.py.
if method.startswith("Moz") and prop.type() != "alias":
method = method[3:] # remove the moz prefix
enum_name = f"eUseCounter_property_{method}"
css_page.append(
(enum_name, prop_name, f"Whether a page used the CSS property {prop.name}.")
)
css_doc.append(
(
enum_name,
prop_name,
f"Whether a document used the CSS property {prop.name}.",
)
)
# Counted unknown properties: AKA - stuff that doesn't exist, but we want
# to count uses of anyway.
# We _might_ decide to implement these in the future, though, so we just add
# them to the css_page, css_doc lists directly for continuity.
# (We do give them a different description, though)
import sys
sys.path.append(os.path.join(buildconfig.topsrcdir, "layout", "style"))
from GenerateCountedUnknownProperties import to_camel_case
unknown_proplist_path = os.path.join(
buildconfig.topsrcdir,
"servo",
"components",
"style",
"properties",
"counted_unknown_properties.py",
)
unknown_properties = runpy.run_path(unknown_proplist_path)[
"COUNTED_UNKNOWN_PROPERTIES"
]
for prop in unknown_properties:
enum_name = f"eUseCounter_unknown_property_{to_camel_case(prop)}"
prop_name = to_snake_case(prop)
css_page.append(
(
enum_name,
prop_name,
f"Whether a page used the (unknown, counted) CSS property {prop}.",
)
)
css_doc.append(
(
enum_name,
prop_name,
f"Whether a document used the (unknown, counted) CSS property {prop}.",
)
)
return (page, doc, dedicated, shared, service, ops_page, ops_doc, css_page, css_doc)
def to_snake_case(kebab_or_pascal):
"""
Takes `kebab_or_pascal` which is in PascalCase or kebab-case
and conjugates it to "snake_case" (all lowercase, "_"-delimited).
"""
return (
re.sub("([A-Z]+)", r"_\1", kebab_or_pascal).replace("-", "_").lower().strip("_")
)
def metric_map(f, *inputs):
"""
Parses all use counters and outputs UseCounter.cpp which contains implementations
for two functions defined in UseCounter.h:
* void IncrementUseCounter(UseCounter aUseCounter, bool aIsPage)
* void IncrementWorkerUseCounter(UseCounterWorker aUseCounter, dom::WorkerKind aKind)
(Basically big switch statements mapping from enums to glean metrics, calling Add())
"""
(
page,
doc,
dedicated,
shared,
service,
ops_page,
ops_doc,
css_page,
css_doc,
) = parse_use_counters()
f.write(
"""\
/* AUTOGENERATED by usecounters.py. DO NOT EDIT */
/* 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/dom/UseCounterMetrics.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/glean/GleanMetrics.h"
namespace mozilla::dom {
void IncrementUseCounter(UseCounter aUseCounter, bool aIsPage) {
if (aIsPage) {
switch (aUseCounter) {
"""
)
for uc in page:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_page::{uc[1]}.Add();
break;
"""
)
for uc in ops_page:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_deprecated_ops_page::{uc[1]}.Add();
break;
"""
)
for uc in css_page:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_css_page::{uc[1]}.Add();
break;
"""
)
f.write(
"""\
default:
MOZ_ASSERT_UNREACHABLE("Unknown page usecounter.");
glean::use_counter_error::unknown_counter.Get("page"_ns).Add();
break;
}
} else {
switch (aUseCounter) {
"""
)
for uc in doc:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_doc::{uc[1]}.Add();
break;
"""
)
for uc in ops_doc:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_deprecated_ops_doc::{uc[1]}.Add();
break;
"""
)
for uc in css_doc:
f.write(
f"""\
case UseCounter::{uc[0]}:
glean::use_counter_css_doc::{uc[1]}.Add();
break;
"""
)
f.write(
"""\
default:
MOZ_ASSERT_UNREACHABLE("Unknown document usecounter.");
glean::use_counter_error::unknown_counter.Get("doc"_ns).Add();
break;
}
}
}
void IncrementWorkerUseCounter(UseCounterWorker aUseCounter, WorkerKind aKind) {
switch(aKind) {
case WorkerKind::WorkerKindDedicated:
switch (aUseCounter) {
"""
)
for uc in dedicated:
f.write(
f"""\
case UseCounterWorker::{uc[0]}:
glean::use_counter_worker_dedicated::{uc[1]}.Add();
break;
"""
)
f.write(
"""\
default:
MOZ_ASSERT_UNREACHABLE("Unknown dedicated worker usecounter.");
glean::use_counter_error::unknown_counter.Get("dedicated"_ns).Add();
break;
}
break;
case WorkerKind::WorkerKindShared:
switch (aUseCounter) {
"""
)
for uc in shared:
f.write(
f"""\
case UseCounterWorker::{uc[0]}:
glean::use_counter_worker_shared::{uc[1]}.Add();
break;
"""
)
f.write(
"""\
default:
MOZ_ASSERT_UNREACHABLE("Unknown shared worker usecounter.");
glean::use_counter_error::unknown_counter.Get("shared"_ns).Add();
break;
}
break;
case WorkerKind::WorkerKindService:
switch (aUseCounter) {
"""
)
for uc in service:
f.write(
f"""\
case UseCounterWorker::{uc[0]}:
glean::use_counter_worker_service::{uc[1]}.Add();
break;
"""
)
f.write(
"""\
default:
MOZ_ASSERT_UNREACHABLE("Unknown service worker usecounter.");
glean::use_counter_error::unknown_counter.Get("service"_ns).Add();
break;
}
break;
}
}
} // namespace mozilla
"""
)