author | Mitchell Hentges <mhentges@mozilla.com> |
Tue, 15 Sep 2020 21:15:20 +0000 | |
changeset 548946 | 92cacd2f7294e545e970cbfbc941dd8e72c4462c |
parent 548945 | db9899666fa8beabb0bdde4a92688691c52d3a5f |
child 548947 | f420690e43612a43ff317145823492dbf29405b2 |
push id | 37790 |
push user | btara@mozilla.com |
push date | Thu, 17 Sep 2020 10:09:40 +0000 |
treeherder | mozilla-central@5f3283738794 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | firefox-build-system-reviewers, rstewart |
bugs | 1654074 |
milestone | 82.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/build/mach_bootstrap.py +++ b/build/mach_bootstrap.py @@ -452,32 +452,33 @@ def _finalize_telemetry_legacy(context, def _finalize_telemetry_glean(telemetry, is_bootstrap, success): """Submit telemetry collected by Glean. Finalizes some metrics (command success state and duration, system information) and requests Glean to send the collected data. """ + from mach.telemetry import MACH_METRICS_PATH from mozbuild.telemetry import get_cpu_brand, get_psutil_stats - system_metrics = telemetry.metrics.mach.system + mach_metrics = telemetry.metrics(MACH_METRICS_PATH) + mach_metrics.mach.duration.stop() + mach_metrics.mach.success.set(success) + system_metrics = mach_metrics.mach.system system_metrics.cpu_brand.set(get_cpu_brand()) has_psutil, logical_cores, physical_cores, memory_total = get_psutil_stats() if has_psutil: # psutil may not be available if a successful build hasn't occurred yet. system_metrics.logical_cores.add(logical_cores) system_metrics.physical_cores.add(physical_cores) if memory_total is not None: system_metrics.memory.accumulate(int( math.ceil(float(memory_total) / (1024 * 1024 * 1024)))) - - telemetry.metrics.mach.duration.stop() - telemetry.metrics.mach.success.set(success) telemetry.submit(is_bootstrap) # Hook import such that .pyc/.pyo files without a corresponding .py file in # the source directory are essentially ignored. See further below for details # and caveats. # Objdirs outside the source directory are ignored because in most cases, if # a .pyc/.pyo file exists there, a .py file will be next to it anyways.
--- a/python/mach/docs/metrics.md +++ b/python/mach/docs/metrics.md @@ -22,17 +22,17 @@ This ping includes the [client id](https - <https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34> **Bugs related to this ping:** - <https://bugzilla.mozilla.org/show_bug.cgi?id=1291053> The following metrics are added to the ping: -| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefix/Data_Collection) | +| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) | | --- | --- | --- | --- | --- | --- | --- | | mach.argv |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |Parameters provided to mach. Absolute paths are sanitized to be relative to one of a few key base paths, such as the "$topsrcdir", "$topobjdir", or "$HOME". For example: "/home/mozilla/dev/firefox/python/mozbuild" would be replaced with "$topsrcdir/python/mozbuild". If a valid replacement base path cannot be found, the path is replaced with "<path omitted>". |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.command |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the mach command that was invoked, such as "build", "doc", or "try". |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.duration |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |How long it took for the command to complete. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.success |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if the mach invocation succeeded. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.system.cpu_brand |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |CPU brand string from CPUID. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.system.logical_cores |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |Number of logical CPU cores present. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | | | mach.system.memory |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |Amount of system memory. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
--- a/python/mach/docs/telemetry.rst +++ b/python/mach/docs/telemetry.rst @@ -1,22 +1,39 @@ .. _mach_telemetry: ============== Mach Telemetry ============== `Glean <https://mozilla.github.io/glean/>`_ is used to collect telemetry, and uses the metrics -defined in ``/python/mach/metrics.yaml``. -Associated generated documentation can be found in :ref:`the metrics document<metrics>`. +defined in the ``metrics.yaml`` files in-tree. +These files are all documented in a single :ref:`generated file here<metrics>`. .. toctree:: :maxdepth: 1 metrics +Adding Metrics to a new Command +=============================== + +If you would like to submit telemetry metrics from your mach ``@Command``, you should take two steps: + +#. Parameterize your class's ``@CommandProvider`` annotation with ``metrics_path``. +#. Use the ``self.metrics`` handle provided by ``MachCommandBase`` + +For example:: + + METRICS_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'metrics.yaml')) + + @CommandProvider(metrics_path=METRICS_PATH) + class CustomCommand(MachCommandBase): + @Command('custom-command') + def custom_command(self): + self.metrics.custom.foo.set('bar') + Updating Generated Metrics Docs =============================== -When ``metrics.yaml`` is changed, :ref:`the metrics document<metrics>` will need to be updated. -Glean provides doc-generation tooling for us:: +When a ``metrics.yaml`` is added/changed/removed, :ref:`the metrics document<metrics>` will need to be updated:: - glean_parser translate -f markdown -o python/mach/docs/ python/mach/metrics.yaml python/mach/pings.yaml + ./mach doc mach-telemetry
--- a/python/mach/mach/base.py +++ b/python/mach/mach/base.py @@ -1,21 +1,21 @@ # 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/. from __future__ import absolute_import, unicode_literals -from mach.telemetry import Telemetry +from mach.telemetry import NoopTelemetry class CommandContext(object): """Holds run-time state so it can easily be passed to command providers.""" def __init__(self, cwd=None, settings=None, log_manager=None, commands=None, - telemetry=Telemetry.as_noop(), **kwargs): + telemetry=NoopTelemetry(False), **kwargs): self.cwd = cwd self.settings = settings self.log_manager = log_manager self.commands = commands self.telemetry = telemetry self.command_attrs = {} for k, v in kwargs.items():
--- a/python/mach/mach/decorators.py +++ b/python/mach/mach/decorators.py @@ -34,16 +34,20 @@ class _MachCommand(object): # Describes how dispatch is performed. # The Python class providing the command. This is the class type not # an instance of the class. Mach will instantiate a new instance of # the class if the command is executed. 'cls', + # The path to the `metrics.yaml` file that describes data that telemetry will + # gather for this command. This path is optional. + 'metrics_path', + # The name of the method providing the command. In other words, this # is the str name of the attribute on the class type corresponding to # the name of the function. 'method', # Dict of string to _MachCommand defining sub-commands for this # command. 'subcommand_handlers', @@ -67,20 +71,27 @@ class _MachCommand(object): self.virtualenv_name = virtualenv_name self.order = order if ok_if_tests_disabled and category != 'testing': raise ValueError('ok_if_tests_disabled should only be set for ' '`testing` mach commands') self.ok_if_tests_disabled = ok_if_tests_disabled self.cls = None + self.metrics_path = None self.method = None self.subcommand_handlers = {} self.decl_order = None + def create_instance(self, context, virtualenv_name): + metrics = None + if self.metrics_path: + metrics = context.telemetry.metrics(self.metrics_path) + return self.cls(context, virtualenv_name=virtualenv_name, metrics=metrics) + @property def parser(self): # Creating CLI parsers at command dispatch time can be expensive. Make # it possible to lazy load them by using functions. if callable(self._parser): self._parser = self._parser() return self._parser @@ -95,100 +106,107 @@ class _MachCommand(object): for a in self.__slots__: if not getattr(self, a): setattr(self, a, getattr(other, a)) return self -def CommandProvider(cls): - """Class decorator to denote that it provides subcommands for Mach. - - When this decorator is present, mach looks for commands being defined by - methods inside the class. - """ +def CommandProvider(_cls=None, metrics_path=None): + def finalize(cls): + if not issubclass(cls, MachCommandBase): + raise MachError( + 'Mach command provider class %s must be a subclass of ' + 'mozbuild.base.MachComandBase' % cls.__name__) - # The implementation of this decorator relies on the parse-time behavior of - # decorators. When the module is imported, the method decorators (like - # @Command and @CommandArgument) are called *before* this class decorator. - # The side-effect of the method decorators is to store specifically-named - # attributes on the function types. We just scan over all functions in the - # class looking for the side-effects of the method decorators. - - if not issubclass(cls, MachCommandBase): - raise MachError( - 'Mach command provider class %s must be a subclass of ' - 'mozbuild.base.MachComandBase' % cls.__name__) - - seen_commands = set() + seen_commands = set() - # We scan __dict__ because we only care about the classes' own attributes, - # not inherited ones. If we did inherited attributes, we could potentially - # define commands multiple times. We also sort keys so commands defined in - # the same class are grouped in a sane order. - command_methods = sorted([ - (name, value._mach_command) - for name, value in cls.__dict__.items() - if hasattr(value, '_mach_command') - ]) - - for method, command in command_methods: - # Ignore subcommands for now: we handle them later. - if command.subcommand: - continue + # We scan __dict__ because we only care about the classes' own attributes, + # not inherited ones. If we did inherited attributes, we could potentially + # define commands multiple times. We also sort keys so commands defined in + # the same class are grouped in a sane order. + command_methods = sorted([ + (name, value._mach_command) + for name, value in cls.__dict__.items() + if hasattr(value, '_mach_command') + ]) - seen_commands.add(command.name) + for method, command in command_methods: + # Ignore subcommands for now: we handle them later. + if command.subcommand: + continue - if not command.conditions and Registrar.require_conditions: - continue + seen_commands.add(command.name) - msg = 'Mach command \'%s\' implemented incorrectly. ' + \ - 'Conditions argument must take a list ' + \ - 'of functions. Found %s instead.' + if not command.conditions and Registrar.require_conditions: + continue - if not isinstance(command.conditions, collections.Iterable): - msg = msg % (command.name, type(command.conditions)) - raise MachError(msg) + msg = 'Mach command \'%s\' implemented incorrectly. ' + \ + 'Conditions argument must take a list ' + \ + 'of functions. Found %s instead.' - for c in command.conditions: - if not hasattr(c, '__call__'): - msg = msg % (command.name, type(c)) + if not isinstance(command.conditions, collections.Iterable): + msg = msg % (command.name, type(command.conditions)) raise MachError(msg) - command.cls = cls - command.method = method + for c in command.conditions: + if not hasattr(c, '__call__'): + msg = msg % (command.name, type(c)) + raise MachError(msg) - Registrar.register_command_handler(command) + command.cls = cls + command.metrics_path = metrics_path + command.method = method + + Registrar.register_command_handler(command) - # Now do another pass to get sub-commands. We do this in two passes so - # we can check the parent command existence without having to hold - # state and reconcile after traversal. - for method, command in command_methods: - # It is a regular command. - if not command.subcommand: - continue + # Now do another pass to get sub-commands. We do this in two passes so + # we can check the parent command existence without having to hold + # state and reconcile after traversal. + for method, command in command_methods: + # It is a regular command. + if not command.subcommand: + continue + + if command.name not in seen_commands: + raise MachError('Command referenced by sub-command does not ' + 'exist: %s' % command.name) + + if command.name not in Registrar.command_handlers: + continue - if command.name not in seen_commands: - raise MachError('Command referenced by sub-command does not ' - 'exist: %s' % command.name) + command.cls = cls + command.metrics_path = metrics_path + command.method = method + parent = Registrar.command_handlers[command.name] - if command.name not in Registrar.command_handlers: - continue + if command.subcommand in parent.subcommand_handlers: + raise MachError('sub-command already defined: %s' % command.subcommand) + + parent.subcommand_handlers[command.subcommand] = command + + return cls - command.cls = cls - command.method = method - parent = Registrar.command_handlers[command.name] - - if command.subcommand in parent.subcommand_handlers: - raise MachError('sub-command already defined: %s' % command.subcommand) - - parent.subcommand_handlers[command.subcommand] = command - - return cls + if _cls: + # The CommandProvider was used without parameters, e.g.: + # + # @CommandProvider + # class Example: + # ... + # Invoke finalize() immediately + return finalize(_cls) + else: + # The CommandProvider was used with parameters, e.g.: + # + # @CommandProvider(metrics_path='...') + # class Example: + # ... + # Return a callback which will be parameterized with the decorated class + return finalize class Command(object): """Decorator for functions or methods that provide a mach command. The decorator accepts arguments that define basic attributes of the command. The following arguments are recognized:
--- a/python/mach/mach/dispatcher.py +++ b/python/mach/mach/dispatcher.py @@ -253,17 +253,18 @@ class CommandAction(argparse.Action): for command in sorted(r.commands_by_category[category]): handler = r.command_handlers[command] # Instantiate a handler class to see if it should be filtered # out for the current context or not. Condition functions can be # applied to the command's decorator. if handler.conditions: - instance = handler.cls(self._context, handler.virtualenv_name) + instance = handler.create_instance(self._context, + handler.virtualenv_name) is_filtered = False for c in handler.conditions: if not c(instance): is_filtered = True break if is_filtered: description = handler.description
--- a/python/mach/mach/main.py +++ b/python/mach/mach/main.py @@ -29,17 +29,17 @@ from .base import ( UnrecognizedArgumentError, FailedCommandError, ) from .config import ConfigSettings from .dispatcher import CommandAction from .logging import LoggingManager from .registrar import Registrar from .sentry import register_sentry, NoopErrorReporter -from .telemetry import report_invocation_metrics, Telemetry +from .telemetry import report_invocation_metrics, create_telemetry_from_environment from .util import setenv, UserError SUGGEST_MACH_BUSTED_TEMPLATE = r''' You can invoke |./mach busted| to check if this issue is already on file. If it isn't, please use |./mach busted file %s| to report it. If |./mach busted| is misbehaving, you can also inspect the dependencies of bug 1543241. '''.lstrip() @@ -386,17 +386,17 @@ To see more help for a specific command, os.environ.clear() os.environ.update(orig_env) sys.stdin = orig_stdin sys.stdout = orig_stdout sys.stderr = orig_stderr def _run(self, argv, sentry): - telemetry = Telemetry.from_environment(self.settings) + telemetry = create_telemetry_from_environment(self.settings) context = CommandContext(cwd=self.cwd, settings=self.settings, log_manager=self.log_manager, commands=Registrar, telemetry=telemetry) if self.populate_context_handler: context = ContextWrapper(context, self.populate_context_handler) parser = self.get_argument_parser(context) @@ -424,17 +424,17 @@ To see more help for a specific command, print(UNRECOGNIZED_ARGUMENT_ERROR % (e.command, ' '.join(e.arguments))) return 1 if not hasattr(args, 'mach_handler'): raise MachError('ArgumentParser result missing mach handler info.') handler = getattr(args, 'mach_handler') - report_invocation_metrics(context.telemetry.metrics, handler.name) + report_invocation_metrics(context.telemetry, handler.name) # Add JSON logging to a file if requested. if args.logfile: self.log_manager.add_json_handler(args.logfile) # Up the logging level if requested. log_level = logging.INFO if args.verbose:
--- a/python/mach/mach/registrar.py +++ b/python/mach/mach/registrar.py @@ -57,27 +57,25 @@ class MachRegistrar(object): part = [' %s' % getattr(c, '__name__', c)] if c.__doc__ is not None: part.append(c.__doc__) msg.append(' - '.join(part)) return INVALID_COMMAND_CONTEXT % (name, '\n'.join(msg)) @classmethod def _instance(_, handler, context, **kwargs): - cls = handler.cls - if context is None: raise ValueError('Expected a non-None context.') prerun = getattr(context, 'pre_dispatch_handler', None) if prerun: prerun(context, handler, args=kwargs) context.handler = handler - return cls(context, handler.virtualenv_name) + return handler.create_instance(context, handler.virtualenv_name) @classmethod def _fail_conditions(_, handler, instance): fail_conditions = [] if handler.conditions: for c in handler.conditions: if not c(instance): fail_conditions.append(c)
--- a/python/mach/mach/telemetry.py +++ b/python/mach/mach/telemetry.py @@ -1,83 +1,109 @@ # 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/. from __future__ import print_function, absolute_import import os import sys + +import six from mock import Mock -from mozboot.util import get_state_dir +from mozboot.util import get_state_dir, get_mach_virtualenv_binary from mozbuild.base import MozbuildObject, BuildEnvironmentNotFoundException from mozbuild.telemetry import filter_args +import mozpack.path + +MACH_METRICS_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'metrics.yaml')) -class Telemetry(object): +class NoopTelemetry(object): + def __init__(self, failed_glean_import): + self._failed_glean_import = failed_glean_import + + def metrics(self, metrics_path): + return Mock() + + def submit(self, is_bootstrap): + if self._failed_glean_import and not is_bootstrap: + print("Glean could not be found, so telemetry will not be reported. " + "You may need to run |mach bootstrap|.", file=sys.stderr) + + +class GleanTelemetry(object): """Records and sends Telemetry using Glean. Metrics are defined in python/mozbuild/metrics.yaml. Pings are defined in python/mozbuild/pings.yaml. The "metrics" and "pings" properties may be replaced with no-op implementations if Glean isn't available. This allows consumers to report telemetry without having to guard against incompatible environments. """ - def __init__(self, metrics, pings, failed_glean_import): - self.metrics = metrics - self._pings = pings - self._failed_glean_import = failed_glean_import - - def submit(self, is_bootstrap): - self._pings.usage.submit() - - if self._failed_glean_import and not is_bootstrap: - print("Glean could not be found, so telemetry will not be reported. " - "You may need to run |mach bootstrap|.", file=sys.stderr) - - @classmethod - def as_noop(cls, failed_glean_import=False): - return cls(Mock(), Mock(), failed_glean_import) - - @classmethod - def from_environment(cls, settings): - """Creates and configures a Telemetry instance based on system details. + def __init__(self, ): + self._metrics_cache = {} - If telemetry isn't enabled, the current interpreter isn't Python 3, or Glean - can't be imported, then a "mock" telemetry instance is returned that doesn't - set or record any data. This allows consumers to optimistically set metrics - data without needing to specifically handle the case where the current system - doesn't support it. - """ - # Glean is not compatible with Python 2 - if not (sys.version_info >= (3, 0) and is_applicable_telemetry_environment()): - return cls.as_noop() + def metrics(self, metrics_path): + if metrics_path not in self._metrics_cache: + from glean import load_metrics + metrics = load_metrics(metrics_path) + self._metrics_cache[metrics_path] = metrics - try: - from glean import Glean, load_metrics, load_pings - except ImportError: - return cls.as_noop(failed_glean_import=True) - - from pathlib import Path + return self._metrics_cache[metrics_path] - Glean.initialize( - 'mozilla.mach', - 'Unknown', - is_telemetry_enabled(settings), - data_dir=Path(get_state_dir()) / 'glean', - ) + def submit(self, _): from pathlib import Path - metrics = load_metrics(Path(__file__).parent.parent / 'metrics.yaml') + from glean import load_pings pings = load_pings(Path(__file__).parent.parent / 'pings.yaml') - return cls(metrics, pings, False) + pings.usage.submit() -def report_invocation_metrics(metrics, command): +def create_telemetry_from_environment(settings): + """Creates and a Telemetry instance based on system details. + + If telemetry isn't enabled, the current interpreter isn't Python 3, or Glean + can't be imported, then a "mock" telemetry instance is returned that doesn't + set or record any data. This allows consumers to optimistically set telemetry + data without needing to specifically handle the case where the current system + doesn't support it. + """ + + is_mach_virtualenv = (mozpack.path.normpath(sys.executable) == + mozpack.path.normpath(get_mach_virtualenv_binary(py2=six.PY2))) + + if not (is_applicable_telemetry_environment() + # Glean is not compatible with Python 2 + and sys.version_info >= (3, 0) + # If not using the mach virtualenv (e.g.: bootstrap uses native python) + # then we can't guarantee that the glean package that we import is a + # compatible version. Therefore, don't use glean. + and is_mach_virtualenv): + return NoopTelemetry(False) + + try: + from glean import Glean + except ImportError: + return NoopTelemetry(True) + + from pathlib import Path + + Glean.initialize( + 'mozilla.mach', + 'Unknown', + is_telemetry_enabled(settings), + data_dir=Path(get_state_dir()) / 'glean', + ) + return GleanTelemetry() + + +def report_invocation_metrics(telemetry, command): + metrics = telemetry.metrics(MACH_METRICS_PATH) metrics.mach.command.set(command) metrics.mach.duration.start() try: instance = MozbuildObject.from_environment() except BuildEnvironmentNotFoundException: # Mach may be invoked with the state dir as the current working # directory, in which case we're not able to find the topsrcdir (so
--- a/python/mach/metrics.yaml +++ b/python/mach/metrics.yaml @@ -1,11 +1,14 @@ # 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/. + +# If this file is changed, update the generated docs: +# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs --- $schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 mach: command: type: string description: > The name of the mach command that was invoked, such as "build", @@ -123,122 +126,8 @@ mach.system: data_reviews: - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 notification_emails: - build-telemetry@mozilla.com - mhentges@mozilla.com expires: never send_in_pings: - usage - -mozbuild: - compiler: - type: string - description: The compiler type in use (CC_TYPE), such as "clang" or "gcc". - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - artifact: - type: boolean - description: True if `--enable-artifact-builds`. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - debug: - type: boolean - description: True if `--enable-debug`. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - opt: - type: boolean - description: True if `--enable-optimize`. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - ccache: - type: boolean - description: True if `--with-ccache`. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - sccache: - type: boolean - description: True if ccache in use is sccache. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - icecream: - type: boolean - description: True if icecream in use. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage - clobber: - type: boolean - description: True if the build was a clobber/full build. - lifetime: application - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15 - notification_emails: - - build-telemetry@mozilla.com - - mhentges@mozilla.com - expires: never - send_in_pings: - - usage
--- a/python/mach/pings.yaml +++ b/python/mach/pings.yaml @@ -1,11 +1,14 @@ # 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/. + +# If this file is changed, update the generated docs: +# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs --- $schema: moz://mozilla.org/schemas/glean/pings/1-0-0 usage: description: > Sent when the mach invocation is completed (regardless of result). Contains information about the mach invocation that was made, its result, and some details about the current environment and hardware.
new file mode 100644 --- /dev/null +++ b/python/mozbuild/metrics.yaml @@ -0,0 +1,122 @@ +# 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/. + +# If this file is changed, update the generated docs: +# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs +--- +$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 + +mozbuild: + compiler: + type: string + description: The compiler type in use (CC_TYPE), such as "clang" or "gcc". + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + artifact: + type: boolean + description: True if `--enable-artifact-builds`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + debug: + type: boolean + description: True if `--enable-debug`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + opt: + type: boolean + description: True if `--enable-optimize`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + ccache: + type: boolean + description: True if `--with-ccache`. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + sccache: + type: boolean + description: True if ccache in use is sccache. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + icecream: + type: boolean + description: True if icecream in use. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage + clobber: + type: boolean + description: True if the build was a clobber/full build. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15 + notification_emails: + - build-telemetry@mozilla.com + - mhentges@mozilla.com + expires: never + send_in_pings: + - usage
--- a/python/mozbuild/mozbuild/base.py +++ b/python/mozbuild/mozbuild/base.py @@ -853,17 +853,17 @@ class MozbuildObject(ProcessExecutionMix class MachCommandBase(MozbuildObject): """Base class for mach command providers that wish to be MozbuildObjects. This provides a level of indirection so MozbuildObject can be refactored without having to change everything that inherits from it. """ - def __init__(self, context, virtualenv_name=None): + def __init__(self, context, virtualenv_name=None, metrics=None): # Attempt to discover topobjdir through environment detection, as it is # more reliable than mozconfig when cwd is inside an objdir. topsrcdir = context.topdir topobjdir = None detect_virtualenv_mozinfo = True if hasattr(context, 'detect_virtualenv_mozinfo'): detect_virtualenv_mozinfo = getattr(context, 'detect_virtualenv_mozinfo') @@ -900,16 +900,17 @@ class MachCommandBase(MozbuildObject): sys.exit(1) MozbuildObject.__init__( self, topsrcdir, context.settings, context.log_manager, topobjdir=topobjdir, virtualenv_name=virtualenv_name) self._mach_context = context + self.metrics = metrics # Incur mozconfig processing so we have unified error handling for # errors. Otherwise, the exceptions could bubble back to mach's error # handler. try: self.mozconfig except MozconfigFindException as e:
--- a/python/mozbuild/mozbuild/build_commands.py +++ b/python/mozbuild/mozbuild/build_commands.py @@ -10,32 +10,32 @@ import subprocess from mach.decorators import ( CommandArgument, CommandProvider, Command, ) from mozbuild.base import MachCommandBase -from mozbuild.util import ensure_subprocess_env +from mozbuild.util import ensure_subprocess_env, MOZBUILD_METRICS_PATH from mozbuild.mozconfig import MozconfigLoader import mozpack.path as mozpath from mozbuild.backend import ( backends, ) BUILD_WHAT_HELP = ''' What to build. Can be a top-level make target or a relative directory. If multiple options are provided, they will be built serially. BUILDING ONLY PARTS OF THE TREE CAN RESULT IN BAD TREE STATE. USE AT YOUR OWN RISK. '''.strip() -@CommandProvider +@CommandProvider(metrics_path=MOZBUILD_METRICS_PATH) class Build(MachCommandBase): """Interface to build the tree.""" @Command('build', category='build', description='Build the tree.') @CommandArgument('--jobs', '-j', default='0', metavar='jobs', type=int, help='Number of concurrent jobs to run. Default is the number of CPUs.') @CommandArgument('-C', '--directory', default=None, help='Change to a subdirectory of the build directory first.') @@ -117,16 +117,17 @@ class Build(MachCommandBase): subprocess.check_call(pgo_cmd, cwd=instr.topobjdir, env=ensure_subprocess_env(pgo_env)) # Set the default build to MOZ_PROFILE_USE append_env = {'MOZ_PROFILE_USE': '1'} driver = self._spawn(BuildDriver) return driver.build( + self.metrics, what=what, jobs=jobs, directory=directory, verbose=verbose, keep_going=keep_going, mach_context=self._mach_context, append_env=append_env) @@ -138,16 +139,17 @@ class Build(MachCommandBase): from mozbuild.controller.building import ( BuildDriver, ) self.log_manager.enable_all_structured_loggers() driver = self._spawn(BuildDriver) return driver.configure( + self.metrics, options=options, buildstatus_messages=buildstatus_messages, line_handler=line_handler) @Command('resource-usage', category='post-build', description='Show information about system resource usage for a build.') @CommandArgument('--address', default='localhost', help='Address the HTTP server should listen on.')
--- a/python/mozbuild/mozbuild/controller/building.py +++ b/python/mozbuild/mozbuild/controller/building.py @@ -1026,25 +1026,27 @@ class CCacheStats(object): return '%.1f Kbytes' % (float(v) / CCacheStats.KiB) class BuildDriver(MozbuildObject): """Provides a high-level API for build actions.""" def __init__(self, *args, **kwargs): MozbuildObject.__init__(self, *args, **kwargs) + self.metrics = None self.mach_context = None - def build(self, what=None, jobs=0, directory=None, verbose=False, + def build(self, metrics, what=None, jobs=0, directory=None, verbose=False, keep_going=False, mach_context=None, append_env=None): """Invoke the build backend. ``what`` defines the thing to build. If not defined, the default target is used. """ + self.metrics = metrics self.mach_context = mach_context warnings_path = self._get_state_filename('warnings.json') monitor = self._spawn(BuildMonitor) monitor.init(warnings_path) ccache_start = monitor.ccache_stats() footer = BuildProgressFooter(self.log_manager.terminal, monitor) # Disable indexing in objdir because it is not necessary and can slow @@ -1062,27 +1064,26 @@ class BuildDriver(MozbuildObject): if directory is not None: directory = mozpath.normsep(directory) if directory.startswith('/'): directory = directory[1:] monitor.start_resource_recording() self.mach_context.command_attrs['clobber'] = False - self.mach_context.telemetry.metrics.mozbuild.clobber.set(False) + self.metrics.mozbuild.clobber.set(False) config = None try: config = self.config_environment except Exception: # If we don't already have a config environment this is either # a fresh objdir or $OBJDIR/config.status has been removed for # some reason, which indicates a clobber of sorts. self.mach_context.command_attrs['clobber'] = True - mozbuild_telemetry = self.mach_context.telemetry.metrics.mozbuild - mozbuild_telemetry.clobber.set(True) + self.metrics.mozbuild.clobber.set(True) # Record whether a clobber was requested so we can print # a special message later if the build fails. clobber_requested = False # Write out any changes to the current mozconfig in case # they should invalidate configure. self._write_mozconfig_json() @@ -1099,28 +1100,29 @@ class BuildDriver(MozbuildObject): mozpath.join(self.topobjdir, 'config_status_deps.in')): if previous_backend and 'Make' not in previous_backend: clobber_requested = self._clobber_configure() if config is None: print(" Config object not found by mach.") - config_rc = self.configure(buildstatus_messages=True, + config_rc = self.configure(metrics, + buildstatus_messages=True, line_handler=output.on_line, append_env=append_env) if config_rc != 0: return config_rc config = self.reload_config_environment() # Collect glean metrics substs = config.substs - mozbuild_metrics = self.mach_context.telemetry.metrics.mozbuild + mozbuild_metrics = metrics.mozbuild mozbuild_metrics.compiler.set(substs.get('CC_TYPE', None)) def get_substs_flag(name): return bool(substs.get(name, None)) mozbuild_metrics.artifact.set(get_substs_flag('MOZ_ARTIFACT_BUILDS')) mozbuild_metrics.debug.set(get_substs_flag('MOZ_DEBUG')) mozbuild_metrics.opt.set(get_substs_flag('MOZ_OPTIMIZE')) @@ -1383,20 +1385,21 @@ class BuildDriver(MozbuildObject): ) except Exception: # Ignore Exceptions in case we can't find config.status (such # as when doing OSX Universal builds) pass return status - def configure(self, options=None, buildstatus_messages=False, + def configure(self, metrics, options=None, buildstatus_messages=False, line_handler=None, append_env=None): # Disable indexing in objdir because it is not necessary and can slow # down builds. + self.metrics = metrics mkdir(self.topobjdir, not_indexed=True) self._write_mozconfig_json() def on_line(line): self.log(logging.INFO, 'build_output', {'line': line}, '{line}') line_handler = line_handler or on_line @@ -1594,17 +1597,17 @@ class BuildDriver(MozbuildObject): clobber_output.seek(0) for line in clobber_output.readlines(): self.log(logging.WARNING, 'clobber', {'msg': line.rstrip()}, '{msg}') clobber_required, clobber_performed, clobber_message = res if self.mach_context is not None and clobber_performed: self.mach_context.command_attrs['clobber'] = True - self.mach_context.telemetry.metrics.mozbuild.clobber.set(True) + self.metrics.mozbuild.clobber.set(True) if not clobber_required or clobber_performed: if clobber_performed and env.get('TINDERBOX_OUTPUT'): self.log(logging.WARNING, 'clobber', {'msg': 'TinderboxPrint: auto clobber'}, '{msg}') else: for line in clobber_message.splitlines(): self.log(logging.WARNING, 'clobber', {'msg': line.rstrip()}, '{msg}')
--- a/python/mozbuild/mozbuild/mach_commands.py +++ b/python/mozbuild/mozbuild/mach_commands.py @@ -29,16 +29,17 @@ from mach.decorators import ( from mozbuild.base import ( BinaryNotFoundException, BuildEnvironmentNotFoundException, MachCommandBase, MachCommandConditions as conditions, MozbuildObject, ) +from mozbuild.util import MOZBUILD_METRICS_PATH here = os.path.abspath(os.path.dirname(__file__)) EXCESSIVE_SWAP_MESSAGE = ''' =================== PERFORMANCE WARNING Your machine experienced a lot of swap activity during the build. This is @@ -169,17 +170,17 @@ class Doctor(MachCommandBase): help='Attempt to fix found problems.') def doctor(self, fix=None): self.activate_virtualenv() from mozbuild.doctor import Doctor doctor = Doctor(self.topsrcdir, self.topobjdir, fix) return doctor.check_all() -@CommandProvider +@CommandProvider(metrics_path=MOZBUILD_METRICS_PATH) class Clobber(MachCommandBase): NO_AUTO_LOG = True CLOBBER_CHOICES = set(['objdir', 'python', 'gradle']) @Command('clobber', category='build', description='Clobber the tree (delete the object directory).') @CommandArgument('what', default=['objdir', 'python'], nargs='*', help='Target to clobber, must be one of {{{}}} (default '
--- a/python/mozbuild/mozbuild/util.py +++ b/python/mozbuild/mozbuild/util.py @@ -24,16 +24,19 @@ import sys import time from collections import ( OrderedDict, ) from io import (BytesIO, StringIO) import six +MOZBUILD_METRICS_PATH = os.path.abspath( + os.path.join(__file__, '..', '..', 'metrics.yaml')) + if sys.platform == 'win32': _kernel32 = ctypes.windll.kernel32 _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000 system_encoding = 'mbcs' else: system_encoding = 'utf-8'
--- a/tools/moztreedocs/mach_commands.py +++ b/tools/moztreedocs/mach_commands.py @@ -13,21 +13,23 @@ import subprocess import sys import time import yaml import uuid from functools import partial from pprint import pprint +from mach.registrar import Registrar from mozbuild.base import MachCommandBase from mach.decorators import ( Command, CommandArgument, CommandProvider, + SubCommand, ) here = os.path.abspath(os.path.dirname(__file__)) topsrcdir = os.path.abspath(os.path.dirname(os.path.dirname(here))) DOC_ROOT = os.path.join(topsrcdir, "docs") BASE_LINK = "http://gecko-docs.mozilla.org-l1.s3-website.us-west-2.amazonaws.com/" JSDOC_NOT_FOUND = """\ JSDoc==3.5.5 is required to build the docs but was not found on your system. @@ -394,16 +396,40 @@ class Documentation(MachCommandBase): print("Redirects currently staged") pprint(all_redirects, indent=1) s3_set_redirects(all_redirects) unique_link = BASE_LINK + unique_id + "/index.html" print("Uploaded documentation can be accessed here " + unique_link) + @SubCommand( + "doc", + "mach-telemetry", + description="Generate documentation from Glean metrics.yaml files", + ) + def generate_telemetry_docs(self): + args = [ + "glean_parser", + "translate", + "-f", + "markdown", + "-o", + os.path.join(topsrcdir, "python/mach/docs/"), + os.path.join(topsrcdir, "python/mach/pings.yaml"), + os.path.join(topsrcdir, "python/mach/metrics.yaml"), + ] + metrics_paths = [ + handler.metrics_path + for handler in Registrar.command_handlers.values() + if handler.metrics_path is not None + ] + args.extend([os.path.join(self.topsrcdir, path) for path in set(metrics_paths)]) + subprocess.check_output(args) + def check_jsdoc(self): try: from mozfile import which exe_name = which("jsdoc") if not exe_name: return 1 out = subprocess.check_output([exe_name, "--version"])