• Docs >
  • Program Listing for File node.hpp
Shortcuts

Program Listing for File node.hpp

Return to documentation for file (include/ripple/graph/node.hpp)

#ifndef RIPPLE_GRAPH_NODE_HPP
#define RIPPLE_GRAPH_NODE_HPP

#include <ripple/container/tuple.hpp>
#include <ripple/execution/execution_traits.hpp>
#include <ripple/functional/invocable.hpp>
#include <ripple/math/math.hpp>
#include <ripple/utility/forward.hpp>
#include <array>
#include <atomic>
#include <cassert>
#include <string>
#include <vector>

namespace ripple {

class Graph;

enum class NodeKind : uint8_t {
  normal = 0,
  split  = 1,
  sync   = 2
};

/*==--- [node executor] ----------------------------------------------------==*/

struct NodeExecutor {
  // clang-format off
  NodeExecutor() noexcept          = default;
  virtual ~NodeExecutor() noexcept = default;

  NodeExecutor(const NodeExecutor&) = delete;
  NodeExecutor(NodeExecutor&&)      = delete;

  auto operator=(const NodeExecutor&) = delete;
  auto operator=(NodeExecutor&&)      = delete;
  // clang-format on

  virtual auto clone(void* storage) const noexcept -> NodeExecutor* = 0;

  virtual auto execute() noexcept -> void = 0;
};

/*==--- [node executor impl] -----------------------------------------------==*/

template <typename Callable, typename... Args>
struct NodeExecutorImpl final : public NodeExecutor {
 private:
  // clang-format off
  using InvocableType = Invocable<std::decay_t<Callable>>;
  using ArgContainer  = Tuple<Args...>;

  static constexpr size_t num_args = sizeof...(Args);
  // clang-format on

 public:
  template <typename InvType, typename... ArgTypes>
  NodeExecutorImpl(InvType&& invocable, ArgTypes&&... args) noexcept
  : invocable_{ripple_forward(invocable)}, args_{ripple_forward(args)...} {}

  ~NodeExecutorImpl() noexcept final = default;

  NodeExecutorImpl(const NodeExecutorImpl& other) noexcept
  : invocable_{other.invocable_}, args_{other.args_} {}

  NodeExecutorImpl(NodeExecutorImpl&& other) noexcept
  : invocable_{ripple_move(other._invocable)},
    args_{ripple_move(other._args)} {}

  auto operator=(const NodeExecutorImpl& other) noexcept -> NodeExecutorImpl& {
    invocable_ = other.invocable_;
    args_      = other.args_;
    return *this;
  }

  auto operator=(NodeExecutorImpl&& other) noexcept -> NodeExecutorImpl& {
    if (&other != this) {
      invocable_ = ripple_move(other.invocable_);
      args_      = ripple_move(other.args_);
    }
    return *this;
  }

  /*==--- [interface impl] -------------------------------------------------==*/

  auto execute() noexcept -> void final {
    execute_impl(std::make_index_sequence<num_args>());
  }

  auto clone(void* storage) const noexcept -> NodeExecutorImpl* final {
    new (storage) NodeExecutorImpl(*this);
    return reinterpret_cast<NodeExecutorImpl*>(storage);
  }

 private:
  InvocableType invocable_;
  ArgContainer  args_;

  template <size_t... I>
  auto execute_impl(std::index_sequence<I...>) noexcept -> void {
    invocable_(get<I>(args_)...);
  }
};

/*==--- [node info] --------------------------------------------------------==*/

struct NodeInfo {
  // clang-format off
  using Name    = std::string;
  using IdType  = uint64_t;
  using Friends = std::vector<IdType>;

  static constexpr IdType default_id   = std::numeric_limits<IdType>::max();
  static constexpr auto   default_name = "";
  // clang-format on

  template <size_t Size>
  static auto
  id_from_indices(const std::array<uint32_t, Size>& indices) -> uint64_t {
    using namespace math;
    static_assert(Size <= 3, "Node id only valid for up to 3 dimensions!");
    return Size == 1   ? indices[0]
           : Size == 2 ? hash_combine(indices[0], indices[1])
           : Size == 3
             ? hash_combine(indices[2], hash_combine(indices[0], indices[1]))
             : 0;
  }

  template <size_t Size>
  static auto
  name_from_indices(const std::array<uint32_t, Size>& indices) noexcept
    -> Name {
    Name name = Size == 0 ? "" : std::to_string(indices[0]);
    for (auto i : range(Size - 1)) {
      name += "_" + std::to_string(indices[i + 1]);
    }
    return name;
  }

  /*==--- [construction] ---------------------------------------------------==*/

  NodeInfo() = default;

  explicit NodeInfo(ExecutionKind exec_kind_) noexcept : exec{exec_kind_} {}

  explicit NodeInfo(NodeKind kind_, ExecutionKind exec_kind_) noexcept
  : kind{kind_}, exec{exec_kind_} {}

  NodeInfo(Name name_) noexcept : name{ripple_move(name_)} {}

  NodeInfo(Name name_, IdType id_) noexcept
  : name{ripple_move(name_)}, id{id_} {}

  NodeInfo(Name name_, IdType id_, NodeKind kind_) noexcept
  : name{ripple_move(name_)}, id{id_}, kind{kind_} {}

  NodeInfo(Name name_, IdType id_, NodeKind kind_, ExecutionKind exec_) noexcept
  : name{ripple_move(name_)}, id{id_}, kind{kind_}, exec{exec_} {}

  /*==--- [deleted] --------------------------------------------------------==*/

  // clang-format off
  NodeInfo(const NodeInfo& other) = default;
  NodeInfo(NodeInfo&& other)      = default;
  auto operator=(const NodeInfo&) = delete;
  auto operator=(NodeInfo&&)      = delete;
  // clang-format on

  /*==--- [members] --------------------------------------------------------==*/

  Name          name    = default_name;
  Friends       friends = {};
  IdType        id      = default_id;
  NodeKind      kind    = NodeKind::normal;
  ExecutionKind exec    = ExecutionKind::gpu;
};

/*==--- [node impl] --------------------------------------------------------==*/

template <size_t Alignment, size_t MinStorage = 72>
class alignas(Alignment) Node {
  friend Graph;

  // clang-format off
  using Successors      = std::vector<Node*>;
  using CounterValue    = uint32_t;
  using Counter         = std::atomic<CounterValue>;
  using NodeInfoPtr     = NodeInfo*;
  using NodeExecutorPtr = NodeExecutor*;

  // clang-format off
  static constexpr size_t data_mem_size =
    sizeof(Successors)      +
    sizeof(NodeExecutorPtr) +
    sizeof(NodeInfoPtr)     +
    sizeof(CounterValue)    +
    sizeof(Counter);

  static constexpr size_t spare_mem  = data_mem_size % Alignment;
  static constexpr size_t align_mult = (data_mem_size + MinStorage) / Alignment;
  static constexpr size_t size       = spare_mem + (align_mult * Alignment);
  // clang-format on

  template <typename T>
  static constexpr bool is_not_node_v = !std::is_same_v<std::decay_t<T>, Node>;

  template <typename T>
  using non_node_enable_t = std::enable_if_t<is_not_node_v<T>, int>;

 public:
  /*==--- [construction] ---------------------------------------------------==*/

  // clang-format off
  Node() noexcept  = default;
  ~Node() noexcept = default;
  // clang-format on

  Node(const Node& other) noexcept {
    debug_assert_node_valid(other);
    executor_ = other.executor_->clone(&storage_);
    incoming_ = other.incoming_;
    dependents_.store(
      other.dependents_.load(std::memory_order_relaxed),
      std::memory_order_relaxed);

    for (size_t i = 0; i < other.successors_.size(); ++i) {
      successors_[i] = other.successors_[i];
    }
  }

  Node(Node&& other) noexcept
  : executor_(other.executor_),
    info_(other.info_),
    incoming_(other.incoming_),
    successors_(ripple_move(other.successors_)),
    dependents_(other.dependents_.load(std::memory_order_relaxed)) {
    other._executor = nullptr;
    other._info     = nullptr;
    other._incoming = 0;
    other._dependents.store(0, std::memory_order_relaxed);
  }

  template <typename F, typename... Args, non_node_enable_t<F> = 0>
  Node(F&& callable, Args&&... args) noexcept {
    set_executor(ripple_forward(callable), ripple_forward(args)...);
  }

  /*==--- [operator overloads] ---------------------------------------------==*/

  auto operator=(const Node& other) noexcept -> Node& {
    if (this == &other) {
      return *this;
    }

    debug_assert_valid_node(other);
    executor_ = other.executor_->clone(&storage_);
    incoming_ = other.incoming_;
    dependents_.store(
      other.dependents_.load(std::memory_order_relaxed),
      std::memory_order_relaxed);

    for (int i = 0; i < other.successors_.size(); ++i) {
      successors_[i] = other.successors_[i];
    }
    return *this;
  }

  auto operator=(Node&& other) noexcept -> Node& {
    if (this == &other) {
      return *this;
    }

    debug_assert_node_valid(other);
    executor_   = other.executor_;
    info_       = other.info_;
    incoming_   = other.incoming_;
    successors_ = ripple_move(other.successors_);
    dependents_.store(
      other.dependents_.load(std::memory_order_relaxed),
      std::memory_order_relaxed);

    other.executor_ = nullptr;
    other.info_     = nullptr;
    other.incoming_ = 0;
    other.dependents_.store(0, std::memory_order_relaxed);
    return *this;
  }

  /*==--- [interface] ------ -----------------------------------------------==*/

  template <typename F, typename... Args>
  auto set_executor(F&& callable, Args&&... args) noexcept -> void {
    using Executor = NodeExecutorImpl<F, Args...>;
    if constexpr (size < sizeof(Executor)) {
      static_assert(
        Tuple<
          Num<size - sizeof(Executor)>,
          Num<size>,
          Num<sizeof(Executor)>,
          Num<sizeof(F)>,
          Tuple<Args, Num<sizeof(Args)>>...>::too_large,
        "Node storage is too small to allocate callable and its args!");
    }

    new (&storage_) Executor{ripple_forward(callable), ripple_forward(args)...};
    executor_ = reinterpret_cast<Executor*>(&storage_);
  }

  auto try_run() noexcept -> bool {
    if (dependents_.load(std::memory_order_relaxed) != size_t{0}) {
      return false;
    }

    /* Reset the node incase it needs to be run again, as well as to make sure
     * that it can't be run again until all dependents have run;
     *
     * \note This is very improtant that this is reset *before* the executor
     *       executes. If not, it's possible that multiple threads could see
     *       this node as having no dependencies, and both run the node.
     */
    dependents_.store(incoming_, std::memory_order_relaxed);
    executor_->execute();

    for (auto* successor : successors_) {
      if (successor && successor->num_dependents() > 0) {
        successor->dependents_.fetch_sub(1, std::memory_order_relaxed);
      }
    }
    return true;
  }

  auto add_successor(Node& node) noexcept -> void {
    for (auto* successor : successors_) {
      if (successor == &node) {
        return;
      }
    }
    successors_.push_back(&node);
    node.increment_num_dependents();
  }

  auto add_friend(typename NodeInfo::IdType friend_id) noexcept -> void {
    info_->friends.emplace_back(friend_id);
  }

  auto friends() const noexcept -> typename NodeInfo::Friends& {
    return info_->friends;
  }

  auto increment_num_dependents() noexcept -> void {
    dependents_.fetch_add(1, std::memory_order_relaxed);
    incoming_ = dependents_.load(std::memory_order_relaxed);
  }

  auto name() const noexcept -> typename NodeInfo::Name {
    return info_ ? info_->name : NodeInfo::default_name;
  }

  auto id() const noexcept -> typename NodeInfo::IdType {
    return info_ ? info_->id : NodeInfo::default_id;
  }

  auto kind() const noexcept -> NodeKind {
    return info_ ? info_->kind : NodeKind::normal;
  }

  auto execution_kind() const noexcept -> ExecutionKind {
    return info_ ? info_->exec : default_execution_kind;
  }

  auto num_dependents() const noexcept -> CounterValue {
    return dependents_.load(std::memory_order_relaxed);
  }

 private:
  /*==--- [members] --------------------------------------------------------==*/

  Successors      successors_    = {};
  NodeExecutorPtr executor_      = nullptr;
  NodeInfoPtr     info_          = nullptr;
  CounterValue    incoming_      = 0;
  Counter         dependents_    = 0;
  char            storage_[size] = {};

  auto debug_assert_node_valid(const Node& node) const noexcept -> void {
    assert(node.executor_ != nullptr && "Node executor can't be a nullptr!");
  }
};

} // namespace ripple

#endif // RIPPLE_GRAPH_NODE_HPP

Docs

Access comprehensive developer documentation for Ripple

View Docs

Tutorials

Get tutorials to help with understand all features

View Tutorials

Examples

Find examples to help get started

View Examples