Files
TheRock/rocm-systems/projects/rocprof-trace-decoder/test/unit/funcmap_test.cpp
T
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

547 lines
19 KiB
C++

// MIT License
//
// Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include "rocprof_trace_decoder/cxx/funcmap.hpp"
#include <gtest/gtest.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
using namespace rocprof_trace_decoder::codeobj;
namespace
{
// RAII redirect of std::cerr → an internal ostringstream.
class CerrCapture
{
public:
CerrCapture() : prev(std::cerr.rdbuf(buf.rdbuf())) {}
~CerrCapture() { std::cerr.rdbuf(prev); }
std::string str() const { return buf.str(); }
private:
std::ostringstream buf;
std::streambuf* prev;
};
bool has_warning(const std::vector<FuncmapDiagnostic>& diags, const std::string& needle)
{
for (const auto& d : diags)
{
if (d.severity == FuncmapDiagnostic::Severity::Warning && d.message.find(needle) != std::string::npos)
return true;
}
return false;
}
bool has_error(const std::vector<FuncmapDiagnostic>& diags)
{
for (const auto& d : diags)
if (d.severity == FuncmapDiagnostic::Severity::Error) return true;
return false;
}
} // namespace
// ─── decode_marker_value ────────────────────────────────────────────────────
TEST(DecodeMarkerValue, DecodesFlagCombinations)
{
struct Case
{
const char* name;
uint32_t raw;
uint32_t id;
bool isEnter;
bool exitsPrevious;
};
for (const Case& test : {
Case{"enter", (42u << 2) | 0b10u, 42, true, false},
Case{"exit", 1u, 0, false, true },
Case{"point", 7u << 2, 7, false, false},
Case{"exit_enter", (9u << 2) | 0b11u, 9, true, true }
})
{
SCOPED_TRACE(test.name);
MarkerValue marker = decode_marker_value(test.raw);
EXPECT_EQ(marker.id, test.id);
EXPECT_EQ(marker.is_enter, test.isEnter);
EXPECT_EQ(marker.exit_prev, test.exitsPrevious);
}
}
TEST(MarkerEncoding, ComputesMasksAndDecodesClock)
{
struct Case
{
const char* name;
MarkerEncoding encoding;
bool hasClock;
uint32_t idMask;
uint32_t packedClockMask;
uint32_t sourceClockMask;
};
for (const Case& test : {
Case{"packed", {12, 4}, true, 0x000FFFFCu, 0xFFF00000u, 0x0000FFF0u},
Case{"legacy", {}, false, 0xFFFFFFFCu, 0u, 0u },
Case{"high_bit", {1, 31}, true, 0x7FFFFFFCu, 0x80000000u, 0x80000000u},
Case{"wide", {29, 3}, true, 0x00000004u, 0xFFFFFFF8u, 0xFFFFFFF8u},
})
{
SCOPED_TRACE(test.name);
EXPECT_TRUE(test.encoding.is_valid());
EXPECT_EQ(test.encoding.has_shader_clock(), test.hasClock);
EXPECT_EQ(test.encoding.marker_id_mask(), test.idMask);
EXPECT_EQ(test.encoding.packed_shader_clock_mask(), test.packedClockMask);
EXPECT_EQ(test.encoding.shader_clock_source_mask(), test.sourceClockMask);
}
MarkerEncoding packed{12, 4};
uint32_t raw = (0xABCu << 20) | (42u << 2) | 0b10u;
MarkerValue marker = decode_marker_value(raw, packed);
EXPECT_EQ(marker.id, 42u);
EXPECT_EQ(packed.decode_shader_clock(raw), 0xABCu);
EXPECT_TRUE(marker.is_enter);
EXPECT_FALSE(marker.exit_prev);
EXPECT_EQ((raw & packed.marker_id_mask()) >> 2, 42u);
EXPECT_EQ(raw & packed.packed_shader_clock_mask(), 0xABC00000u);
Funcmap fm;
fm.marker_encoding = packed;
EXPECT_EQ(decode_marker_value(raw, fm).id, raw >> 2);
uint32_t legacyRaw = (0x3FFFFFFFu << 2) | 0b01u;
EXPECT_EQ(decode_marker_value(legacyRaw, MarkerEncoding{}).id, 0x3FFFFFFFu);
EXPECT_EQ(MarkerEncoding{}.decode_shader_clock(legacyRaw), 0u);
MarkerEncoding invalid{30, 0};
EXPECT_FALSE(invalid.is_valid());
EXPECT_EQ(decode_marker_value(legacyRaw, invalid).id, 0x3FFFFFFFu);
}
TEST(DecodeMarkerValue, FuncmapLeavesUnregisteredValuesLegacy)
{
Funcmap fm;
fm.marker_encoding = {12, 4};
auto entry = std::make_shared<FuncmapEntry>();
entry->kind = FuncmapEntryKind::Point;
entry->id = 42;
fm.entries.push_back(entry);
fm.by_id.emplace(entry->id, entry);
uint32_t known = (0xABCu << 20) | (42u << 2);
EXPECT_EQ(decode_marker_value(known, fm).id, 42u);
EXPECT_EQ(fm.marker_encoding.decode_shader_clock(known), 0xABCu);
uint32_t numeric = (0xABCu << 20) | (123u << 2);
EXPECT_EQ(decode_marker_value(numeric, fm).id, numeric >> 2);
uint32_t exit = (0xABCu << 20) | 1u;
EXPECT_EQ(decode_marker_value(exit, fm).id, 0u);
EXPECT_EQ(fm.marker_encoding.decode_shader_clock(exit), 0xABCu);
}
TEST(FuncmapCompatibility, ExistingAggregateInitializersStillWork)
{
// Keep source compatibility for consumers that aggregate-initialize the
// public structs from the pre-packed-marker layout.
std::vector<FuncmapDiagnostic> diagnostics;
Funcmap map{
std::vector<Funcmap::EntryPtr>{},
std::unordered_map<uint32_t, Funcmap::EntryPtr>{},
0,
diagnostics,
};
FuncmapEntry entry{FuncmapEntryKind::Point, 7, "point", "source.cpp:1", 0x100000000ull};
MarkerValue marker{7, true, false};
EXPECT_TRUE(map.diagnostics.empty());
EXPECT_FALSE(map.marker_encoding.has_shader_clock());
EXPECT_EQ(entry.vaddr, 0x100000000ull);
EXPECT_EQ(entry.extra_payload_count, 0u);
EXPECT_EQ(marker.id, 7u);
EXPECT_TRUE(marker.is_enter);
EXPECT_FALSE(marker.exit_prev);
}
// ─── parse_funcmap_section ──────────────────────────────────────────────────
TEST(ParseFuncmap, EachRowKind)
{
std::string blob = "F:1:my_device_fn@/p/foo.cpp:42\n"
"K:my_kernel\n"
"U:2:my_scope\n"
"P:3:vmem_load@a.cpp:7\n"
"W:64\n";
CerrCapture cap;
Funcmap m = parse_funcmap_section(blob);
EXPECT_EQ(m.entries.size(), 4u);
EXPECT_EQ(m.wave_size, 64u);
EXPECT_TRUE(m.diagnostics.empty()) << "unexpected diagnostics emitted";
auto f = m.find(1);
ASSERT_TRUE(f);
EXPECT_EQ(f->kind, FuncmapEntryKind::Function);
EXPECT_EQ(f->name, "my_device_fn");
EXPECT_EQ(f->source_loc, "/p/foo.cpp:42");
// K: row carries no ID, so by_id should not contain it; entries[1] should be it.
EXPECT_EQ(m.entries[1]->kind, FuncmapEntryKind::Kernel);
EXPECT_EQ(m.entries[1]->name, "my_kernel");
auto u = m.find(2);
ASSERT_TRUE(u);
EXPECT_EQ(u->kind, FuncmapEntryKind::UserScope);
EXPECT_EQ(u->name, "my_scope");
EXPECT_TRUE(u->source_loc.empty());
auto p = m.find(3);
ASSERT_TRUE(p);
EXPECT_EQ(p->kind, FuncmapEntryKind::Point);
EXPECT_EQ(p->name, "vmem_load");
EXPECT_EQ(p->source_loc, "a.cpp:7");
}
TEST(ParseFuncmap, ParsesExtraPayloadAndMarkerEncoding)
{
std::string blob = "M:shader_clock_bits=12;shader_clock_shift=4\n"
"P:7:payload_point\n"
"R:7:extra_payload_count=1\n";
Funcmap m = parse_funcmap_section(blob, /*silent=*/true);
auto p = m.find(7);
ASSERT_TRUE(p);
EXPECT_EQ(p->extra_payload_count, 1u);
EXPECT_EQ(m.marker_encoding.shader_clock_bits, 12u);
EXPECT_EQ(m.marker_encoding.shader_clock_shift, 4u);
EXPECT_EQ(m.marker_encoding.marker_id_mask(), 0x000FFFFCu);
EXPECT_EQ(m.marker_encoding.packed_shader_clock_mask(), 0xFFF00000u);
EXPECT_EQ(m.marker_encoding.shader_clock_source_mask(), 0x0000FFF0u);
}
TEST(ParseFuncmap, PackedMarkerEncodingRequiresClockShift)
{
Funcmap m = parse_funcmap_section("M:shader_clock_bits=12\n", /*silent=*/true);
EXPECT_FALSE(m.marker_encoding.has_shader_clock());
EXPECT_TRUE(has_warning(m.diagnostics, "malformed M:"));
}
TEST(ParseFuncmap, ToleratesBlankLinesCRLFAndTrailingNUL)
{
std::string blob = "F:5:foo\r\n\r\nK:k1\r\n";
blob.push_back('\0'); // common: ConstantDataArray::getString(AddNull=true)
CerrCapture cap;
Funcmap m = parse_funcmap_section(blob);
ASSERT_EQ(m.entries.size(), 2u);
EXPECT_TRUE(m.diagnostics.empty()) << cap.str();
auto f = m.find(5);
ASSERT_TRUE(f);
EXPECT_EQ(f->name, "foo");
}
TEST(ParseFuncmap, SourceLocPreservesColonsAndAtSigns)
{
// First `@` splits name from source_loc; subsequent `@` and `:` survive in source_loc.
std::string blob = "F:1:my_fn@/p/file.cpp:10:5\n";
Funcmap m = parse_funcmap_section(blob, /*silent=*/true);
ASSERT_EQ(m.entries.size(), 1u);
auto f = m.find(1);
ASSERT_TRUE(f);
EXPECT_EQ(f->name, "my_fn");
EXPECT_EQ(f->source_loc, "/p/file.cpp:10:5");
}
TEST(ParseFuncmap, DuplicateIdsLastWriterWinsAndWarns)
{
std::string blob = "F:7:first\n"
"U:7:second\n";
CerrCapture cap;
Funcmap m = parse_funcmap_section(blob);
EXPECT_EQ(m.entries.size(), 2u); // both rows retained in entries
auto e = m.find(7);
ASSERT_TRUE(e);
EXPECT_EQ(e->name, "second"); // last-writer-wins in by_id
EXPECT_TRUE(has_warning(m.diagnostics, "duplicate marker ID 7"));
EXPECT_NE(cap.str().find("duplicate marker ID 7"), std::string::npos);
}
TEST(ParseFuncmap, MalformedRowWarnsAndContinues)
{
std::string blob = "F:1:good\n"
"garbage line\n"
"F:notanumber:bad\n"
"X:9:unknown\n"
"F:2:also_good\n";
CerrCapture cap;
Funcmap m = parse_funcmap_section(blob);
EXPECT_TRUE(m.find(1));
EXPECT_TRUE(m.find(2));
EXPECT_FALSE(m.find(9));
EXPECT_GE(m.diagnostics.size(), 3u);
EXPECT_TRUE(has_warning(m.diagnostics, "malformed row"));
EXPECT_TRUE(has_warning(m.diagnostics, "malformed F:"));
EXPECT_TRUE(has_warning(m.diagnostics, "unknown row prefix"));
// Each diagnostic must carry the offending line content + 1-based line no.
bool found_line_2 = false;
for (const auto& d : m.diagnostics)
{
if (d.line_no == 2 && d.message.find("garbage line") != std::string::npos) found_line_2 = true;
}
EXPECT_TRUE(found_line_2);
EXPECT_FALSE(cap.str().empty()); // echoed to cerr by default
}
TEST(ParseFuncmap, SilentSuppressesCerrButPopulatesDiagnostics)
{
std::string blob = "garbage\n"
"F:7:first\n"
"F:7:second\n";
CerrCapture cap;
Funcmap m = parse_funcmap_section(blob, /*silent=*/true);
EXPECT_TRUE(cap.str().empty()) << "expected silent mode to leave cerr untouched";
EXPECT_GE(m.diagnostics.size(), 2u); // garbage + duplicate
}
TEST(ParseFuncmap, FindReturnsSameInstanceAcrossLookups)
{
std::string blob = "F:1:foo\n";
Funcmap m = parse_funcmap_section(blob, /*silent=*/true);
auto a = m.find(1);
auto b = m.find(1);
ASSERT_TRUE(a);
ASSERT_TRUE(b);
EXPECT_EQ(a.get(), b.get()) << "Funcmap::find should not allocate per call";
}
TEST(ParseFuncmap, EmptyBlobYieldsEmptyFuncmap)
{
CerrCapture cap;
Funcmap m = parse_funcmap_section("");
EXPECT_TRUE(m.entries.empty());
EXPECT_TRUE(m.diagnostics.empty());
EXPECT_TRUE(cap.str().empty());
EXPECT_EQ(m.wave_size, 0u);
}
TEST(ParseFuncmap, MalformedWaveSizeWarns)
{
std::string blob = "W:notanumber\n";
Funcmap m = parse_funcmap_section(blob, /*silent=*/true);
EXPECT_EQ(m.wave_size, 0u);
EXPECT_TRUE(has_warning(m.diagnostics, "malformed W:"));
}
// ─── extract_elf_section: build a synthetic ELF64 in memory ─────────────────
namespace
{
// Lay out: ehdr | sections (data) | shstrtab data | shdr table.
// Keeps offsets simple to compute deterministically.
struct ElfSection
{
std::string name;
std::string data;
};
std::vector<uint8_t> buildElf(const std::vector<ElfSection>& sections)
{
std::string names(1, '\0');
std::vector<detail::Elf64Shdr> headers(sections.size() + 2);
uint64_t offset = sizeof(detail::Elf64Ehdr);
size_t index = 1; // Header 0 is SHT_NULL.
for (const ElfSection& section : sections)
{
detail::Elf64Shdr& header = headers[index++];
header.sh_name = static_cast<uint32_t>(names.size());
names += section.name;
names += '\0';
header.sh_offset = offset;
header.sh_size = section.data.size();
offset += section.data.size();
}
detail::Elf64Shdr& stringTable = headers.back();
stringTable.sh_name = static_cast<uint32_t>(names.size());
names += ".shstrtab";
names += '\0';
stringTable.sh_offset = offset;
stringTable.sh_size = names.size();
const uint64_t sectionHeadersOffset = offset + names.size();
std::vector<uint8_t> elf(sectionHeadersOffset + headers.size() * sizeof(detail::Elf64Shdr), 0);
detail::Elf64Ehdr header{};
std::memcpy(header.e_ident, detail::ElfMagic, detail::ElfMagicSize);
header.e_ident[detail::EiClass] = detail::ElfClass64;
header.e_shoff = sectionHeadersOffset;
header.e_ehsize = sizeof(header);
header.e_shentsize = sizeof(detail::Elf64Shdr);
header.e_shnum = static_cast<uint16_t>(headers.size());
header.e_shstrndx = static_cast<uint16_t>(headers.size() - 1);
std::memcpy(elf.data(), &header, sizeof(header));
index = 1;
for (const ElfSection& section : sections)
{
const detail::Elf64Shdr& sectionHeader = headers[index++];
if (!section.data.empty())
std::memcpy(elf.data() + sectionHeader.sh_offset, section.data.data(), section.data.size());
}
std::memcpy(elf.data() + stringTable.sh_offset, names.data(), names.size());
std::memcpy(elf.data() + sectionHeadersOffset, headers.data(), headers.size() * sizeof(detail::Elf64Shdr));
return elf;
}
void expectFuncmapExtractionError(const char* data, size_t size)
{
std::vector<FuncmapDiagnostic> diagnostics;
EXPECT_FALSE(extract_elf_section(data, size, ".sqtt_funcmap", diagnostics));
EXPECT_TRUE(has_error(diagnostics));
}
void expectFuncmapExtractionError(const std::vector<uint8_t>& elf)
{
expectFuncmapExtractionError(reinterpret_cast<const char*>(elf.data()), elf.size());
}
enum class ElfCorruption
{
InvalidClass,
InvalidSectionHeaderSize,
SectionTableOutOfRange,
MissingSectionNameTable,
OverflowingSectionSize,
};
std::vector<uint8_t> malformedFuncmapElf(ElfCorruption corruption)
{
auto elf = buildElf({
{".sqtt_funcmap", "F:1:x\n"}
});
detail::Elf64Ehdr header{};
std::memcpy(&header, elf.data(), sizeof(header));
switch (corruption)
{
case ElfCorruption::InvalidClass:
header.e_ident[detail::EiClass] = static_cast<uint8_t>(~header.e_ident[detail::EiClass]);
break;
case ElfCorruption::InvalidSectionHeaderSize:
header.e_shentsize = static_cast<uint16_t>(sizeof(detail::Elf64Shdr) - 1);
break;
case ElfCorruption::SectionTableOutOfRange: header.e_shoff = elf.size() + sizeof(header); break;
case ElfCorruption::MissingSectionNameTable: header.e_shstrndx = detail::ShnUndef; break;
case ElfCorruption::OverflowingSectionSize:
{
detail::Elf64Shdr section;
size_t offset = header.e_shoff + sizeof(section); // NULL=0, funcmap=1
std::memcpy(&section, elf.data() + offset, sizeof(section));
section.sh_size = ~uint64_t(0) - section.sh_offset + 1;
std::memcpy(elf.data() + offset, &section, sizeof(section));
break;
}
}
std::memcpy(elf.data(), &header, sizeof(header));
return elf;
}
} // namespace
TEST(ExtractElfSection, HandlesBasicSectionLayouts)
{
struct Case
{
const char* name;
std::vector<ElfSection> sections;
const char* expected;
bool warning;
};
for (const Case& test : {
Case{"find", {{".sqtt_funcmap", "F:1:foo\n"}}, "F:1:foo\n", false},
Case{"absent", {{".text", "xx"}}, nullptr, false},
Case{"empty", {{".sqtt_funcmap", {}}}, nullptr, true },
Case{
"multiple", {{".text", "xxx"}, {".rodata", "hello"}, {".sqtt_funcmap", "F:1:hit\n"}},
"F:1:hit\n", false},
})
{
SCOPED_TRACE(test.name);
auto elf = buildElf(test.sections);
std::vector<FuncmapDiagnostic> diags;
auto section =
extract_elf_section(reinterpret_cast<const char*>(elf.data()), elf.size(), ".sqtt_funcmap", diags);
if (test.expected)
{
ASSERT_TRUE(section);
EXPECT_EQ(*section, test.expected);
}
else
EXPECT_FALSE(section);
if (test.warning)
{
ASSERT_EQ(diags.size(), 1u);
EXPECT_EQ(diags[0].severity, FuncmapDiagnostic::Severity::Warning);
}
else
EXPECT_TRUE(diags.empty());
}
}
TEST(ExtractElfSection, RejectsMalformedHeadersAndBounds)
{
struct Case
{
const char* name;
ElfCorruption corruption;
};
for (const Case& test : {
Case{"non_elf64_class", ElfCorruption::InvalidClass },
Case{"bad_shentsize", ElfCorruption::InvalidSectionHeaderSize},
Case{"shoff_out_of_range", ElfCorruption::SectionTableOutOfRange },
Case{"missing_shstrndx", ElfCorruption::MissingSectionNameTable },
Case{"overflowing_sh_size", ElfCorruption::OverflowingSectionSize }
})
{
SCOPED_TRACE(test.name);
expectFuncmapExtractionError(malformedFuncmapElf(test.corruption));
}
}
TEST(ExtractElfSection, RejectsTinyAndNullBuffers)
{
char tiny[4] = {0};
expectFuncmapExtractionError(tiny, sizeof(tiny));
expectFuncmapExtractionError(nullptr, 0);
}