Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/agent_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Check Code Formatting
run: |
find sources/agent/src -name "*.cc" -o -name "*.h" | xargs clang-format --dry-run --Werror -style=Google
find sources/agent/src sources/agent/tests -name "*.cc" -o -name "*.h" | xargs clang-format --dry-run --Werror -style=Google

- name: CMake Configure
working-directory: sources/agent
Expand All @@ -54,13 +54,18 @@ jobs:
working-directory: sources/agent
run: cmake --build --preset default -j$(nproc)

- name: Test
working-directory: sources/agent/build
run: ctest --output-on-failure

- name: Analyze Logic
run: |
cppcheck --enable=warning,performance,portability \
--error-exitcode=1 \
--suppress=missingIncludeSystem \
--inline-suppr \
--force \
--library=googletest \
-i sources/agent/build \
-i sources/agent/gen \
sources/agent
47 changes: 35 additions & 12 deletions sources/agent/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,38 @@ target_link_libraries(volta_proto PUBLIC
gRPC::grpc++_reflection
)

# --- Agent executable ---
add_executable(volta)
# --- Agent library (shared by volta and volta_tests) ---
file(GLOB_RECURSE AGENT_LIB_SOURCES CONFIGURE_DEPENDS "src/*.cc")
list(FILTER AGENT_LIB_SOURCES EXCLUDE REGEX ".*/main\\.cc$")

add_library(volta_lib STATIC)
target_sources(volta_lib PRIVATE ${AGENT_LIB_SOURCES})

target_compile_options(volta PRIVATE -std=c++20)
target_compile_options(volta_lib PRIVATE -std=c++20)

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build - adding DEBUG macro")
target_compile_definitions(volta PRIVATE DEBUG)
target_compile_definitions(volta_lib PUBLIC DEBUG)
endif()

file(GLOB_RECURSE AGENT_SOURCES CONFIGURE_DEPENDS "src/*.cc")
target_sources(volta PRIVATE ${AGENT_SOURCES})

target_include_directories(volta PRIVATE
target_include_directories(volta_lib PUBLIC
src
${CMAKE_SOURCE_DIR}/libs/include/nvidia
)

target_link_libraries(volta PRIVATE
target_link_libraries(volta_lib PUBLIC
volta_proto
Threads::Threads
fmt::fmt
tomlplusplus::tomlplusplus
${CMAKE_DL_LIBS}
)

target_compile_definitions(volta PRIVATE NVML_NO_UNVERSIONED_FUNC_DEFS)
target_compile_definitions(volta_lib PUBLIC NVML_NO_UNVERSIONED_FUNC_DEFS)

# --- Agent executable ---
add_executable(volta src/main.cc)
target_link_libraries(volta PRIVATE volta_lib)

# --- Config file ---
if(NOT EXISTS ${CMAKE_SOURCE_DIR}/agent.conf)
Expand Down Expand Up @@ -131,18 +136,36 @@ add_custom_command(
COMMENT "Creating hardlink voltad..."
)

# --- Unit tests ---
include(CTest)
if(BUILD_TESTING)
find_package(GTest CONFIG REQUIRED)

add_executable(volta_tests)
target_sources(volta_tests PRIVATE
tests/series_buffer_test.cc
)
target_link_libraries(volta_tests PRIVATE
volta_lib
GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(volta_tests)
endif()

# --- Formatting ---
find_program(CLANG_FORMAT_EXE clang-format)

if(CLANG_FORMAT_EXE)
add_custom_target(format
COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} -i -style=Google
COMMAND find src tests -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} -i -style=Google
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-format on agent sources..."
VERBATIM
)
add_custom_target(check-format
COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} --dry-run --Werror -style=Google
COMMAND find src tests -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} --dry-run --Werror -style=Google
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Checking agent source formatting..."
VERBATIM
Expand Down
16 changes: 15 additions & 1 deletion sources/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,21 @@ cmake -B build -S . --preset default
cmake --build build
```

### 4. Run Agent
### 4. Run Tests

Tests build by default (`BUILD_TESTING=ON`). After a successful build:

```bash
# Run the discovered GoogleTest suite via CTest
ctest --test-dir build --output-on-failure

# Or run the test binary directly
./build/volta_tests
```

To configure without tests: `cmake -B build -S . --preset default -DBUILD_TESTING=OFF`.

### 5. Run Agent

```bash
# May require root/admin privileges to access RAPL/Affinity features.
Expand Down
1 change: 1 addition & 0 deletions sources/agent/src/buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void SeriesBuffer::SetCapacity(size_t capacity) {
samples_.reserve(capacity_);
samples_.resize(capacity_);
head_ = 0;
tail_ = 0;
}

void SeriesBuffer::Push(const Sample& sample) {
Expand Down
4 changes: 2 additions & 2 deletions sources/agent/src/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ class SeriesBuffer {

void SetCapacity(size_t capacity);
size_t Capacity() const { return capacity_; }
size_t Size() const { return samples_.size(); }
bool Empty() const { return samples_.empty(); }
size_t Size() const { return head_ - tail_; }
bool Empty() const { return Size() == 0; }
size_t GetHead() const { return head_; }
size_t GetTail() const { return tail_; }

Expand Down
189 changes: 189 additions & 0 deletions sources/agent/tests/series_buffer_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#include <cstddef>

#include "buffer.h"
#include "gtest/gtest.h"

namespace volta {
namespace agent {
namespace {

Sample MakeSample(int64_t timestamp_ns, double value) {
return Sample{.timestamp_ns = timestamp_ns, .value = value};
}

TEST(SeriesBufferTest, FreshBufferIsEmptyAndHasNoLatest) {
SeriesBuffer buffer(4);

EXPECT_TRUE(buffer.Empty());
EXPECT_EQ(buffer.Size(), 0u);
EXPECT_EQ(buffer.Capacity(), 4u);
EXPECT_FALSE(buffer.Latest().has_value());
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);
}

TEST(SeriesBufferTest, PushToZeroCapacityIsNoOp) {
SeriesBuffer buffer(0);

buffer.Push(MakeSample(1, 1.0));

EXPECT_TRUE(buffer.Empty());
EXPECT_EQ(buffer.Size(), 0u);
EXPECT_FALSE(buffer.Latest().has_value());
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);
}

TEST(SeriesBufferTest, PushUpToCapacityRetainsSamplesInOrder) {
SeriesBuffer buffer(3);
buffer.Push(MakeSample(10, 1.0));
buffer.Push(MakeSample(20, 2.0));
buffer.Push(MakeSample(30, 3.0));

EXPECT_FALSE(buffer.Empty());
EXPECT_EQ(buffer.Size(), 3u);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 3u);

const auto snapshot = buffer.GetSnapshot();
ASSERT_EQ(snapshot.samples.size(), 3u);
EXPECT_EQ(snapshot.samples[0].timestamp_ns, 10);
EXPECT_EQ(snapshot.samples[0].value, 1.0);
EXPECT_EQ(snapshot.samples[1].timestamp_ns, 20);
EXPECT_EQ(snapshot.samples[1].value, 2.0);
EXPECT_EQ(snapshot.samples[2].timestamp_ns, 30);
EXPECT_EQ(snapshot.samples[2].value, 3.0);
EXPECT_EQ(snapshot.end, buffer.GetHead());
}

TEST(SeriesBufferTest, PushBeyondCapacityEvictsOldestAndAdvancesTail) {
SeriesBuffer buffer(2);
buffer.Push(MakeSample(10, 1.0));
buffer.Push(MakeSample(20, 2.0));
const size_t tail_before_overflow = buffer.GetTail();

buffer.Push(MakeSample(30, 3.0));

EXPECT_EQ(buffer.Size(), 2u);
EXPECT_EQ(buffer.GetTail(), tail_before_overflow + 1);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 2u);

const auto snapshot = buffer.GetSnapshot();
ASSERT_EQ(snapshot.samples.size(), 2u);
EXPECT_EQ(snapshot.samples[0].timestamp_ns, 20);
EXPECT_EQ(snapshot.samples[0].value, 2.0);
EXPECT_EQ(snapshot.samples[1].timestamp_ns, 30);
EXPECT_EQ(snapshot.samples[1].value, 3.0);
}

TEST(SeriesBufferTest, LatestReturnsMostRecentSampleIncludingAfterWrap) {
SeriesBuffer buffer(2);
buffer.Push(MakeSample(10, 1.0));
ASSERT_TRUE(buffer.Latest().has_value());
EXPECT_EQ(buffer.Latest()->timestamp_ns, 10);
EXPECT_EQ(buffer.Latest()->value, 1.0);

buffer.Push(MakeSample(20, 2.0));
buffer.Push(MakeSample(30, 3.0));

ASSERT_TRUE(buffer.Latest().has_value());
EXPECT_EQ(buffer.Latest()->timestamp_ns, 30);
EXPECT_EQ(buffer.Latest()->value, 3.0);
}

TEST(SeriesBufferTest, GetNextSnapshotSizeEqualsUnsentCount) {
SeriesBuffer buffer(4);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);

buffer.Push(MakeSample(1, 1.0));
buffer.Push(MakeSample(2, 2.0));
EXPECT_EQ(buffer.GetNextSnapshotSize(), 2u);

const auto snapshot = buffer.GetSnapshot();
buffer.AckSnapshot(snapshot);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);

buffer.Push(MakeSample(3, 3.0));
EXPECT_EQ(buffer.GetNextSnapshotSize(), 1u);
}

TEST(SeriesBufferTest, GetSnapshotReturnsUnsentSamplesAndEndMarker) {
SeriesBuffer buffer(4);
buffer.Push(MakeSample(11, 1.5));
buffer.Push(MakeSample(22, 2.5));

const auto snapshot = buffer.GetSnapshot();

ASSERT_EQ(snapshot.samples.size(), 2u);
EXPECT_EQ(snapshot.samples[0].timestamp_ns, 11);
EXPECT_EQ(snapshot.samples[0].value, 1.5);
EXPECT_EQ(snapshot.samples[1].timestamp_ns, 22);
EXPECT_EQ(snapshot.samples[1].value, 2.5);
EXPECT_EQ(snapshot.end, buffer.GetHead());
}

TEST(SeriesBufferTest, AckSnapshotAdvancesTailPastAckedSamples) {
SeriesBuffer buffer(4);
buffer.Push(MakeSample(1, 1.0));
buffer.Push(MakeSample(2, 2.0));
buffer.Push(MakeSample(3, 3.0));

const auto snapshot = buffer.GetSnapshot();
const size_t head_at_ack = snapshot.end;
buffer.AckSnapshot(snapshot);

EXPECT_EQ(buffer.GetTail(), head_at_ack);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);
EXPECT_TRUE(buffer.Empty());
}

TEST(SeriesBufferTest, AckSnapshotIsMonotonicAndIdempotent) {
SeriesBuffer buffer(4);
buffer.Push(MakeSample(1, 1.0));
buffer.Push(MakeSample(2, 2.0));
buffer.Push(MakeSample(3, 3.0));

const auto first = buffer.GetSnapshot();
buffer.AckSnapshot(first);
const size_t tail_after_first = buffer.GetTail();

buffer.AckSnapshot(first);
EXPECT_EQ(buffer.GetTail(), tail_after_first);

SeriesBuffer::Snapshot stale{.samples = {}, .end = 0};
buffer.AckSnapshot(stale);
EXPECT_EQ(buffer.GetTail(), tail_after_first);

buffer.Push(MakeSample(4, 4.0));
const auto second = buffer.GetSnapshot();
ASSERT_GT(second.end, first.end);
buffer.AckSnapshot(second);
EXPECT_EQ(buffer.GetTail(), second.end);
EXPECT_GE(buffer.GetTail(), tail_after_first);
}

TEST(SeriesBufferTest, SetCapacityResetsToEmptyConsistentState) {
SeriesBuffer buffer(3);
buffer.Push(MakeSample(1, 1.0));
buffer.Push(MakeSample(2, 2.0));
buffer.Push(MakeSample(3, 3.0));
buffer.Push(MakeSample(4, 4.0)); // wrap; advances tail
ASSERT_GT(buffer.GetTail(), 0u);
ASSERT_GT(buffer.GetHead(), 0u);

buffer.SetCapacity(5);

EXPECT_EQ(buffer.Capacity(), 5u);
EXPECT_EQ(buffer.GetHead(), 0u);
EXPECT_EQ(buffer.GetTail(), 0u);
EXPECT_TRUE(buffer.Empty());
EXPECT_EQ(buffer.Size(), 0u);
EXPECT_EQ(buffer.GetNextSnapshotSize(), 0u);
EXPECT_FALSE(buffer.Latest().has_value());

// Snapshot sizing must not underflow after reset on a previously live buffer.
const auto snapshot = buffer.GetSnapshot();
EXPECT_TRUE(snapshot.samples.empty());
EXPECT_EQ(snapshot.end, 0u);
}

} // namespace
} // namespace agent
} // namespace volta
1 change: 1 addition & 0 deletions sources/agent/vcpkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": [
"fmt",
"grpc",
"gtest",
"protobuf",
"tomlplusplus"
]
Expand Down
Loading