Skip to content

Commit a15608a

Browse files
authored
Merge pull request #193 from hhenson/Performance-enhancements
Improve performance by caching access to the engine_time and clock
2 parents a0903a1 + 42d1067 commit a15608a

File tree

8 files changed

+115
-37
lines changed

8 files changed

+115
-37
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,3 +1554,9 @@ Version 0.5.6 (09-11-2025)
15541554
--------------------------
15551555

15561556
* Add observer support and a few bug fixes.
1557+
1558+
Version 0.5.7 (10-11-2025)
1559+
--------------------------
1560+
1561+
* Refactoring cleanup to prepare for further C++ improvements.
1562+
* Performance optimisation for accessing evaluation_time.

cpp/include/hgraph/runtime/evaluation_engine.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ namespace hgraph {
4141

4242
virtual void reset_push_node_requires_scheduling() = 0;
4343

44+
// Performance: Direct access to evaluation time for caching
45+
[[nodiscard]] virtual const engine_time_t* evaluation_time_ptr() const = 0;
46+
4447
static void register_with_nanobind(nb::module_ &m);
4548
};
4649

@@ -69,6 +72,8 @@ namespace hgraph {
6972

7073
void reset_push_node_requires_scheduling() override;
7174

75+
[[nodiscard]] const engine_time_t* evaluation_time_ptr() const override;
76+
7277
static void register_with_nanobind(nb::module_ &m);
7378

7479
private:
@@ -261,6 +266,9 @@ namespace hgraph {
261266

262267
void update_next_scheduled_evaluation_time(engine_time_t scheduled_time) override;
263268

269+
// Performance: Direct access to evaluation time for caching
270+
[[nodiscard]] const engine_time_t* evaluation_time_ptr() const override;
271+
264272
static void register_with_nanobind(nb::module_ &m);
265273

266274
private:

cpp/include/hgraph/types/graph.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ namespace hgraph {
6565

6666
void dispose_subgraph(int64_t start, int64_t end);
6767

68+
// Performance: Cached clock pointer and evaluation time reference set during initialization
69+
[[nodiscard]] EngineEvaluationClock* cached_engine_clock() const { return _cached_engine_clock; }
70+
[[nodiscard]] const engine_time_t* cached_evaluation_time_ptr() const { return _cached_evaluation_time_ptr; }
71+
6872
protected:
6973
void initialise() override;
7074

@@ -85,6 +89,11 @@ namespace hgraph {
8589
SenderReceiverState _receiver;
8690
engine_time_t _last_evaluation_time{MIN_DT};
8791
int64_t _push_source_nodes_end{-1};
92+
93+
// Performance optimization: Cache clock pointer and evaluation time pointer at initialization
94+
// Set once when evaluation engine is assigned, never changes
95+
EngineEvaluationClock* _cached_engine_clock{nullptr};
96+
const engine_time_t* _cached_evaluation_time_ptr{nullptr};
8897
};
8998
} // namespace hgraph
9099

cpp/include/hgraph/types/node.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,11 @@ namespace hgraph {
221221

222222
void set_error_output(time_series_output_ptr value);
223223

224+
// Performance optimization: provide access to cached evaluation time pointer
225+
[[nodiscard]] const engine_time_t* cached_evaluation_time_ptr() const { return _cached_evaluation_time_ptr; }
226+
224227
friend struct Graph;
228+
friend struct NodeScheduler;
225229

226230
void add_start_input(nb::ref<TimeSeriesReferenceInput> input);
227231

@@ -266,6 +270,10 @@ namespace hgraph {
266270
// Cache for these calculated values.
267271
std::vector<nb::ref<TimeSeriesInput> > _check_valid_inputs;
268272
std::vector<nb::ref<TimeSeriesInput> > _check_all_valid_inputs;
273+
274+
// Performance optimization: Cache evaluation time pointer from graph
275+
// Set once when graph is assigned to node, never changes
276+
const engine_time_t* _cached_evaluation_time_ptr{nullptr};
269277
};
270278
} // namespace hgraph
271279

cpp/src/cpp/runtime/evaluation_engine.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ namespace hgraph {
8686
_engine_evalaution_clock->reset_push_node_requires_scheduling();
8787
}
8888

89+
const engine_time_t* EngineEvaluationClockDelegate::evaluation_time_ptr() const {
90+
return _engine_evalaution_clock->evaluation_time_ptr();
91+
}
92+
8993
void EngineEvaluationClockDelegate::register_with_nanobind(nb::module_ &m) {
9094
nb::class_ < EngineEvaluationClockDelegate, EngineEvaluationClock > (m, "EngineEvaluationClockDelegate")
9195
.def(nb::init<EngineEvaluationClock::ptr>());
@@ -298,6 +302,8 @@ namespace hgraph {
298302
std::max(next_cycle_evaluation_time(), std::min(_next_scheduled_evaluation_time, scheduled_time));
299303
}
300304

305+
const engine_time_t *BaseEvaluationClock::evaluation_time_ptr() const { return &_evaluation_time; }
306+
301307
void BaseEvaluationClock::register_with_nanobind(nb::module_ &m) {
302308
nb::class_ < BaseEvaluationClock, EngineEvaluationClock > (m, "BaseEvaluationClock");
303309
}

cpp/src/cpp/types/base_time_series.cpp

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ namespace hgraph {
2727
void BaseTimeSeriesOutput::reset_parent_or_node() { _parent_ts_or_node.reset(); }
2828

2929
// Implement re_parent methods
30-
void BaseTimeSeriesOutput::re_parent(const node_ptr &parent) { _parent_ts_or_node = parent; }
31-
void BaseTimeSeriesOutput::re_parent(const TimeSeriesType::ptr &parent) { _parent_ts_or_node = parent; }
30+
void BaseTimeSeriesOutput::re_parent(const node_ptr &parent) {
31+
_parent_ts_or_node = parent;
32+
}
33+
void BaseTimeSeriesOutput::re_parent(const TimeSeriesType::ptr &parent) {
34+
_parent_ts_or_node = parent;
35+
}
3236

3337
// TimeSeriesType helper methods
3438
TimeSeriesType::ptr &BaseTimeSeriesOutput::_parent_time_series() const {
@@ -119,9 +123,10 @@ namespace hgraph {
119123
}
120124

121125
bool BaseTimeSeriesOutput::modified() const {
122-
auto g = owning_graph();
123-
if (!g) { return false; }
124-
return g->evaluation_clock()->evaluation_time() == _last_modified_time;
126+
auto n = owning_node();
127+
if (n.get() == nullptr) { return false; }
128+
// Use cached evaluation time pointer from node for performance
129+
return *n->cached_evaluation_time_ptr() == _last_modified_time;
125130
}
126131

127132
bool BaseTimeSeriesOutput::valid() const { return _last_modified_time > MIN_DT; }
@@ -135,24 +140,26 @@ namespace hgraph {
135140
void BaseTimeSeriesOutput::mark_invalid() {
136141
if (_last_modified_time > MIN_DT) {
137142
_last_modified_time = MIN_DT;
138-
auto g = owning_graph();
139-
if (g) {
140-
_notify(g->evaluation_clock()->evaluation_time());
143+
auto n = owning_node();
144+
if (n.get() != nullptr) {
145+
// Use cached evaluation time pointer from node for performance
146+
_notify(*n->cached_evaluation_time_ptr());
141147
} else {
142-
// Owning graph not yet attached; skip notify to avoid dereferencing null during start/recover
148+
// Owning node not yet attached; skip notify to avoid dereferencing null during start/recover
143149
}
144150
}
145151
}
146152

147153
void BaseTimeSeriesOutput::mark_modified() {
148154
if (has_parent_or_node()) {
149-
auto g = owning_graph();
150-
if (g != nullptr) {
151-
mark_modified(g->evaluation_clock()->evaluation_time());
155+
auto n = owning_node();
156+
if (n.get() != nullptr) {
157+
// Use cached evaluation time pointer from node for performance
158+
mark_modified(*n->cached_evaluation_time_ptr());
152159
} else {
153-
// Graph not yet attached; mark with a maximal time to preserve monotonicity without dereferencing
160+
// Owning node not yet attached; mark with a maximal time to preserve monotonicity without dereferencing
154161
// This is a bad situation, I would probably prefer to find out why,
155-
// TODO: find the root cause of why this could be called without a bound graph.
162+
// TODO: find the root cause of why this could be called without a bound node.
156163
}
157164
} else {
158165
mark_modified(MAX_ET);
@@ -203,8 +210,12 @@ namespace hgraph {
203210
void BaseTimeSeriesInput::reset_parent_or_node() { _parent_ts_or_node.reset(); }
204211

205212
// Implement re_parent methods
206-
void BaseTimeSeriesInput::re_parent(const node_ptr &parent) { _parent_ts_or_node = parent; }
207-
void BaseTimeSeriesInput::re_parent(const TimeSeriesType::ptr &parent) { _parent_ts_or_node = parent; }
213+
void BaseTimeSeriesInput::re_parent(const node_ptr &parent) {
214+
_parent_ts_or_node = parent;
215+
}
216+
void BaseTimeSeriesInput::re_parent(const TimeSeriesType::ptr &parent) {
217+
_parent_ts_or_node = parent;
218+
}
208219

209220
// TimeSeriesType helper methods
210221
TimeSeriesType::ptr &BaseTimeSeriesInput::_parent_time_series() const {
@@ -301,9 +312,10 @@ namespace hgraph {
301312
// - The input was previously bound (rebinding case), OR
302313
// - The new output is valid
303314
// This matches the Python implementation: (was_bound or self._output.valid)
304-
if ((owning_node()->is_started() || owning_node()->is_starting()) && _output.get() && (was_bound || _output->
305-
valid())) {
306-
_sample_time = owning_graph()->evaluation_clock()->evaluation_time();
315+
auto n = owning_node();
316+
if ((n->is_started() || n->is_starting()) && _output.get() && (was_bound || _output->valid())) {
317+
// Use cached evaluation time pointer from node for performance
318+
_sample_time = *n->cached_evaluation_time_ptr();
307319
if (active()) {
308320
notify(_sample_time);
309321
// TODO: This might belong to make_active, or not? There is a race with setting sample_time too.
@@ -325,11 +337,13 @@ namespace hgraph {
325337
if (bound()) {
326338
do_un_bind_output(unbind_refs);
327339

328-
if (owning_node()->is_started() && was_valid) {
329-
_sample_time = owning_graph()->evaluation_clock()->evaluation_time();
340+
auto n = owning_node();
341+
if (n->is_started() && was_valid) {
342+
// Use cached evaluation time pointer from node for performance
343+
_sample_time = *n->cached_evaluation_time_ptr();
330344
if (active()) {
331345
// Notify as the state of the node has changed from bound to un_bound
332-
owning_node()->notify(_sample_time);
346+
n->notify(_sample_time);
333347
}
334348
}
335349
}
@@ -433,7 +447,10 @@ namespace hgraph {
433447
engine_time_t BaseTimeSeriesInput::sample_time() const { return _sample_time; }
434448

435449
bool BaseTimeSeriesInput::sampled() const {
436-
return _sample_time != MIN_DT && _sample_time == owning_graph()->evaluation_clock()->evaluation_time();
450+
auto n = owning_node();
451+
if (n.get() == nullptr) { return false; }
452+
// Use cached evaluation time pointer from node for performance
453+
return _sample_time != MIN_DT && _sample_time == *n->cached_evaluation_time_ptr();
437454
}
438455

439456
time_series_reference_output_ptr BaseTimeSeriesInput::reference_output() const { return _reference_output; }

cpp/src/cpp/types/graph.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ namespace hgraph {
4848
}
4949
_evaluation_engine = std::move(value);
5050

51+
// Cache the clock pointer and evaluation time pointer once at initialization for performance
52+
_cached_engine_clock = _evaluation_engine->engine_evaluation_clock().get();
53+
_cached_evaluation_time_ptr = _cached_engine_clock->evaluation_time_ptr();
54+
5155
if (_push_source_nodes_end > 0) { _receiver.set_evaluation_clock(evaluation_engine_clock()); }
5256
}
5357

@@ -56,8 +60,8 @@ namespace hgraph {
5660
void Graph::schedule_node(int64_t node_ndx, engine_time_t when) { schedule_node(node_ndx, when, false); }
5761

5862
void Graph::schedule_node(int64_t node_ndx, engine_time_t when, bool force_set) {
59-
auto clock = this->evaluation_engine_clock();
60-
auto et = clock->evaluation_time();
63+
// Use cached evaluation time pointer (set at initialization) - direct memory access
64+
auto et = *_cached_evaluation_time_ptr;
6165

6266
// Match Python: just throw if scheduling in the past
6367
if (when < et) {
@@ -72,16 +76,17 @@ namespace hgraph {
7276

7377
auto &st = this->_schedule[node_ndx];
7478
if (force_set || st <= et || st > when) { st = when; }
75-
clock->update_next_scheduled_evaluation_time(when);
79+
_cached_engine_clock->update_next_scheduled_evaluation_time(when);
7680
}
7781

7882
std::vector<engine_time_t> &Graph::schedule() { return _schedule; }
7983

8084
void Graph::evaluate_graph() {
8185
NotifyGraphEvaluation nge{evaluation_engine(), graph_ptr{this}};
8286

83-
engine_time_t now = evaluation_engine_clock()->evaluation_time();
84-
auto clock = evaluation_engine_clock();
87+
// Use cached pointers (set at initialization) for direct memory access
88+
auto clock = _cached_engine_clock;
89+
engine_time_t now = *_cached_evaluation_time_ptr;
8590
auto &nodes = _nodes;
8691
auto &schedule = _schedule;
8792

cpp/src/cpp/types/node.cpp

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,10 @@ namespace hgraph {
538538
bool NodeScheduler::is_scheduled() const { return !_scheduled_events.empty() || !_alarm_tags.empty(); }
539539

540540
bool NodeScheduler::is_scheduled_now() const {
541-
return !_scheduled_events.empty() &&
542-
_scheduled_events.begin()->first == _node->graph()->evaluation_clock()->evaluation_time();
541+
if (_scheduled_events.empty()) return false;
542+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
543+
auto eval_time = *_node->_cached_evaluation_time_ptr;
544+
return _scheduled_events.begin()->first == eval_time;
543545
}
544546

545547
bool NodeScheduler::has_tag(const std::string &tag) const { return _tags.contains(tag); }
@@ -578,7 +580,9 @@ namespace hgraph {
578580
}
579581

580582
auto is_started{_node->is_started()};
581-
auto now_{is_started ? _node->graph()->evaluation_clock()->evaluation_time() : MIN_DT};
583+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
584+
engine_time_t now_ = is_started ? *_node->_cached_evaluation_time_ptr : MIN_DT;
585+
582586
if (when > now_) {
583587
_tags[tag.value_or("")] = when;
584588
auto current_first = !_scheduled_events.empty() ? _scheduled_events.begin()->first : MAX_DT;
@@ -592,7 +596,9 @@ namespace hgraph {
592596
}
593597

594598
void NodeScheduler::schedule(engine_time_delta_t when, std::optional<std::string> tag, bool on_wall_clock) {
595-
auto when_{_node->graph()->evaluation_clock()->evaluation_time() + when};
599+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
600+
auto eval_time = *_node->_cached_evaluation_time_ptr;
601+
auto when_ = eval_time + when;
596602
schedule(when_, std::move(tag), on_wall_clock);
597603
}
598604

@@ -622,7 +628,8 @@ namespace hgraph {
622628

623629
void NodeScheduler::advance() {
624630
if (_scheduled_events.empty()) { return; }
625-
auto until = _node->graph()->evaluation_clock()->evaluation_time();
631+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
632+
auto until = *_node->_cached_evaluation_time_ptr;
626633
// Note: empty string is considered smallest in std::string comparison,
627634
// so upper_bound will correctly find elements <= until regardless of tag value
628635
_scheduled_events.erase(_scheduled_events.begin(), _scheduled_events.upper_bound({until, VERY_LARGE_STRING}));
@@ -690,19 +697,27 @@ namespace hgraph {
690697
if (is_started() || is_starting()) {
691698
// When a node is starting, it might be notified with a historical time (from inputs that ticked in the past).
692699
// We should schedule for MAX(modified_time, current_evaluation_time) to avoid scheduling in the past.
693-
auto eval_time = graph()->evaluation_clock()->evaluation_time();
700+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
701+
auto eval_time = *_cached_evaluation_time_ptr;
694702
auto schedule_time = std::max(modified_time, eval_time);
695703
graph()->schedule_node(node_ndx(), schedule_time);
696704
} else {
697705
scheduler()->schedule(MIN_ST, "start");
698706
}
699707
}
700708

701-
void Node::notify() { notify(graph()->evaluation_clock()->evaluation_time()); }
709+
void Node::notify() {
710+
// Use node's cached evaluation time pointer - direct memory access, no pointer chasing
711+
auto eval_time = *_cached_evaluation_time_ptr;
712+
notify(eval_time);
713+
}
702714

703715
void Node::notify_next_cycle() {
704716
if (is_started() || is_starting()) {
705-
graph()->schedule_node(node_ndx(), graph()->evaluation_clock()->next_cycle_evaluation_time());
717+
// Use node's cached evaluation time pointer and calculate next_cycle directly
718+
// next_cycle_evaluation_time is just evaluation_time + MIN_TD
719+
auto next_time = *_cached_evaluation_time_ptr + MIN_TD;
720+
graph()->schedule_node(node_ndx(), next_time);
706721
} else {
707722
notify();
708723
}
@@ -729,7 +744,11 @@ namespace hgraph {
729744
graph_ptr Node::graph() { return _graph; }
730745
graph_ptr Node::graph() const { return _graph; }
731746

732-
void Node::set_graph(graph_ptr value) { _graph = value; }
747+
void Node::set_graph(graph_ptr value) {
748+
_graph = value;
749+
// Cache the evaluation time pointer from the graph for performance
750+
_cached_evaluation_time_ptr = value->cached_evaluation_time_ptr();
751+
}
733752

734753
time_series_bundle_input_ptr Node::input() { return _input; }
735754
time_series_bundle_input_ptr Node::input() const { return _input; }

0 commit comments

Comments
 (0)