# Copyright 2013 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import print_function

import collections
import os
import re

from core import path_util
from core import perf_benchmark

from telemetry import benchmark
from telemetry import page as page_module
from telemetry.core import exceptions
from telemetry.core import memory_cache_http_server
from telemetry.page import legacy_page_test
from telemetry.page import shared_page_state
from telemetry import story
from telemetry.timeline import bounds
from telemetry.timeline import model as model_module
from telemetry.timeline import tracing_config


BLINK_PERF_BASE_DIR = os.path.join(
  path_util.GetChromiumSrcDir(), 'third_party', 'blink', 'perf_tests'
)
SKIPPED_FILE = os.path.join(BLINK_PERF_BASE_DIR, 'Skipped')

EventBoundary = collections.namedtuple(
  'EventBoundary', ['type', 'wall_time', 'thread_time']
)

MergedEvent = collections.namedtuple(
  'MergedEvent', ['bounds', 'thread_or_wall_duration']
)


class _BlinkPerfPage(page_module.Page):
  def RunPageInteractions(self, action_runner):
    action_runner.ExecuteJavaScript('testRunner.scheduleTestRun()')
    action_runner.WaitForJavaScriptCondition('testRunner.isDone', timeout=600)


def StoryNameFromUrl(url, prefix):
  filename = url[len(prefix) :].strip('/')
  baseName, extension = filename.split('.')
  if extension.find('?') != -1:
    query = extension.split('?')[1]
    baseName += "_" + query  # So that queried page-names don't collide
  return "{b}.{e}".format(b=baseName, e=extension)


def CreateStorySetFromPath(
  path,
  skipped_file,
  shared_page_state_class=(shared_page_state.SharedPageState),
  append_query=None,
  extra_tags=None,
  page_class=_BlinkPerfPage,
):
  assert os.path.exists(path)

  page_urls = []
  serving_dirs = set()

  def _AddPage(path):
    if not path.endswith('.html'):
      return
    if b'../' in open(path, 'rb').read():
      # If the page looks like it references its parent dir, include it.
      serving_dirs.add(os.path.dirname(os.path.dirname(path)))
    page_url = 'file://' + path.replace('\\', '/')
    if append_query:
      page_url += '?' + append_query
    page_urls.append(page_url)

  def _AddDir(dir_path, skipped):
    for candidate_path in os.listdir(dir_path):
      if candidate_path == 'resources':
        continue
      candidate_path = os.path.join(dir_path, candidate_path)
      if candidate_path.startswith(skipped):
        continue
      if os.path.isdir(candidate_path):
        _AddDir(candidate_path, skipped)
      else:
        _AddPage(candidate_path)

  if os.path.isdir(path):
    skipped = []
    if os.path.exists(skipped_file):
      for line in open(skipped_file, 'r').readlines():
        line = line.strip()
        if line and not line.startswith('#'):
          skipped_path = os.path.join(os.path.dirname(skipped_file), line)
          skipped.append(skipped_path.replace('/', os.sep))
    _AddDir(path, tuple(skipped))
  else:
    _AddPage(path)
  ps = story.StorySet(base_dir=os.getcwd() + os.sep, serving_dirs=serving_dirs)

  all_urls = [p.rstrip('/') for p in page_urls]
  common_prefix = os.path.dirname(os.path.commonprefix(all_urls))
  for url in sorted(page_urls):
    name = StoryNameFromUrl(url, common_prefix)
    ps.AddStory(
      page_class(
        url,
        ps,
        ps.base_dir,
        shared_page_state_class=shared_page_state_class,
        name=name,
        tags=extra_tags,
      )
    )
  return ps


def AddScriptToPage(page, script):
  if page.script_to_evaluate_on_commit is None:
    page.script_to_evaluate_on_commit = script
  else:
    page.script_to_evaluate_on_commit += script


def _CreateMergedEventsBoundaries(events, max_start_time):
  """Merge events with the given |event_name| and return a list of MergedEvent
  objects. All events that are overlapping are megred together. Same as a union
  operation.

  Note: When merging multiple events, we approximate the thread_duration:
  duration = (last.thread_start + last.thread_duration) - first.thread_start

  Args:
    events: a list of TimelineEvents
    max_start_time: the maximum time that a TimelineEvent's start value can be.
    Events past this this time will be ignored.

  Returns:
    a sorted list of MergedEvent objects which contain a Bounds object of the
    wall time boundary and a thread_or_wall_duration which contains the thread
    duration if possible, and otherwise the wall duration.
  """
  event_boundaries = []  # Contains EventBoundary objects.
  merged_event_boundaries = []  # Contains MergedEvent objects.

  # Deconstruct our trace events into boundaries, sort, and then create
  # MergedEvents.
  # This is O(N*log(N)), although this can be done in O(N) with fancy
  # datastructures.
  # Note: When merging multiple events, we approximate the thread_duration:
  # dur = (last.thread_start + last.thread_duration) - first.thread_start
  for event in events:
    # Handle events where thread duration is None (async events).
    thread_start = None
    thread_end = None
    if event.thread_start and event.thread_duration:
      thread_start = event.thread_start
      thread_end = event.thread_start + event.thread_duration
    event_boundaries.append(EventBoundary("start", event.start, thread_start))
    event_boundaries.append(EventBoundary("end", event.end, thread_end))
  event_boundaries.sort(key=lambda e: e[1])

  # Merge all trace events that overlap.
  event_counter = 0
  curr_bounds = None
  curr_thread_start = None
  for event_boundary in event_boundaries:
    if event_boundary.type == "start":
      event_counter += 1
    else:
      event_counter -= 1
    # Initialization
    if curr_bounds is None:
      assert event_boundary.type == "start"
      # Exit early if we reach the max time.
      if event_boundary.wall_time > max_start_time:
        return merged_event_boundaries
      curr_bounds = bounds.Bounds()
      curr_bounds.AddValue(event_boundary.wall_time)
      curr_thread_start = event_boundary.thread_time
      continue
    # Create a the final bounds and thread duration when our event grouping
    # is over.
    if event_counter == 0:
      curr_bounds.AddValue(event_boundary.wall_time)
      thread_or_wall_duration = curr_bounds.bounds
      if curr_thread_start and event_boundary.thread_time:
        thread_or_wall_duration = event_boundary.thread_time - curr_thread_start
      merged_event_boundaries.append(
        MergedEvent(curr_bounds, thread_or_wall_duration)
      )
      curr_bounds = None
  return merged_event_boundaries


def _ComputeTraceEventsThreadTimeForBlinkPerf(
  model, renderer_thread, trace_events_to_measure
):
  """Compute the CPU duration for each of |trace_events_to_measure| during
  blink_perf test.

  Args:
    renderer_thread: the renderer thread which run blink_perf test.
    trace_events_to_measure: a list of string names of trace events to measure
    CPU duration for.

  Returns:
    a dictionary in which each key is a trace event' name (from
    |trace_events_to_measure| list), and value is a list of numbers that
    represents to total cpu time of that trace events in each blink_perf test.
  """
  trace_cpu_time_metrics = {}

  # Collect the bounds of "blink_perf.runTest" events.
  test_runs_bounds = []
  for event in renderer_thread.async_slices:
    if event.name == "blink_perf.runTest":
      test_runs_bounds.append(bounds.Bounds.CreateFromEvent(event))
  test_runs_bounds.sort(key=lambda b: b.min)

  for t in trace_events_to_measure:
    trace_cpu_time_metrics[t] = [0.0] * len(test_runs_bounds)

  # Handle case where there are no tests.
  if not test_runs_bounds:
    return trace_cpu_time_metrics

  for event_name in trace_events_to_measure:
    merged_event_boundaries = _CreateMergedEventsBoundaries(
      model.IterAllEventsOfName(event_name), test_runs_bounds[-1].max
    )

    curr_test_runs_bound_index = 0
    for b in merged_event_boundaries:
      if b.bounds.bounds == 0:
        continue
      # Fast forward (if needed) to the first relevant test.
      while (
        curr_test_runs_bound_index < len(test_runs_bounds)
        and b.bounds.min > test_runs_bounds[curr_test_runs_bound_index].max
      ):
        curr_test_runs_bound_index += 1
      if curr_test_runs_bound_index >= len(test_runs_bounds):
        break
      # Add metrics for all intersecting tests, as there may be multiple
      # tests that intersect with the event bounds.
      start_index = curr_test_runs_bound_index
      while curr_test_runs_bound_index < len(
        test_runs_bounds
      ) and b.bounds.Intersects(test_runs_bounds[curr_test_runs_bound_index]):
        intersect_wall_time = bounds.Bounds.GetOverlapBetweenBounds(
          test_runs_bounds[curr_test_runs_bound_index], b.bounds
        )
        intersect_cpu_or_wall_time = (
          intersect_wall_time * b.thread_or_wall_duration / b.bounds.bounds
        )
        trace_cpu_time_metrics[event_name][curr_test_runs_bound_index] += (
          intersect_cpu_or_wall_time
        )
        curr_test_runs_bound_index += 1
      # Rewind to the last intersecting test as it might intersect with the
      # next event.
      curr_test_runs_bound_index = max(
        start_index, curr_test_runs_bound_index - 1
      )
  return trace_cpu_time_metrics


class _BlinkPerfMeasurement(legacy_page_test.LegacyPageTest):
  """Runs a blink performance test and reports the results."""

  def __init__(self):
    super(_BlinkPerfMeasurement, self).__init__()
    with open(
      os.path.join(os.path.dirname(__file__), 'blink_perf.js'), 'r'
    ) as f:
      self._blink_perf_js = f.read()
    self._is_tracing = False
    self._extra_chrome_categories = None
    self._enable_systrace = None

  def WillNavigateToPage(self, page, tab):
    del tab  # unused
    AddScriptToPage(page, self._blink_perf_js)

  def DidNavigateToPage(self, page, tab):
    tab.WaitForJavaScriptCondition('testRunner.isWaitingForTelemetry')
    self._StartTracingIfNeeded(tab)

  def CustomizeBrowserOptions(self, options):
    options.AppendExtraBrowserArgs(
      [
        '--js-flags=--expose_gc',
        '--enable-experimental-web-platform-features',
        '--autoplay-policy=no-user-gesture-required',
      ]
    )

  def SetOptions(self, options):
    super(_BlinkPerfMeasurement, self).SetOptions(options)
    browser_type = options.browser_options.browser_type
    if browser_type and 'content-shell' in browser_type:
      options.AppendExtraBrowserArgs('--expose-internals-for-testing')
    if options.extra_chrome_categories:
      self._extra_chrome_categories = options.extra_chrome_categories
    if options.enable_systrace:
      self._enable_systrace = True

  def _StartTracingIfNeeded(self, tab):
    tracing_categories = tab.EvaluateJavaScript('testRunner.tracingCategories')
    if (
      not tracing_categories
      and not self._extra_chrome_categories
      and not self._enable_systrace
    ):
      return

    self._is_tracing = True
    config = tracing_config.TracingConfig()
    config.enable_chrome_trace = True
    config.chrome_trace_config.category_filter.AddFilterString(
      'blink.console'
    )  # This is always required for js land trace event
    if tracing_categories:
      config.chrome_trace_config.category_filter.AddFilterString(
        tracing_categories
      )
    if self._extra_chrome_categories:
      config.chrome_trace_config.category_filter.AddFilterString(
        self._extra_chrome_categories
      )
    if self._enable_systrace:
      config.chrome_trace_config.SetEnableSystrace()
    tab.browser.platform.tracing_controller.StartTracing(config)

  def PrintAndCollectTraceEventMetrics(self, trace_cpu_time_metrics, results):
    unit = 'ms'
    print()
    for trace_event_name, cpu_times in trace_cpu_time_metrics.items():
      print('CPU times of trace event "%s":' % trace_event_name)
      cpu_times_string = ', '.join(['{0:.10f}'.format(t) for t in cpu_times])
      print('values %s %s' % (cpu_times_string, unit))
      avg = 0.0
      if cpu_times:
        avg = sum(cpu_times) / len(cpu_times)
      print('avg', '{0:.10f}'.format(avg), unit)
      results.AddMeasurement(trace_event_name, unit, cpu_times)
      print()
    print('\n')

  def ValidateAndMeasurePage(self, page, tab, results):
    trace_cpu_time_metrics = {}
    if self._is_tracing:
      trace_data = tab.browser.platform.tracing_controller.StopTracing()
      results.AddTraces(trace_data)
      self._is_tracing = False

      trace_events_to_measure = tab.EvaluateJavaScript(
        'window.testRunner.traceEventsToMeasure'
      )
      if trace_events_to_measure:
        model = model_module.TimelineModel(trace_data)
        renderer_thread = model.GetFirstRendererThread(tab.id)
        trace_cpu_time_metrics = _ComputeTraceEventsThreadTimeForBlinkPerf(
          model, renderer_thread, trace_events_to_measure
        )

    log = tab.EvaluateJavaScript('document.getElementById("log").innerHTML')

    for line in log.splitlines():
      if line.startswith("FATAL: "):
        print(line)
        continue
      if not line.startswith('values '):
        continue
      parts = line.split()
      values = [float(v.replace(',', '')) for v in parts[1:-1]]
      units = parts[-1]
      metric = page.name.split('.')[0].replace('/', '_')
      if values:
        results.AddMeasurement(metric, units, values)
      else:
        raise legacy_page_test.MeasurementFailure('Empty test results')

      break

    print(log)

    self.PrintAndCollectTraceEventMetrics(trace_cpu_time_metrics, results)


class _BlinkPerfBenchmark(perf_benchmark.PerfBenchmark):
  test = _BlinkPerfMeasurement
  TAGS = []

  def CreateStorySet(self, options):
    path = os.path.join(BLINK_PERF_BASE_DIR, self.SUBDIR)
    return CreateStorySetFromPath(path, SKIPPED_FILE, extra_tags=self.TAGS)


@benchmark.Info(
  emails=['gregoryd@google.com', 'lucasradaelli@google.com'],
  component='Blink>Accessibility',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfAccessibility(_BlinkPerfBenchmark):
  SUBDIR = 'accessibility'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.accessibility'

  def SetExtraBrowserOptions(self, options):
    options.AppendExtraBrowserArgs(['--force-renderer-accessibility=complete'])


@benchmark.Info(
  component='Blink>Bindings',
  emails=[
    'jbroman@chromium.org',
    'japhet@chromium.org',
    'haraken@chromium.org',
  ],
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfBindings(_BlinkPerfBenchmark):
  SUBDIR = 'bindings'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.bindings'


class _ServiceWorkerPerfPage(page_module.Page):
  def RunPageInteractions(self, action_runner):
    action_runner.ExecuteJavaScript('testRunner.scheduleTestRun()')

    # If |serviceWorkerPerfTools| is enabled in the test, some actions are
    # performed for each iteration.
    perf_tools_enabled = False
    try:
      perf_tools_enabled = action_runner.EvaluateJavaScript(
        'serviceWorkerPerfTools.enabled'
      )
    except exceptions.EvaluateException:
      pass

    if perf_tools_enabled:
      done = False
      while not done:
        action_runner.WaitForJavaScriptCondition(
          'serviceWorkerPerfTools.actionRequired'
        )
        action = action_runner.EvaluateJavaScript(
          'serviceWorkerPerfTools.action'
        )
        if action == 'stop-workers':
          action_runner.tab.StopAllServiceWorkers()
        elif action == 'quit':
          done = True
        else:
          raise Exception(
            'Not supported ServiceWorkerPerfTools action: {}'.format(action)
          )
        action_runner.EvaluateJavaScript(
          'serviceWorkerPerfTools.notifyActionDone()'
        )
    action_runner.WaitForJavaScriptCondition('testRunner.isDone', timeout=600)


class ServiceWorkerRequestHandler(
  memory_cache_http_server.MemoryCacheDynamicHTTPRequestHandler
):
  """This handler returns dynamic responses for service worker perf tests."""

  _request_count = 0
  _SIZE_1K = 1024
  _SIZE_10K = 10240
  _SIZE_1M = 1048576
  _FILE_NAME_PATTERN_1K = re.compile(
    '.*/service_worker/resources/data/1K_[0-9]+\\.txt'
  )
  _WORKER_NAME_PATTERN = re.compile(
    '.*/service_worker/resources/service-worker-[0-9]+\\.generated\\.js'
  )
  _CHANGING_WORKER_NAME_PATTERN = re.compile(
    '.*/service_worker/resources/changing-service-worker\\.generated\\.js'
  )
  _WORKER_BODY = '''
      self.addEventListener('fetch', (event) => {
        event.respondWith(new Response('hello'));
      });'''

  def ResponseFromHandler(self, path):
    self._request_count += 1
    # normalize the path by replacing backslashes with slashes.
    normpath = path.replace('\\', '/')
    if normpath.endswith('/service_worker/resources/data/10K.txt'):
      return self.MakeResponse('c' * self._SIZE_10K, 'text/plain', False)
    if normpath.endswith('/service_worker/resources/data/1M.txt'):
      return self.MakeResponse('c' * self._SIZE_1M, 'text/plain', False)
    if self._FILE_NAME_PATTERN_1K.match(normpath):
      return self.MakeResponse('c' * self._SIZE_1K, 'text/plain', False)
    if self._WORKER_NAME_PATTERN.match(normpath):
      return self.MakeResponse(self._WORKER_BODY, 'text/javascript', False)
    if self._CHANGING_WORKER_NAME_PATTERN.match(normpath):
      # Return different script content for each request.
      new_body = self._WORKER_BODY + '//' + str(self._request_count)
      return self.MakeResponse(new_body, 'text/javascript', False)
    return None


@benchmark.Info(
  component='Blink>ServiceWorker',
  emails=[
    'yyanagisawa@chromium.org',
    'chrome-worker@google.com',
  ],
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfServiceWorker(_BlinkPerfBenchmark):
  SUBDIR = 'service_worker'

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.service_worker'

  def CreateStorySet(self, options):
    path = os.path.join(BLINK_PERF_BASE_DIR, self.SUBDIR)
    story_set = CreateStorySetFromPath(
      path,
      SKIPPED_FILE,
      extra_tags=self.TAGS,
      page_class=_ServiceWorkerPerfPage,
    )
    story_set.SetRequestHandlerClass(ServiceWorkerRequestHandler)
    with open(
      os.path.join(os.path.dirname(__file__), 'service_worker_perf.js'), 'r'
    ) as f:
      service_worker_perf_js = f.read()
      for page in story_set.stories:
        AddScriptToPage(page, service_worker_perf_js)
    return story_set


@benchmark.Info(
  emails=['csharrison@chromium.org'],
  component='Blink>Internals>WTF',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfBase64(_BlinkPerfBenchmark):
  SUBDIR = 'base64'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.base64'


@benchmark.Info(
  emails=['futhark@chromium.org', 'andruud@chromium.org'],
  documentation_url='https://bit.ly/blink-perf-benchmarks',
  component='Blink>CSS',
)
class BlinkPerfCSS(_BlinkPerfBenchmark):
  SUBDIR = 'css'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.css'


@benchmark.Info(
  emails=['masonf@chromium.org'],
  component='Blink>DOM',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfDOM(_BlinkPerfBenchmark):
  SUBDIR = 'dom'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.dom'


@benchmark.Info(
  emails=['masonf@chromium.org'],
  component='Blink>DOM',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfEvents(_BlinkPerfBenchmark):
  SUBDIR = 'events'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.events'


@benchmark.Info(
  emails=['dalecurtis@chromium.org', 'vmpstr@chromium.org'],
  component='Internals>Images>Codecs',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfImageDecoder(_BlinkPerfBenchmark):
  SUBDIR = 'image_decoder'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.image_decoder'

  def SetExtraBrowserOptions(self, options):
    options.AppendExtraBrowserArgs(
      [
        '--enable-blink-features=JSImageDecode',
      ]
    )


@benchmark.Info(
  emails=['ikilpatrick@chromium.org', 'kojii@chromium.org'],
  component='Blink>Layout',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfLayout(_BlinkPerfBenchmark):
  SUBDIR = 'layout'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.layout'


@benchmark.Info(
  emails=['pmeenan@chromium.org', 'kouhei@chromium.org'],
  component='Blink>Loader',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfLoading(_BlinkPerfBenchmark):
  SUBDIR = 'loading'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.loading'


@benchmark.Info(
  emails=['fergal@chromium.org', 'stevebe@microsoft.com'],
  component='Blink>Storage',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfOWPStorage(_BlinkPerfBenchmark):
  SUBDIR = 'owp_storage'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.owp_storage'

  def SetExtraBrowserOptions(self, options):
    options.AppendExtraBrowserArgs(
      [
        # This ensures that all blobs >= 20MB will be transported by files.
        # TODO(crbug.com/517803856): Fix switch names and the above comment.
        '--blob-transport-by-file-trigger=307300',
        '--blob-transport-min-file-size=2048',
        '--blob-transport-max-file-size=10240',
        '--blob-transport-shared-memory-max-size=30720',
        # Enables tests to optionally exercise database cold-open.
        '--idb-expedite-backing-store-shutdown',
      ]
    )


@benchmark.Info(
  emails=['pdr@chromium.org', 'wangxianzhu@chromium.org'],
  component='Blink>Paint',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfPaint(_BlinkPerfBenchmark):
  SUBDIR = 'paint'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.paint'


@benchmark.Info(
  emails=['sullivan@chromium.org'],
  component='Blink>PerformanceAPIs',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfPerformanceAPIs(_BlinkPerfBenchmark):
  SUBDIR = 'performance_apis'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.performance_apis'


@benchmark.Info(
  emails=['masonf@chromium.org'],
  component='Blink>HTML>Parser',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfParser(_BlinkPerfBenchmark):
  SUBDIR = 'parser'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.parser'


@benchmark.Info(
  emails=['fs@opera.com', 'pdr@chromium.org'],
  component='Blink>SVG',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfSVG(_BlinkPerfBenchmark):
  SUBDIR = 'svg'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.svg'


@benchmark.Info(
  emails=['masonf@chromium.org'],
  component='Blink>DOM>ShadowDOM',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfShadowDOM(_BlinkPerfBenchmark):
  SUBDIR = 'shadow_dom'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.shadow_dom'


@benchmark.Info(
  emails=['vmpstr@chromium.org', 'pdr@chromium.org'],
  component='Blink>Paint',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfDisplayLocking(_BlinkPerfBenchmark):
  SUBDIR = 'display_locking'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.display_locking'

  def SetExtraBrowserOptions(self, options):
    options.AppendExtraBrowserArgs(
      ['--enable-blink-features=DisplayLocking,CSSContentSize']
    )


@benchmark.Info(
  emails=['hongchan@chromium.org', 'mjwilson@chromium.org'],
  component='Blink>WebAudio',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfWebAudio(_BlinkPerfBenchmark):
  SUBDIR = 'webaudio'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.webaudio'


@benchmark.Info(
  emails=['media-dev@chromium.org', 'eugene@chromium.org'],
  component='Blink>WebCodecs',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfWebCodecs(_BlinkPerfBenchmark):
  SUBDIR = 'webcodecs'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'blink_perf.webcodecs'


@benchmark.Info(
  emails=[
    'kbr@chromium.org',
    'kainino@chromium.org',
    'webgl-team@google.com',
  ],
  component='Blink>WebGL',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfWebGL(_BlinkPerfBenchmark):
  SUBDIR = 'webgl'

  @classmethod
  def Name(cls):
    return 'blink_perf.webgl'


@benchmark.Info(
  emails=[
    'kainino@chromium.org',
    'cwallez@chromium.org',
    'webgpu-dev-team@google.com',
  ],
  component='Blink>WebGPU',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfWebGPU(_BlinkPerfBenchmark):
  # Run the WebGPU perf tests ONLY on core-capable adapters.
  SUBDIR = 'webgpu'

  @classmethod
  def Name(cls):
    return 'blink_perf.webgpu'

  def SetExtraBrowserOptionsWithBrowser(self, options, possible_browser):
    # Make sure WebGPU is enabled if possible, even if not shipped.
    # The test itself ensures it won't use a fallback (software) backend.
    options.AppendExtraBrowserArgs(['--enable-unsafe-webgpu'])

    if possible_browser.platform.GetOSName() == 'linux':
      # Flags to make sure Chromium is in the correct mode to run WebGPU.
      # TODO(442791440): TODO(40218893): Remove these flags and switch to
      # testing the backend we get by default. Or, if needed, test both.
      options.AppendExtraBrowserArgs(
        [
          '--enable-features=Vulkan,VulkanFromANGLE',
          '--use-angle=vulkan',
          '--ozone-platform=x11',
        ]
      )


@benchmark.Info(
  emails=[
    'kainino@chromium.org',
    'cwallez@chromium.org',
    'webgpu-dev-team@google.com',
  ],
  component='Blink>WebGPU',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfWebGPUCompat(_BlinkPerfBenchmark):
  # Run the WebGPU perf tests ONLY on core-INcapable adapters.
  SUBDIR = 'webgpu'
  # Currently, Android is the only production target platform for WebGPU
  # Compat's core-incapable backend(s). Enable other Compat configs as needed.
  SUPPORTED_PLATFORMS = [story.expectations.ALL_ANDROID]

  @classmethod
  def Name(cls):
    return 'blink_perf.webgpu_compat'

  def CreateStorySet(self, options):
    path = os.path.join(BLINK_PERF_BASE_DIR, self.SUBDIR)
    return CreateStorySetFromPath(
      path, SKIPPED_FILE, extra_tags=self.TAGS, append_query='compatonly'
    )

  def SetExtraBrowserOptionsWithBrowser(self, options, possible_browser):
    # Make sure WebGPU is enabled if possible, even if not shipped.
    # The test itself ensures it won't use a fallback (software) backend.
    options.AppendExtraBrowserArgs(['--enable-unsafe-webgpu'])

    # Make sure we're only using the GLES backend. Change this if different
    # flags are needed on other Core-incapable configs.
    options.AppendExtraBrowserArgs(['--use-webgpu-adapter=opengles'])


@benchmark.Info(
  emails=[
    'bokan@chromium.org',
    'khushalsagar@chromium.org',
    'vmpstr@chromium.org',
  ],
  component='Blink>ViewTransitions',
  documentation_url='https://bit.ly/blink-perf-benchmarks',
)
class BlinkPerfViewTransitions(_BlinkPerfBenchmark):
  SUBDIR = 'view_transitions'
  TAGS = _BlinkPerfBenchmark.TAGS + ['all']

  @classmethod
  def Name(cls):
    return 'UNSCHEDULED_blink_perf.view_transitions'
