Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more possible types for the entries of Benchmark::ResultTable #1134

Merged
merged 11 commits into from
Nov 9, 2023
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
12 changes: 6 additions & 6 deletions benchmark/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ BenchmarkResults runAllBenchmarks(){
results.addMeasurement(identifier, dummyFunctionToMeasure);

/*
Create an empty table with a set number of rows and columns. Doesn't
measure anything.
Create an empty table with a number of rows and columns. Doesn't measure anything.
The number of columns can not be changed after creation, but the number of rows can.
Important: The row names aren't saved in a seperate container, but INSIDE the
first column of the table.
*/
Expand All @@ -76,7 +76,7 @@ BenchmarkResults runAllBenchmarks(){
// You can add measurements to the table as entries, but you can also
// read and set entries.
table.addMeasurement(0, 2, dummyFunctionToMeasure); // Row 0, column 1.
table.setEntry(0, 1, "A custom entry can be a float, or a string.");
table.setEntry(0, 1, "A custom entry can be any type in 'ad_benchmark::ResultTable::EntryType', except 'std::monostate'.");
table.getEntry(0, 2); // The measured time of the dummy function.

// Replacing a row name.
Expand Down Expand Up @@ -119,11 +119,11 @@ Setting metadata is handled by the `BenchmarkMetadata` class. The set metadata i

You can find instances of `BenchmarkMetadata` for your usage at 4 locations:

- At `metadata_` of created `ResultEntry` objects, in order to give metadata information about the benchmark measurement.
- At `metadata()` of created `ResultEntry` objects, in order to give metadata information about the benchmark measurement.

- At `metadata_` of created `ResultGroup` objects, in order to give metadata information about the group.
- At `metadata()` of created `ResultGroup` objects, in order to give metadata information about the group.

- At `metadata_` of created `ResultTable` objects, in order to give metadata information about the table.
- At `metadata()` of created `ResultTable` objects, in order to give metadata information about the table.

- Writing a `getMetadata` function, like in the `BenchmarkInterface`, in order to give more general metadata information about your benchmark class. This is mostly, so that you don't have to constantly repeat metadata information, that are true for all the things you are measuring, in other places. For example, this would be a good place to give the name of an algorithm, if your whole benchmark class is about measuring the runtimes of one. Or you could give the time, at which those benchmark measurements were taken.

Expand Down
32 changes: 24 additions & 8 deletions benchmark/infrastructure/BenchmarkMeasurementContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
#include <memory>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "../benchmark/infrastructure/BenchmarkToString.h"
Expand All @@ -25,6 +27,8 @@
#include "util/Iterators.h"
#include "util/StringUtils.h"

using namespace std::string_literals;

namespace ad_benchmark {

// ____________________________________________________________________________
Expand Down Expand Up @@ -151,12 +155,15 @@ ResultTable::ResultTable(const std::string& descriptor,
// ____________________________________________________________________________
void ResultTable::setEntry(const size_t& row, const size_t& column,
const EntryType& newEntryContent) {
// 'Deleting' an entry doesn't make much sense.
AD_CONTRACT_CHECK(!std::holds_alternative<std::monostate>(newEntryContent));

entries_.at(row).at(column) = newEntryContent;
}

// ____________________________________________________________________________
ResultTable::operator std::string() const {
// Used for the formating of numbers in the table. They will always be
// Used for the formating of floats in the table. They will always be
// formated as having 4 values after the decimal point.
static constexpr absl::string_view floatFormatSpecifier = "%.4f";

Expand All @@ -166,19 +173,28 @@ ResultTable::operator std::string() const {
// Convert an `EntryType` of `ResultTable` to a screen friendly
// format.
auto entryToStringVisitor = []<typename T>(const T& entry) {
// We have 3 possible types, because `EntryType` has three distinct possible
// types, that all need different handeling.
// Fortunaly, we can decide the handeling at compile time and throw the
// others away, using `if constexpr(std::is_same<...,...>::value)`.
/*
`EntryType` has multiple distinct possible types, that all need different
handeling. Fortunaly, we can decide the handeling at compile time and
throw the others away, using `if constexpr(std::is_same<...,...>::value)`.
*/
if constexpr (std::is_same_v<T, std::monostate>) {
// No value, print it as NA.
return (std::string) "NA";
return "NA"s;
} else if constexpr (std::is_same_v<T, float>) {
// There is a value, format it as specified.
return absl::StrFormat(floatFormatSpecifier, entry);
} else {
// Only other possible type is a string, which needs no formating.
} else if constexpr (std::is_same_v<T, size_t> || std::is_same_v<T, int>) {
// There is already a `std` function for this.
return std::to_string(entry);
} else if constexpr (std::is_same_v<T, std::string>) {
return entry;
} else if constexpr (std::is_same_v<T, bool>) {
// Simple conversion.
return entry ? "true"s : "false"s;
} else {
// Unsupported type.
AD_FAIL();
}
};
auto entryToString = [&entryToStringVisitor](const EntryType& entry) {
Expand Down
18 changes: 15 additions & 3 deletions benchmark/infrastructure/BenchmarkMeasurementContainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
#include <concepts>
#include <memory>
#include <utility>
#include <variant>
#include <vector>

#include "../benchmark/infrastructure/BenchmarkMetadata.h"
#include "util/CopyableUniquePtr.h"
#include "util/Exception.h"
#include "util/Log.h"
#include "util/Timer.h"
#include "util/TypeTraits.h"
#include "util/json.h"

namespace ad_benchmark {
Expand Down Expand Up @@ -125,6 +127,16 @@ class ResultEntry : public BenchmarkMetadataGetter {

// Describes a table of measured execution times of functions.
class ResultTable : public BenchmarkMetadataGetter {
public:
/*
What type of entry a `ResultTable` can hold. The float is for the measured
time in seconds, the `monostate` for empty entries, and the rest for custom
entries by the user for better readability.
*/
using EntryType =
std::variant<std::monostate, float, std::string, bool, size_t, int>;

private:
// For identification.
std::string descriptor_;
/*
Expand All @@ -137,7 +149,6 @@ class ResultTable : public BenchmarkMetadataGetter {
std::vector<std::string> columnNames_;
// The entries in the table. Access is [row, column]. Can be the time in
// seconds, a string, or empty.
using EntryType = std::variant<std::monostate, float, std::string>;
std::vector<std::vector<EntryType>> entries_;

// Needed for testing purposes.
Expand Down Expand Up @@ -210,16 +221,17 @@ class ResultTable : public BenchmarkMetadataGetter {
@brief Returns the content of a table entry, if the the correct type was
given. Otherwise, causes an error.

@tparam T What type the entry has. Must be either `float`, or `string`. If
@tparam T What type the entry has. Must be contained in `EntryType`. If
you give the wrong one, or the entry was never set/added, then this
function will cause an exception.

@param row, column Which table entry to read. Starts with `(0,0)`.
*/
template <typename T>
requires std::is_same_v<T, float> || std::is_same_v<T, std::string>
requires ad_utility::isTypeContainedIn<T, EntryType>
T getEntry(const size_t row, const size_t column) const {
AD_CONTRACT_CHECK(row < numRows() && column < numColumns());
static_assert(!ad_utility::isSimilar<T, std::monostate>);

// There is a chance, that the entry of the table does NOT have type T,
// in which case this will cause an error. As this is a mistake on the
Expand Down
159 changes: 117 additions & 42 deletions test/BenchmarkMeasurementContainerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
// Author: Andre Schlegel (April of 2023,
// schlegea@informatik.uni-freiburg.de)

#include <absl/strings/str_cat.h>
#include <gtest/gtest.h>

#include <chrono>
#include <string>

#include "../benchmark/infrastructure/BenchmarkMeasurementContainer.h"
#include "util/Exception.h"

using namespace std::chrono_literals;
using namespace std::string_literals;

namespace ad_benchmark {
/// Create a lambda that waits the given amount of time.
Expand All @@ -19,8 +25,6 @@ static auto createWaitLambda(std::chrono::milliseconds waitDuration) {
};
}

using namespace std::chrono_literals;

TEST(BenchmarkMeasurementContainerTest, ResultEntry) {
// There's really no special cases.
const std::string entryDescriptor{"entry"};
Expand Down Expand Up @@ -79,6 +83,49 @@ TEST(BenchmarkMeasurementContainerTest, ResultGroup) {
ASSERT_EQ(table.getEntry<std::string>(1, 0), rowNames.at(1));
}

/*
@brief Call the function with each of the alternatives in
`ad_benchmark::ResultTable::EntryType`, except `std::monostate`, as template
parameter.

@tparam Function The loop body should be a templated function, with one
`typename` template argument and no more. It also shouldn't take any function
arguments. Should be passed per deduction.
*/
template <typename Function>
static void doForTypeInResultTableEntryType(Function function) {
ad_utility::ConstexprForLoop(
std::make_index_sequence<std::variant_size_v<ResultTable::EntryType>>{},
[&function]<size_t index, typename IndexType = std::variant_alternative_t<
index, ResultTable::EntryType>>() {
// `std::monostate` is not important for these kinds of tests.
if constexpr (!ad_utility::isSimilar<IndexType, std::monostate>) {
function.template operator()<IndexType>();
}
});
}

// Helper function for creating `ad_benchmark::ResultTable::EntryType` dummy
// values.
template <typename Type>
requires ad_utility::isTypeContainedIn<Type, ResultTable::EntryType>
static Type createDummyValueEntryType() {
if constexpr (ad_utility::isSimilar<Type, float>) {
return 4.2f;
} else if constexpr (ad_utility::isSimilar<Type, std::string>) {
return "test"s;
} else if constexpr (ad_utility::isSimilar<Type, bool>) {
return true;
} else if constexpr (ad_utility::isSimilar<Type, size_t>) {
return 17361644613946UL;
} else if constexpr (ad_utility::isSimilar<Type, int>) {
return -42;
} else {
// Not a supported type.
AD_FAIL();
}
}

TEST(BenchmarkMeasurementContainerTest, ResultTable) {
// Looks, if the general form is correct.
auto checkForm = [](const ResultTable& table, const std::string& name,
Expand Down Expand Up @@ -114,8 +161,9 @@ TEST(BenchmarkMeasurementContainerTest, ResultTable) {
table.entries_.at(row).at(column)));

// Does trying to access it anyway cause an error?
ASSERT_ANY_THROW(table.getEntry<std::string>(row, column));
ASSERT_ANY_THROW(table.getEntry<float>(row, column));
doForTypeInResultTableEntryType([&table, &row, &column]<typename T>() {
ASSERT_ANY_THROW(table.getEntry<T>(row, column));
});
};

/*
Expand All @@ -126,10 +174,20 @@ TEST(BenchmarkMeasurementContainerTest, ResultTable) {
const auto&... wantedContent) {
size_t column = 0;
auto check = [&table, &rowNumber, &column,
&assertEqual](const auto& wantedContent) mutable {
assertEqual(wantedContent,
table.getEntry<std::decay_t<decltype(wantedContent)>>(
rowNumber, column++));
&assertEqual]<typename T>(const T& wantedContent) mutable {
// `getEntry` should ONLY work with `T`
doForTypeInResultTableEntryType(
[&table, &rowNumber, &column, &assertEqual,
&wantedContent]<typename PossiblyWrongType>() {
if constexpr (ad_utility::isSimilar<PossiblyWrongType, T>) {
assertEqual(wantedContent,
table.getEntry<std::decay_t<T>>(rowNumber, column));
} else {
ASSERT_ANY_THROW(
table.getEntry<PossiblyWrongType>(rowNumber, column));
}
});
column++;
};
((check(wantedContent)), ...);
};
Expand All @@ -155,42 +213,59 @@ TEST(BenchmarkMeasurementContainerTest, ResultTable) {
// Was it created correctly?
checkForm(table, "My table", "My table", rowNames, columnNames);

// Add measured function to it.
table.addMeasurement(0, 1, createWaitLambda(10ms));

// Set custom entries.
table.setEntry(0, 2, 4.9f);
table.setEntry(1, 1, "Custom entry");

// Check the entries.
checkRow(table, 0, std::string{"row1"}, 0.01f, 4.9f);
checkRow(table, 1, std::string{"row2"}, std::string{"Custom entry"});
checkNeverSet(table, 1, 2);

// Trying to get entries with the wrong type, should cause an error.
ASSERT_ANY_THROW(table.getEntry<std::string>(0, 2));
ASSERT_ANY_THROW(table.getEntry<float>(1, 1));

// Can we add a new row, without changing things?
table.addRow();
table.setEntry(2, 0, std::string{"row3"});
checkForm(table, "My table", "My table", {"row1", "row2", "row3"},
columnNames);
checkRow(table, 0, std::string{"row1"}, 0.01f, 4.9f);
checkRow(table, 1, std::string{"row2"}, std::string{"Custom entry"});

// Are the entries of the new row empty?
checkNeverSet(table, 2, 1);
checkNeverSet(table, 2, 2);

// To those new fields work like the old ones?
table.addMeasurement(2, 1, createWaitLambda(29ms));
table.setEntry(2, 2, "Custom entry #2");
checkRow(table, 2, std::string{"row3"}, 0.029f,
std::string{"Custom entry #2"});

// Does the constructor for the custom log name work?
checkForm(ResultTable("My table", "T", rowNames, columnNames), "My table",
"T", rowNames, columnNames);

// Add measured function to it.
table.addMeasurement(0, 1, createWaitLambda(10ms));

// Check, if it works with custom entries.
doForTypeInResultTableEntryType(
[&table, &checkNeverSet, &checkRow]<typename T1>() {
doForTypeInResultTableEntryType([&table, &checkNeverSet,
&checkRow]<typename T2>() {
// Set custom entries.
table.setEntry(0, 2, createDummyValueEntryType<T1>());
table.setEntry(1, 1, createDummyValueEntryType<T2>());

// Check the entries.
checkRow(table, 0, "row1"s, 0.01f, createDummyValueEntryType<T1>());
checkRow(table, 1, "row2"s, createDummyValueEntryType<T2>());
checkNeverSet(table, 1, 2);
});
});

// For keeping track of the new row names.
std::vector<std::string> addRowRowNames(rowNames);
// Testing `addRow`.
doForTypeInResultTableEntryType([&table, &checkNeverSet, &checkRow,
&checkForm, &columnNames,
&addRowRowNames]<typename T>() {
// What is the index of the new row?
const size_t indexNewRow = table.numRows();

// Can we add a new row, without changing things?
table.addRow();
addRowRowNames.emplace_back(absl::StrCat("row", indexNewRow + 1));
table.setEntry(indexNewRow, 0, addRowRowNames.back());
checkForm(table, "My table", "My table", addRowRowNames, columnNames);
checkRow(table, 0, "row1"s, 0.01f);
checkRow(table, 1, "row2"s);
checkNeverSet(table, 1, 2);

// Are the entries of the new row empty?
checkNeverSet(table, indexNewRow, 1);
checkNeverSet(table, indexNewRow, 2);

// To those new fields work like the old ones?
table.addMeasurement(indexNewRow, 1, createWaitLambda(29ms));
table.setEntry(indexNewRow, 2, createDummyValueEntryType<T>());
checkRow(table, indexNewRow, addRowRowNames.back(), 0.029f,
createDummyValueEntryType<T>());
});

// Just a simple existence test for printing.
const auto tableAsString = static_cast<std::string>(table);
}
} // namespace ad_benchmark