blob: ffb05c946ea738533481398e64a1259551e1b195 [file]
# Copyright 2026 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
import argparse
import json
import os
import subprocess
import sys
import tempfile
import unittest
_TOOL_PATHS = argparse.Namespace(fixture_generator=None, iree_profile=None)
def _runfile_path(relative_path):
workspace = os.environ.get("TEST_WORKSPACE")
roots = [
os.environ.get("RUNFILES_DIR"),
os.environ.get("TEST_SRCDIR"),
os.getcwd(),
]
suffixes = ["", ".exe"] if os.name == "nt" else [""]
for root in roots:
if not root:
continue
prefixes = [root]
if workspace:
prefixes.insert(0, os.path.join(root, workspace))
for prefix in prefixes:
for suffix in suffixes:
path = os.path.join(prefix, relative_path) + suffix
if os.path.exists(path):
return path
raise FileNotFoundError(relative_path)
def _run_checked(argv):
completed = subprocess.run(
argv,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
command = " ".join(argv)
raise AssertionError(
f"command failed: {command}\n"
f"exit_code={completed.returncode}\n"
f"stdout:\n{completed.stdout}\n"
f"stderr:\n{completed.stderr}"
)
return completed
def _run_jsonl(argv):
completed = _run_checked(argv)
return [json.loads(line) for line in completed.stdout.splitlines() if line.strip()]
def _find_row(rows, row_type):
for row in rows:
if row.get("type") == row_type or row.get("record_type") == row_type:
return row
raise AssertionError(f"missing row type {row_type!r}: {rows!r}")
def _row_type_set(rows):
return {row.get("type", row.get("record_type")) for row in rows}
def _parse_tool_paths(argv):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--fixture-generator")
parser.add_argument("--iree-profile")
return parser.parse_known_args(argv)
class IreeProfileCliTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.fixture_generator = (
_TOOL_PATHS.fixture_generator
if _TOOL_PATHS.fixture_generator
else _runfile_path(
"runtime/src/iree/tooling/profile/test_fixture_generator"
)
)
cls.iree_profile = (
_TOOL_PATHS.iree_profile
if _TOOL_PATHS.iree_profile
else _runfile_path("tools/iree-profile")
)
def _create_profile(self, directory, *fixture_flags):
profile_path = os.path.join(directory, "smoke.ireeprof")
_run_checked(
[self.fixture_generator, *fixture_flags, profile_path],
)
return profile_path
def _profile_jsonl(self, profile_path, command, *flags):
return _run_jsonl(
[self.iree_profile, "--format=jsonl", *flags, command, profile_path]
)
def test_report_commands_load_production_sink_bundle(self):
with tempfile.TemporaryDirectory() as directory:
profile_path = self._create_profile(directory)
summary_rows = self._profile_jsonl(profile_path, "summary")
queue_rows = self._profile_jsonl(profile_path, "queue")
command_rows = self._profile_jsonl(profile_path, "command")
executable_rows = self._profile_jsonl(profile_path, "executable")
dispatch_rows = self._profile_jsonl(profile_path, "dispatch")
statistics_rows = self._profile_jsonl(profile_path, "statistics")
self.assertIn("summary", _row_type_set(summary_rows))
self.assertIn("device_summary", _row_type_set(summary_rows))
summary = _find_row(summary_rows, "summary")
self.assertGreater(summary["device_metric_sample_records"], 0)
self.assertIn("queue_event", _row_type_set(queue_rows))
self.assertIn("queue_device_event", _row_type_set(queue_rows))
self.assertIn("host_execution_event", _row_type_set(queue_rows))
self.assertIn("command_operation", _row_type_set(command_rows))
self.assertIn("command_execution", _row_type_set(command_rows))
self.assertIn("command_host_execution", _row_type_set(command_rows))
self.assertIn(
"executable_function_host_dispatch_group", _row_type_set(executable_rows)
)
self.assertIn("host_dispatch_group", _row_type_set(dispatch_rows))
self.assertIn("statistics_summary", _row_type_set(statistics_rows))
self.assertIn("statistics_row", _row_type_set(statistics_rows))
def test_statistics_reports_aggregate_row_families(self):
with tempfile.TemporaryDirectory() as directory:
rows = self._profile_jsonl(self._create_profile(directory), "statistics")
summary = _find_row(rows, "statistics_summary")
self.assertGreater(summary["row_count"], 0)
self.assertEqual(summary["dropped_record_count"], 0)
row_types = {
row["row_type"] for row in rows if row.get("type") == "statistics_row"
}
self.assertIn("dispatch_function", row_types)
self.assertIn("queue_device_operation", row_types)
self.assertIn("queue_host_operation", row_types)
self.assertIn("host_execution_function", row_types)
self.assertIn("memory_lifecycle", row_types)
function_row = next(
row
for row in rows
if row.get("type") == "statistics_row"
and row.get("row_type") == "dispatch_function"
)
self.assertEqual(function_row["function_name"], "smoke_function")
self.assertGreater(function_row["total_duration_ns"], 0)
def test_queue_projection_separates_host_and_device_time(self):
with tempfile.TemporaryDirectory() as directory:
rows = self._profile_jsonl(self._create_profile(directory), "queue")
queue_event = _find_row(rows, "queue_event")
self.assertEqual(queue_event["host_time_domain"], "iree_host_time_ns")
self.assertLessEqual(
queue_event["host_time_ns"], queue_event["ready_host_time_ns"]
)
device_event = _find_row(rows, "queue_device_event")
self.assertIn("start_tick", device_event)
self.assertIn("end_tick", device_event)
self.assertIn("duration_ns", device_event)
host_event = _find_row(rows, "host_execution_event")
self.assertEqual(host_event["host_time_domain"], "iree_host_time_ns")
self.assertEqual(queue_event["submission_id"], device_event["submission_id"])
self.assertEqual(queue_event["submission_id"], host_event["submission_id"])
self.assertEqual(queue_event["op"], device_event["op"])
self.assertEqual(queue_event["op"], host_event["op"])
def test_command_and_dispatch_projections_join_ids(self):
with tempfile.TemporaryDirectory() as directory:
profile_path = self._create_profile(directory)
command_rows = self._profile_jsonl(profile_path, "command")
dispatch_rows = self._profile_jsonl(
profile_path, "dispatch", "--dispatch_events"
)
operation = _find_row(command_rows, "command_operation")
self.assertEqual(operation["op"], "dispatch")
execution = _find_row(command_rows, "command_execution")
self.assertEqual(operation["command_buffer_id"], execution["command_buffer_id"])
dispatch_event = _find_row(dispatch_rows, "dispatch_event")
self.assertEqual(
operation["command_buffer_id"], dispatch_event["command_buffer_id"]
)
self.assertEqual(operation["command_index"], dispatch_event["command_index"])
self.assertEqual(dispatch_event["device_tick_domain"], "device_tick")
self.assertEqual(
dispatch_event["duration_time_domain"], "device_tick_duration_ns"
)
host_dispatch_event = _find_row(dispatch_rows, "host_dispatch_event")
self.assertEqual(
operation["command_buffer_id"], host_dispatch_event["command_buffer_id"]
)
self.assertEqual(
operation["command_index"], host_dispatch_event["command_index"]
)
self.assertEqual(host_dispatch_event["timing_source"], "host_execution_event")
self.assertEqual(host_dispatch_event["time_domain"], "iree_host_time_ns")
self.assertEqual(
host_dispatch_event["duration_time_domain"], "iree_host_duration_ns"
)
def test_explain_uses_host_dispatch_spans_without_device_dispatches(self):
with tempfile.TemporaryDirectory() as directory:
profile_path = self._create_profile(directory, "--omit-dispatch-events")
rows = self._profile_jsonl(profile_path, "explain")
summary = _find_row(rows, "explain_summary")
self.assertEqual(summary["valid_dispatches"], 0)
self.assertGreater(summary["valid_host_dispatches"], 0)
top_function = _find_row(rows, "explain_top_function")
self.assertEqual(top_function["timing_source"], "host_execution_event")
self.assertGreater(top_function["total_ns"], 0)
self.assertGreater(top_function["total_tile_count"], 0)
top_dispatch = _find_row(rows, "explain_top_dispatch")
self.assertEqual(top_dispatch["timing_source"], "host_execution_event")
self.assertGreater(top_dispatch["duration_ns"], 0)
self.assertGreater(top_dispatch["tile_count"], 0)
def test_export_ireeperf_jsonl_decodes_record_families(self):
with tempfile.TemporaryDirectory() as directory:
profile_path = self._create_profile(directory)
output_path = os.path.join(directory, "smoke.ireeperf.jsonl")
_run_checked(
[
self.iree_profile,
"--format=ireeperf-jsonl",
f"--output={output_path}",
"export",
profile_path,
]
)
with open(output_path, "r", encoding="utf-8") as file:
rows = [json.loads(line) for line in file if line.strip()]
schema = _find_row(rows, "schema")
self.assertEqual(schema["format"], "ireeperf-jsonl")
self.assertEqual(schema["row_key"], "record_type")
queue_event = _find_row(rows, "queue_event")
self.assertEqual(queue_event["host_time_domain"], "iree_host_time_ns")
self.assertLessEqual(
queue_event["host_time_ns"], queue_event["ready_host_time_ns"]
)
device_event = _find_row(rows, "queue_device_event")
self.assertIn("derived_time_available", device_event)
self.assertIn("duration_ns", device_event)
relationship = _find_row(rows, "event_relationship")
self.assertEqual(relationship["kind"], "queue_event_host_execution_event")
self.assertEqual(relationship["source_type"], "queue_event")
self.assertEqual(relationship["target_type"], "host_execution_event")
metric_source = _find_row(rows, "device_metric_source")
self.assertEqual(metric_source["device_class"], "gpu")
self.assertEqual(metric_source["name"], "smoke.metrics")
metric_descriptor = _find_row(rows, "device_metric_descriptor")
self.assertIn(metric_descriptor["unit"], {"count", "millipercent"})
metric_sample = _find_row(rows, "device_metric_sample")
self.assertEqual(metric_sample["host_time_domain"], "iree_host_time_ns")
self.assertEqual(len(metric_sample["values"]), 2)
value_names = {value["name"] for value in metric_sample["values"]}
self.assertIn("activity.compute", value_names)
self.assertIn("smoke.source_specific", value_names)
self.assertTrue(all("value" in value for value in metric_sample["values"]))
if __name__ == "__main__":
_TOOL_PATHS, remaining_argv = _parse_tool_paths(sys.argv[1:])
unittest.main(argv=[sys.argv[0], *remaining_argv])