# 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' """ # TODO: What are good notification emails? # TODO: What's good info to include in these descriptions? DENOMINATOR_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 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 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 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706 notification_emails: - dom-core@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 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706 notification_emails: - dom-core@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 data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706 notification_emails: - dom-core@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 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. """ # 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": name = f"{counter['interface_name']}_{counter['method_name']}".lower() method = f"called {counter['interface_name']}.{counter['method_name']}" page.append((name, f"Whether a page called {method}.")) doc.append((name, f"Whether a document called {method}.")) elif counter["type"] == "attribute": name = f"{counter['interface_name']}_{counter['attribute_name']}".lower() attr = f"{counter['interface_name']}.{counter['attribute_name']}" page.append((f"{name}_getter", f"Whether a page got {attr}.")) page.append((f"{name}_setter", f"Whether a page set {attr}.")) doc.append((f"{name}_getter", f"Whether a document got {attr}.")) doc.append((f"{name}_setter", f"Whether a document set {attr}.")) elif counter["type"] == "custom": page.append( (to_snake_case(counter["name"]), f"Whether a page {counter['desc']}.") ) doc.append( ( 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": name = f"{counter['interface_name']}_{counter['method_name']}".lower() method = f"called {counter['interface_name']}.{counter['method_name']}" dedicated.append((name, f"Whether a dedicated worker called {method}.")) shared.append((name, f"Whether a shared worker called {method}.")) service.append((name, f"Whether a service worker called {method}.")) elif counter["type"] == "attribute": name = f"{counter['interface_name']}_{counter['attribute_name']}".lower() attr = f"{counter['interface_name']}.{counter['attribute_name']}" dedicated.append((name, f"Whether a dedicated worker got {attr}.")) dedicated.append((name, f"Whether a dedicated worker set {attr}.")) shared.append((name, f"Whether a shared worker got {attr}.")) shared.append((name, f"Whether a shared worker set {attr}.")) service.append((name, f"Whether a service worker got {attr}.")) service.append((name, f"Whether a service worker set {attr}.")) elif counter["type"] == "custom": dedicated.append( ( to_snake_case(counter["name"]), f"Whether a dedicated worker {counter['desc']}.", ) ) shared.append( ( to_snake_case(counter["name"]), f"Whether a shared worker {counter['desc']}.", ) ) service.append( ( 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) ops_page.append((op_name, f"Whether a page used {op}.")) ops_doc.append((op_name, f"Whether a document used {op}.")) # TODO: 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) css_page.append( (prop_name, f"Whether a page used the CSS property {prop.name}.") ) css_doc.append( (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) 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: prop_name = to_snake_case(prop) css_page.append( ( prop_name, f"Whether a page used the (unknown, counted) CSS property {prop}.", ) ) css_doc.append( ( prop_name, f"Whether a document used the (unknown, counted) CSS property {prop}.", ) ) from mozbuild.util import FileAvoidWrite # TODO: Up for discussion: organization. # Especially e.g. use.counter.css.page.css_float, but also: perhaps suffixes are preferred over categories? 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(DENOMINATOR_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 uc in page: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in doc: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in dedicated: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in shared: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in service: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in ops_page: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in ops_doc: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in css_page: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], 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 uc in css_doc: f.write( USE_COUNTER_TEMPLATE.format( name=uc[0], desc=uc[1], denominator="use.counter.content_documents_destroyed", ) ) return 0 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("_") )