yams/tests/support/test_rusage.hpp
Trevon 777e35adae
feat(profiling): Tracy for every library, zones on p2p and memory-sync, per-test CPU/RSS (#294)
* feat(profiling): Tracy zones on p2p and memory-sync background paths

Zone the memory-sync cycle (publish, sync, reconcile, apply, backfill,
delete-outbox drain), the p2p reconnect cycle, accept loop, per-session
worker and frame read, and the delta send/apply/bootstrap stages. Plot the
retained session count and pending erase intents. No behavior change; the
macros are no-ops without Tracy.

Signed-off-by: trvon <me@trevon.dev>

* test(infra): per-test-case CPU and peak RSS accounting for Catch2 binaries

A Catch2 listener in the shared test main records getrusage (GetProcessTimes
on Windows) CPU per case and prints the five heaviest cases plus the run's
peak RSS to stderr at run end. YAMS_TEST_RUSAGE=1 adds one line per case.
The measurement helpers are header-only and unit tested.

Signed-off-by: trvon <me@trevon.dev>

* fix(vector): make vector_database.cpp compile with Tracy enabled

The Profiling lane did not build: eight YAMS_PLOT sites in
vector_database.cpp used -> on a Result<T>, and two passed an int that is
ambiguous between Tracy's PlotData overloads. Use .value() and int64_t
casts. No behavior change outside TRACY_ENABLE builds.

Signed-off-by: trvon <me@trevon.dev>

* build: enable Tracy for every yams library in the Profiling lane

Define yams_tracy_args/yams_tracy_deps once in the root meson.build
(Tracy found, optional rocprofiler hooks, link check) and apply them to
every library and executable under src/, replacing the per-library copies
in search, topology, vector, daemon and tests/benchmarks. Empty when
enable-profiling is off.

Make YAMS_PLOT accept any numeric type and fix a Result deref in
search_service so the newly instrumented TUs compile with Tracy.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
2026-10-14 08:32:56 -06:00

121 lines
3.8 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
// Per-process CPU/RSS measurement helpers used by the Catch2 resource listener in
// catch2_main.cpp. Header-only so the unit test can exercise them without the listener.
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
// psapi.h needs windows.h first; keep this order.
#include <psapi.h>
#else
#include <sys/resource.h>
#include <sys/time.h>
#endif
namespace yams::test::rusage {
struct Sample {
std::uint64_t cpuMicros{0}; // user + system CPU of this process
std::uint64_t peakRssBytes{0}; // high-water resident set size
};
// ru_maxrss is bytes on macOS and kilobytes on Linux/BSD.
constexpr std::uint64_t normalizeMaxRss(std::uint64_t raw, bool rawIsBytes) noexcept {
return rawIsBytes ? raw : raw * 1024ULL;
}
inline Sample sampleSelf() noexcept {
Sample out;
#if defined(_WIN32)
FILETIME creation{}, exit{}, kernel{}, user{};
if (::GetProcessTimes(::GetCurrentProcess(), &creation, &exit, &kernel, &user)) {
const auto toMicros = [](const FILETIME& ft) {
ULARGE_INTEGER v;
v.LowPart = ft.dwLowDateTime;
v.HighPart = ft.dwHighDateTime;
return static_cast<std::uint64_t>(v.QuadPart / 10ULL); // 100 ns ticks
};
out.cpuMicros = toMicros(kernel) + toMicros(user);
}
PROCESS_MEMORY_COUNTERS counters{};
if (::GetProcessMemoryInfo(::GetCurrentProcess(), &counters, sizeof(counters))) {
out.peakRssBytes = static_cast<std::uint64_t>(counters.PeakWorkingSetSize);
}
#else
::rusage usage{};
if (::getrusage(RUSAGE_SELF, &usage) == 0) {
const auto toMicros = [](const timeval& tv) {
return static_cast<std::uint64_t>(tv.tv_sec) * 1000000ULL +
static_cast<std::uint64_t>(tv.tv_usec);
};
out.cpuMicros = toMicros(usage.ru_utime) + toMicros(usage.ru_stime);
#if defined(__APPLE__)
constexpr bool kRawBytes = true;
#else
constexpr bool kRawBytes = false;
#endif
out.peakRssBytes = normalizeMaxRss(static_cast<std::uint64_t>(usage.ru_maxrss), kRawBytes);
}
#endif
return out;
}
inline std::uint64_t cpuDeltaMicros(const Sample& before, const Sample& after) noexcept {
return after.cpuMicros >= before.cpuMicros ? after.cpuMicros - before.cpuMicros : 0;
}
struct CaseCost {
std::string name;
std::uint64_t cpuMicros{0};
};
// The n most CPU-expensive cases, heaviest first; ties keep insertion order.
inline std::vector<CaseCost> topByCpu(std::vector<CaseCost> cases, std::size_t n) {
std::stable_sort(cases.begin(), cases.end(), [](const CaseCost& a, const CaseCost& b) {
return a.cpuMicros > b.cpuMicros;
});
if (cases.size() > n) {
cases.resize(n);
}
return cases;
}
// Names are clamped so one pathological test name cannot flood the summary line.
inline std::string clampName(const std::string& name, std::size_t maxLen = 60) {
return name.size() <= maxLen ? name : name.substr(0, maxLen - 3) + "...";
}
inline std::string formatTopCpu(const std::vector<CaseCost>& top) {
std::string line = "[rusage] top cpu:";
bool first = true;
for (const auto& c : top) {
line += first ? " " : ", ";
first = false;
line += clampName(c.name) + " " + std::to_string(c.cpuMicros / 1000ULL) + "ms";
}
if (top.empty()) {
line += " (none)";
}
return line;
}
inline std::string formatPeakRss(std::uint64_t bytes) {
return "[rusage] peak rss: " + std::to_string(bytes / (1024ULL * 1024ULL)) + " MB";
}
} // namespace yams::test::rusage