chromium/tools/tracing/metadata_extractor.py

# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Extracts metadata information from proto traces.
"""

import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'perf'))

from core.tbmv3 import trace_processor

VERSION_NUM_QUERY = (
    'select str_value from metadata where name="cr-product-version"')
OS_NAME_QUERY = 'select str_value from metadata where name="cr-os-name"'
ARCH_QUERY = 'select str_value from metadata where name="cr-os-arch"'
BITNESS_QUERY = (
    'select int_value from metadata where name="cr-chrome-bitness"')
VERSION_CODE_QUERY = (
    'select int_value from metadata where name="cr-playstore_version_code"')
MODULES_QUERY = 'select name, build_id from stack_profile_mapping'


class OSName():
  ANDROID = 'Android'
  LINUX = 'Linux'
  MAC = 'Mac OS X'
  WINDOWS = 'Windows NT'
  CROS = 'CrOS'
  FUSCHIA = 'Fuschia'


class MetadataExtractor:
  """Extracts and stores metadata from a perfetto trace.

  Attributes:
    _initialized: boolean of whether the class has been
      initialized or not by calling the Initialize function.
    _trace_processor_path: path to the trace_processor executable.
    _trace_file: path to a perfetto system trace file.
    version_number: chrome version number (eg: 93.0.4537.0).
    os_name: platform of the trace writer of type OSName
      (eg. OSName.Android).
    architecture: OS arch of the trace writer, as returned by
      base::SysInfo::OperatingSystemArchitecture() (eg: 'x86_64).
    bitness: integer of architecture bitness (eg. 32, 64).
    version_code: version code of chrome used by Android play store.
    modules: map from module name to module debug ID, for all
      modules that need symbolization.
  """

  def __init__(self, trace_processor_path, trace_file):
    self._initialized = False
    self._trace_processor_path = trace_processor_path
    self._trace_file = trace_file
    self.version_number = None
    self.os_name = None
    self.architecture = None
    self.bitness = None
    self.version_code = None
    self.modules = None

  def __str__(self):
    return ('Initialized: {initialized}\n'
            'Trace Processor Path: {trace_processor_path}\n'
            'Trace File: {trace_file}\n'
            'Version Number: {version_number}\n'
            'OS Name: {os_name}\n'
            'Architecture: {architecture}\n'
            'Bitness: {bitness}\n'
            'Version Code: {version_code}\n'
            'Modules: {modules}\n'.format(
                initialized=self._initialized,
                trace_processor_path=self._trace_processor_path,
                trace_file=self._trace_file,
                version_number=self.version_number,
                os_name=self.os_name,
                architecture=self.architecture,
                bitness=self.bitness,
                version_code=self.version_code,
                modules=self.modules))

  @property
  def trace_file(self):
    return self._trace_file

  def GetModuleIds(self):
    """Returns set of all module IDs in |modules| field.
    """
    self.Initialize()
    if self.modules is None:
      return None
    return set(self.modules.values())

  def Initialize(self):
    """Extracts metadata from perfetto system trace.
    """
    # TODO(crbug.com/40193968): Implement Trace Processor method to run multiple
    # SQL queries without processing trace for every query.

    if self._initialized:
      return
    self._initialized = True

    # Version Number query returns the name and number (Chrome/93.0.4537.0).
    # Parse the result to only get the version number.
    version_number = self._GetStringValueFromQuery(VERSION_NUM_QUERY)
    if version_number is None:
      self.version_number = None
    elif version_number.count('/') == 1:
      self.version_number = version_number.split('/')[1]
    else:
      self.version_number = version_number
    # Mac 64 traces add '-64' after the version number.
    if self.version_number is not None and self.version_number.endswith('-64'):
      self.version_number = self.version_number[:-3]

    raw_os_name = self._GetStringValueFromQuery(OS_NAME_QUERY)
    self.os_name = self._ParseOSName(raw_os_name)

    self.architecture = self._GetStringValueFromQuery(ARCH_QUERY)
    self.bitness = self._GetIntValueFromQuery(BITNESS_QUERY)
    self.version_code = self._GetIntValueFromQuery(VERSION_CODE_QUERY)

    # Parse module to be a mapping between module name and debug id
    self.modules = self._ExtractValidModuleMap()

  def _ParseOSName(self, raw_os_name):
    """Parsed OS name string into an enum.

    Args:
      raw_os_name: An OS name string returned from
        base::SysInfo::OperatingSystemName().

    Returns:
      An enum of type OSName.

    Raises:
      Exception: If OS name string is not recognized.
    """
    if raw_os_name is None:
      return None

    if raw_os_name == 'Android':
      return OSName.ANDROID
    if raw_os_name == 'Linux':
      return OSName.LINUX
    if raw_os_name == 'Mac OS X':
      return OSName.MAC
    if raw_os_name == 'Windows NT':
      return OSName.WINDOWS
    if raw_os_name == 'CrOS':
      return OSName.CROS
    if raw_os_name == 'Fuschia':
      return OSName.FUSCHIA
    raise Exception('OS name "%s" not recognized: %s' %
                    (raw_os_name, self._trace_file))

  def InitializeForTesting(self,
                           version_number=None,
                           os_name=None,
                           architecture=None,
                           bitness=None,
                           version_code=None,
                           modules=None):
    """Sets class parameter values for test cases.

    The |trace_processor_path| and |trace_file| parameters should
    be specified in the constructor.
    """
    self._initialized = True
    self.version_number = version_number
    self.os_name = os_name
    self.architecture = architecture
    self.bitness = bitness
    self.version_code = version_code
    self.modules = modules

  def _GetStringValueFromQuery(self, sql):
    """Runs SQL query on trace processor and returns 'str_value' result.
    """
    try:
      return trace_processor.RunQuery(self._trace_processor_path,
                                      self._trace_file, sql)[0]['str_value']
    except Exception:
      return None

  def _GetIntValueFromQuery(self, sql):
    """Runs SQL query on trace processor and returns 'int_value' result.
    """
    try:
      return trace_processor.RunQuery(self._trace_processor_path,
                                      self._trace_file, sql)[0]['int_value']
    except Exception:
      return None

  def _ExtractValidModuleMap(self):
    """Extracts valid module name to module debug ID map/dict from trace.
    """
    try:
      query_result = trace_processor.RunQuery(self._trace_processor_path,
                                              self._trace_file, MODULES_QUERY)
      module_map = {}
      for row in query_result:
        row_name = row['name']
        row_debug_id = row['build_id']
        # Discard invalid key, value pairs
        if ((row_name is None or row_name == '/missing')
            or (row_debug_id is None or row_debug_id == '/missing')):
          continue
        module_map[row_name] = row_debug_id.upper()

      if not module_map:
        return None
      return module_map

    except Exception:
      return None