diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3050124 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CTEST_OUTPUT_ON_FAILURE: "1" + +jobs: + sanitizers: + name: ${{ matrix.compiler }}-${{ matrix.sanitizer }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + sanitizer: [address, undefined, thread] + include: + - compiler: gcc + c: gcc + cxx: g++ + - compiler: clang + c: clang + cxx: clang++ + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure CMake + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=${{ matrix.c }} \ + -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} \ + -DCMAKE_C_FLAGS="-O1 -g -fno-omit-frame-pointer -fno-common -fsanitize=${{ matrix.sanitizer }}" \ + -DCMAKE_CXX_FLAGS="-O1 -g -fno-omit-frame-pointer -fno-common -fsanitize=${{ matrix.sanitizer }}" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=${{ matrix.sanitizer }}" + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure + env: + ASAN_OPTIONS: detect_leaks=0 + UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 + TSAN_OPTIONS: halt_on_error=1 + + coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install gcovr + run: python3 -m pip install --user gcovr + + - name: Configure CMake (coverage) + env: + CC: gcc + CXX: g++ + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_C_FLAGS="--coverage -O0" \ + -DCMAKE_CXX_FLAGS="--coverage -O0" \ + -DCMAKE_EXE_LINKER_FLAGS="--coverage" + + - name: Build + run: cmake --build build --parallel + + - name: Run tests with coverage + run: ctest --test-dir build --output-on-failure + + - name: Generate coverage report + run: | + ~/.local/bin/gcovr --root . --object-directory build --txt --output coverage-summary.txt + ~/.local/bin/gcovr --root . --object-directory build --xml-pretty --output coverage.xml + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + coverage-summary.txt + coverage.xml + + perf: + uses: ./.github/workflows/perf-smoke.yml + with: + threshold_ns: 300 diff --git a/.github/workflows/perf-smoke.yml b/.github/workflows/perf-smoke.yml new file mode 100644 index 0000000..241f5c7 --- /dev/null +++ b/.github/workflows/perf-smoke.yml @@ -0,0 +1,34 @@ +name: perf-smoke + +on: + workflow_call: + inputs: + threshold_ns: + description: Maximum acceptable nanoseconds per iteration + required: false + type: number + default: 300 + iterations: + description: Number of iterations to run in the benchmark + required: false + type: number + default: 5000000 + +jobs: + perf-smoke: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure CMake + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build perf benchmark + run: cmake --build build --target perf_smoke --parallel + + - name: Run perf benchmark + run: ./build/tests/perf_smoke + env: + TSD_PERF_THRESHOLD_NS: ${{ inputs.threshold_ns }} + TSD_PERF_ITERATIONS: ${{ inputs.iterations }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fa61ee..b4b5d05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.15) -project(thermal_simd_dispatcher VERSION 0.1.0 LANGUAGES C) +project(thermal_simd_dispatcher VERSION 0.1.0 LANGUAGES C CXX) set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(THERMAL_SIMD_DISPATCHER_CPU_FLAGS "-msse4.1" CACHE STRING "CPU-specific compiler flags for thermal_simd") @@ -66,6 +68,12 @@ if(BUILD_TESTING) target_compile_options(test_thermal_simd PRIVATE -Wall -Wextra -O1 -pthread -fPIC) target_include_directories(test_thermal_simd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_test(NAME thermal_simd COMMAND test_thermal_simd) + + add_executable(perf_smoke tests/perf_smoke.cpp) + target_link_libraries(perf_smoke PRIVATE thermal_simd_core_tests) + target_compile_features(perf_smoke PRIVATE cxx_std_17) + target_compile_options(perf_smoke PRIVATE -Wall -Wextra -O3) + set_target_properties(perf_smoke PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests) endif() install(TARGETS thermal_simd_core diff --git a/tests/perf_smoke.cpp b/tests/perf_smoke.cpp new file mode 100644 index 0000000..534555d --- /dev/null +++ b/tests/perf_smoke.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +namespace { + +double parse_env_double(const char *name, double fallback) { + const char *value = std::getenv(name); + if (!value || value[0] == '\0') { + return fallback; + } + try { + return std::stod(value); + } catch (const std::exception &) { + std::cerr << "[perf_smoke] Ignoring invalid value for " << name << ": " << value + << std::endl; + return fallback; + } +} + +std::uint64_t parse_env_uint64(const char *name, std::uint64_t fallback) { + const char *value = std::getenv(name); + if (!value || value[0] == '\0') { + return fallback; + } + try { + return static_cast(std::stoull(value)); + } catch (const std::exception &) { + std::cerr << "[perf_smoke] Ignoring invalid value for " << name << ": " << value + << std::endl; + return fallback; + } +} + +std::uint64_t run_benchmark(std::uint64_t iterations) { + std::mt19937 rng(1337u); + std::uniform_int_distribution dist(800, 1200); + + std::uint64_t ewma = 0; + const unsigned ewma_shift = 3; + + auto start = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < iterations; ++i) { + ewma = tsd_update_ewma(ewma, dist(rng), ewma_shift); + } + auto stop = std::chrono::steady_clock::now(); + + return static_cast( + std::chrono::duration_cast(stop - start).count()); +} + +} // namespace + +int main() { + const std::uint64_t iterations = parse_env_uint64("TSD_PERF_ITERATIONS", 5'000'000ULL); + const double threshold_ns = parse_env_double("TSD_PERF_THRESHOLD_NS", 300.0); + + if (iterations == 0) { + std::cerr << "[perf_smoke] iteration count must be non-zero" << std::endl; + return EXIT_FAILURE; + } + + const std::uint64_t total_ns = run_benchmark(iterations); + const double per_iteration = static_cast(total_ns) / static_cast(iterations); + + std::cout << std::fixed << std::setprecision(2); + std::cout << "[perf_smoke] iterations=" << iterations << ", total_ns=" << total_ns + << ", per_iteration_ns=" << per_iteration + << ", threshold_ns=" << threshold_ns << std::endl; + + if (per_iteration > threshold_ns) { + std::cerr << "[perf_smoke] performance regression detected: " << per_iteration + << " ns/iter exceeds threshold of " << threshold_ns << " ns/iter" << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/tests/test_thermal_simd.c b/tests/test_thermal_simd.c index c73c105..5d98e76 100644 --- a/tests/test_thermal_simd.c +++ b/tests/test_thermal_simd.c @@ -56,11 +56,11 @@ static int run_monitor_thread_scenario(void) { return 1; } int rc = 0; - if (wait_for_width(SIMD_SSE41, 500) != 0) { + if (wait_for_width(SIMD_SSE41, 5000) != 0) { fprintf(stderr, "width did not downgrade\n"); rc = 1; } - if (rc == 0 && wait_for_width(SIMD_AVX2, 500) != 0) { + if (rc == 0 && wait_for_width(SIMD_AVX2, 5000) != 0) { fprintf(stderr, "width did not upgrade\n"); rc = 1; }