Skip to content

Commit

Permalink
Configuration-based microbenchmarks (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anilm3 authored Jan 10, 2024
1 parent 9f2205c commit 8a0d79b
Show file tree
Hide file tree
Showing 48 changed files with 1,962 additions and 982 deletions.
9 changes: 2 additions & 7 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ jobs:
set -ex
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DLIBDDWAF_ENABLE_LTO=ON
VERBOSE=1 make -j benchmark benchcmp
VERBOSE=1 make -j benchmark
- name: Run Benchmark
run: |
cd build
make run_benchmark
make run_benchmark_human
- name: Download previous run
uses: dawidd6/action-download-artifact@v2
Expand All @@ -45,11 +45,6 @@ jobs:
branch: master
continue-on-error: true

- name: Compare performance
run: |
./build/perf/benchcmp previous/benchmark_results.json benchmark_results.json
continue-on-error: true

- name: Artifact
uses: actions/upload-artifact@v3
if: ${{ always() }}
Expand Down
54 changes: 27 additions & 27 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,30 +219,30 @@ jobs:
run: VERBOSE=1 make -j $(nproc) verify_rule verify_ruleset waf_runner validate_schema
working-directory: Debug

verify_ruleset:
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Create directories
run: mkdir Debug

- name: CMake
env:
CC: gcc-12
CXX: g++-12
run: |
cmake .. -DCMAKE_BUILD_TYPE=Debug
working-directory: Debug

- name: Build
run: VERBOSE=1 make -j $(nproc) verify_ruleset proj_event_rules
working-directory: Debug

- name: Verify
run: ./tools/verify_ruleset ./third_party/proj_event_rules-prefix/src/proj_event_rules/build/recommended.json
working-directory: Debug
#verify_ruleset:
#runs-on: ubuntu-latest
#strategy:
#fail-fast: false
#steps:
#- uses: actions/checkout@v3
#with:
#submodules: recursive

#- name: Create directories
#run: mkdir Debug

#- name: CMake
#env:
#CC: gcc-12
#CXX: g++-12
#run: |
#cmake .. -DCMAKE_BUILD_TYPE=Debug
#working-directory: Debug

#- name: Build
#run: VERBOSE=1 make -j $(nproc) verify_ruleset proj_event_rules
#working-directory: Debug

#- name: Verify
#run: ./tools/verify_ruleset ./third_party/proj_event_rules-prefix/src/proj_event_rules/build/recommended.json
#working-directory: Debug
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ if (LIBDDWAF_TESTING)
add_subdirectory(third_party EXCLUDE_FROM_ALL)
add_subdirectory(tests EXCLUDE_FROM_ALL)
add_subdirectory(validator EXCLUDE_FROM_ALL)
add_subdirectory(perf EXCLUDE_FROM_ALL)
add_subdirectory(benchmark EXCLUDE_FROM_ALL)
add_subdirectory(fuzzing EXCLUDE_FROM_ALL)
add_subdirectory(tools EXCLUDE_FROM_ALL)

Expand Down
31 changes: 31 additions & 0 deletions benchmark/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
file(GLOB_RECURSE LIBDDWAF_BENCHMARK_SOURCE *.cpp)

add_executable(benchmark ${LIBDDWAF_BENCHMARK_SOURCE})
target_compile_options(benchmark PRIVATE $<$<BOOL:${LIBDDWAF_ENABLE_LTO}>:-flto>)
target_link_libraries(benchmark PRIVATE libddwaf_objects lib_yamlcpp lib_rapidjson m)
target_include_directories(benchmark PRIVATE ${libddwaf_SOURCE_DIR}/src)

set_target_properties(benchmark PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO)

add_custom_target(run_benchmark_json
COMMAND $<TARGET_FILE:benchmark>
--scenarios=${CMAKE_CURRENT_SOURCE_DIR}/scenarios
--iterations=1000
--format=json
--output=benchmark_results.json
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
add_dependencies(run_benchmark_json benchmark)

add_custom_target(run_benchmark_human
COMMAND $<TARGET_FILE:benchmark>
--scenarios=${CMAKE_CURRENT_SOURCE_DIR}/scenarios
--runs 2
--iterations=1000
--format=human
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
add_dependencies(run_benchmark_human benchmark)
File renamed without changes.
249 changes: 249 additions & 0 deletions benchmark/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Unless explicitly stated otherwise all files in this repository are
// dual-licensed under the Apache-2.0 License or BSD-3-Clause License.
//
// This product includes software developed at Datadog
// (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.

#include <charconv>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <regex>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>

#include <clock.hpp>
#include <ddwaf.h>
#include <yaml-cpp/node/node.h>

#include "object_generator.hpp"
#include "output_formatter.hpp"
#include "random.hpp"
#include "rule_parser.hpp"
#include "run_fixture.hpp"
#include "runner.hpp"
#include "settings.hpp"
#include "utils.hpp"
#include "yaml_helpers.hpp"

namespace benchmark = ddwaf::benchmark;
namespace fs = std::filesystem;
namespace utils = ddwaf::benchmark::utils;

using test_result = ddwaf::benchmark::runner::test_result;
using generator_type = benchmark::object_generator::generator_type;

std::map<std::string, benchmark::object_generator::settings> default_tests = {
{"random.any", {.type = generator_type::random}},
{"random.long_strings", {.string_length = {512, 1024}, .type = generator_type::random}},
{"random.deep_containers", {.container_depth = {5, 10}, .type = generator_type::random}},
{"valid", {.type = generator_type::valid}},
{"mixed", {.type = generator_type::mixed}},
};

// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
void print_help_and_exit(std::string_view name, std::string_view error = {})
{
std::cerr << "Usage: " << name << " [OPTION]...\n"
<< " --scenarios VALUE Scenarios repository path\n"
<< " --runs VALUE Number of runs per scenario\n"
<< " --iterations VALUE Number of iterations per run\n"
<< " --seed VALUE Seed for the random number generator\n"
<< " --format VALUE Output format: csv, json, human, none\n"
<< " --output VALUE Results output file\n";

if (!error.empty()) {
std::cerr << "\nError: " << error << "\n";
utils::exit_failure();
}
utils::exit_success();
}

void print_tests_and_exit()
{
for (auto &[k, v] : default_tests) { std::cerr << k << std::endl; }
utils::exit_success();
}

std::map<std::string_view, std::string_view> parse_args(const std::vector<std::string> &args)
{
std::map<std::string_view, std::string_view> parsed_args;

for (std::size_t i = 1; i < args.size(); i++) {
std::string_view arg = args[i];
if (arg.substr(0, 2) != "--") {
continue;
}

auto assignment = arg.find('=');
if (assignment != std::string::npos) {
std::string_view opt_name = arg.substr(2, assignment - 2);
parsed_args[opt_name] = arg.substr(assignment + 1);
} else {
std::string_view opt_name = arg.substr(2);
parsed_args[opt_name] = {};

if ((i + 1) < args.size()) {
std::string_view value = args[i + 1];
if (value.substr(0, 2) != "--") {
parsed_args[opt_name] = value;
}
}
}
}

return parsed_args;
}

bool contains(std::map<std::string_view, std::string_view> &opts, std::string_view name)
{
return opts.find(name) != opts.end();
}

benchmark::settings generate_settings(const std::vector<std::string> &args)
{
benchmark::settings s;

auto opts = parse_args(args);

if (contains(opts, "help")) {
print_help_and_exit(args[0]);
}

if (!contains(opts, "scenarios")) {
print_help_and_exit(args[0], "Missing option --scenarios");
} else {
auto root = opts["scenarios"];
if (fs::is_directory(root)) {
for (const auto &dir_entry : fs::directory_iterator{root}) {
const fs::path &scenario_path = dir_entry;

if (is_regular_file(scenario_path) && scenario_path.extension() == ".json") {
s.scenarios.emplace_back(scenario_path);
}
}
} else {
s.scenarios.emplace_back(root);
}
}

if (contains(opts, "format")) {
auto format = opts["format"];
if (format == "csv") {
s.format = benchmark::output_fmt::csv;
} else if (format == "human") {
s.format = benchmark::output_fmt::human;
} else if (format == "json") {
s.format = benchmark::output_fmt::json;
} else if (format == "none") {
s.format = benchmark::output_fmt::none;
} else {
print_help_and_exit(args[0], "Unsupported value for --format");
}
}

if (s.format != benchmark::output_fmt::none && contains(opts, "output")) {
s.output_file = opts["output"];
}

if (contains(opts, "runs")) {
s.runs = utils::from_string<unsigned>(opts["runs"]);
if (s.runs == 0) {
print_help_and_exit(args[0], "Runs should be a positive number");
}
}

if (contains(opts, "iterations")) {
s.iterations = utils::from_string<unsigned>(opts["iterations"]);
if (s.iterations == 0) {
print_help_and_exit(args[0], "Iterations should be a positive number");
}
}

if (contains(opts, "seed")) {
s.seed = utils::from_string<uint64_t>(opts["seed"]);
} else {
s.seed = std::random_device()();
}

return s;
}

void initialise_runner(
benchmark::runner &runner, ddwaf_handle handle, benchmark::settings &s, const YAML::Node &spec)
{
std::unordered_set<std::string> fixtures;
auto fixtures_spec = spec["fixtures"];
if (fixtures_spec.IsDefined()) {
fixtures.reserve(fixtures_spec.size());
for (auto it = fixtures_spec.begin(); it != fixtures_spec.end(); ++it) {
fixtures.emplace(it->as<std::string>());
}
}

uint32_t addrs_len;
const auto *const addrs = ddwaf_known_addresses(handle, &addrs_len);
std::vector<std::string_view> addresses{addrs, addrs + static_cast<size_t>(addrs_len)};

benchmark::object_generator generator(addresses, spec);

unsigned num_objects = std::min(s.max_objects, s.iterations);
for (auto &[k, v] : default_tests) {
if (!fixtures.empty() && !fixtures.contains(k)) {
continue;
}
runner.register_fixture<benchmark::run_fixture>(k, handle, generator(v, num_objects));
}
}

int main(int argc, char *argv[])
{
std::vector<std::string> args(argv, argv + argc);

auto s = generate_settings(args);

benchmark::random::seed(s.seed);

std::map<std::string, std::vector<test_result>> all_results;
for (unsigned i = 0; i < s.runs; ++i) {
for (const auto &scenario : s.scenarios) {
YAML::Node spec = YAML::Load(utils::read_file(scenario));

auto name = spec["scenario"].as<std::string>();
auto ruleset = spec["ruleset"].as<ddwaf_object>();

ddwaf_config cfg{{0, 0, 0}, {nullptr, nullptr}, nullptr};
ddwaf_handle handle = ddwaf_init(&ruleset, &cfg, nullptr);
ddwaf_object_free(&ruleset);
if (handle == nullptr) {
std::cerr << "Invalid ruleset file" << std::endl;
utils::exit_failure();
}

benchmark::runner runner(std::move(name), s);
initialise_runner(runner, handle, s, spec);

auto results = runner.run();

for (auto &[key, value] : results) {
auto it = all_results.find(key);
if (it == all_results.end()) {
all_results.emplace(key, std::vector<test_result>{std::move(value)});
} else {
it->second.emplace_back(std::move(value));
}
}

ddwaf_destroy(handle);
}
}

benchmark::output_results(s, all_results);

return EXIT_SUCCESS;
}
Loading

0 comments on commit 8a0d79b

Please sign in to comment.