blob: c665ca95d1379c1e41efa56aa755785f931f7116 [file]
# Copyright 2023 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 annotations
import argparse
import logging
import unittest
from unittest import mock
from typing_extensions import override
import crossbench.path as pth
from crossbench.browsers.settings import Settings
from crossbench.cli.config.probe import ProbeConfig
from crossbench.cli.config.probe_list import ProbeListConfig
from crossbench.plt.android_adb import Adb
from crossbench.plt.arch import MachineArch
from crossbench.probes.all import PerfettoProbe
from crossbench.probes.perfetto.context.android import \
AndroidPerfettoProbeContext
from crossbench.probes.perfetto.context.chromeos import \
ChromeOsPerfettoProbeContext
from crossbench.probes.perfetto.context.desktop import \
DesktopPerfettoProbeContext
from crossbench.probes.perfetto.downloader import PerfettoToolDownloader
from crossbench.probes.perfetto.perfetto import TraceConfig
from protoc import trace_config_pb2
from tests import test_helper
from tests.crossbench.base import CrossbenchConfigTestMixin, \
CrossbenchFakeFsTestCase
from tests.crossbench.mock_browser import MockChromeStable
from tests.crossbench.mock_helper import AndroidAdbMockPlatform, \
ChromeOsSshMockPlatform, LinuxMockPlatform, MacOsMockPlatform, MockPopen, \
MockPopenState, WinMockPlatform
from tests.crossbench.runner.helper import MockRun
class TraceConfigTestCase(unittest.TestCase):
def test_parse_preset(self):
config = TraceConfig.parse("v8")
self.assertIsInstance(config, TraceConfig)
# v8 preset has some data sources
self.assertTrue(len(config.trace_config.data_sources) > 0)
def test_parse_dict_raw_proto(self):
config = TraceConfig.parse({
"buffers": [{
"size_kb": 1024
}],
"data_sources": [{
"config": {
"name": "linux.process_stats"
}
}]
})
self.assertIsInstance(config, TraceConfig)
self.assertEqual(len(config.trace_config.buffers), 1)
self.assertEqual(config.trace_config.buffers[0].size_kb, 1024)
self.assertEqual(len(config.trace_config.data_sources), 1)
self.assertEqual(config.trace_config.data_sources[0].config.name,
"linux.process_stats")
def test_parse_str_close_match(self):
with self.assertLogs(level="ERROR") as cm:
# "v8-profiling" is a valid preset, "v8-profiler" is close.
config = TraceConfig.parse_str("v8-profiler")
self.assertIsInstance(config, TraceConfig)
# It should have picked "v8-profiling"
self.assertEqual(config, TraceConfig.parse_str("v8-profiling"))
self.assertIn("v8-profiling", cm.output[0])
class PerfettoProbeTestCase(unittest.TestCase):
def test_parse_empty(self):
with self.assertRaisesRegex(argparse.ArgumentTypeError, "empty"):
PerfettoProbe.parse_str("")
def test_create_empty(self):
with self.assertRaisesRegex(argparse.ArgumentTypeError, "empty"):
_ = PerfettoProbe()
def test_parse_tags_mixed(self):
probe = PerfettoProbe.parse_tags("tag1,+tag2,-tag3")
self.assertEqual(probe.enabled_tags, ("tag1", "tag2"))
self.assertEqual(probe.disabled_tags, ("tag3",))
def test_merged_simple(self):
probe = PerfettoProbe.parse_str("v8")
merged = probe.trace_config
self.assertIsInstance(merged, trace_config_pb2.TraceConfig)
def test_merged_tags(self):
probe = PerfettoProbe.parse_str("tag1,-tag2")
merged = probe.trace_config
track_event_configs = [
ds.config.track_event_config
for ds in merged.data_sources
if ds.config.name == "track_event"
]
self.assertEqual(len(track_event_configs), 1)
te_config = track_event_configs[0]
self.assertIn("tag1", te_config.enabled_tags)
self.assertIn("tag2", te_config.disabled_tags)
def test_merged_categories(self):
probe = PerfettoProbe.parse_dict({
"enabled_categories": ["cat1"],
"disabled_categories": ["cat2"],
"enabled_tags": ["cat4"]
})
merged = probe.trace_config
self.assertIsInstance(merged, trace_config_pb2.TraceConfig)
track_event_configs = [
ds.config.track_event_config
for ds in merged.data_sources
if ds.config.name == "track_event"
]
self.assertEqual(len(track_event_configs), 1)
te_config = track_event_configs[0]
self.assertIn("cat1", te_config.enabled_categories)
self.assertIn("cat2", te_config.disabled_categories)
self.assertIn("cat4", te_config.enabled_tags)
def test_parse_dict_combined(self):
probe = PerfettoProbe.parse_dict({
"trace_config": "v8",
"tags": ["tag1"],
"categories": ["cat1"]
})
self.assertIsInstance(probe, PerfettoProbe)
# v8 preset has some data sources
self.assertTrue(len(probe.trace_config.data_sources) > 0)
merged = probe.trace_config
track_event_configs = [
ds.config.track_event_config
for ds in merged.data_sources
if ds.config.name == "track_event"
]
self.assertEqual(len(track_event_configs), 1)
te_config = track_event_configs[0]
self.assertIn("tag1", te_config.enabled_tags)
self.assertIn("cat1", te_config.enabled_categories)
def test_empty_dict_config(self):
with self.assertRaisesRegex(argparse.ArgumentTypeError, "empty"):
PerfettoProbe.parse_dict({})
def test_missing_config(self):
with self.assertRaisesRegex(argparse.ArgumentTypeError, "unknown 1"):
PerfettoProbe.parse_dict({"preset": "unknown 1"})
with self.assertRaisesRegex(argparse.ArgumentTypeError, "unknown 1"):
PerfettoProbe.parse_str("unknown 1")
def test_parse_config(self):
trace_config = """
buffers: {
size_kb: 1234
fill_policy: DISCARD
}
"""
probe: PerfettoProbe = PerfettoProbe.parse_dict(
{"trace_config": trace_config})
self.assertEqual(probe.trace_config.buffers[0].size_kb, 1234)
self.assertEqual(pth.AnyPath("perfetto"), probe.perfetto_bin)
def test_parse_example_config(self):
config_file = test_helper.config_dir() / "doc/probe/perfetto.config.hjson"
self.assertTrue(config_file.is_file())
probes = ProbeListConfig.parse(config_file).probes
self.assertEqual(len(probes), 1)
probe = probes[0]
self.assertIsInstance(probe, PerfettoProbe)
def test_trace_config_preset_invalid_file(self):
trace_config_dir = test_helper.config_dir() / "probe/perfetto/trace_config"
for config_file in trace_config_dir.glob("*.pbtxt"):
self.fail(f"Invalid preset file extension, use .textpb: {config_file}")
def test_trace_config_preset(self):
trace_config_dir = test_helper.config_dir() / "probe/perfetto/trace_config"
preset_count = 0
for config_file in trace_config_dir.glob("*.txtpb"):
preset_count += 1
with self.subTest(config_file=str(config_file)):
probe_a = PerfettoProbe.parse_dict({"trace_config": config_file.stem})
probe_b = PerfettoProbe.parse_str(config_file.stem)
probe_c = PerfettoProbe.parse_str(str(config_file))
self.assertEqual(probe_a.trace_config, probe_b.trace_config)
self.assertEqual(probe_a.trace_config, probe_c.trace_config)
for data_source in probe_b.trace_config.data_sources:
config = data_source.config
self.assertNotEqual(
config.name, "org.chromium.trace_metadata",
"Please use the new org.chromium.trace_metadata2 data_source "
"without the added json-serialized categories.")
self.assertFalse(
config.chrome_config and config.chrome_config.trace_config,
"Please use the org.chromium.trace_metadata2 data source "
"which does not require the json-serialized trace_config")
self.assertGreater(preset_count, 0)
def test_preset_file_from_probe_config(self):
trace_config_file = TraceConfig.preset_dir() / "v8.txtpb"
self.assertTrue(trace_config_file.is_file())
probe = PerfettoProbe.parse_str(str(trace_config_file))
probe_a = ProbeConfig.parse("perfetto:v8").new_instance()
self.assertTrue(trace_config_file.is_file())
src_str = f"perfetto:{trace_config_file}"
probe_b = ProbeConfig.parse(src_str).new_instance()
self.assertEqual(probe, probe_a)
self.assertEqual(probe, probe_b)
class PerfettoToolDownloaderTestCase(CrossbenchFakeFsTestCase):
def test_download_linux(self):
platform = LinuxMockPlatform()
self._download_perfetto_tool(platform, "linux-arm64")
platform = LinuxMockPlatform()
platform.machine = MachineArch.ARM_32
self._download_perfetto_tool(platform, "linux-arm")
platform = LinuxMockPlatform()
platform.machine = MachineArch.X64
self._download_perfetto_tool(platform, "linux-x64")
def test_download_macos(self):
platform = MacOsMockPlatform()
self._download_perfetto_tool(platform, "mac-arm64")
platform = MacOsMockPlatform()
platform.machine = MachineArch.X64
self._download_perfetto_tool(platform, "mac-amd64")
def test_download_win_invalid(self):
platform = WinMockPlatform()
with self.assertRaises(ValueError):
self._download_perfetto_tool(platform, "win-arm64")
def _download_perfetto_tool(self, platform, key):
platform.use_mock_name = False
download_path = platform.cache_dir("perfetto") / "v53.0/traceconv"
platform.expect_download(
"https://commondatastorage.googleapis.com/perfetto-luci-artifacts/"
f"v53.0/{key}/traceconv", download_path)
platform.expect_sh(
download_path,
"--version",
result=("Perfetto v53.0-7a9a6a0 "
"(7a9a6a0587348bffd1796b66a1da33cc1ea421d8)"))
result = PerfettoToolDownloader("traceconv", platform=platform).download()
self.assertTrue(platform.exists(result))
# downloading the same will use the locally cached version
result = PerfettoToolDownloader("traceconv", platform=platform).download()
self.assertTrue(platform.exists(result))
class PerfettoProbeFunctionalTestCase(CrossbenchConfigTestMixin,
CrossbenchFakeFsTestCase):
@override
def setUp(self) -> None:
super().setUp()
self.setup_perfetto_config_presets()
def test_attach_v8_code_logger(self):
trace_config = trace_config_pb2.TraceConfig()
data_source = trace_config.data_sources.add()
data_source.config.name = "dev.v8.code"
probe = PerfettoProbe(trace_config=trace_config)
platform = LinuxMockPlatform()
MockChromeStable.setup_fs(self.fs, platform)
browser = MockChromeStable(
"mock browser", settings=Settings(platform=platform))
probe.attach(browser)
self.assertIn("--perfetto-code-logger", browser.js_flags)
def test_log_results(self):
probe = PerfettoProbe.parse_str("v8")
run = mock.Mock()
run.results = {probe: mock.Mock(file=self.create_file("perfetto.trace.pb"))}
with self.assertLogs(level=logging.INFO) as cm:
probe.log_run_result(run)
self.assertIn("Perfetto trace results", cm.output[1])
group = mock.Mock()
group.runs = [run]
with self.assertLogs(level=logging.INFO) as cm:
probe.log_browsers_result(group)
self.assertIn("Perfetto trace results", cm.output[1])
def test_help_text_items(self):
TraceConfig.presets.cache_clear()
# Ensure there is at least one preset in the fake FS
dummy_preset = TraceConfig.preset_dir() / "dummy_preset.txtpb"
self.fs.create_file(dummy_preset)
help_items = TraceConfig.help_text_items()
self.assertTrue(any(k == "presets" for k, v in help_items))
presets_str = dict(help_items)["presets"]
self.assertIn("dummy_preset", presets_str)
def test_create_context_desktop(self):
probe = PerfettoProbe.parse_str("v8")
host_platform = LinuxMockPlatform()
run_linux = mock.Mock()
run_linux.out_dir = pth.LocalPath("/tmp")
run_linux.browser_platform = host_platform
context = probe.create_context(run_linux)
self.assertIsInstance(context, DesktopPerfettoProbeContext)
def test_create_context_android(self):
probe = PerfettoProbe.parse_str("v8")
host_platform = LinuxMockPlatform()
adb_bin = host_platform.path("/usr/bin/adb")
self.fs.create_file(adb_bin)
adb = mock.Mock(spec=Adb)
adb.host_platform = host_platform
adb.serial_id = "777"
run_android = mock.Mock()
run_android.out_dir = pth.LocalPath("/tmp")
run_android.browser_platform = AndroidAdbMockPlatform(
host_platform=host_platform, adb=adb)
context = probe.create_context(run_android)
self.assertIsInstance(context, AndroidPerfettoProbeContext)
def test_create_context_chrome_os(self):
probe = PerfettoProbe.parse_str("v8")
host_platform = LinuxMockPlatform()
run_chromeos = mock.Mock()
run_chromeos.out_dir = pth.LocalPath("/tmp")
run_chromeos.browser_platform = ChromeOsSshMockPlatform(
host_platform=host_platform,
host="host",
port=22,
ssh_port=22,
ssh_user="user")
context = probe.create_context(run_chromeos)
self.assertIsInstance(context, ChromeOsPerfettoProbeContext)
def test_get_extra_probes(self):
probe = PerfettoProbe.parse_str("v8")
runner = mock.Mock()
extra_probes = list(probe.get_extra_probes(runner))
self.assertIsInstance(extra_probes, list)
class PerfettoProbeContextTestCase(CrossbenchConfigTestMixin,
CrossbenchFakeFsTestCase):
@override
def setUp(self) -> None:
super().setUp()
self.setup_perfetto_config_presets()
def test_desktop_context_lifecycle(self):
probe = PerfettoProbe.parse_str("v8")
platform = LinuxMockPlatform(fake_fs=self.fs)
platform.install_mock_binary("tracebox", "/usr/bin/tracebox")
platform.install_mock_binary("perfetto", "/usr/bin/perfetto")
tracebox_proc = MockPopen()
platform.popens.append(tracebox_proc)
MockChromeStable.setup_fs(self.fs, platform)
browser = MockChromeStable(
"mock browser", settings=Settings(platform=platform))
browser.performance_mark = mock.Mock()
browser_session = mock.Mock()
browser_session.browser = browser
browser_session.root_dir = pth.LocalPath("/tmp/results")
self.fs.create_dir(browser_session.root_dir)
run = MockRun(
runner=mock.Mock(), browser_session=browser_session, story=mock.Mock())
# MockRun calculates out_dir from browser_session.root_dir
self.fs.create_dir(run.out_dir)
run.get_default_probe_result_path = mock.Mock(return_value=run.out_dir /
"perfetto")
context = DesktopPerfettoProbeContext(probe, run)
# setup
platform.expect_sh("tracebox", "traced", "traced_probes", result="")
self.assertEqual(tracebox_proc.state, MockPopenState.UNUSED)
context.setup()
self.assertTrue(self.fs.exists(run.out_dir / "perfetto_config.textproto"))
self.assertEqual(tracebox_proc.state, MockPopenState.RUNNING)
# start
platform.expect_sh(
"tracebox",
"perfetto",
"--background",
"--config",
str(run.out_dir / "perfetto_config.textproto"),
"--txt",
"--out",
str(run.out_dir / "perfetto.trace.pb"),
result="123")
context.start()
run.browser.performance_mark.assert_called_with("probe-perfetto-start")
self.assertEqual(tracebox_proc.state, MockPopenState.RUNNING)
context.stop()
self.assertEqual(tracebox_proc.state, MockPopenState.RUNNING)
run.browser.performance_mark.assert_called_with("probe-perfetto-stop")
# teardown
trace_file = run.browser_platform.local_path(context.result_path)
self.fs.create_file(trace_file)
platform.expect_sh("gzip", str(trace_file))
self.fs.create_file(trace_file.with_suffix(".pb.gz"))
context.teardown()
self.assertEqual(tracebox_proc.state, MockPopenState.TERMINATED)
if __name__ == "__main__":
test_helper.run_pytest(__file__)