Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/shamsolvergraph/include/shamsolvergraph/LifetimeTracker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// -------------------------------------------------------//
//
// SHAMROCK code for hydrodynamics
// Copyright (c) 2021-2026 Timothée David--Cléris <tim.shamrock@proton.me>
// SPDX-License-Identifier: CeCILL Free Software License Agreement v2.1
// Shamrock is licensed under the CeCILL 2.1 License, see LICENSE for more information
//
// -------------------------------------------------------//

#pragma once

/**
* @file LifetimeTracker.hpp
* @author Timothée David--Cléris (tim.shamrock@proton.me)
* @brief Callback API to track the lifetime, state updates and operations of solvergraph objects
*
*/

#include "shambase/WithUUID.hpp"
#include "shambase/aliases_int.hpp"
#include <string_view>
#include <utility>

namespace shamrock::solvergraph {

/**
* @brief Tracks the lifetime of an object of type T and notifies observers through static
* callbacks.
*
* Held by the tracked object as a plain value member (not a base class), so
* trace_state_update() can take a `T&` to the enclosing object.
*
* Move-safety comes from the base class: a moved-from instance's is_alive() reports false,
* so it won't emit a duplicate destroy notification.
*
* All callbacks are `nullptr` by default, so tracking-disabled cost is one null check per
* notification site.
*
* @tparam T The tracked object type (e.g. INode, IEdge)
*/
template<typename T>
class LifetimeTracker : public shambase::WithUUID<LifetimeTracker<T>, u64> {
public:
/// Called when a tracked object is created
inline static void (*on_create)(u64 uuid) = nullptr;
/// Called when a tracked object is destroyed
inline static void (*on_destroy)(u64 uuid) = nullptr;

/// Called when the state of a tracked object changes (e.g. edges are rebound)
inline static void (*on_state_update)(T &object) = nullptr;
/// Called when an operation is performed on a tracked object (e.g. evaluation)
inline static void (*on_event)(u64 uuid, std::string_view s) = nullptr;

/// Constructor, notifies the creation of the tracked object
LifetimeTracker() : shambase::WithUUID<LifetimeTracker, u64>() {
if (on_create != nullptr) {
on_create(this->get_uuid());
}
};

LifetimeTracker(const LifetimeTracker &) = delete; ///< would duplicate the UUID
LifetimeTracker &operator=(const LifetimeTracker &) = delete; ///< would duplicate the UUID

/// Move constructor: transfers the uuid to `this` and invalidates `other`.
LifetimeTracker(LifetimeTracker &&) noexcept = default;

/// Move assignment: fires `this`'s own destroy notification (if still alive), then
/// transfers `other`'s uuid over and invalidates `other`.
inline LifetimeTracker &operator=(LifetimeTracker &&other) noexcept {
if (this != &other) {
trace_destroy();
shambase::WithUUID<LifetimeTracker, u64>::operator=(std::move(other));
}
return *this;
}

/// Notifies creation of the tracked object.
inline void trace_create() {
if (on_create) {
on_create(this->uuid);
}
}

/// Fires the destroy notification, if not already fired or moved from. Idempotent.
inline void trace_destroy() {
if (this->is_alive()) {
if (on_destroy) {
on_destroy(this->uuid);
}
this->invalidate();
}
}

// notify and update of the owning object.
inline void trace_state_update(T &object) {
if (this->is_alive() && on_state_update) {
on_state_update(object);
}
}

/// Use it like tracker.trace_event([]() {return "evaluate_begin";});
/// This patern allow for almost no overhead if tracing is disabled
template<class F>
inline void trace_event(F &&event_info_builder) {
if (this->is_alive() && on_event) {
on_event(this->uuid, event_info_builder());
}
}

/// Destructor, notifies destruction (unless already notified, or moved from).
~LifetimeTracker() { trace_destroy(); };
};

} // namespace shamrock::solvergraph
12 changes: 10 additions & 2 deletions src/shamsolvergraph/include/shamsolvergraph/edge/IEdge.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@
*
*/

#include "shambase/WithUUID.hpp"
#include "shambase/aliases_int.hpp"
#include "shamsolvergraph/IFreeable.hpp"
#include "shamsolvergraph/LifetimeTracker.hpp"
#include <string>

namespace shamrock::solvergraph {

class INode;

class IEdge : public shambase::WithUUID<IEdge, u64>, public IFreeable {
class IEdge : public IFreeable {

/// Tracks the lifetime of the edge and holds its UUID.
/// Held as a plain value member so trace_state_update() can take a `T&` to this object.
LifetimeTracker<IEdge> tracker;

public:
IEdge() = default;

Expand All @@ -43,6 +48,9 @@ namespace shamrock::solvergraph {
virtual std::string _impl_get_dot_label() const = 0;
virtual std::string _impl_get_tex_symbol() const = 0;

/// Get the UUID of the edge
inline u64 get_uuid() const { return tracker.get_uuid(); }

inline virtual ~IEdge() {}
};

Expand Down
39 changes: 34 additions & 5 deletions src/shamsolvergraph/include/shamsolvergraph/node/INode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
*
*/

#include "shambase/WithUUID.hpp"
#include "shambase/memory.hpp"
#include "shambase/stacktrace.hpp"
#include "shamsolvergraph/LifetimeTracker.hpp"
#include "shamsolvergraph/edge/IEdge.hpp"
#include "shamsolvergraph/edge/INullOptEdge.hpp"
#include <memory>
Expand All @@ -27,14 +27,17 @@
namespace shamrock::solvergraph {

/// Inode is node between data edges, takes multiple inputs, multiple outputs
class INode : public std::enable_shared_from_this<INode>,
public shambase::WithUUID<INode, u64> {
class INode : public std::enable_shared_from_this<INode> {

/// Read only edges
std::vector<std::shared_ptr<IEdge>> ro_edges;
/// Read write edges
std::vector<std::shared_ptr<IEdge>> rw_edges;

/// Tracks the lifetime of the node and holds its UUID.
/// Held as a plain value member so trace_state_update() can take a `T&` to this object.
LifetimeTracker<INode> tracker;

public:
INode() = default;

Expand All @@ -47,6 +50,9 @@ namespace shamrock::solvergraph {
/// Move assignment - automatically delegates to base classes and members
INode &operator=(INode &&) noexcept = default;

/// Get the UUID of the node
inline u64 get_uuid() const { return tracker.get_uuid(); }

/// Get a shared pointer to this node
inline std::shared_ptr<INode> getptr_shared() { return shared_from_this(); }
/// Get a weak pointer to this node
Expand All @@ -70,8 +76,11 @@ namespace shamrock::solvergraph {
template<class Func>
void on_edge_rw_edges(Func &&f);

/// Destructor (virtual) & reset the edges
/// Destructor (virtual) & reset the edges.
/// trace_destroy() fires first so the edges below are cleared as already-dead, silencing
/// their state-update notifications during destruction.
virtual ~INode() {
tracker.trace_destroy();
__internal_set_ro_edges({});
__internal_set_rw_edges({});
}
Expand Down Expand Up @@ -143,7 +152,16 @@ namespace shamrock::solvergraph {
}

/// Evaluate the node
inline void evaluate() { _impl_evaluate_internal(); }
inline void evaluate() {
// if solvergraph tracing is not enabled the .trace_event has the perf of a if statement
tracker.trace_event([]() {
return "evaluate_begin";
});
_impl_evaluate_internal();
tracker.trace_event([]() {
return "evaluate_end";
});
}

/// Get the dot graph of the node (Currently only an alias to get_dot_graph_partial)
inline std::string get_dot_graph() { return get_dot_graph_partial(); };
Expand Down Expand Up @@ -190,6 +208,15 @@ namespace shamrock::solvergraph {
};

protected:
/// Fire a self state_update for this node. Meant to be called at the end of a derived
/// class's own constructor, once that class's members are fully initialized -- never
/// from INode's own constructor. typeid() during a base class's constructor body
/// reports the class currently under construction (INode), not the object's final
/// derived type, so a state_update fired from there would misreport its dynamic type.
/// This lets meta nodes that own no ro/rw edges of their own (e.g. OperationSequence)
/// still record a state_update before they can be evaluated.
inline void notify_self_state_update() { tracker.trace_state_update(*this); }

/// evaluate the node
virtual void _impl_evaluate_internal() = 0;

Expand All @@ -215,6 +242,7 @@ namespace shamrock::solvergraph {
for (auto e : ro_edges) {
// shambase::get_check_ref(e).parent = getptr_weak();
}
tracker.trace_state_update(*this);
}

inline void INode::__internal_set_rw_edges(std::vector<std::shared_ptr<IEdge>> new_rw_edges) {
Expand All @@ -225,6 +253,7 @@ namespace shamrock::solvergraph {
for (auto e : rw_edges) {
// shambase::get_check_ref(e).child = getptr_weak();
}
tracker.trace_state_update(*this);
}

template<class Func>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ namespace shamrock::solvergraph {
shambase::throw_with_loc<std::invalid_argument>(
"OperationSequence must have at least one node");
}
// A sequence owns no ro/rw edges of its own, so it never goes through
// __internal_set_ro_edges/__internal_set_rw_edges. Fire the self state_update
// manually so a sequence is on record as up to date before it can be evaluated,
// same as any other node.
notify_self_state_update();
}
void _impl_evaluate_internal();

Expand Down
Loading
Loading