diff --git a/examples/gui-thing.cpp b/examples/gui-thing.cpp index 13c02b7..6dd3158 100644 --- a/examples/gui-thing.cpp +++ b/examples/gui-thing.cpp @@ -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"}, diff --git a/include/bw/webthing/storage.hpp b/include/bw/webthing/storage.hpp new file mode 100644 index 0000000..cdffdf4 --- /dev/null +++ b/include/bw/webthing/storage.hpp @@ -0,0 +1,90 @@ +// Webthing-CPP +// SPDX-FileCopyrightText: 2023-present Benno Waldhauer +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +namespace bw::webthing { + +template +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 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 diff --git a/include/bw/webthing/thing.hpp b/include/bw/webthing/thing.hpp index c22ec79..9f037b9 100644 --- a/include/bw/webthing/thing.hpp +++ b/include/bw/webthing/thing.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace bw::webthing { @@ -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()); @@ -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] = {}; } @@ -313,7 +314,7 @@ class Thing // Add a new event and notify subscribers void add_event(std::shared_ptr event) { - events.push_back(event); + events.add(event); event_notify(*event); } @@ -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; @@ -368,7 +375,7 @@ class Thing std::map available_actions; std::map available_events; std::map>> actions; - std::vector> events; + SimpleRingBuffer> events; std::string href_prefix; std::optional ui_href; std::vector observers; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index af0846b..9e5fd4f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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" diff --git a/test/unit-test/storage_tests.cpp b/test/unit-test/storage_tests.cpp new file mode 100644 index 0000000..cb66342 --- /dev/null +++ b/test/unit-test/storage_tests.cpp @@ -0,0 +1,94 @@ +// Webthing-CPP +// SPDX-FileCopyrightText: 2023-present Benno Waldhauer +// SPDX-License-Identifier: MIT + +#include +#include +#include + +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> storage(256); // limit + for(int i = 0; i < number_elements; i++) + storage.add(std::make_shared(nullptr, "test-event-" + std::to_string(i), "test-event-data")); + return storage; + }; + + BENCHMARK("SimpleRingBuffer without limit"){ + SimpleRingBuffer> storage; // no limit + for(int i = 0; i < number_elements; i++) + storage.add(std::make_shared(nullptr, "test-event-" + std::to_string(i), "test-event-data")); + return storage; + }; + + BENCHMARK("std::vector"){ + std::vector> storage; // no limitation + for(int i = 0; i < number_elements; i++) + storage.push_back(std::make_shared(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> 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(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 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> storage(3); // 3 elements limit + + REQUIRE( storage.size() == 0 ); + REQUIRE_THROWS( storage.get(0) ); + + storage.add(std::make_shared(nullptr, "test-event-1", "test-event-data")); + storage.add(std::make_shared(nullptr, "test-event-2", "test-event-data")); + storage.add(std::make_shared(nullptr, "test-event-3", "test-event-data")); + storage.add(std::make_shared(nullptr, "test-event-4", "test-event-data")); + storage.add(std::make_shared(nullptr, "test-event-5", "test-event-data")); + storage.add(std::make_shared(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 names; + for(auto const& evt : storage) + { + names.push_back(evt->get_name()); + } + + std::vector expected_names = {"test-event-4", "test-event-5", "test-event-6"}; + REQUIRE( names == expected_names ); +} \ No newline at end of file