CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
1169 lines
42 KiB
Python
1169 lines
42 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""Unit tests for analysis_db.py static methods."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import common
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from rocprof_compute_analyze.analysis_db import db_analysis
|
|
from utils import analysis_orm as orm
|
|
from utils import schema
|
|
from utils.metrics.noise_clamper import (
|
|
clear_noise_clamp_warnings,
|
|
get_noise_clamp_warnings,
|
|
)
|
|
|
|
|
|
def make_dual_issue_arch_config(metric_name: str, peak_col: str = "Peak"):
|
|
"""Build an arch_config with a metric_table carrying one VALU row."""
|
|
metric_df = pd.DataFrame(
|
|
{
|
|
"Metric": [metric_name],
|
|
"Value": ["unused_expression"],
|
|
peak_col: ["unused_peak_expression"],
|
|
},
|
|
index=pd.Index(["1.1"], name="Metric_ID"),
|
|
)
|
|
arch_config = schema.ArchConfig()
|
|
arch_config.dfs = {201: metric_df}
|
|
arch_config.dfs_type = {201: "metric_table"}
|
|
return arch_config
|
|
|
|
|
|
# =============================================================================
|
|
# db_analysis.evaluate() tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_evaluate_parse_false_basic_expressions():
|
|
"""Test parse=False mode with basic expressions and substitutions."""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [10, 20, 30],
|
|
"Counter2": [1, 2, 3],
|
|
})
|
|
sys_info = {"numCUs": 64, "clock_speed": 1500}
|
|
|
|
# Test raw_pmc_df -> pmc_df substitution on flat single-index columns
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"raw_pmc_df['Counter1']",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert isinstance(result, pd.Series)
|
|
assert list(result) == [10, 20, 30]
|
|
|
|
# Test ammolite__ substitution for sys_info access
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"ammolite__numCUs * 2",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result == 128
|
|
|
|
# Test expression with helper function
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"to_sum(raw_pmc_df['Counter1'])",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result == 60
|
|
|
|
|
|
def test_evaluate_parse_true_basic_expressions():
|
|
"""Test parse=True mode with $ substitution and AST transformation."""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [10, 20, 30],
|
|
"Counter2": [2, 4, 6],
|
|
})
|
|
sys_info = {"numCUs": 64, "multiplier": 2}
|
|
|
|
# Test $variable substitution
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"$numCUs * $multiplier",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=True,
|
|
)
|
|
assert result == 128
|
|
|
|
# Test AST transformation with SUPPORTED_CALL functions (SUM -> to_sum)
|
|
# and bare identifiers (Counter1 -> raw_pmc_df["Counter1"])
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"SUM(Counter1)",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=True,
|
|
)
|
|
assert result == 60
|
|
|
|
# Test combined $ substitution and column access with AVG
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"AVG(Counter1) + $numCUs",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=True,
|
|
)
|
|
assert result == 84 # avg(10,20,30)=20 + 64
|
|
|
|
|
|
def test_evaluate_none_and_na_handling():
|
|
"""Test evaluate() handling of None and NA values."""
|
|
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
|
|
sys_info = {}
|
|
|
|
# Explicit None in expression result returns None without warning
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"None",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result is None
|
|
|
|
# Scalar NA values (NaN) return None
|
|
pmc_df_nan = pd.DataFrame({"Counter1": [np.nan, np.nan, np.nan]})
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"to_sum(raw_pmc_df['Counter1'])",
|
|
pmc_df_nan,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result is None
|
|
|
|
# Series with NA values are preserved (not converted to None)
|
|
pmc_df_mixed = pd.DataFrame({"Counter1": [10, np.nan, 30]})
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"raw_pmc_df['Counter1']",
|
|
pmc_df_mixed,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert isinstance(result, pd.Series)
|
|
assert result.iloc[0] == 10
|
|
assert pd.isna(result.iloc[1])
|
|
assert result.iloc[2] == 30
|
|
|
|
# Exceptions return None gracefully
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"raw_pmc_df['NonExistent']",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result is None
|
|
|
|
|
|
def test_evaluate_with_none_in_formula_does_not_nullify_valid_result():
|
|
"""
|
|
Test that expressions containing 'None' in formula string
|
|
still return valid results when evaluation produces a value.
|
|
|
|
This is a regression test for the bugfix where expressions like
|
|
.where(..., None) were incorrectly returning None even when
|
|
the actual result was valid.
|
|
"""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [10, 20, 30],
|
|
"Counter2": [1, 0, 3], # Has a zero for conditional
|
|
})
|
|
sys_info = {}
|
|
|
|
# Expression with None as fallback in .where() - should return valid result
|
|
# when condition is met for at least some values
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"(raw_pmc_df['Counter1'] / "
|
|
"raw_pmc_df['Counter2'].where("
|
|
"raw_pmc_df['Counter2'] != 0, None))",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
# Result should be a Series, not None
|
|
assert result is not None
|
|
assert isinstance(result, pd.Series)
|
|
|
|
# Expression that literally has "None" string but evaluates to a number
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
"10 if True else None",
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
assert result == 10
|
|
|
|
|
|
def test_evaluate_divide_by_zero_silenced_and_logged_at_debug():
|
|
"""
|
|
Divide-by-zero (x/0 -> inf, 0/0 -> NaN) emits a numpy RuntimeWarning
|
|
that is captured and logged via console_debug. The "evaluated to N/A"
|
|
console_warning must not fire when a RuntimeWarning was caught.
|
|
"""
|
|
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
|
|
sys_info = {}
|
|
|
|
cases = [
|
|
# x/0 yields scalar inf; evaluate() collapses to None
|
|
"to_sum(raw_pmc_df['Counter1']) / 0",
|
|
# 0/0 yields scalar NaN; evaluate() collapses to None
|
|
"(to_sum(raw_pmc_df['Counter1']) * 0) / 0",
|
|
]
|
|
|
|
for expr in cases:
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.console_warning"
|
|
) as mock_warning:
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.console_debug"
|
|
) as mock_debug:
|
|
result = db_analysis.evaluate(
|
|
"test_metric",
|
|
expr,
|
|
pmc_df,
|
|
sys_info,
|
|
parse=False,
|
|
)
|
|
|
|
assert result is None, f"Expected None for '{expr}', got {result}"
|
|
|
|
mock_warning.assert_not_called()
|
|
debug_msgs = [str(call) for call in mock_debug.call_args_list]
|
|
assert any("RuntimeWarning" in m for m in debug_msgs), (
|
|
f"Expected RuntimeWarning in console_debug output for '{expr}', "
|
|
f"got {debug_msgs}"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# db_analysis.calc_builtin_vars() tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_calc_builtin_vars_processes_per_xcd_first():
|
|
"""
|
|
Test that PER_XCD variables are processed before non-PER_XCD variables,
|
|
allowing non-PER_XCD vars to reference PER_XCD vars via $placeholder.
|
|
"""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [100, 200],
|
|
})
|
|
sys_info = {"base_value": 10, "gpu_arch": "gfx942"}
|
|
|
|
# Mock BUILD_IN_VARS with dependency chain:
|
|
# - PER_XCD_VAR: computed from base_value
|
|
# - DERIVED_VAR: depends on PER_XCD_VAR via $PER_XCD_VAR
|
|
mock_builtin_vars = {
|
|
"PER_XCD_VAR": "$base_value * 2", # Should be processed first -> 20
|
|
"DERIVED_VAR": "$PER_XCD_VAR + 5", # Depends on PER_XCD_VAR -> 25
|
|
}
|
|
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
|
|
return_value="MI300",
|
|
):
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
with patch(
|
|
"utils.utils_counter_defs.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
db_analysis.calc_builtin_vars(
|
|
pmc_df, sys_info, ["$PER_XCD_VAR", "$DERIVED_VAR"]
|
|
)
|
|
|
|
# Verify PER_XCD var was computed
|
|
assert sys_info["PER_XCD_VAR"] == 20
|
|
|
|
# Verify DERIVED_VAR used the computed PER_XCD_VAR value
|
|
assert sys_info["DERIVED_VAR"] == 25
|
|
|
|
|
|
def test_calc_builtin_vars_with_dataframe_expressions():
|
|
"""Test builtin vars that operate on DataFrame columns."""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [10, 20, 30],
|
|
})
|
|
sys_info = {"multiplier": 2, "gpu_arch": "gfx942"}
|
|
|
|
# Use SUPPORTED_CALL function names (SUM -> to_sum via CodeTransformer)
|
|
mock_builtin_vars = {
|
|
"TOTAL_COUNT": "SUM(Counter1)", # 60
|
|
"SCALED_TOTAL": "$TOTAL_COUNT * $multiplier", # 120
|
|
}
|
|
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
|
|
return_value="MI300",
|
|
):
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
with patch(
|
|
"utils.utils_counter_defs.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
db_analysis.calc_builtin_vars(
|
|
pmc_df, sys_info, ["$TOTAL_COUNT", "$SCALED_TOTAL"]
|
|
)
|
|
|
|
assert sys_info["TOTAL_COUNT"] == 60
|
|
assert sys_info["SCALED_TOTAL"] == 120
|
|
|
|
|
|
# =============================================================================
|
|
# db_analysis.calc_dataframe_expressions() tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_calc_dataframe_expressions_applies_evaluate_to_rows():
|
|
"""Test that expressions are evaluated for each row of expression_df."""
|
|
pmc_df = pd.DataFrame({
|
|
"Counter1": [10, 20, 30],
|
|
"Counter2": [1, 2, 3],
|
|
})
|
|
sys_info = {"scale": 100, "gpu_arch": "gfx942"}
|
|
|
|
expression_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.2"],
|
|
"value_name": ["sum", "scaled"],
|
|
"value": [
|
|
"to_sum(raw_pmc_df['Counter1'])",
|
|
"ammolite__scale * 2",
|
|
],
|
|
})
|
|
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.get_build_in_vars", return_value={}
|
|
):
|
|
result = db_analysis.calc_dataframe_expressions(pmc_df, sys_info, expression_df)
|
|
|
|
assert isinstance(result, pd.Series)
|
|
assert len(result) == 2
|
|
assert result.iloc[0] == 60 # sum of Counter1
|
|
assert result.iloc[1] == 200 # 100 * 2
|
|
|
|
|
|
def test_calc_dataframe_expressions_with_builtin_vars():
|
|
"""Test that calc_dataframe_expressions calls calc_builtin_vars first."""
|
|
pmc_df = pd.DataFrame({"Counter1": [10, 20, 30]})
|
|
sys_info = {"base": 5, "gpu_arch": "gfx942"}
|
|
|
|
# Expression references a builtin var that gets computed
|
|
mock_builtin_vars = {
|
|
"COMPUTED_VAR": "$base * 10", # 50
|
|
}
|
|
|
|
expression_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.2"],
|
|
"value_name": ["test", "none_result"],
|
|
"value": [
|
|
"ammolite__COMPUTED_VAR + 1", # Should be 51
|
|
"None",
|
|
],
|
|
})
|
|
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.mi_gpu_specs.get_gpu_series",
|
|
return_value="MI300",
|
|
):
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
with patch(
|
|
"utils.utils_counter_defs.get_build_in_vars",
|
|
return_value=mock_builtin_vars,
|
|
):
|
|
result = db_analysis.calc_dataframe_expressions(
|
|
pmc_df, sys_info, expression_df
|
|
)
|
|
|
|
assert result.iloc[0] == 51
|
|
# None from evaluate becomes NaN in pandas Series
|
|
assert pd.isna(result.iloc[1])
|
|
|
|
|
|
def test_calc_dataframe_expressions_empty_returns_assignable_series():
|
|
"""An empty expression_df returns an empty Series, not a DataFrame."""
|
|
expression_df = pd.DataFrame(columns=["metric_id", "value_name", "value"])
|
|
|
|
result = db_analysis.calc_dataframe_expressions(
|
|
pd.DataFrame({"Counter1": [1, 2, 3]}),
|
|
{"gpu_arch": "gfx942"},
|
|
expression_df,
|
|
)
|
|
|
|
assert isinstance(result, pd.Series)
|
|
assert result.empty
|
|
# Reproduces the call site: assigning the result as a single column must
|
|
# not raise "Columns must be same length as key".
|
|
expression_df["value"] = result
|
|
|
|
|
|
# =============================================================================
|
|
# calc_metrics_data tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_calc_metrics_data_builds_rows_and_preserves_schema():
|
|
"""Metric tables expand into rows with table-level fields resolved once;
|
|
non-metric tables are skipped and the output frames keep their columns."""
|
|
workload_path = "/fake/workload"
|
|
metric_df = pd.DataFrame(
|
|
{
|
|
"Metric": ["Grid Size"],
|
|
"Avg": [" 10 "],
|
|
"Min": [" 5 "],
|
|
"Max": [" 20 "],
|
|
"Unit": ["Work items"],
|
|
"Description": ["Grid size desc"],
|
|
},
|
|
index=pd.Index(["7.1.0"], name="Metric_ID"),
|
|
)
|
|
arch_config = schema.ArchConfig()
|
|
# Table 1 has no Metric/Channel column and is skipped; table 701 maps to
|
|
# panel 700 (table_name) and sub-table 701 (sub_table_name).
|
|
arch_config.dfs = {
|
|
1: pd.DataFrame({"from_csv": ["pmc_kernel_top.csv"]}),
|
|
701: metric_df,
|
|
}
|
|
arch_config.panel_configs = {
|
|
700: {
|
|
"id": 700,
|
|
"title": "Wavefront",
|
|
"data source": [
|
|
{"metric_table": {"id": 701, "title": "Wavefront Launch Stats"}}
|
|
],
|
|
}
|
|
}
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pmc_df_per_workload = {workload_path: pd.DataFrame({"Counter1": [1]})}
|
|
analyzer._runs = {
|
|
workload_path: MagicMock(sys_info=pd.DataFrame([{"gpu_arch": "gfx942"}]))
|
|
}
|
|
analyzer._arch_configs = {"gfx942": arch_config}
|
|
|
|
metrics_info, expressions = analyzer.calc_metrics_data()
|
|
|
|
info = metrics_info[workload_path]
|
|
assert "pct_of_peak" in info.columns
|
|
assert list(info["metric_id"]) == ["7.1.0"]
|
|
assert list(info["name"]) == ["Grid Size"]
|
|
assert list(info["table_name"]) == ["Wavefront"]
|
|
assert list(info["sub_table_name"]) == ["Wavefront Launch Stats"]
|
|
assert not bool(info["pct_of_peak"].iloc[0])
|
|
|
|
exprs = expressions[workload_path]
|
|
assert list(exprs.columns) == ["metric_id", "value_name", "value"]
|
|
# Metric/Unit/Description are non-expression columns, so only Avg/Min/Max
|
|
# expand into expression rows, stripped and in dataframe-column order.
|
|
assert list(exprs["value_name"]) == ["Avg", "Min", "Max"]
|
|
assert list(exprs["value"]) == ["10", "5", "20"]
|
|
assert set(exprs["metric_id"]) == {"7.1.0"}
|
|
|
|
|
|
# =============================================================================
|
|
# Noise-clamp warning + summary tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_calc_expressions_noise_clamp():
|
|
"""Variance warnings fire only at workload level, summary once per workload.
|
|
|
|
- evaluate(emit_variance_warnings=True) emits the per-metric warning when
|
|
to_noise_clamp advances the global counter; the False kwarg stays silent.
|
|
- calc_expressions emits exactly one variance warning per workload
|
|
(kernel-level pass is silent) and calls print_noise_clamp_summary once.
|
|
"""
|
|
workload_path = "/fake/workload"
|
|
noise_clamp_expression = (
|
|
"to_noise_clamp(to_min(raw_pmc_df['DIFF']), to_max(raw_pmc_df['REF']))"
|
|
)
|
|
# Two distinct kernels so groupby yields two kernel-level evaluate calls
|
|
# in addition to one workload-level call. Without the kwarg gate the
|
|
# unguarded code would emit three warnings; with the gate, exactly one.
|
|
pmc_df = pd.DataFrame({
|
|
"Kernel_Name": ["kernel_a", "kernel_b"],
|
|
"DIFF": [-100.0, -100.0],
|
|
"REF": [1000.0, 1000.0],
|
|
})
|
|
expression_template = pd.DataFrame({
|
|
"metric_id": ["1.1"],
|
|
"value_name": ["clamped"],
|
|
"value": [noise_clamp_expression],
|
|
})
|
|
sys_info_df = pd.DataFrame([{"placeholder": 1, "gpu_arch": "gfx942"}])
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pmc_df_per_workload = {workload_path: pmc_df}
|
|
analyzer._metric_expression_data_per_workload = {workload_path: expression_template}
|
|
analyzer._metrics_info_data_per_workload = {}
|
|
analyzer._roofline_ceilings_per_workload = {workload_path: {}}
|
|
analyzer._runs = {workload_path: MagicMock(sys_info=sys_info_df)}
|
|
analyzer._arch_configs = MagicMock()
|
|
|
|
# Direct evaluate kwarg behavior.
|
|
clear_noise_clamp_warnings()
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.console_warning"
|
|
) as console_warning_mock:
|
|
db_analysis.evaluate(
|
|
"direct_test",
|
|
noise_clamp_expression,
|
|
pmc_df,
|
|
{},
|
|
emit_variance_warnings=True,
|
|
)
|
|
variance_warning_calls = [
|
|
warning_call
|
|
for warning_call in console_warning_mock.call_args_list
|
|
if "Variance corrected for metric: direct_test" in warning_call.args[0]
|
|
]
|
|
assert len(variance_warning_calls) == 1
|
|
assert get_noise_clamp_warnings()["count"] >= 1
|
|
|
|
clear_noise_clamp_warnings()
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.console_warning"
|
|
) as console_warning_mock:
|
|
db_analysis.evaluate(
|
|
"direct_test_off",
|
|
noise_clamp_expression,
|
|
pmc_df,
|
|
{},
|
|
emit_variance_warnings=False,
|
|
)
|
|
assert get_noise_clamp_warnings()["count"] >= 1
|
|
variance_warning_calls = [
|
|
warning_call
|
|
for warning_call in console_warning_mock.call_args_list
|
|
if "Variance corrected for metric:" in warning_call.args[0]
|
|
]
|
|
assert variance_warning_calls == []
|
|
|
|
# calc_expressions per-workload bracket.
|
|
clear_noise_clamp_warnings()
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.get_build_in_vars", return_value={}
|
|
):
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.console_warning"
|
|
) as console_warning_mock:
|
|
with patch(
|
|
"rocprof_compute_analyze.analysis_db.print_noise_clamp_summary"
|
|
) as print_noise_clamp_summary_mock:
|
|
with patch.object(db_analysis, "validate_dual_issue_metrics"):
|
|
analyzer.calc_expressions()
|
|
|
|
variance_warning_calls = [
|
|
warning_call
|
|
for warning_call in console_warning_mock.call_args_list
|
|
if "Variance corrected for metric:" in warning_call.args[0]
|
|
]
|
|
assert len(variance_warning_calls) == 1
|
|
assert "1.1 - clamped" in variance_warning_calls[0].args[0]
|
|
print_noise_clamp_summary_mock.assert_called_once()
|
|
assert get_noise_clamp_warnings()["count"] >= 1
|
|
|
|
|
|
# =============================================================================
|
|
# _derive_pct_of_peak_values tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestDerivePctOfPeakValues:
|
|
"""Tests for db_analysis._derive_pct_of_peak_values."""
|
|
|
|
def _make_values_df(
|
|
self,
|
|
metric_ids: list[str],
|
|
value_names: list[str],
|
|
values: list[float],
|
|
kernel_names: Optional[list[str]] = None,
|
|
):
|
|
"""Build a long-format values DataFrame as produced by calc_expressions."""
|
|
data = {
|
|
"metric_id": metric_ids,
|
|
"value_name": value_names,
|
|
"value": values,
|
|
}
|
|
if kernel_names is not None:
|
|
data["kernel_name"] = kernel_names
|
|
return pd.DataFrame(data)
|
|
|
|
def test_pct_of_peak_true_metric_appends_percent_of_peak_row(self):
|
|
"""A pct_of_peak-enabled metric produces one new Percent of Peak row."""
|
|
values_df = self._make_values_df(
|
|
metric_ids=["1.1", "1.1"],
|
|
value_names=["Avg", "Peak"],
|
|
values=[50.0, 200.0],
|
|
)
|
|
new_rows = db_analysis._derive_pct_of_peak_values({"1.1"}, values_df)
|
|
assert len(new_rows) == 1
|
|
assert new_rows[0]["value_name"] == "Percent of Peak"
|
|
assert new_rows[0]["value"] == pytest.approx(25.0)
|
|
|
|
def test_multi_kernel_produces_one_row_per_kernel(self):
|
|
"""Calling once per kernel produces one Percent of Peak row per kernel."""
|
|
kernel_a_df = self._make_values_df(
|
|
metric_ids=["1.1", "1.1"],
|
|
value_names=["Avg", "Peak"],
|
|
values=[100.0, 200.0],
|
|
kernel_names=["kernel_a", "kernel_a"],
|
|
)
|
|
kernel_b_df = self._make_values_df(
|
|
metric_ids=["1.1", "1.1"],
|
|
value_names=["Avg", "Peak"],
|
|
values=[60.0, 300.0],
|
|
kernel_names=["kernel_b", "kernel_b"],
|
|
)
|
|
rows_a = db_analysis._derive_pct_of_peak_values({"1.1"}, kernel_a_df)
|
|
rows_b = db_analysis._derive_pct_of_peak_values({"1.1"}, kernel_b_df)
|
|
assert len(rows_a) == 1
|
|
assert rows_a[0]["value"] == pytest.approx(50.0) # 100/200*100
|
|
assert len(rows_b) == 1
|
|
assert rows_b[0]["value"] == pytest.approx(20.0) # 60/300*100
|
|
|
|
def test_pct_of_peak_false_metric_produces_no_pct_row(self):
|
|
"""A metric not in pct_of_peak_metric_ids produces no Percent of Peak row."""
|
|
values_df = self._make_values_df(
|
|
metric_ids=["1.1", "1.1"],
|
|
value_names=["Avg", "Peak"],
|
|
values=[50.0, 100.0],
|
|
)
|
|
new_rows = db_analysis._derive_pct_of_peak_values(set(), values_df)
|
|
assert new_rows == []
|
|
|
|
def test_incomplete_data_skips_metric(self):
|
|
"""A metric missing Peak or Avg/Value must be skipped gracefully."""
|
|
incomplete_cases = [
|
|
# Only "Avg" present -- no "Peak" row
|
|
self._make_values_df(
|
|
metric_ids=["1.1"], value_names=["Avg"], values=[50.0]
|
|
),
|
|
# Only "Peak" present -- no "Avg" or "Value" row
|
|
self._make_values_df(
|
|
metric_ids=["1.1"], value_names=["Peak"], values=[100.0]
|
|
),
|
|
]
|
|
for incomplete_values in incomplete_cases:
|
|
new_rows = db_analysis._derive_pct_of_peak_values(
|
|
{"1.1"}, incomplete_values
|
|
)
|
|
assert new_rows == []
|
|
|
|
|
|
# =============================================================================
|
|
# Dual-issue VALU validation tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_validate_dual_issue_metrics_emits_warning_above_peak():
|
|
"""Long-format VALU Utilization above peak triggers the dual-issue warning."""
|
|
arch_config = make_dual_issue_arch_config("VALU Utilization")
|
|
workload_values_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.1"],
|
|
"value_name": ["Value", "Peak"],
|
|
"value": [150.0, 100.0],
|
|
})
|
|
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
|
|
|
|
with patch("utils.metrics.common.console_warning") as console_warning_mock:
|
|
db_analysis.validate_dual_issue_metrics(
|
|
pmc_df,
|
|
{"gpu_arch": "gfx942"},
|
|
workload_values_df,
|
|
arch_config,
|
|
)
|
|
|
|
console_warning_mock.assert_called_once()
|
|
msg = console_warning_mock.call_args.args[0]
|
|
assert "VALU Utilization can go up to 200%" in msg
|
|
|
|
|
|
def test_validate_dual_issue_metrics_silent_below_peak():
|
|
"""Below-peak VALU Utilization stays silent."""
|
|
arch_config = make_dual_issue_arch_config("VALU Utilization")
|
|
workload_values_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.1"],
|
|
"value_name": ["Value", "Peak"],
|
|
"value": [80.0, 100.0],
|
|
})
|
|
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
|
|
|
|
with patch("utils.metrics.common.console_warning") as console_warning_mock:
|
|
db_analysis.validate_dual_issue_metrics(
|
|
pmc_df,
|
|
{"gpu_arch": "gfx942"},
|
|
workload_values_df,
|
|
arch_config,
|
|
)
|
|
|
|
console_warning_mock.assert_not_called()
|
|
|
|
|
|
def test_validate_dual_issue_metrics_uses_peak_empirical_fallback():
|
|
"""Peak (Empirical) wins when present; falls back to Peak otherwise."""
|
|
arch_config = make_dual_issue_arch_config(
|
|
"VALU FLOPs (F64)", peak_col="Peak (Empirical)"
|
|
)
|
|
workload_values_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.1"],
|
|
"value_name": ["Value", "Peak (Empirical)"],
|
|
"value": [600.0, 400.0],
|
|
})
|
|
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
|
|
|
|
with patch("utils.metrics.common.console_warning") as console_warning_mock:
|
|
db_analysis.validate_dual_issue_metrics(
|
|
pmc_df,
|
|
{"gpu_arch": "gfx942"},
|
|
workload_values_df,
|
|
arch_config,
|
|
)
|
|
|
|
console_warning_mock.assert_called_once()
|
|
msg = console_warning_mock.call_args.args[0]
|
|
assert "VALU FLOPs can exceed the peak value" in msg
|
|
|
|
|
|
def test_validate_dual_issue_metrics_appends_valu2_suffix_on_gfx950():
|
|
"""gfx950 with non-zero SQ_ACTIVE_INST_VALU2 appends the confirmation."""
|
|
arch_config = make_dual_issue_arch_config("VALU Utilization")
|
|
workload_values_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.1"],
|
|
"value_name": ["Value", "Peak"],
|
|
"value": [150.0, 100.0],
|
|
})
|
|
pmc_df = pd.DataFrame({"SQ_ACTIVE_INST_VALU2": [1, 2, 3]})
|
|
|
|
with patch("utils.metrics.common.console_warning") as console_warning_mock:
|
|
db_analysis.validate_dual_issue_metrics(
|
|
pmc_df,
|
|
{"gpu_arch": "gfx950"},
|
|
workload_values_df,
|
|
arch_config,
|
|
)
|
|
|
|
msg = console_warning_mock.call_args.args[0]
|
|
assert "Dual-issue activity detected via SQ_ACTIVE_INST_VALU2 counter" in msg
|
|
|
|
|
|
def test_validate_dual_issue_metrics_skips_non_metric_table_dfs():
|
|
"""dfs entries whose dfs_type is not metric_table are ignored."""
|
|
arch_config = make_dual_issue_arch_config("VALU Utilization")
|
|
arch_config.dfs_type = {201: "raw_csv_table"}
|
|
workload_values_df = pd.DataFrame({
|
|
"metric_id": ["1.1", "1.1"],
|
|
"value_name": ["Value", "Peak"],
|
|
"value": [150.0, 100.0],
|
|
})
|
|
pmc_df = pd.DataFrame({"GRBM_GUI_ACTIVE": [1000]})
|
|
|
|
with patch("utils.metrics.common.console_warning") as console_warning_mock:
|
|
db_analysis.validate_dual_issue_metrics(
|
|
pmc_df,
|
|
{"gpu_arch": "gfx942"},
|
|
workload_values_df,
|
|
arch_config,
|
|
)
|
|
|
|
console_warning_mock.assert_not_called()
|
|
|
|
|
|
# =============================================================================
|
|
# PC-sampling population
|
|
# =============================================================================
|
|
|
|
|
|
def make_pc_sampling_tool_data():
|
|
"""Two offsets under two kernels sharing one code object, with counts."""
|
|
stall = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_NOT_ISSUED_REASON_WAITCNT"
|
|
inst_type = "ROCPROFILER_PC_SAMPLING_INSTRUCTION_TYPE_VALU"
|
|
return {
|
|
"metadata": {"pid": 42},
|
|
"buffer_records": {
|
|
"pc_sample_host_trap": [],
|
|
"pc_sample_stochastic": [
|
|
{
|
|
"inst_index": 0,
|
|
"record": {
|
|
"pc": {"code_object_id": 5, "code_object_offset": 0x10},
|
|
"dispatch_id": 0,
|
|
"wave_issued": False,
|
|
"snapshot": {"stall_reason": stall},
|
|
"inst_type": inst_type,
|
|
},
|
|
},
|
|
{
|
|
"inst_index": 1,
|
|
"record": {
|
|
"pc": {"code_object_id": 5, "code_object_offset": 0x20},
|
|
"dispatch_id": 1,
|
|
"wave_issued": True,
|
|
"snapshot": {},
|
|
"inst_type": inst_type,
|
|
},
|
|
},
|
|
],
|
|
"kernel_dispatch": [
|
|
{
|
|
"start_timestamp": 0,
|
|
"end_timestamp": 0,
|
|
"dispatch_info": {
|
|
"dispatch_id": 0,
|
|
"kernel_id": 100,
|
|
"agent_id": {"handle": 1},
|
|
},
|
|
},
|
|
{
|
|
"start_timestamp": 0,
|
|
"end_timestamp": 0,
|
|
"dispatch_info": {
|
|
"dispatch_id": 1,
|
|
"kernel_id": 101,
|
|
"agent_id": {"handle": 1},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
"strings": {
|
|
"pc_sample_instructions": ["v_mov", "v_add"],
|
|
"pc_sample_comments": ["/s/a.cpp:1", "/s/a.cpp:2"],
|
|
},
|
|
"kernel_symbols": [
|
|
{
|
|
"kernel_id": 100,
|
|
"code_object_id": 5,
|
|
"kernel_name": "_Z7vecCopyv.kd",
|
|
"formatted_kernel_name": "vecCopy",
|
|
},
|
|
{
|
|
"kernel_id": 101,
|
|
"code_object_id": 5,
|
|
"kernel_name": "vecAdd.kd",
|
|
"formatted_kernel_name": "vecAdd",
|
|
},
|
|
],
|
|
"code_objects": [{"code_object_id": 5, "load_base": 0x1000}],
|
|
"agents": [],
|
|
}
|
|
|
|
|
|
def test_add_pc_sampling_data_no_tool_data_is_noop(db_session):
|
|
"""A workload without tool data inserts no rows."""
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {"/fake/workload": None}
|
|
|
|
analyzer.add_pc_sampling_data("/fake/workload", workload, {})
|
|
db_session.commit()
|
|
|
|
assert db_session.query(orm.CodeObjectStore).count() == 0
|
|
assert db_session.query(orm.InstructionLine).count() == 0
|
|
|
|
|
|
def test_add_pc_sampling_data_populates_and_attributes_kernels(db_session):
|
|
"""Instruction lines are inserted, attributed to their dispatch kernel, and
|
|
the code object records pid/load_base."""
|
|
workload_path = "/fake/workload"
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
kernel_objs = {
|
|
"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload),
|
|
"vecAdd": orm.Kernel(kernel_name="vecAdd", workload=workload),
|
|
}
|
|
for kernel in kernel_objs.values():
|
|
db_session.add(kernel)
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {
|
|
workload_path: make_pc_sampling_tool_data()
|
|
}
|
|
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
|
|
db_session.commit()
|
|
|
|
code_object = db_session.query(orm.CodeObjectStore).one()
|
|
assert code_object.pid == 42
|
|
assert code_object.load_base == 0x1000
|
|
|
|
lines = db_session.query(orm.InstructionLine).all()
|
|
kernel_by_offset = {
|
|
line.code_object_offset: line.kernel.kernel_name for line in lines
|
|
}
|
|
assert kernel_by_offset == {0x10: "vecCopy", 0x20: "vecAdd"}
|
|
|
|
# The stalled sample carries a stall-reason count; both carry an inst type.
|
|
stalled = next(line for line in lines if line.code_object_offset == 0x10)
|
|
assert stalled.pc_sample_state.stall_count == 1
|
|
assert {
|
|
r.stall_reason_lookup.text for r in stalled.pc_sample_state.stall_reasons
|
|
} == {"WAITCNT"}
|
|
|
|
|
|
def test_add_pc_sampling_data_drops_lines_without_kernel(db_session):
|
|
"""Lines whose kernel is absent from kernel_objs (filtered out) are dropped
|
|
along with their sample state and child counts, not attributed to no kernel."""
|
|
workload_path = "/fake/workload"
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
# Only vecCopy survives filtering; vecAdd's line must be dropped.
|
|
kernel_objs = {"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload)}
|
|
db_session.add(kernel_objs["vecCopy"])
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {
|
|
workload_path: make_pc_sampling_tool_data()
|
|
}
|
|
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
|
|
db_session.commit()
|
|
|
|
lines = db_session.query(orm.InstructionLine).all()
|
|
assert [line.code_object_offset for line in lines] == [0x10]
|
|
assert all(line.kernel is not None for line in lines)
|
|
# No orphaned child rows for the dropped line.
|
|
assert db_session.query(orm.PCSampleState).count() == 1
|
|
|
|
|
|
# =============================================================================
|
|
# Code-object ISA ingestion (add_code_object_isa)
|
|
# =============================================================================
|
|
|
|
|
|
def make_disasm_code_object(code_object_id, instructions, symbol_name="sym"):
|
|
"""Build one code_obj_info code object with a single symbol."""
|
|
return {
|
|
"id": code_object_id,
|
|
"symbols": [{"name": symbol_name, "instructions": instructions}],
|
|
}
|
|
|
|
|
|
def test_add_code_object_isa_adds_unsampled_lines(db_session):
|
|
"""Un-sampled instructions of a dispatched kernel are added and attributed
|
|
via the mangled-name join; a disassembly offset that matches an already-
|
|
sampled offset inserts no duplicate row; ISA of an un-dispatched symbol is
|
|
not stored at all; a non-surviving pid's disassembly is skipped."""
|
|
workload_path = common.get_output_dir()
|
|
Path(workload_path).mkdir(parents=True, exist_ok=True)
|
|
load_base = 0x1000
|
|
try:
|
|
# 0x1010 -> offset 0x10 is already sampled; 0x1030 -> offset 0x30 is new
|
|
# and joins vecCopy through its mangled ELF name; 0x1040 -> offset 0x40
|
|
# belongs to a symbol never dispatched, so it is dropped entirely.
|
|
code_objects = [
|
|
{
|
|
"id": 5,
|
|
"symbols": [
|
|
{
|
|
"name": "_Z7vecCopyv",
|
|
"instructions": [
|
|
{
|
|
"virtual_address": load_base + 0x10,
|
|
"name": "v_mov",
|
|
"comment": "",
|
|
},
|
|
{
|
|
"virtual_address": load_base + 0x30,
|
|
"name": "s_nop",
|
|
"comment": "c",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"name": "_Z7unknownv",
|
|
"instructions": [
|
|
{
|
|
"virtual_address": load_base + 0x40,
|
|
"name": "s_endpgm",
|
|
"comment": "",
|
|
}
|
|
],
|
|
},
|
|
],
|
|
}
|
|
]
|
|
(Path(workload_path) / "42_code_obj_info.json").write_text(
|
|
json.dumps({"code_objects": code_objects}), encoding="utf-8"
|
|
)
|
|
# A non-surviving pid (metadata pid is 42) reuses code_object_id 5; its
|
|
# 0x50 offset must never be stored since only pid 42's load_base is known.
|
|
(Path(workload_path) / "99_code_obj_info.json").write_text(
|
|
json.dumps({
|
|
"code_objects": [
|
|
make_disasm_code_object(
|
|
5,
|
|
[
|
|
{
|
|
"virtual_address": load_base + 0x50,
|
|
"name": "s_nop",
|
|
"comment": "",
|
|
}
|
|
],
|
|
symbol_name="_Z7vecCopyv",
|
|
)
|
|
]
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
kernel_objs = {
|
|
"vecCopy": orm.Kernel(kernel_name="vecCopy", workload=workload),
|
|
"vecAdd": orm.Kernel(kernel_name="vecAdd", workload=workload),
|
|
}
|
|
for kernel in kernel_objs.values():
|
|
db_session.add(kernel)
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {
|
|
workload_path: make_pc_sampling_tool_data()
|
|
}
|
|
analyzer.add_pc_sampling_data(workload_path, workload, kernel_objs)
|
|
analyzer.add_code_object_isa(workload_path, workload, kernel_objs)
|
|
db_session.commit()
|
|
|
|
lines = db_session.query(orm.InstructionLine).all()
|
|
by_offset = {line.code_object_offset: line for line in lines}
|
|
# Two sampled offsets + one new un-sampled offset; the un-dispatched
|
|
# symbol's 0x40 line and the non-surviving pid's 0x50 line are dropped,
|
|
# and 0x10 is not duplicated.
|
|
assert set(by_offset) == {0x10, 0x20, 0x30}
|
|
# Only the surviving pid (42) is stored.
|
|
assert {store.pid for store in db_session.query(orm.CodeObjectStore)} == {42}
|
|
# The disassembly-only line joins its kernel and carries no sample state.
|
|
isa_line = by_offset[0x30]
|
|
assert isa_line.kernel.kernel_name == "vecCopy"
|
|
assert isa_line.pc_sample_state is None
|
|
assert isa_line.instruction == "s_nop"
|
|
# The sampled line at 0x10 kept its kernel attribution and sample state.
|
|
assert by_offset[0x10].kernel.kernel_name == "vecCopy"
|
|
assert by_offset[0x10].pc_sample_state is not None
|
|
# Both belong to the same (reused) code object store.
|
|
assert isa_line.code_object_store is by_offset[0x10].code_object_store
|
|
finally:
|
|
common.clean_output_dir(True, workload_path)
|
|
|
|
|
|
def test_add_code_object_isa_creates_store_for_unsampled_code_object(db_session):
|
|
"""A dispatched-but-never-sampled code object present only in code_obj_info
|
|
gets a new store, using the load_base from the results' code_objects list,
|
|
and its ISA is attributed to the dispatched kernel."""
|
|
workload_path = common.get_output_dir()
|
|
Path(workload_path).mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
tool_data = make_pc_sampling_tool_data()
|
|
# code object 9 has a load_base and a dispatched kernel, but no samples.
|
|
tool_data["code_objects"].append({"code_object_id": 9, "load_base": 0x2000})
|
|
tool_data["kernel_symbols"].append({
|
|
"kernel_id": 102,
|
|
"code_object_id": 9,
|
|
"kernel_name": "_Z6helperv.kd",
|
|
"formatted_kernel_name": "helper",
|
|
})
|
|
code_objects = [
|
|
make_disasm_code_object(
|
|
9,
|
|
[{"virtual_address": 0x2000 + 0x8, "name": "s_endpgm", "comment": ""}],
|
|
symbol_name="_Z6helperv",
|
|
)
|
|
]
|
|
(Path(workload_path) / "42_code_obj_info.json").write_text(
|
|
json.dumps({"code_objects": code_objects}), encoding="utf-8"
|
|
)
|
|
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
helper = orm.Kernel(kernel_name="helper", workload=workload)
|
|
db_session.add(helper)
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {workload_path: tool_data}
|
|
analyzer.add_pc_sampling_data(workload_path, workload, {"helper": helper})
|
|
analyzer.add_code_object_isa(workload_path, workload, {"helper": helper})
|
|
db_session.commit()
|
|
|
|
store = db_session.query(orm.CodeObjectStore).filter_by(code_object_id=9).one()
|
|
assert store.pid == 42
|
|
assert store.load_base == 0x2000
|
|
line = (
|
|
db_session
|
|
.query(orm.InstructionLine)
|
|
.filter_by(code_object_offset=0x8)
|
|
.one()
|
|
)
|
|
assert line.code_object_store is store
|
|
assert line.kernel is helper
|
|
finally:
|
|
common.clean_output_dir(True, workload_path)
|
|
|
|
|
|
def test_add_code_object_isa_skips_code_object_without_load_base(db_session):
|
|
"""A code object with no known load_base cannot be offset-mapped, so it is
|
|
skipped rather than stored with an inconsistent offset."""
|
|
workload_path = common.get_output_dir()
|
|
Path(workload_path).mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
tool_data = make_pc_sampling_tool_data()
|
|
tool_data["code_objects"].append({"code_object_id": 9, "load_base": None})
|
|
tool_data["kernel_symbols"].append({
|
|
"kernel_id": 102,
|
|
"code_object_id": 9,
|
|
"kernel_name": "_Z6helperv.kd",
|
|
"formatted_kernel_name": "helper",
|
|
})
|
|
code_objects = [
|
|
make_disasm_code_object(
|
|
9,
|
|
[{"virtual_address": 0x500, "name": "s_endpgm", "comment": ""}],
|
|
symbol_name="_Z6helperv",
|
|
)
|
|
]
|
|
(Path(workload_path) / "42_code_obj_info.json").write_text(
|
|
json.dumps({"code_objects": code_objects}), encoding="utf-8"
|
|
)
|
|
|
|
workload = orm.Workload(name="w", sub_name="s")
|
|
db_session.add(workload)
|
|
helper = orm.Kernel(kernel_name="helper", workload=workload)
|
|
db_session.add(helper)
|
|
|
|
analyzer = db_analysis(MagicMock(), {})
|
|
analyzer._pc_sampling_tool_data_per_workload = {workload_path: tool_data}
|
|
analyzer.add_pc_sampling_data(workload_path, workload, {"helper": helper})
|
|
analyzer.add_code_object_isa(workload_path, workload, {"helper": helper})
|
|
db_session.commit()
|
|
|
|
# The store exists (its kernel was dispatched) but no ISA line was added.
|
|
store = db_session.query(orm.CodeObjectStore).filter_by(code_object_id=9).one()
|
|
assert store.instruction_lines == []
|
|
finally:
|
|
common.clean_output_dir(True, workload_path)
|