# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
""" Orderfile Generation and Testing

This provides a suite of benchmarks for orderfile generation and testing based
on the mobile system health benchmarks.

The orderfile_generation.training benchmark is meant to be run with an
orderfile instrumented build to produce function usage profiles. It can be
invoked as follows:
    (CHROMIUM_OUTPUT_DIR=out/Instrumented; \
     ./tools/perf/run_benchmark --device=${YOUR_DEVICE_SN} --browser=exact \
        --browser-executable=${CHROMIUM_OUTPUT_DIR}/apks/Monochrome.apk \
        orderfile_generation.training)

The orderfile_generation.testing benchmark has a smaller test set whose
benchmarks are distinct from those in orderfile_generation.training. Run it as
follows, note the --orderfile-memory-optimization flag is necessary only with
legacy orderfile builds of chrome.
    (CHROMIUM_OUTPUT_DIR=out/Official; \
     ./tools/perf/run_benchmark --device=${YOUR_DEVICE_SN} --browser=exact \
        --browser-executable=${CHROMIUM_OUTPUT_DIR}/apks/Monochrome.apk \
        --extra-browser-args=--orderfile-memory-optimization \
        --results-label=${LABEL_CORRESPONDING_TO_YOUR_BUILD} \
        orderfile_generation.training)

The orderfile_generation.variation.* benchmarks use a smaller training set so
that several test set variations can be produced. They are run as above but
using the following benchmark names.
    orderfile_generation.variation.training
    orderfile_generation.variation.testing0
    orderfile_generation.variation.testing1
    orderfile_generation.variation.testing2

The orderfile_generation.debugging benchmark is a short benchmark of 3 stories
that is useful for debugging hardware and test setup problems.
"""

import random

from benchmarks import system_health, speedometer3
from page_sets.system_health import platforms
from page_sets.system_health import system_health_stories
from telemetry import benchmark
from telemetry import story
from telemetry.timeline import chrome_trace_category_filter
from telemetry.timeline import chrome_trace_config
from telemetry.web_perf import timeline_based_measurement


class OrderfileStorySet(story.StorySet):
  """User stories for orderfile generation.

  The run set specified in the constructor splits the stories into training and
  testing sets (or debugging, see the code for details).
  """

  # Run set names.
  TRAINING = 'training'
  TESTING = 'testing'
  DEBUGGING = 'debugging'

  _PLATFORM = 'mobile'

  _BLOCKLIST = set(
    [
      # 0% success rate on arm (measured 2024 June).
      'browse:chrome:newtab:2019',
      # 0% success rate on arm (measured 2024 June).
      'browse:chrome:omnibox:2019',
      # 0% success rate on arm64, 2% on arm (measured 2024 June).
      'browse:news:globo:2019',
      # 35% success rate on arm64, 64% on arm (measured 2024 June).
      'browse:news:washingtonpost:2019',
      # 1% success rate on arm64, 17% on arm (measured 2024 June).
      'browse:shopping:amazon:2019',
      # Carried over from previous blocklist.
      'browse:tech:discourse_infinite_scroll:2018',
      # Carried over from previous blocklist.
      'long_running:tools:gmail-background',
      # Carried over from previous blocklist.
      'long_running:tools:gmail-foreground',
    ]
  )

  # The random seed used for reproducible runs.
  SEED = 8675309

  # These defaults are current best practice for production orderfiles.
  DEFAULT_TRAINING = 25
  DEFAULT_TESTING = 8
  DEFAULT_VARIATIONS = 1

  # The number of variations to use with the variation benchmarks. If this is
  # changed, the number of OrderfileVariationTesting* classes declared below
  # should change as well.
  NUM_VARIATION_BENCHMARKS = 3

  def __init__(
    self,
    run_set,
    num_training=DEFAULT_TRAINING,
    num_testing=DEFAULT_TESTING,
    num_variations=DEFAULT_VARIATIONS,
    test_variation=0,
  ):
    """Create an orderfile training or testing benchmark set.

    Args:
      run_set: one of TRAINING, TESTING or DEBUGGING.
      num_training: the number of benchmarks to use for training.
      num_testing: the number of benchmarks to use in each test set.
      num_variations: the number of test set variations.
      test_variation: the test set variation to use.
    """
    super(OrderfileStorySet, self).__init__(
      archive_data_file=(
        '../../page_sets/data/system_health_%s.json' % self._PLATFORM
      ),
      cloud_storage_bucket=story.PARTNER_BUCKET,
    )

    assert self._PLATFORM in platforms.ALL_PLATFORMS, '{} not in {}'.format(
      self._PLATFORM, str(platforms.ALL_PLATFORMS)
    )
    assert run_set in (self.TRAINING, self.TESTING, self.DEBUGGING)
    assert 0 <= test_variation < num_variations

    self._run_set = run_set
    self._num_training = num_training
    self._num_testing = num_testing
    self._num_variations = num_variations
    self._test_variation = test_variation

    # We want the story selection to be consistent across runs.
    random.seed(self.SEED)

    for story_class in self.RunSetStories():
      # pylint: disable=E1102
      self.AddStory(story_class(self, take_memory_measurement=True))

  def RunSetStories(self):
    possible_stories = [
      s
      for s in system_health_stories.IterAllSystemHealthStoryClasses()
      if (
        s.NAME not in self._BLOCKLIST
        and not s.ABSTRACT_STORY
        and self._PLATFORM in s.SUPPORTED_PLATFORMS
      )
    ]
    assert self._num_training + self._num_variations * self._num_testing <= len(
      possible_stories
    ), 'We only have {} stories to work with, but want {} + {}*{}'.format(
      len(possible_stories),
      self._num_training,
      self._num_variations,
      self._num_testing,
    )

    if self._run_set == self.DEBUGGING:
      return random.sample(possible_stories, 3)

    random.shuffle(possible_stories)
    if self._run_set == self.TRAINING:
      return possible_stories[: self._num_training]
    if self._run_set == self.TESTING:
      return possible_stories[
        (self._num_training + self._test_variation * self._num_testing) : (
          self._num_training + (self._test_variation + 1) * self._num_testing
        )
      ]
    assert False, 'Bad run set {}'.format(self._run_set)
    return None


class _OrderfileBenchmark(system_health.MobileMemorySystemHealth):
  """Base benchmark for orderfile generation."""

  STORY_RUN_SET = None  # Must be set in subclasses.

  def CreateStorySet(self, options):
    return OrderfileStorySet(run_set=self.STORY_RUN_SET)


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileTraining(_OrderfileBenchmark):
  STORY_RUN_SET = OrderfileStorySet.TRAINING

  options = {'pageset_repeat': 2}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.training'


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileTesting(_OrderfileBenchmark):
  STORY_RUN_SET = OrderfileStorySet.TESTING

  options = {'pageset_repeat': 7}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.testing'


class _OrderfileVariation(system_health.MobileMemorySystemHealth):
  """Orderfile generation with test set variations."""

  STORY_RUN_SET = None  # Must be set in all subclasses.
  TEST_VARIATION = 0  # Can be overridden testing subclasses.

  options = {'pageset_repeat': 7}

  def CreateStorySet(self, options):
    return OrderfileStorySet(
      run_set=self.STORY_RUN_SET,
      num_training=25,
      num_testing=8,
      num_variations=OrderfileStorySet.NUM_VARIATION_BENCHMARKS,
      test_variation=self.TEST_VARIATION,
    )

  @classmethod
  def Name(cls):
    if cls.STORY_RUN_SET == OrderfileStorySet.TESTING:
      return 'orderfile_generation.variation.testing{}'.format(
        cls.TEST_VARIATION
      )
    if cls.STORY_RUN_SET == OrderfileStorySet.TRAINING:
      return 'orderfile_generation.variation.training'
    assert False, 'Bad STORY_RUN_SET {}'.format(cls.STORY_RUN_SET)
    return None


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileVariationTraining(_OrderfileVariation):
  STORY_RUN_SET = OrderfileStorySet.TRAINING


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileVariationTesting0(_OrderfileVariation):
  STORY_RUN_SET = OrderfileStorySet.TESTING
  TEST_VARIATION = 0


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileVariationTesting1(_OrderfileVariation):
  STORY_RUN_SET = OrderfileStorySet.TESTING
  TEST_VARIATION = 1


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileVariationTesting2(_OrderfileVariation):
  STORY_RUN_SET = OrderfileStorySet.TESTING
  TEST_VARIATION = 2


# pylint: disable=R0901
@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileDebugging(_OrderfileBenchmark):
  """A very short benchmark for debugging metrics collection."""

  STORY_RUN_SET = OrderfileStorySet.DEBUGGING

  options = {'pageset_repeat': 1}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.debugging'


@benchmark.Owner(emails=['mattcary@chromium.org'])
class OrderfileMemory(system_health.MobileMemorySystemHealth):
  """Benchmark for native code memory footprint evaluation."""

  class OrderfileMemoryStorySet(story.StorySet):
    _STORY_SET = set(['browse:news:cnn:2021', 'browse:social:facebook:2019'])

    def __init__(self, platform, take_memory_measurement=True):
      super(OrderfileMemory.OrderfileMemoryStorySet, self).__init__(
        archive_data_file=(
          '../../page_sets/data/system_health_%s.json' % platform
        ),
        cloud_storage_bucket=story.PARTNER_BUCKET,
      )

      assert platform in platforms.ALL_PLATFORMS
      for story_cls in system_health_stories.IterAllSystemHealthStoryClasses():
        if (
          story_cls.ABSTRACT_STORY
          or platform not in story_cls.SUPPORTED_PLATFORMS
          or story_cls.NAME not in self._STORY_SET
        ):
          continue
        self.AddStory(story_cls(self, take_memory_measurement))

  def CreateStorySet(self, options):
    return self.OrderfileMemoryStorySet(platform=self.PLATFORM)

  def CreateCoreTimelineBasedMeasurementOptions(self):
    cat_filter = chrome_trace_category_filter.ChromeTraceCategoryFilter(
      filter_string='-*,disabled-by-default-memory-infra'
    )
    options = timeline_based_measurement.Options(cat_filter)
    # options.config.enable_android_graphics_memtrack = True
    options.SetTimelineBasedMetrics(['nativeCodeResidentMemoryMetric'])
    # Setting an empty memory dump config disables periodic dumps.
    options.config.chrome_trace_config.SetMemoryDumpConfig(
      chrome_trace_config.MemoryDumpConfig()
    )
    return options

  @classmethod
  def Name(cls):
    return 'orderfile.memory_mobile'


@benchmark.Owner(emails=['rasikan@google.com'])
class OrderfileWebViewStartup(
  system_health.WebviewStartupSystemHealthBenchmark
):
  """Benchmark for orderfile generation with WebView startup profiles.

  We need this class to wrap the system_health.webview_startup benchmark so
  we can apply the additional configs that trigger the devtools memory dump.
  We rely on the devtools memory dump to collect the profiles, even though
  this is a startup profile. The reason for this is so we can avoid rebuilding
  the native library between benchmarks, as the memory_mobile based benchmarks
  use this. Rebuilidng/relinking would make it difficult to merge the offsets.
  TODO(b/326927766): remove the devtools_instrumentation_dumping compiler flag
  so we can switch to startup profiling for webview startup.
  """

  def CreateStorySet(self, options):
    return system_health_stories.SystemHealthBlankStorySet(
      take_memory_measurement=True
    )

  def CreateCoreTimelineBasedMeasurementOptions(self):
    cat_filter = chrome_trace_category_filter.ChromeTraceCategoryFilter(
      filter_string='-*,disabled-by-default-memory-infra'
    )
    options = timeline_based_measurement.Options(cat_filter)
    return options

  @classmethod
  def Name(cls):
    return 'orderfile_generation.webview_startup'


@benchmark.Owner(emails=['rasikan@google.com'])
class OrderfileWebViewDebugging(OrderfileWebViewStartup):
  """A very short benchmark for debugging metrics collection."""

  options = {'pageset_repeat': 1}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.webview_startup_debugging'


@benchmark.Owner(emails=['rasikan@google.com'])
class OrderfileSpeedometer3(speedometer3.Speedometer3):
  enable_systrace = True
  take_memory_measurement = True
  options = {'pageset_repeat': 5}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.speedometer3'


@benchmark.Owner(emails=['rasikan@google.com'])
class OrderfileSpeedometer3Debugging(OrderfileSpeedometer3):
  iteration_count = 1
  options = {'pageset_repeat': 1}

  @classmethod
  def Name(cls):
    return 'orderfile_generation.speedometer3_debugging'
