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

Port Graph Optimizations #52

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 2 additions & 3 deletions include/reactor-cpp/environment.hh
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ private:

const Duration timeout_{};

Graph<BasePort*, ConnectionProperties> graph_{};
Graph<BasePort*, ConnectionProperties> optimized_graph_{};
Graph<BasePort> graph_{};
Graph<BasePort> optimized_graph_{};

void build_dependency_graph(Reactor* reactor);
void calculate_indexes();
Expand Down Expand Up @@ -93,7 +93,6 @@ public:
void optimize();

void register_reactor(Reactor* reactor);
void register_port(BasePort* port) noexcept;
void register_input_action(BaseAction* action);
void assemble();
auto startup() -> std::thread;
Expand Down
235 changes: 204 additions & 31 deletions include/reactor-cpp/graph.hh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,35 @@
#include <iostream>
#include <map>
#include <optional>
#include <type_traits>
#include <vector>

namespace reactor {
class GraphElement {
public:
GraphElement() noexcept = default;
GraphElement(const GraphElement& graph) noexcept = default;
GraphElement(GraphElement&& graph) noexcept = default;

virtual ~GraphElement() noexcept = default;
[[nodiscard]] virtual auto connected_to_downstream_actions() const noexcept -> bool = 0;
[[nodiscard]] virtual auto connected_to_upstream_actions() const noexcept -> bool = 0;
[[nodiscard]] virtual auto rating() const noexcept -> std::size_t = 0;

auto operator=([[maybe_unused]] const GraphElement& other) noexcept -> GraphElement& = default;
auto operator=([[maybe_unused]] GraphElement&& other) noexcept -> GraphElement& = default;
};

// this graph is special, because to every edge properties are annotated
template <class E, class P> class Graph {
template <class X> class Graph {
// static_assert(std::is_base_of_v<GraphElement, X>);
using E = X*;
using P = ConnectionProperties;

private:
std::map<E, std::vector<std::pair<P, E>>> graph_;
using Path = std::vector<std::tuple<E, P, E>>;
std::map<E, std::vector<std::pair<P, E>>> graph_{};
std::set<E> nodes_{};

// custom compare operator this is required if u want special key values in std::map
// this is required for the Graph::get_edges() method
Expand Down Expand Up @@ -50,14 +72,20 @@ public:

// adds a single edge to the graph structure
void add_edge(E source, E destination, P properties) noexcept {
nodes_.insert(source);
nodes_.insert(destination);

if (graph_.find(source) == std::end(graph_)) {
std::vector<std::pair<P, E>> edges{std::make_pair(properties, destination)};
std::vector<std::pair<P, E>> edges;
edges.emplace_back(properties, destination);
graph_[source] = edges;
} else {
graph_[source].emplace_back(properties, destination);
}
}

auto get_nodes() -> std::set<E> { return nodes_; }

// this groups connections by same source and properties
[[nodiscard]] auto get_edges() const noexcept -> std::map<map_key, std::vector<E>, map_key_compare> {
std::map<map_key, std::vector<E>, map_key_compare> all_edges{};
Expand Down Expand Up @@ -85,43 +113,188 @@ public:
return keys;
}

// returns the spanning tree of a given source including properties
[[nodiscard]] auto spanning_tree(E source) noexcept -> std::map<E, std::vector<std::pair<P, E>>> {
std::map<E, std::vector<std::pair<P, E>>> tree{};
std::vector<E> work_nodes{source};

while (!work_nodes.empty()) {
auto parent = *work_nodes.begin();

for (auto child : graph_[parent]) {
// figuring out the properties until this node
std::vector<std::pair<P, E>> parent_properties{};
if (tree.find(parent) != std::end(tree)) {
// this if should always be the case except the first time when tree is empty
parent_properties = tree[parent]; // TODO: make sure this is a copy otherwise we change this properties as
// well
}
// the return type looks a little bit cursed what is happening here ?
// we have a map from the destination as a key to a list of paths through the graph.
// A path here is modelled by a list of edges (with properties and the next vertex).
auto naive_spanning_tree(E source) noexcept -> std::vector<std::vector<std::tuple<E, P, E>>> {
return recursive_spanning_tree(source, std::vector<E>{});
}

// appending the new property and inserting into the tree
parent_properties.push_back(child);
work_nodes.push_back(child.second);
tree[child.second] = parent_properties;
}
// this function goes recursively though the graph and tries to find every possible path
auto recursive_spanning_tree(E source_node, std::vector<E> visited_nodes)
-> std::vector<std::vector<std::tuple<E, P, E>>> {
std::vector<Path> paths{};

work_nodes.erase(std::begin(work_nodes));
if (graph_[source_node].empty()) {
return std::vector<Path>{Path{}};
}

return tree;
// if this node has an action we need to append the path
if (source_node->connected_to_downstream_actions()) {
paths.push_back(Path{});
}

for (auto child : graph_[source_node]) {
E current_node = child.second;

// we dont need to check for cycles because lf semantics assure that there wont be any cycles
for (auto path : recursive_spanning_tree(current_node, visited_nodes)) {
path.push_back(std::make_tuple(source_node, child.first, current_node));
paths.push_back(path);
}
}

return paths;
}

[[nodiscard]] auto get_destinations(E source) const noexcept -> std::vector<std::pair<P, E>> {
return graph_[source];
return this->graph_.at(source);
}

[[nodiscard]] auto get_upstream(E vertex) const noexcept -> std::optional<E> {
for (const auto& [source, sinks] : graph_) {
if (sinks.second.contains(vertex)) {
return source;
[[nodiscard]] auto to_mermaid() const noexcept -> std::string {
std::string mermaid_string = "graph TD;\n";
std::size_t index{0};
std::map<E, std::string> name_map{};

auto name_resolver = [&](E object) -> std::string {
char names[] = "ABCDEFGHIJKLMNOPQRSTUVGXYZabcdefghijklmnopqrstuvgxyz"; // NOLINT
if (name_map.find(object) == std::end(name_map)) {
name_map[object] = names[index];
index++;
return std::string{names[index - 1], 1};
}
return name_map[object];
};

for (const auto& [source, destinations] : graph_) {
for (auto dest : destinations) {
mermaid_string += std::string(" ") + name_resolver(source) + std::string("-->") +
name_resolver(dest.second) + std::string(";\n");
}
}
return mermaid_string;
}

void optimize(Graph<X>& optimized_graph) {
optimized_graph.clear();

static std::map<std::pair<ConnectionType, ConnectionType>, ConnectionType> construction_table = {
// Normal + x
{std::make_pair<ConnectionType, ConnectionType>(Normal, Normal), Normal},
{std::make_pair<ConnectionType, ConnectionType>(Normal, Delayed), Delayed},
{std::make_pair<ConnectionType, ConnectionType>(Normal, Enclaved), Enclaved},
{std::make_pair<ConnectionType, ConnectionType>(Normal, Physical), Physical},
{std::make_pair<ConnectionType, ConnectionType>(Normal, DelayedEnclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Normal, PhysicalEnclaved), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Normal, Plugin), Plugin},
// Delayed + x
{std::make_pair<ConnectionType, ConnectionType>(Delayed, Normal), Delayed},
{std::make_pair<ConnectionType, ConnectionType>(Delayed, Delayed), Delayed},
{std::make_pair<ConnectionType, ConnectionType>(Delayed, Enclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Delayed, Physical), Invalid}, //!!!
{std::make_pair<ConnectionType, ConnectionType>(Delayed, DelayedEnclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Delayed, PhysicalEnclaved), Invalid}, //!!!
{std::make_pair<ConnectionType, ConnectionType>(Delayed, Plugin), Invalid},
// Enclaved + x
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, Normal), Enclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, Delayed), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, Enclaved), Enclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, Physical), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, DelayedEnclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, PhysicalEnclaved), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Enclaved, Plugin), Invalid},
// Physical + x
{std::make_pair<ConnectionType, ConnectionType>(Physical, Normal), Physical},
{std::make_pair<ConnectionType, ConnectionType>(Physical, Delayed), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Physical, Enclaved), PhysicalEnclaved},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all these cases should be invalid for now (except for Phycical + Normal)

{std::make_pair<ConnectionType, ConnectionType>(Physical, Physical), Physical},
{std::make_pair<ConnectionType, ConnectionType>(Physical, DelayedEnclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Physical, PhysicalEnclaved), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(Physical, Plugin), Invalid},
// DelayedEnclaved + x
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, Normal), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, Delayed), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, Enclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, Physical), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, DelayedEnclaved), DelayedEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, PhysicalEnclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(DelayedEnclaved, Plugin), Invalid},
// PhysicalEnclaved + x
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, Normal), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, Delayed), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, Enclaved), PhysicalEnclaved},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all these cases should be invalid for now (except for PhycicalEnclaved + Normal)

{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, Physical), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, DelayedEnclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, PhysicalEnclaved), PhysicalEnclaved},
{std::make_pair<ConnectionType, ConnectionType>(PhysicalEnclaved, Plugin), Invalid},
// Plugin + x = Invalid
{std::make_pair<ConnectionType, ConnectionType>(Plugin, Normal), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, Delayed), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, Enclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, Physical), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, DelayedEnclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, PhysicalEnclaved), Invalid}, // !!!
{std::make_pair<ConnectionType, ConnectionType>(Plugin, Plugin), Invalid}, // !!!
};

// getting all the sources from the graph
auto keys = this->keys();

std::vector<E> has_downstreams{};
std::copy_if(keys.begin(), keys.end(), std::back_inserter(has_downstreams),
[](auto element) { return element->connected_to_downstream_actions(); });

std::vector<E> has_upstreams{};
std::copy_if(keys.begin(), keys.end(), std::back_inserter(has_upstreams),
[](auto element) { return element->connected_to_upstream_actions(); });

// generating all the possible destinations for all sources
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be really helpful to have a more lengthy comment explaining the high level idea of the algorithm.

for (auto* source : has_upstreams) {
auto spanning_tree = naive_spanning_tree(source);

for (auto& path : spanning_tree) {
if (path.empty()) {
continue;
}

ConnectionProperties merged_properties{};
auto* final_destination = std::get<2>(*std::begin(path));
std::size_t current_rating = 0;

for (auto edge : path) {
auto property = std::get<1>(edge);
// auto source_port = std::get<0>(edge);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

auto* destination_port = std::get<2>(edge);

current_rating += destination_port->rating();

if (current_rating > 0) {
auto return_type =
construction_table[std::pair<ConnectionType, ConnectionType>(merged_properties.type_, property.type_)];
// invalid will split the connections
if (return_type == Invalid) {
// first add connection until this point
optimized_graph.add_edge(destination_port, final_destination, merged_properties); // NOLINT

// resetting the properties and destination_port
final_destination = destination_port;
merged_properties = property;

} else {

// merging the connections
merged_properties.type_ = return_type;

// adding up delays
merged_properties.delay_ += property.delay_;

// updating target enclave if not nullptr
merged_properties.enclave_ =
(property.enclave_ != nullptr) ? property.enclave_ : merged_properties.enclave_;
}
}
}
optimized_graph.add_edge(std::get<0>(*(std::end(path) - 1)), final_destination, merged_properties);
}
}
}
Expand Down
18 changes: 15 additions & 3 deletions include/reactor-cpp/port.hh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "assert.hh"
#include "connection_properties.hh"
#include "fwd.hh"
#include "graph.hh"
#include "multiport.hh"
#include "reactor_element.hh"
#include "value_ptr.hh"
Expand All @@ -23,7 +24,7 @@ namespace reactor {

enum class PortType { Input, Output, Delay };

class BasePort : public ReactorElement {
class BasePort : public GraphElement, public ReactorElement { // NOLINT
private:
BasePort* inward_binding_{nullptr};
std::set<BasePort*> outward_bindings_{};
Expand Down Expand Up @@ -71,6 +72,8 @@ protected:
}

public:
~BasePort() noexcept override = default;

void set_inward_binding(BasePort* port) noexcept { inward_binding_ = port; }
void add_outward_binding(BasePort* port) noexcept {
outward_bindings_.insert(port); // NOLINT
Expand All @@ -92,6 +95,7 @@ public:
[[nodiscard]] inline auto has_outward_bindings() const noexcept -> bool { return !outward_bindings_.empty(); }
[[nodiscard]] inline auto has_dependencies() const noexcept -> bool { return !dependencies_.empty(); }
[[nodiscard]] inline auto has_anti_dependencies() const noexcept -> bool { return !anti_dependencies_.empty(); }
[[nodiscard]] inline auto has_triggers() const noexcept -> bool { return !triggers_.empty(); }

[[nodiscard]] inline auto inward_binding() const noexcept -> BasePort* { return inward_binding_; }
[[nodiscard]] inline auto outward_bindings() const noexcept -> const auto& { return outward_bindings_; }
Expand All @@ -101,6 +105,14 @@ public:
[[nodiscard]] inline auto anti_dependencies() const noexcept -> const auto& { return anti_dependencies_; }
[[nodiscard]] inline auto port_type() const noexcept -> PortType { return type_; }

[[nodiscard]] auto connected_to_downstream_actions() const noexcept -> bool final {
return has_dependencies() || has_triggers();
};
[[nodiscard]] auto connected_to_upstream_actions() const noexcept -> bool final { return has_anti_dependencies(); };
[[nodiscard]] auto rating() const noexcept -> std::size_t final {
return dependencies_.size() + triggers_.size() + ((set_callback_ != nullptr) ? 1 : 0);
}

void register_set_callback(const PortCallback& callback);
void register_clean_callback(const PortCallback& callback);

Expand Down Expand Up @@ -170,15 +182,15 @@ public:
Input(const std::string& name, Reactor* container)
: Port<T>(name, PortType::Input, container) {}

Input(Input&&) = default; // NOLINT(performance-noexcept-move-constructor)
Input(Input&&) noexcept = default; // NOLINT(performance-noexcept-move-constructor)
};

template <class T> class Output : public Port<T> { // NOLINT
public:
Output(const std::string& name, Reactor* container)
: Port<T>(name, PortType::Output, container) {}

Output(Output&&) = default; // NOLINT(performance-noexcept-move-constructor)
Output(Output&&) noexcept = default; // NOLINT(performance-noexcept-move-constructor)
};

} // namespace reactor
Expand Down
2 changes: 1 addition & 1 deletion include/reactor-cpp/reactor_element.hh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public:
virtual ~ReactorElement() = default;

// not copyable, but movable
ReactorElement(const ReactorElement&) = delete;
ReactorElement(const ReactorElement&) = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy constructor is deleted on purpose and we should try to avoid enabling it

ReactorElement(ReactorElement&&) = default;

[[nodiscard]] auto container() const noexcept -> Reactor* { return container_; }
Expand Down
7 changes: 7 additions & 0 deletions lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ set_target_properties(${LIB_TARGET} PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1)

option(GRAPH_OPTIMIZATIONS "Graph optimizations" ON)
if(GRAPH_OPTIMIZATIONS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be handled more conveniently in config.hh.in

target_compile_definitions(${LIB_TARGET} PRIVATE GRAPH_OPTIMIZATIONS=1 )
else (GRAPH_OPTIMIZATIONS)
target_compile_definitions(${LIB_TARGET} PRIVATE GRAPH_OPTIMIZATIONS=0 )
endif(GRAPH_OPTIMIZATIONS)

if(DEFINED LF_REACTOR_CPP_SUFFIX)
install(FILES "${PROJECT_BINARY_DIR}/include/reactor-cpp/config.hh" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/${LIB_TARGET}/reactor-cpp")
else()
Expand Down
Loading