From 9b01ba7d80738e0093294b6d08b49338a3b75b56 Mon Sep 17 00:00:00 2001 From: GhostofCookie Date: Thu, 9 Oct 2025 10:53:39 -0400 Subject: [PATCH 1/3] Some cleanup and improvements, added benchmarks. --- .github/workflows/cmake.yml | 64 ++++++- CMakeLists.txt | 10 + benchmarks/CMakeLists.txt | 51 +++++ benchmarks/event_bench.cpp | 31 +++ benchmarks/graph_bench.cpp | 22 +++ benchmarks/indexable_name_bench.cpp | 41 ++++ benchmarks/node_data_bench.cpp | 29 +++ benchmarks/type_name_bench.cpp | 18 ++ benchmarks/uuid_bench.cpp | 53 +++++ cabin.toml | 9 - docs/getting-started.md | 2 +- include/flow/core/Env.hpp | 8 +- include/flow/core/Event.hpp | 4 +- include/flow/core/Graph.hpp | 19 +- include/flow/core/IndexableName.hpp | 276 ++++++++++++++++++++++----- include/flow/core/Node.hpp | 1 + include/flow/core/NodeFactory.hpp | 11 +- include/flow/core/TypeConversion.hpp | 22 ++- include/flow/core/UUID.hpp | 4 +- src/Env.cpp | 5 + src/lib.cpp | 0 tests/factory_test.cpp | 6 +- tests/graph_test.cpp | 2 +- tests/indexable_name_test.cpp | 1 + tests/module_test.cpp | 2 +- tests/node_test.cpp | 32 ++-- 26 files changed, 615 insertions(+), 108 deletions(-) create mode 100644 benchmarks/CMakeLists.txt create mode 100644 benchmarks/event_bench.cpp create mode 100644 benchmarks/graph_bench.cpp create mode 100644 benchmarks/indexable_name_bench.cpp create mode 100644 benchmarks/node_data_bench.cpp create mode 100644 benchmarks/type_name_bench.cpp create mode 100644 benchmarks/uuid_bench.cpp delete mode 100644 cabin.toml delete mode 100644 src/lib.cpp diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 41d9f6d..e7b6246 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -23,7 +23,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} continue-on-error: true @@ -50,6 +50,68 @@ jobs: name: Test Module (${{matrix.os}}) path: ${{github.workspace}}/build/tests/test_module.fmod + - name: Cache Build Output + uses: actions/cache@v4 + with: + path: ${{github.workspace}}/build + key: ${{matrix.os}}-build-${{github.sha}} + + test: + needs: build + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + + runs-on: ${{ matrix.os }} + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + - name: Restore build output cache + uses: actions/cache@v4 + with: + path: ${{github.workspace}}/build + key: ${{matrix.os}}-build-${{github.sha}} + - name: Test working-directory: ${{github.workspace}}/build run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure + + benchmark: + needs: build + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + + runs-on: ${{ matrix.os }} + continue-on-error: true + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Restore build output cache + uses: actions/cache@v4 + with: + path: ${{github.workspace}}/build + key: ${{matrix.os}}-build-${{github.sha}} + + - name: Benchmark + run: ${{github.workspace}}/build/benchmark/flow_core_bench + + cleanup_cache: + needs: [test, benchmark] + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + + runs-on: ${{ matrix.os }} + continue-on-error: true + timeout-minutes: 10 + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + + if: always() # Run even if previous jobs failed + steps: + - name: Delete Cache + run: | + gh cache delete ${{matrix.os}}-build-${{github.sha}} diff --git a/CMakeLists.txt b/CMakeLists.txt index f208656..cd17929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ endif() # ----------------------------------------------------------------------------- option(${PROJECT_NAME}_BUILD_TESTS "Build tests (gtest)" OFF) +option(${PROJECT_NAME}_BUILD_BENCHMARKS "Build benchmarks (googlebenchmark)" OFF) option(${PROJECT_NAME}_BUILD_TOOLS "Build tools" OFF) option(${PROJECT_NAME}_INSTALL "Add installation targets" OFF) @@ -152,6 +153,15 @@ if (${PROJECT_NAME}_BUILD_TESTS) add_subdirectory(tests) endif() +# ----------------------------------------------------------------------------- +# Benchmarks +# ----------------------------------------------------------------------------- + +if (${PROJECT_NAME}_BUILD_BENCHMARKS) + enable_testing() + add_subdirectory(benchmarks) +endif() + # ----------------------------------------------------------------------------- # Tools # ----------------------------------------------------------------------------- diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..7f3b064 --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.21) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +#=============================================================================# +# Dependencies +#=============================================================================# + +set(BENCHMARK_USE_BUNDLED_GTEST OFF) +set(BENCHMARK_ENABLE_TESTING OFF) +CPMAddPackage("gh:google/benchmark@1.9.4") + +#=============================================================================# +# Test Executable +#=============================================================================# + +set(BENCH_EXE flow_core_bench) +add_executable( + ${BENCH_EXE} + + event_bench.cpp + graph_bench.cpp + indexable_name_bench.cpp + node_data_bench.cpp + type_name_bench.cpp + uuid_bench.cpp +) + +if(MSVC) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) + target_compile_options(${BENCH_EXE} PRIVATE /W4 /MP) +endif() + +target_link_libraries(${BENCH_EXE} PRIVATE + flow-core::flow-core + benchmark::benchmark_main +) + +target_include_directories(${BENCH_EXE} PRIVATE + ${CMAKE_SOURCE_DIR}/include/flow/core + ${thread_pool_SOURCE_DIR}/include +) + +if(MSVC) + add_custom_command(TARGET ${BENCH_EXE} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) +endif() diff --git a/benchmarks/event_bench.cpp b/benchmarks/event_bench.cpp new file mode 100644 index 0000000..05b90a4 --- /dev/null +++ b/benchmarks/event_bench.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include +#include +#include +#include + +#include +#include + +static void EventDispatcher_Broadcast(benchmark::State& state) +{ + flow::EventDispatcher<> dispatcher; + std::array names; + + for (int i = 0; i < names.size(); ++i) + { + names[i] = "Event_" + std::to_string(i); + dispatcher.Bind(flow::IndexableName{names[i]}, [&] { ++i; }); + } + + for ([[maybe_unused]] const auto& _ : state) + { + dispatcher.Broadcast(); + } +} + +BENCHMARK(EventDispatcher_Broadcast); diff --git a/benchmarks/graph_bench.cpp b/benchmarks/graph_bench.cpp new file mode 100644 index 0000000..10ffedf --- /dev/null +++ b/benchmarks/graph_bench.cpp @@ -0,0 +1,22 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include +#include +#include + +static void Graph_Construct(benchmark::State& state) +{ + auto factory = flow::NodeFactory::Create(); + auto env = flow::Env::Create(factory); + for ([[maybe_unused]] const auto& _ : state) + { + flow::Graph graph("benchmark", env); + benchmark::DoNotOptimize(graph); + benchmark::ClobberMemory(); + } +} + +BENCHMARK(Graph_Construct); diff --git a/benchmarks/indexable_name_bench.cpp b/benchmarks/indexable_name_bench.cpp new file mode 100644 index 0000000..793690d --- /dev/null +++ b/benchmarks/indexable_name_bench.cpp @@ -0,0 +1,41 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include + +static void IndexableName_Construct(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + flow::IndexableName name{"benchmark"}; + benchmark::DoNotOptimize(name); + benchmark::ClobberMemory(); + } +} + +static void IndexableName_ConstexprConstruct(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + constexpr flow::IndexableName name{"benchmark"}; + benchmark::DoNotOptimize(name); + benchmark::ClobberMemory(); + } +} + +static void IndexableName_Hash(benchmark::State& state) +{ + constexpr flow::IndexableName name{"benchmark"}; + for ([[maybe_unused]] const auto& _ : state) + { + auto h = std::hash{}(name); + benchmark::DoNotOptimize(h); + benchmark::ClobberMemory(); + } +} + +BENCHMARK(IndexableName_Construct); +BENCHMARK(IndexableName_ConstexprConstruct); +BENCHMARK(IndexableName_Hash); diff --git a/benchmarks/node_data_bench.cpp b/benchmarks/node_data_bench.cpp new file mode 100644 index 0000000..87fd903 --- /dev/null +++ b/benchmarks/node_data_bench.cpp @@ -0,0 +1,29 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include + +static void NodeData_Construct(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + flow::NodeData data; + benchmark::DoNotOptimize(data); + benchmark::ClobberMemory(); + } +} + +static void NodeData_ConstructShared(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + auto data = flow::MakeNodeData(0); + benchmark::DoNotOptimize(data); + benchmark::ClobberMemory(); + } +} + +BENCHMARK(NodeData_Construct); +BENCHMARK(NodeData_ConstructShared); diff --git a/benchmarks/type_name_bench.cpp b/benchmarks/type_name_bench.cpp new file mode 100644 index 0000000..3c93f74 --- /dev/null +++ b/benchmarks/type_name_bench.cpp @@ -0,0 +1,18 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include + +static void TypeName_Construct(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + flow::TypeName name; + benchmark::DoNotOptimize(name); + benchmark::ClobberMemory(); + } +} + +BENCHMARK(TypeName_Construct); diff --git a/benchmarks/uuid_bench.cpp b/benchmarks/uuid_bench.cpp new file mode 100644 index 0000000..1fa0a18 --- /dev/null +++ b/benchmarks/uuid_bench.cpp @@ -0,0 +1,53 @@ +// Copyright (c) 2025, Cisco Systems, Inc. +// All rights reserved. + +#include + +#include + +static void UUID_ConstructGenerate(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + flow::UUID id{}; + benchmark::DoNotOptimize(id); + benchmark::ClobberMemory(); + } +} + +static void UUID_ConstructFromString(benchmark::State& state) +{ + for ([[maybe_unused]] const auto& _ : state) + { + flow::UUID id{"b24f917e-3626-4246-bf13-c2543145abfd"}; + benchmark::DoNotOptimize(id); + benchmark::ClobberMemory(); + } +} + +static void UUID_ConstructToString(benchmark::State& state) +{ + flow::UUID id{"b24f917e-3626-4246-bf13-c2543145abfd"}; + for ([[maybe_unused]] const auto& _ : state) + { + std::string uuid_str = std::string(id); + benchmark::DoNotOptimize(id); + benchmark::ClobberMemory(); + } +} + +static void UUID_Hash(benchmark::State& state) +{ + flow::UUID id{"b24f917e-3626-4246-bf13-c2543145abfd"}; + for ([[maybe_unused]] const auto& _ : state) + { + auto h = std::hash{}(id); + benchmark::DoNotOptimize(h); + benchmark::ClobberMemory(); + } +} + +BENCHMARK(UUID_ConstructGenerate); +BENCHMARK(UUID_ConstructFromString); +BENCHMARK(UUID_ConstructToString); +BENCHMARK(UUID_Hash); diff --git a/cabin.toml b/cabin.toml deleted file mode 100644 index 3fd60b0..0000000 --- a/cabin.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "flow-core" -version = "1.2.0" -authors = ["Scott Henning ", "Tomas Rigaux "] -edition = "20" - -[dependencies] -json = { git = "https://github.com/nlohmann/json.git" } -thread_pool = { git = "https://github.com/bshoshany/thread-pool.git" } diff --git a/docs/getting-started.md b/docs/getting-started.md index 9c57e49..2c95533 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -19,7 +19,7 @@ Flow Core is built around these key concepts: #include int main() { - auto factory = std::make_shared(); + auto factory = flow::NodeFactory::Create(); auto env = flow::Env::Create(factory); auto graph = std::make_shared("MyGraph", env); diff --git a/include/flow/core/Env.hpp b/include/flow/core/Env.hpp index 32c1834..44bd145 100644 --- a/include/flow/core/Env.hpp +++ b/include/flow/core/Env.hpp @@ -4,7 +4,6 @@ #pragma once #include "Core.hpp" -#include "NodeFactory.hpp" #include @@ -49,7 +48,7 @@ class Env public: /// Type alias for a function that visits a shared node. - using VisitorFunction = std::function; + using VisitorFunction = std::function&)>; Env(const Env&) = delete; @@ -60,10 +59,7 @@ class Env * @param settings The settings for the environment. Defaults to default-constructed Settings. * @returns A shared pointer to the created Env. */ - static std::shared_ptr Create(std::shared_ptr factory, const Settings& settings = {}) - { - return std::shared_ptr(new Env(std::move(factory), settings)); - } + static std::shared_ptr Create(std::shared_ptr factory, const Settings& settings = {}); /** * @brief Gets the current factory for building nodes. diff --git a/include/flow/core/Event.hpp b/include/flow/core/Event.hpp index 4507811..cafe3aa 100644 --- a/include/flow/core/Event.hpp +++ b/include/flow/core/Event.hpp @@ -33,7 +33,7 @@ class EventDispatcher * @param name The unique identifier of the event to be bound. * @param event The event to be bound. */ - void Bind(IndexableName name, EventType&& event) noexcept { _events.emplace(name, std::move(event)); } + void Bind(IndexableName name, EventType&& event) noexcept { _events.try_emplace(name, std::move(event)); } /** * @brief Unbinds an event by name. @@ -48,7 +48,7 @@ class EventDispatcher void UnbindAll() { _events.clear(); } /** - * @brief Broadcasts the given arguments to all bound events. + * @brief Broadcasts the given arguments to all bound events asynchronously. * * @param args Variadic arguments to pass to all bound events. */ diff --git a/include/flow/core/Graph.hpp b/include/flow/core/Graph.hpp index 2b535e5..cba681b 100644 --- a/include/flow/core/Graph.hpp +++ b/include/flow/core/Graph.hpp @@ -169,15 +169,6 @@ class Graph void DisconnectNodes(const UUID& start, const IndexableName& start_key, const UUID& end, const IndexableName& end_key); - /** - * @brief Propagates data through the connections of the given ID. - * - * @param id The ID of the connection where the data came from. - * @param key The name of the port from which data is flowing. - * @param data The data to propagate. - */ - void PropagateConnectionsData(const UUID& id, const IndexableName& key, SharedNodeData data); - /** * @brief Sets the name of the graph. * @param new_name The new Name of the graph. @@ -208,6 +199,16 @@ class Graph */ friend void from_json(const json& j, Graph& g); + protected: + /** + * @brief Propagates data through the connections of the given ID. + * + * @param id The ID of the connection where the data came from. + * @param key The name of the port from which data is flowing. + * @param data The data to propagate. + */ + void PropagateConnectionsData(const UUID& id, const IndexableName& key, SharedNodeData data); + public: /// Event run on Graph errors being thrown. EventDispatcher OnError; diff --git a/include/flow/core/IndexableName.hpp b/include/flow/core/IndexableName.hpp index d17aaa8..a12d8ef 100644 --- a/include/flow/core/IndexableName.hpp +++ b/include/flow/core/IndexableName.hpp @@ -6,7 +6,9 @@ #include "Core.hpp" #include +#include #include +#include #include #include #include @@ -16,75 +18,261 @@ FLOW_NAMESPACE_BEGIN /** * @brief Unique hashable integer representation of string. * - * @details IndexableName creates an integer representation of string using CRC-64-ECMA for + * @details IndexableName creates an integer representation of string using CityHash64 for * collision-resistant hashing. The hash is computed at compile-time when possible. * The name reference is kept to help with visualization and debugging, but the hash * is used for all comparisons and storage. * - * @note Uses CRC-64-ECMA polynomial: 0xC96C5795D7870F42 + * @note Uses CityHash64 */ class IndexableName { private: - /// CRC-64-ECMA lookup table for efficient hash computation - static constexpr std::array crc_table = [] { - std::array table; - for (std::size_t c = 0; c < 256; ++c) + /** + * @brief Computes a CityHash64 hash for a span of bytes. + * + * @note Original algorithm from https://github.com/google/cityhash + * + * Copyright (c) 2011 Google, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + class CityHash64 + { + using uint128_t = std::array; + + static constexpr std::uint64_t k0 = 0xc3a5c85c97cb3127ULL; + static constexpr std::uint64_t k1 = 0xb492b66fbe98f273ULL; + static constexpr std::uint64_t k2 = 0x9ae16a3b2f90404fULL; + + private: + constexpr std::uint32_t SwapBytes(const std::uint32_t value) + { + return ((value >> 24) & 0x000000FF) | ((value >> 8) & 0x0000FF00) | ((value << 8) & 0x00FF0000) | + ((value << 24) & 0xFF000000); + } + + constexpr std::uint64_t SwapBytes(const std::uint64_t value) { - std::size_t crc = c; - for (std::size_t i = 0; i < 8; ++i) + return ((value >> 56) & 0x00000000000000FF) | ((value >> 40) & 0x000000000000FF00) | + ((value >> 24) & 0x0000000000FF0000) | ((value >> 8) & 0x00000000FF000000) | + ((value << 8) & 0x000000FF00000000) | ((value << 24) & 0x0000FF0000000000) | + ((value << 40) & 0x00FF000000000000) | ((value << 56) & 0xFF00000000000000); + } + + template + constexpr T UnalignedLoad(std::string_view bytes) + { + T result = 0; + if (std::is_constant_evaluated()) + { + for (size_t i = 0; i < sizeof(T); ++i) + { + result += bytes[i]; + result <<= 8; + } + } + else { - std::size_t b = (crc & 1); - crc >>= 1; - crc ^= (0 - b) & 0xc96c5795d7870f42ull; + std::memcpy(&result, bytes.data(), sizeof(T)); } - table[c] = crc; + + return result; } - return table; - }(); + template + constexpr T Fetch(std::string_view bytes) + { + return SwapBytes(UnalignedLoad(bytes)); + } - /** - * @brief Reverses bits in a 64-bit integer for CRC finalization. - * @param value The value to reverse - * @returns The bit-reversed value - */ - static constexpr std::size_t reverse_bits(std::size_t value) noexcept - { - std::size_t result = 0; - for (std::size_t i = 0; i < 64; ++i, value >>= 1) + template + constexpr std::uint64_t Rotate(T val, int shift) { - result = (result << 1) | (value & 1); + return shift == 0 ? val : ((val >> shift) | (val << ((sizeof(T) * 8) - shift))); } - return result; - } + constexpr std::uint64_t ShiftMix(std::uint64_t val) { return val ^ (val >> 47); } - /** - * @brief Computes CRC-64-ECMA hash of string. - * @param str The string to hash - * @returns The CRC-64 hash of the given string - * @throws std::invalid_argument if str is empty - */ - static constexpr std::size_t hash(std::string_view str) - { - if (str.empty()) + constexpr std::uint64_t Hash128to64(const uint128_t& x) { - throw std::invalid_argument("IndexableName cannot be empty"); + constexpr std::uint64_t kMul = 0x9ddfea08eb382d69ULL; + std::uint64_t a = (x[0] ^ x[1]) * kMul; + a ^= (a >> 47); + std::uint64_t b = (x[1] ^ a) * kMul; + b ^= (b >> 47); + b *= kMul; + return b; } - std::size_t crc = 0; - for (auto c : str) + constexpr std::uint64_t HashLen16(std::uint64_t u, std::uint64_t v) { return Hash128to64(uint128_t{u, v}); } + + constexpr std::uint64_t HashLen16(std::uint64_t u, std::uint64_t v, std::uint64_t mul) { - crc = crc_table[(crc & 0xFF) ^ c] ^ (crc >> 8); + // Murmur-inspired hashing. + std::uint64_t a = (u ^ v) * mul; + a ^= (a >> 47); + std::uint64_t b = (v ^ a) * mul; + b ^= (b >> 47); + b *= mul; + return b; } - return reverse_bits(crc); - } + constexpr std::uint64_t HashLen0to16(std::string_view bytes) + { + const std::size_t len = bytes.size(); + + if (len >= 8) + { + std::uint64_t mul = k2 + len * 2; + std::uint64_t a = Fetch(bytes) + k2; + std::uint64_t b = Fetch(bytes.substr(len - 8)); + std::uint64_t c = Rotate(b, 37) * mul + a; + std::uint64_t d = (Rotate(a, 25) + b) * mul; + return HashLen16(c, d, mul); + } + if (len >= 4) + { + std::uint64_t mul = k2 + len * 2; + std::uint64_t a = Fetch(bytes); + return HashLen16(len + (a << 3), Fetch(bytes.substr(len - 4)), mul); + } + if (len > 0) + { + std::uint8_t a = static_cast(bytes[0]); + std::uint8_t b = static_cast(bytes[len >> 1]); + std::uint8_t c = static_cast(bytes[len - 1]); + std::uint32_t y = static_cast(a) + (static_cast(b) << 8); + std::uint32_t z = static_cast(len) + (static_cast(c) << 2); + return ShiftMix(y * k2 ^ z * k0) * k2; + } + return k2; + } + + constexpr std::uint64_t HashLen17to32(std::string_view bytes) + { + const std::size_t len = bytes.size(); + + std::uint64_t mul = k2 + len * 2; + std::uint64_t a = Fetch(bytes) * k1; + std::uint64_t b = Fetch(bytes.substr(8)); + std::uint64_t c = Fetch(bytes.substr(len - 8)) * mul; + std::uint64_t d = Fetch(bytes.substr(len - 16)) * k2; + return HashLen16(Rotate(a + b, 43) + Rotate(c, 30) + d, a + Rotate(b + k2, 18) + c, mul); + } + + constexpr uint128_t WeakHashLen32WithSeeds(std::uint64_t w, std::uint64_t x, std::uint64_t y, std::uint64_t z, + std::uint64_t a, std::uint64_t b) + { + a += w; + b = Rotate(b + a + z, 21); + std::uint64_t c = a; + a += x; + a += y; + b += Rotate(a, 44); + return {a + z, b + c}; + } + + constexpr uint128_t WeakHashLen32WithSeeds(std::string_view bytes, std::uint64_t a, std::uint64_t b) + { + return WeakHashLen32WithSeeds(Fetch(bytes), Fetch(bytes.substr(8)), + Fetch(bytes.substr(16)), + Fetch(bytes.substr(24)), a, b); + } + + constexpr std::uint64_t HashLen33to64(std::string_view bytes) + { + const std::size_t len = bytes.size(); + std::uint64_t mul = k2 + len * 2; + std::uint64_t a = Fetch(bytes) * k2; + std::uint64_t b = Fetch(bytes.substr(8)); + std::uint64_t c = Fetch(bytes.substr(len - 24)); + std::uint64_t d = Fetch(bytes.substr(len - 32)); + std::uint64_t e = Fetch(bytes.substr(16)) * k2; + std::uint64_t f = Fetch(bytes.substr(24)) * 9; + std::uint64_t g = Fetch(bytes.substr(len - 8)); + std::uint64_t h = Fetch(bytes.substr(len - 16)) * mul; + std::uint64_t u = Rotate(a + g, 43) + (Rotate(b, 30) + c) * 9; + std::uint64_t v = ((a + g) ^ d) + f + 1; + std::uint64_t w = SwapBytes((u + v) * mul) + h; + std::uint64_t x = Rotate(e + f, 42) + c; + std::uint64_t y = (SwapBytes((v + w) * mul) + g) * mul; + std::uint64_t z = e + f + c; + + a = SwapBytes((x + z) * mul + y) + b; + b = ShiftMix((z + a) * mul + d + h) * mul; + + return b + x; + } + + public: + constexpr std::uint64_t operator()(std::string_view bytes) + { + std::size_t len = bytes.size(); + + if (len <= 16) + { + return HashLen0to16(bytes); + } + else if (len <= 32) + { + return HashLen17to32(bytes); + } + else if (len <= 64) + { + return HashLen33to64(bytes); + } + + // For strings over 64 bytes we hash the end first, and then as we + // loop we keep 56 bytes of state: v, w, x, y, and z. + std::uint64_t x = Fetch(bytes.substr(len - 40)); + std::uint64_t y = + Fetch(bytes.substr(len - 16)) + Fetch(bytes.substr(len - 56)); + std::uint64_t z = HashLen16(Fetch(bytes.substr(len - 48)) + len, + Fetch(bytes.substr(len - 24))); + uint128_t v = WeakHashLen32WithSeeds(bytes.substr(len - 64), len, z); + uint128_t w = WeakHashLen32WithSeeds(bytes.substr(len - 32), y + k1, x); + x = x * k1 + Fetch(bytes); + + // Decrease len to the nearest multiple of 64, and operate on 64-byte chunks. + len = (len - 1) & ~static_cast(63); + do + { + x = Rotate(x + y + v[0] + Fetch(bytes.substr(8)), 37) * k1; + y = Rotate(y + v[1] + Fetch(bytes.substr(48)), 42) * k1; + x ^= w[1]; + y += v[0] + Fetch(bytes.substr(40)); + z = Rotate(z + w[0], 33) * k1; + v = WeakHashLen32WithSeeds(bytes, v[1] * k1, x + w[0]); + w = WeakHashLen32WithSeeds(bytes.substr(32), z + w[1], y + Fetch(bytes.substr(16))); + std::swap(z, x); + bytes = bytes.substr(64); + len -= 64; + } while (len != 0); + + return HashLen16(HashLen16(v[0], w[0]) + ShiftMix(y) * k1 + z, HashLen16(v[1], w[1]) + x); + } + }; public: /// Construct from string view, computing hash at compile time when possible - constexpr IndexableName(std::string_view name) noexcept : _value{hash(name)}, _name{name} {} + constexpr IndexableName(std::string_view name) noexcept : _value{CityHash64{}(name)}, _name{name} {} /// Construct from C-string, computing hash at compile time when possible constexpr IndexableName(const char* name) noexcept : IndexableName(std::string_view(name)) {} @@ -124,7 +312,7 @@ class IndexableName static const IndexableName None; private: - /// CRC-64-ECMA hash of the name + /// Hash of the name std::size_t _value; /// Reference to the original string @@ -144,5 +332,5 @@ FLOW_NAMESPACE_END template<> struct std::hash { - std::size_t operator()(const flow::IndexableName& name) const { return std::size_t(name); } + constexpr std::size_t operator()(const flow::IndexableName& name) const { return name.value(); } }; diff --git a/include/flow/core/Node.hpp b/include/flow/core/Node.hpp index 6f56cde..c81c800 100644 --- a/include/flow/core/Node.hpp +++ b/include/flow/core/Node.hpp @@ -230,6 +230,7 @@ class Node virtual void Compute() = 0; virtual json SaveInputs() const; + virtual void RestoreInputs(const json&); protected: diff --git a/include/flow/core/NodeFactory.hpp b/include/flow/core/NodeFactory.hpp index c6bb37d..2992475 100644 --- a/include/flow/core/NodeFactory.hpp +++ b/include/flow/core/NodeFactory.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -34,9 +33,17 @@ class NodeFactory { using ConstructorCallback = std::function)>; + NodeFactory() = default; + public: virtual ~NodeFactory() = default; + /** + * @brief Creates a shared NodeFactory. + * @returns Shared ptr to a NodeFactory. + */ + static std::shared_ptr Create() { return std::shared_ptr(new NodeFactory()); } + /** * @brief Registers a node's construction method by it's friendly name and a category. * @@ -220,8 +227,6 @@ class NodeFactory CategoryMap _category_map; std::unordered_map _friendly_names; TypeRegistry _conversion_registry; - - mutable std::mutex _mutex; }; /** diff --git a/include/flow/core/TypeConversion.hpp b/include/flow/core/TypeConversion.hpp index f521793..04d477f 100644 --- a/include/flow/core/TypeConversion.hpp +++ b/include/flow/core/TypeConversion.hpp @@ -48,6 +48,7 @@ class TypeRegistry return MakeNodeData(*from_data); } } + return data; } @@ -151,16 +152,17 @@ void TypeRegistry::RegisterUnidirectionalConversion(const ConversionFunc& conver if constexpr (std::is_same_v, std::decay_t>) { - RegisterConversion, std::add_lvalue_reference_t>>(); - RegisterConversion, std::add_lvalue_reference_t>>>(); - RegisterConversion, std::add_rvalue_reference_t>>(); - RegisterConversion>, - std::add_lvalue_reference_t>>>(); - RegisterConversion>, std::decay_t>(); - RegisterConversion>, - std::add_lvalue_reference_t>>>(); - RegisterConversion>>, std::decay_t>(); - RegisterConversion>, std::decay_t>(); + using T = std::decay_t; + using U = std::decay_t; + + RegisterConversion>(); + RegisterConversion>>(); + RegisterConversion>(); + RegisterConversion, std::add_lvalue_reference_t>>(); + RegisterConversion, U>(); + RegisterConversion, std::add_lvalue_reference_t>>(); + RegisterConversion>, U>(); + RegisterConversion, U>(); } } diff --git a/include/flow/core/UUID.hpp b/include/flow/core/UUID.hpp index 769585e..7da78a7 100644 --- a/include/flow/core/UUID.hpp +++ b/include/flow/core/UUID.hpp @@ -60,12 +60,10 @@ class UUID */ [[nodiscard]] std::size_t hash() const noexcept { - const auto* half = std::bit_cast(_id.data()); + const auto* half = reinterpret_cast(_id.data()); return half[0] ^ half[1]; } - friend struct std::hash; - private: /// Storage for 16-byte UUID value std::array _id; diff --git a/src/Env.cpp b/src/Env.cpp index 5e47407..6160e20 100644 --- a/src/Env.cpp +++ b/src/Env.cpp @@ -25,6 +25,11 @@ Env::Env(std::shared_ptr factory, const Settings& settings) std::chrono::days, std::chrono::months, std::chrono::years>(); } +std::shared_ptr Env::Create(std::shared_ptr factory, const Settings& settings) +{ + return std::shared_ptr(new Env(std::move(factory), settings)); +} + void Env::Wait() { _pool->wait(); } std::string Env::GetVar(const std::string& varname) const diff --git a/src/lib.cpp b/src/lib.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/tests/factory_test.cpp b/tests/factory_test.cpp index 3be8ced..faf957e 100644 --- a/tests/factory_test.cpp +++ b/tests/factory_test.cpp @@ -36,17 +36,17 @@ struct TestNode : public Node }; } // namespace -TEST(FactoryTest, Construction) { ASSERT_NO_THROW(auto factory = std::make_shared()); } +TEST(FactoryTest, Construction) { ASSERT_NO_THROW(auto factory = NodeFactory::Create()); } TEST(FactoryTest, RegisterNodeClass) { - auto factory = std::make_shared(); + auto factory = NodeFactory::Create(); ASSERT_NO_THROW(factory->RegisterNodeClass("Test")); } TEST(FactoryTest, CreateNode) { - auto factory = std::make_shared(); + auto factory = NodeFactory::Create(); auto env = Env::Create(factory); ASSERT_NO_THROW(factory->RegisterNodeClass("Test")); ASSERT_NO_THROW(auto node = factory->CreateNode(std::string{TypeName_v}, UUID{}, "test", env)); diff --git a/tests/graph_test.cpp b/tests/graph_test.cpp index f8e150a..c2eb6c6 100644 --- a/tests/graph_test.cpp +++ b/tests/graph_test.cpp @@ -13,7 +13,7 @@ using namespace flow; namespace { -auto factory = std::make_shared(); +auto factory = NodeFactory::Create(); auto env = Env::Create(factory); struct TestNode : public Node diff --git a/tests/indexable_name_test.cpp b/tests/indexable_name_test.cpp index 4fb1163..1228634 100644 --- a/tests/indexable_name_test.cpp +++ b/tests/indexable_name_test.cpp @@ -24,6 +24,7 @@ TEST(IndexableNameTest, Equality) ASSERT_EQ(IndexableName{"tests"}, IndexableName{std::string("tests")}); ASSERT_EQ(IndexableName{"tests"}, IndexableName{std::string_view("tests")}); + ASSERT_NE(IndexableName{"tests"}, IndexableName{"test"}); ASSERT_NE(IndexableName{"tests"}, IndexableName{"stset"}); } diff --git a/tests/module_test.cpp b/tests/module_test.cpp index bbf86b9..1a33702 100644 --- a/tests/module_test.cpp +++ b/tests/module_test.cpp @@ -14,7 +14,7 @@ using namespace flow; const std::filesystem::path module_path = std::filesystem::current_path() / "test_module.fmod"; -auto factory = std::make_shared(); +auto factory = NodeFactory::Create(); auto env = Env::Create(factory); TEST(ModuleTest, Load) diff --git a/tests/node_test.cpp b/tests/node_test.cpp index 89b9a6e..42b536e 100644 --- a/tests/node_test.cpp +++ b/tests/node_test.cpp @@ -14,7 +14,7 @@ using namespace flow; namespace test { -auto factory = std::make_shared(); +auto factory = NodeFactory::Create(); auto env = Env::Create(factory); } // namespace test @@ -97,6 +97,7 @@ TEST(NodeTest, AddInputPorts) ASSERT_EQ(data->Get(), 101); } } + TEST(NodeTest, AddOutputPorts) { NodeTest::TestNode node; @@ -157,21 +158,22 @@ int return_ref_test_method(int& i) { return i; } struct TestData { }; -void custom_type(const TestData&) {} +void custom_type_test_method(const TestData&) {} + +#define DECLARE_FUNCTION_NODE(f) FunctionNode f##_node({}, #f, test::env); TEST(NodeTest, WrapFunctions) { - FunctionNode void_node({}, "void_test_method", test::env); - FunctionNode return_node({}, "return_test_method", test::env); - FunctionNode return_ref_node({}, "return_ref_test_method", - test::env); - FunctionNode custom_type_node({}, "custom_type", test::env); - - ASSERT_EQ(void_node.GetInputPorts().size(), 1); - ASSERT_EQ(return_node.GetInputPorts().size(), 1); - ASSERT_TRUE(return_ref_node.GetInputPorts().empty()); - - ASSERT_TRUE(void_node.GetOutputPorts().empty()); - ASSERT_EQ(return_node.GetOutputPorts().size(), 1); - ASSERT_EQ(return_ref_node.GetOutputPorts().size(), 2); + DECLARE_FUNCTION_NODE(void_test_method); + DECLARE_FUNCTION_NODE(return_test_method); + DECLARE_FUNCTION_NODE(return_ref_test_method); + DECLARE_FUNCTION_NODE(custom_type_test_method); + + ASSERT_EQ(void_test_method_node.GetInputPorts().size(), 1); + ASSERT_EQ(return_test_method_node.GetInputPorts().size(), 1); + ASSERT_TRUE(return_ref_test_method_node.GetInputPorts().empty()); + + ASSERT_TRUE(void_test_method_node.GetOutputPorts().empty()); + ASSERT_EQ(return_test_method_node.GetOutputPorts().size(), 1); + ASSERT_EQ(return_ref_test_method_node.GetOutputPorts().size(), 2); } From abb1e5651ed31aa56c37ce9b36791249585751b4 Mon Sep 17 00:00:00 2001 From: GhostofCookie Date: Thu, 9 Oct 2025 12:55:50 -0400 Subject: [PATCH 2/3] Fix actions. Fix actions. Try getting benchmark action working. Add missing benchmark cmake arg. Actions. Try to fix cmake config argument. Explicitly name variables Fix bad rename. Fix real issue in action. Fix bench Getting cache deletion working. Try permissions. Output available dirs on windows build. Set bench dir for platforms. Fix bad syntax --- .github/workflows/cmake.yml | 13 ++++++------- CMakeLists.txt | 3 ++- benchmarks/graph_bench.cpp | 4 +--- benchmarks/indexable_name_bench.cpp | 15 +-------------- include/flow/core/IndexableName.hpp | 1 + 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index e7b6246..b905ae3 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,15 +34,14 @@ jobs: submodules: recursive - name: Install Packages (Ubuntu) - run: | - sudo apt-get install -y uuid-dev if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y uuid-dev - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -Dflow-core_BUILD_TESTS=ON + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -Dflow-core_BUILD_TESTS=ON -Dflow-core_BUILD_BENCHMARKS=ON - name: Build - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 18 + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel 10 - name: Upload Artifacts uses: actions/upload-artifact@v4 @@ -95,7 +94,7 @@ jobs: key: ${{matrix.os}}-build-${{github.sha}} - name: Benchmark - run: ${{github.workspace}}/build/benchmark/flow_core_bench + run: ${{github.workspace}}/build/benchmarks/${{ matrix.os == 'windows-latest' && env.BUILD_TYPE || '' }}/flow_core_bench cleanup_cache: needs: [test, benchmark] @@ -105,6 +104,7 @@ jobs: runs-on: ${{ matrix.os }} continue-on-error: true + permissions: write-all timeout-minutes: 10 env: GH_TOKEN: ${{ github.token }} @@ -113,5 +113,4 @@ jobs: if: always() # Run even if previous jobs failed steps: - name: Delete Cache - run: | - gh cache delete ${{matrix.os}}-build-${{github.sha}} + run: gh cache delete ${{matrix.os}}-build-${{github.sha}} diff --git a/CMakeLists.txt b/CMakeLists.txt index cd17929..c95d0fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.10) +set (CMAKE_POLICY_VERSION_MINIMUM 3.5) + project(flow-core VERSION 1.1.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) @@ -158,7 +160,6 @@ endif() # ----------------------------------------------------------------------------- if (${PROJECT_NAME}_BUILD_BENCHMARKS) - enable_testing() add_subdirectory(benchmarks) endif() diff --git a/benchmarks/graph_bench.cpp b/benchmarks/graph_bench.cpp index 10ffedf..23591d9 100644 --- a/benchmarks/graph_bench.cpp +++ b/benchmarks/graph_bench.cpp @@ -13,9 +13,7 @@ static void Graph_Construct(benchmark::State& state) auto env = flow::Env::Create(factory); for ([[maybe_unused]] const auto& _ : state) { - flow::Graph graph("benchmark", env); - benchmark::DoNotOptimize(graph); - benchmark::ClobberMemory(); + flow::Graph("benchmark", env); } } diff --git a/benchmarks/indexable_name_bench.cpp b/benchmarks/indexable_name_bench.cpp index 793690d..14116a8 100644 --- a/benchmarks/indexable_name_bench.cpp +++ b/benchmarks/indexable_name_bench.cpp @@ -9,19 +9,7 @@ static void IndexableName_Construct(benchmark::State& state) { for ([[maybe_unused]] const auto& _ : state) { - flow::IndexableName name{"benchmark"}; - benchmark::DoNotOptimize(name); - benchmark::ClobberMemory(); - } -} - -static void IndexableName_ConstexprConstruct(benchmark::State& state) -{ - for ([[maybe_unused]] const auto& _ : state) - { - constexpr flow::IndexableName name{"benchmark"}; - benchmark::DoNotOptimize(name); - benchmark::ClobberMemory(); + flow::IndexableName{"benchmark"}; } } @@ -37,5 +25,4 @@ static void IndexableName_Hash(benchmark::State& state) } BENCHMARK(IndexableName_Construct); -BENCHMARK(IndexableName_ConstexprConstruct); BENCHMARK(IndexableName_Hash); diff --git a/include/flow/core/IndexableName.hpp b/include/flow/core/IndexableName.hpp index a12d8ef..2c90e1a 100644 --- a/include/flow/core/IndexableName.hpp +++ b/include/flow/core/IndexableName.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include From e63520a580f2bf2ca6e9156dbee9606e84d7bb7e Mon Sep 17 00:00:00 2001 From: GhostofCookie Date: Fri, 9 Jan 2026 09:15:08 -0500 Subject: [PATCH 3/3] Adding benchmarka ands fixing some stuff. --- benchmarks/event_bench.cpp | 22 +++++++++- benchmarks/graph_bench.cpp | 3 ++ benchmarks/indexable_name_bench.cpp | 6 +-- include/flow/core/FunctionNode.hpp | 62 ++++++++++++++++++++++------ include/flow/core/Node.hpp | 4 +- include/flow/core/NodeData.hpp | 20 ++++++++- include/flow/core/NodeFactory.hpp | 14 ++++++- include/flow/core/TypeConversion.hpp | 2 +- include/flow/core/TypeName.hpp | 3 -- src/Port.cpp | 3 +- tests/CMakeLists.txt | 3 +- tests/data_test.cpp | 38 +++++++++++++++++ tests/graph_test.cpp | 27 ++++++------ tests/node_test.cpp | 36 +++++++++++----- 14 files changed, 192 insertions(+), 51 deletions(-) create mode 100644 tests/data_test.cpp diff --git a/benchmarks/event_bench.cpp b/benchmarks/event_bench.cpp index 05b90a4..8426804 100644 --- a/benchmarks/event_bench.cpp +++ b/benchmarks/event_bench.cpp @@ -11,7 +11,7 @@ #include #include -static void EventDispatcher_Broadcast(benchmark::State& state) +static void EventDispatcher_BroadcastBrutal(benchmark::State& state) { flow::EventDispatcher<> dispatcher; std::array names; @@ -28,4 +28,22 @@ static void EventDispatcher_Broadcast(benchmark::State& state) } } -BENCHMARK(EventDispatcher_Broadcast); +static void EventDispatcher_BroadcastRealistic(benchmark::State& state) +{ + flow::EventDispatcher<> dispatcher; + std::array names; + + for (int i = 0; i < names.size(); ++i) + { + names[i] = "Event_" + std::to_string(i); + dispatcher.Bind(flow::IndexableName{names[i]}, [&] { ++i; }); + } + + for ([[maybe_unused]] const auto& _ : state) + { + dispatcher.Broadcast(); + } +} + +BENCHMARK(EventDispatcher_BroadcastBrutal); +BENCHMARK(EventDispatcher_BroadcastRealistic); diff --git a/benchmarks/graph_bench.cpp b/benchmarks/graph_bench.cpp index 23591d9..1e85428 100644 --- a/benchmarks/graph_bench.cpp +++ b/benchmarks/graph_bench.cpp @@ -4,9 +4,12 @@ #include #include +#include #include #include +#include + static void Graph_Construct(benchmark::State& state) { auto factory = flow::NodeFactory::Create(); diff --git a/benchmarks/indexable_name_bench.cpp b/benchmarks/indexable_name_bench.cpp index 14116a8..64f6a97 100644 --- a/benchmarks/indexable_name_bench.cpp +++ b/benchmarks/indexable_name_bench.cpp @@ -9,7 +9,7 @@ static void IndexableName_Construct(benchmark::State& state) { for ([[maybe_unused]] const auto& _ : state) { - flow::IndexableName{"benchmark"}; + benchmark::DoNotOptimize(flow::IndexableName{"benchmark"}); } } @@ -18,9 +18,7 @@ static void IndexableName_Hash(benchmark::State& state) constexpr flow::IndexableName name{"benchmark"}; for ([[maybe_unused]] const auto& _ : state) { - auto h = std::hash{}(name); - benchmark::DoNotOptimize(h); - benchmark::ClobberMemory(); + benchmark::DoNotOptimize(std::hash{}(name)); } } diff --git a/include/flow/core/FunctionNode.hpp b/include/flow/core/FunctionNode.hpp index 948979a..b3aa35d 100644 --- a/include/flow/core/FunctionNode.hpp +++ b/include/flow/core/FunctionNode.hpp @@ -13,12 +13,21 @@ FLOW_NAMESPACE_BEGIN +using json = nlohmann::json; + +template +concept JsonSerialisable = requires(T t) { + { + json(t) + }; +}; + /** * @brief Extract function signature information at compile-time * * @tparam F Function type to analyze */ -template +template struct FunctionTraits; /** @@ -35,6 +44,26 @@ struct FunctionTraits static constexpr std::size_t arg_count = sizeof...(Args); }; +template +struct FunctionTraits : FunctionTraits +{ +}; + +template +struct FunctionTraits : FunctionTraits +{ +}; + +template +struct FunctionTraits : FunctionTraits +{ +}; + +template +struct FunctionTraits> : FunctionTraits +{ +}; + /** * @brief Node that wraps a function into the graph system * @@ -43,10 +72,9 @@ struct FunctionTraits * - Output port named "return" for the return value * - Reference parameters become output ports * - * @tparam F Function type (e.g., int(float, bool)) * @tparam Func Pointer to concrete function implementation */ -template> Func> +template class FunctionNode : public Node { /// Helper to decay tuple types while preserving references @@ -63,7 +91,8 @@ class FunctionNode : public Node using decayed_tuple_t = typename decayed_tuple::type; protected: - using traits = FunctionTraits>; + using F = decltype(Func); + using traits = FunctionTraits>>; using output_t = typename traits::ReturnType; using arg_ts = typename traits::ArgTypes; @@ -128,18 +157,17 @@ class FunctionNode : public Node template json SaveInputs(std::integer_sequence) const { - json inputs_json; + json inputs_json = json::object(); const auto& inputs = GetInputPorts(); ( [&, this] { - if constexpr (!std::is_convertible_v, json>) + if constexpr (!JsonSerialisable>) { return; } else { - const auto& key = _arg_names[Idx]; if (!inputs.contains(IndexableName{key})) { @@ -150,6 +178,10 @@ class FunctionNode : public Node { inputs_json[key] = x->Get(); } + else if (auto y = GetInputData>>(IndexableName{key})) + { + inputs_json[key] = y->Get(); + } } }(), ...); @@ -179,7 +211,7 @@ class FunctionNode : public Node public: explicit FunctionNode(const UUID& uuid, const std::string& name, std::shared_ptr env, std::vector arg_names = {}) - : Node(uuid, TypeName_v>, name, std::move(env)), _func{Func} + : Node(uuid, TypeName_v>, name, std::move(env)), _func{Func} { ParseArguments(std::make_integer_sequence>{}, arg_names); @@ -230,21 +262,21 @@ class FunctionNode : public Node } private: - std::add_pointer_t> _func; + std::decay_t _func; static inline std::array> _arg_names{""}; decayed_tuple_t _arguments; }; -template +template void NodeFactory::RegisterFunction(const std::string& category, const std::string& name, std::vector arg_names) { - constexpr std::string_view class_name = TypeName_v>; + constexpr std::string_view class_name = TypeName_v>; _constructor_map.emplace( class_name, [names = std::move(arg_names)](const std::string& uuid_str, const std::string& name, std::shared_ptr env) { - return new FunctionNode(uuid_str, name, std::move(env), std::move(names)); + return new FunctionNode(uuid_str, name, std::move(env), std::move(names)); }); _category_map.emplace(category, class_name); _friendly_names.emplace(class_name, name); @@ -252,4 +284,10 @@ void NodeFactory::RegisterFunction(const std::string& category, const std::strin OnNodeClassRegistered.Broadcast(std::string_view{class_name}); } +template +SharedNode NodeFactory::CreateFunctionNode(const UUID& uuid, const std::string& name, std::shared_ptr env) +{ + return NodeFactory::CreateNode>(uuid, name, env); +} + FLOW_NAMESPACE_END diff --git a/include/flow/core/Node.hpp b/include/flow/core/Node.hpp index c81c800..3b2edc9 100644 --- a/include/flow/core/Node.hpp +++ b/include/flow/core/Node.hpp @@ -148,7 +148,7 @@ class Node template [[nodiscard]] auto GetInputData(const IndexableName& key) const noexcept { - return CastNodeData(this->GetInputData(key)); + return DynamicCastNodeData(this->GetInputData(key)); } /** @@ -163,7 +163,7 @@ class Node template [[nodiscard]] auto GetOutputData(const IndexableName& key) const noexcept { - return CastNodeData(this->GetOutputData(key)); + return DynamicCastNodeData(this->GetOutputData(key)); } /** diff --git a/include/flow/core/NodeData.hpp b/include/flow/core/NodeData.hpp index 544d36d..3512e06 100644 --- a/include/flow/core/NodeData.hpp +++ b/include/flow/core/NodeData.hpp @@ -437,9 +437,27 @@ template } template -[[nodiscard]] constexpr TSharedNodeData CastNodeData(const SharedNodeData& value) +[[nodiscard]] constexpr TSharedNodeData StaticCastNodeData(const SharedNodeData& value) +{ + return std::static_pointer_cast>(value); +} + +template +[[nodiscard]] constexpr TSharedNodeData DynamicCastNodeData(const SharedNodeData& value) { return std::dynamic_pointer_cast>(value); } +template +[[nodiscard]] constexpr TSharedNodeData ReinterpretCastNodeData(const SharedNodeData& value) +{ + return std::reinterpret_pointer_cast>(value); +} + +template +[[deprecated]] [[nodiscard]] constexpr TSharedNodeData CastNodeData(const SharedNodeData& value) +{ + return DynamicCastNodeData(value); +} + FLOW_NAMESPACE_END diff --git a/include/flow/core/NodeFactory.hpp b/include/flow/core/NodeFactory.hpp index 2992475..445a9ac 100644 --- a/include/flow/core/NodeFactory.hpp +++ b/include/flow/core/NodeFactory.hpp @@ -33,6 +33,7 @@ class NodeFactory { using ConstructorCallback = std::function)>; + protected: NodeFactory() = default; public: @@ -61,7 +62,7 @@ class NodeFactory template void UnregisterNodeClass(const std::string& category); - template + template void RegisterFunction(const std::string& category, const std::string& name, std::vector arg_names = {}); @@ -84,6 +85,15 @@ class NodeFactory SharedNode CreateNode(const std::string& class_name, const UUID& uuid, const std::string& name, std::shared_ptr env); + template + SharedNode CreateNode(const UUID& uuid, const std::string& name, std::shared_ptr env) + { + return CreateNode(std::string{TypeName_v}, uuid, name, env); + } + + template + SharedNode CreateFunctionNode(const UUID& uuid, const std::string& name, std::shared_ptr env); + const CategoryMap& GetCategories() const; std::string GetFriendlyName(const std::string& class_name) const; @@ -329,7 +339,7 @@ void NodeFactory::RegisterCompleteConversion() template TSharedNodeData NodeFactory::Convert(const SharedNodeData& data) { - return CastNodeData(_conversion_registry.Convert(data, TypeName_v)); + return DynamicCastNodeData(_conversion_registry.Convert(data, TypeName_v)); } template diff --git a/include/flow/core/TypeConversion.hpp b/include/flow/core/TypeConversion.hpp index 04d477f..da3b07a 100644 --- a/include/flow/core/TypeConversion.hpp +++ b/include/flow/core/TypeConversion.hpp @@ -37,7 +37,7 @@ class TypeRegistry template static SharedNodeData Convert(const SharedNodeData& data) { - if (auto from_data = CastNodeData(data)) + if (auto from_data = DynamicCastNodeData(data)) { if constexpr (std::is_rvalue_reference_v) { diff --git a/include/flow/core/TypeName.hpp b/include/flow/core/TypeName.hpp index 83636c3..2f19274 100644 --- a/include/flow/core/TypeName.hpp +++ b/include/flow/core/TypeName.hpp @@ -73,9 +73,6 @@ constexpr std::string_view get_typename() noexcept template struct TypeName { - /** - * @brief The string representation of the given type. - */ static constexpr std::string_view value = detail::get_typename(); static constexpr bool is_reference = false; diff --git a/src/Port.cpp b/src/Port.cpp index 99fa719..908dac0 100644 --- a/src/Port.cpp +++ b/src/Port.cpp @@ -2,6 +2,7 @@ // All rights reserved. #include "flow/core/Port.hpp" +#include "Port.hpp" FLOW_NAMESPACE_BEGIN @@ -51,4 +52,4 @@ void Port::SetData(SharedNodeData data, bool output) void Port::SetCaption(std::string new_caption) { _caption = std::move(new_caption); } -FLOW_NAMESPACE_END +FLOW_NAMESPACE_END \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a6a0d73..2a5b774 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,12 +18,13 @@ set(TEST_EXE flow_core_tests) add_executable( ${TEST_EXE} + data_test.cpp factory_test.cpp graph_test.cpp indexable_name_test.cpp + module_test.cpp node_test.cpp type_name_test.cpp - module_test.cpp ) if(MSVC) diff --git a/tests/data_test.cpp b/tests/data_test.cpp new file mode 100644 index 0000000..e2bf30c --- /dev/null +++ b/tests/data_test.cpp @@ -0,0 +1,38 @@ +// Copyright (c) 2024, Cisco Systems, Inc. +// All rights reserved. + +#include "flow/core/Env.hpp" +#include "flow/core/NodeData.hpp" +#include "flow/core/NodeFactory.hpp" + +#include +#include + +using namespace flow; + +TEST(Data, Construction) +{ + auto data = MakeNodeData(101); + EXPECT_EQ(data->Get(), 101); +} + +TEST(Data, Copy) +{ + auto data = MakeNodeData(101); + auto x = data->Get(); + EXPECT_EQ(x, 101); +} + +TEST(Data, Move) +{ + auto data = MakeNodeData(101); + auto x = std::move(data); + EXPECT_EQ(x->Get(), 101); +} + +TEST(Data, Get) +{ + auto data = MakeNodeData(101); + auto x = data->Get(); + EXPECT_EQ(x, 101); +} diff --git a/tests/graph_test.cpp b/tests/graph_test.cpp index c2eb6c6..50dbdbc 100644 --- a/tests/graph_test.cpp +++ b/tests/graph_test.cpp @@ -2,6 +2,7 @@ // All rights reserved. #include "flow/core/Env.hpp" +#include "flow/core/FunctionNode.hpp" #include "flow/core/Graph.hpp" #include "flow/core/Node.hpp" #include "flow/core/NodeData.hpp" @@ -9,16 +10,18 @@ #include +#include + using namespace flow; namespace { -auto factory = NodeFactory::Create(); -auto env = Env::Create(factory); +auto factory = NodeFactory::Create(); +auto test_env = Env::Create(factory); struct TestNode : public Node { - TestNode() : Node(UUID{}, TypeName_v, "Test", env) + TestNode() : Node(UUID{}, TypeName_v, "Test", test_env) { AddInput("in", ""); AddInput("other_in", ""); @@ -40,11 +43,11 @@ struct TestNode : public Node }; } // namespace -TEST(GraphTest, Construction) { ASSERT_NO_THROW(auto graph = std::make_shared("test", env)); } +TEST(GraphTest, Construction) { ASSERT_NO_THROW(auto graph = std::make_shared("test", test_env)); } TEST(GraphTest, AddNodes) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); @@ -61,7 +64,7 @@ TEST(GraphTest, AddNodes) TEST(GraphTest, RemoveNodes) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); @@ -81,7 +84,7 @@ TEST(GraphTest, RemoveNodes) TEST(GraphTest, ConnectNodes) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); @@ -97,7 +100,7 @@ TEST(GraphTest, ConnectNodes) TEST(GraphTest, DisconnectNodes) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); @@ -114,7 +117,7 @@ TEST(GraphTest, DisconnectNodes) TEST(GraphTest, PropagateConnectionData) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); @@ -130,7 +133,7 @@ TEST(GraphTest, PropagateConnectionData) EXPECT_NE(node1->GetOutputData("out"), nullptr); EXPECT_EQ(node1->GetOutputData("other_out"), nullptr); - env->Wait(); + test_env->Wait(); EXPECT_NE(node2->GetInputData("in"), nullptr); EXPECT_EQ(node2->GetInputData("other_in"), nullptr); @@ -139,7 +142,7 @@ TEST(GraphTest, PropagateConnectionData) ASSERT_NO_THROW(node1->SetInputData("other_in", MakeNodeData(202))); - env->Wait(); + test_env->Wait(); EXPECT_NE(node2->GetInputData("in"), nullptr); EXPECT_NE(node2->GetInputData("other_in"), nullptr); @@ -150,7 +153,7 @@ TEST(GraphTest, PropagateConnectionData) TEST(GraphTest, DistinguishNodes) { - auto graph = std::make_shared("test", env); + auto graph = std::make_shared("test", test_env); auto node1 = std::make_shared<::TestNode>(); auto node2 = std::make_shared<::TestNode>(); auto node3 = std::make_shared<::TestNode>(); diff --git a/tests/node_test.cpp b/tests/node_test.cpp index 42b536e..cbe7632 100644 --- a/tests/node_test.cpp +++ b/tests/node_test.cpp @@ -52,7 +52,7 @@ struct TestNode : public Node }; } // namespace NodeTest -TEST(NodeTest, Construction) +TEST(Node, Construction) { ASSERT_NO_THROW(NodeTest::TestNode()); @@ -62,7 +62,7 @@ TEST(NodeTest, Construction) EXPECT_EQ(node.GetEnv(), test::env); } -TEST(NodeTest, AddInputPorts) +TEST(Node, AddInputPorts) { NodeTest::TestNode node; @@ -98,7 +98,7 @@ TEST(NodeTest, AddInputPorts) } } -TEST(NodeTest, AddOutputPorts) +TEST(Node, AddOutputPorts) { NodeTest::TestNode node; @@ -134,7 +134,7 @@ TEST(NodeTest, AddOutputPorts) } } -TEST(NodeTest, Compute) +TEST(Node, Compute) { NodeTest::TestNode node; node.AddInput("in", ""); @@ -160,20 +160,36 @@ struct TestData }; void custom_type_test_method(const TestData&) {} -#define DECLARE_FUNCTION_NODE(f) FunctionNode f##_node({}, #f, test::env); +struct TestFunctor +{ + void operator()(int) {} +}; -TEST(NodeTest, WrapFunctions) +TEST(Node, WrapFunctions) { - DECLARE_FUNCTION_NODE(void_test_method); - DECLARE_FUNCTION_NODE(return_test_method); - DECLARE_FUNCTION_NODE(return_ref_test_method); - DECLARE_FUNCTION_NODE(custom_type_test_method); + FunctionNode void_test_method_node({}, "void_test_method", test::env); + FunctionNode return_test_method_node({}, "return_test_method", test::env); + FunctionNode return_ref_test_method_node({}, "return_ref_test_method", test::env); + FunctionNode custom_type_test_method_node({}, "custom_type_test_method", test::env); + FunctionNode functor_node({}, "functor", test::env); ASSERT_EQ(void_test_method_node.GetInputPorts().size(), 1); ASSERT_EQ(return_test_method_node.GetInputPorts().size(), 1); ASSERT_TRUE(return_ref_test_method_node.GetInputPorts().empty()); + ASSERT_EQ(custom_type_test_method_node.GetInputPorts().size(), 1); + ASSERT_EQ(functor_node.GetInputPorts().size(), 1); ASSERT_TRUE(void_test_method_node.GetOutputPorts().empty()); ASSERT_EQ(return_test_method_node.GetOutputPorts().size(), 1); ASSERT_EQ(return_ref_test_method_node.GetOutputPorts().size(), 2); + ASSERT_TRUE(custom_type_test_method_node.GetOutputPorts().empty()); + ASSERT_TRUE(functor_node.GetOutputPorts().empty()); +} + +TEST(Node, Save) +{ + NodeTest::TestNode node; + + auto x = node.Save(); + EXPECT_EQ(x, json({{"id", node.ID()}, {"class", node.GetClass()}, {"name", node.GetName()}, {"inputs", {}}}) ); }