Skip to content

Commit

Permalink
add option to limit stored events
Browse files Browse the repository at this point in the history
  • Loading branch information
bw-hro committed Nov 9, 2024
1 parent ab983cb commit 08e119f
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 7 deletions.
1 change: 1 addition & 0 deletions examples/gui-thing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ auto make_gui_thing()
{
auto thing = make_thing("urn:gui-thing-123", "The WebThing Slot Machine", "SLOT_MACHINE_THING", "A slot machine thing with GUI");
thing->set_ui_href("/gui");
thing->set_event_storage_limit(1024);

link_property(thing, "coins", slot_machine.coins_inserted, {
{"title", "coins"},
Expand Down
90 changes: 90 additions & 0 deletions include/bw/webthing/storage.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Webthing-CPP
// SPDX-FileCopyrightText: 2023-present Benno Waldhauer
// SPDX-License-Identifier: MIT

#pragma once

#include <cstddef>
#include <vector>

namespace bw::webthing {

template<class T>
class SimpleRingBuffer
{
public:
SimpleRingBuffer(size_t capacity = SIZE_MAX)
: max_size(capacity)
, current_size(0)
, start_pos(0)
{
if(max_size < SIZE_MAX)
buffer.reserve(max_size);
}

T get(size_t index) const
{
if (index >= current_size)
throw std::out_of_range("Index out of range");

return buffer[(start_pos + index) % max_size];
}

void add(T element)
{
if (current_size < max_size)
{
buffer.push_back(element);
++current_size;
}
else
{
buffer[start_pos] = element;
start_pos = (start_pos + 1) % max_size;
}
}

size_t size() const
{
return current_size;
}

auto begin() const { return Iterator(this, 0); }
auto end() const { return Iterator(this, current_size); }

private:
std::vector<T> buffer;
size_t max_size;
size_t current_size;
size_t start_pos;

struct Iterator
{
Iterator(const SimpleRingBuffer* buffer, size_t position)
: buffer(buffer)
, position(position)
{}

bool operator!=(const Iterator& other) const
{
return position != other.position;
}

Iterator& operator++()
{
++position;
return *this;
}

T operator*() const
{
return buffer->get(position);
}

private:
const SimpleRingBuffer* buffer;
size_t position;
};
};

} // bw::webthing
21 changes: 14 additions & 7 deletions include/bw/webthing/thing.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <bw/webthing/event.hpp>
#include <bw/webthing/json.hpp>
#include <bw/webthing/property.hpp>
#include <bw/webthing/storage.hpp>

namespace bw::webthing {

Expand Down Expand Up @@ -151,7 +152,7 @@ class Thing
{
json descriptions = json::array();

for(auto& evt : events)
for(const auto& evt : events)
if(!event_name || event_name == evt->get_name())
descriptions.push_back(evt->as_event_description());

Expand Down Expand Up @@ -267,7 +268,7 @@ class Thing
{
if(!metadata.is_object())
throw ActionError("Action metadata must be encoded as json object.");

available_actions[name] = { metadata, class_supplier };
actions[name] = {};
}
Expand Down Expand Up @@ -313,7 +314,7 @@ class Thing
// Add a new event and notify subscribers
void add_event(std::shared_ptr<Event> event)
{
events.push_back(event);
events.add(event);
event_notify(*event);
}

Expand Down Expand Up @@ -354,9 +355,15 @@ class Thing
}

void add_message_observer(MessageCallback observer)
{
observers.push_back(observer);
}
{
observers.push_back(observer);
}

// limits the number of stored events, should be set in initialization phase
void set_event_storage_limit(size_t limit)
{
events = {limit};
}

protected:
std::string id;
Expand All @@ -368,7 +375,7 @@ class Thing
std::map<std::string, AvailableAction> available_actions;
std::map<std::string, json> available_events;
std::map<std::string, std::vector<std::shared_ptr<Action>>> actions;
std::vector<std::shared_ptr<Event>> events;
SimpleRingBuffer<std::shared_ptr<Event>> events;
std::string href_prefix;
std::optional<std::string> ui_href;
std::vector<MessageCallback> observers;
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ add_executable(tests
"unit-test/json_validator_tests.cpp"
"unit-test/property_tests.cpp"
"unit-test/server_tests.cpp"
"unit-test/storage_tests.cpp"
"unit-test/thing_tests.cpp"
"unit-test/utils_tests.cpp"
"unit-test/value_tests.cpp"
Expand Down
94 changes: 94 additions & 0 deletions test/unit-test/storage_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Webthing-CPP
// SPDX-FileCopyrightText: 2023-present Benno Waldhauer
// SPDX-License-Identifier: MIT

#include <catch2/catch_all.hpp>
#include <bw/webthing/event.hpp>
#include <bw/webthing/storage.hpp>

using namespace bw::webthing;

TEST_CASE( "SimpleRingBuffer must not have too much overhead compared to std::vector", "[.][benchmark][storage]" )
{
int number_elements = 10000;

BENCHMARK("SimpleRingBuffer with 256 limit")
{
SimpleRingBuffer<std::shared_ptr<Event>> storage(256); // limit
for(int i = 0; i < number_elements; i++)
storage.add(std::make_shared<Event>(nullptr, "test-event-" + std::to_string(i), "test-event-data"));
return storage;
};

BENCHMARK("SimpleRingBuffer without limit"){
SimpleRingBuffer<std::shared_ptr<Event>> storage; // no limit
for(int i = 0; i < number_elements; i++)
storage.add(std::make_shared<Event>(nullptr, "test-event-" + std::to_string(i), "test-event-data"));
return storage;
};

BENCHMARK("std::vector"){
std::vector<std::shared_ptr<Event>> storage; // no limitation
for(int i = 0; i < number_elements; i++)
storage.push_back(std::make_shared<Event>(nullptr, "test-event-" + std::to_string(i), "test-event-data"));
return storage;
};
}

TEST_CASE( "SimpleRingBuffer does not limit number of stored elements by default", "[storage]" )
{
SimpleRingBuffer<std::shared_ptr<Event>> storage; // no limitation

REQUIRE( storage.size() == 0 );
REQUIRE_THROWS( storage.get(0) );

int number_elements = 1000000;
for(int i = 0; i < number_elements; i++)
storage.add(std::make_shared<Event>(nullptr, "test-event-" + std::to_string(i), "test-event-data"));

REQUIRE( storage.size() == number_elements );
REQUIRE( storage.get(0)->get_name() == "test-event-0" );
REQUIRE( storage.get(42)->get_name() == "test-event-42" );
REQUIRE( storage.get(number_elements-1)->get_name() == "test-event-999999" );
REQUIRE_THROWS( storage.get(number_elements) );

std::vector<std::string> names;
for(auto const& evt : storage)
{
names.push_back(evt->get_name());
}

REQUIRE( names[0] == storage.get(0)->get_name() );
REQUIRE( names[42] == storage.get(42)->get_name() );
REQUIRE( names[number_elements-1] == storage.get(number_elements-1)->get_name() );
}

TEST_CASE( "SimpleRingBuffer can limit number of stored elements", "[storage]" )
{
SimpleRingBuffer<std::shared_ptr<Event>> storage(3); // 3 elements limit

REQUIRE( storage.size() == 0 );
REQUIRE_THROWS( storage.get(0) );

storage.add(std::make_shared<Event>(nullptr, "test-event-1", "test-event-data"));
storage.add(std::make_shared<Event>(nullptr, "test-event-2", "test-event-data"));
storage.add(std::make_shared<Event>(nullptr, "test-event-3", "test-event-data"));
storage.add(std::make_shared<Event>(nullptr, "test-event-4", "test-event-data"));
storage.add(std::make_shared<Event>(nullptr, "test-event-5", "test-event-data"));
storage.add(std::make_shared<Event>(nullptr, "test-event-6", "test-event-data"));

REQUIRE( storage.size() == 3 );
REQUIRE( storage.get(0)->get_name() == "test-event-4" );
REQUIRE( storage.get(1)->get_name() == "test-event-5" );
REQUIRE( storage.get(2)->get_name() == "test-event-6" );
REQUIRE_THROWS( storage.get(4) );

std::vector<std::string> names;
for(auto const& evt : storage)
{
names.push_back(evt->get_name());
}

std::vector<std::string> expected_names = {"test-event-4", "test-event-5", "test-event-6"};
REQUIRE( names == expected_names );
}

0 comments on commit 08e119f

Please sign in to comment.