VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
consolidation.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef>
4#include <utility>
5#include <vector>
6
8
9namespace velographx {
10
17
22
23// Conservative bounded envelope for very large graphs. The default 1.25x cap
24// remains the general-purpose policy. For 100M+ directed-arc graphs, callers
25// running an explicit maintenance loop may choose a wider but still bounded
26// storage envelope to amortize expensive O(E) canonicalization.
28 std::size_t directed_edges,
29 double latency_ratio = 1.25) noexcept {
30 constexpr std::size_t kLargeGraphDirectedArcs = 100'000'000;
31 return {directed_edges >= kLargeGraphDirectedArcs ? 1.50 : 1.25,
32 latency_ratio};
33}
34
42
44 std::size_t current_storage_bytes,
45 std::size_t canonical_storage_bytes,
46 double current_neighbor_latency,
47 double canonical_neighbor_latency,
48 ConsolidationPolicy policy = {}) noexcept {
49 const auto storage_ratio = canonical_storage_bytes == 0
50 ? 1.0
51 : static_cast<double>(current_storage_bytes) /
52 static_cast<double>(canonical_storage_bytes);
53 const auto latency_ratio = canonical_neighbor_latency <= 0.0
54 ? 1.0
55 : current_neighbor_latency / canonical_neighbor_latency;
56 const bool storage_exceeded = storage_ratio >= policy.max_storage_growth_ratio;
57 const bool latency_exceeded = latency_ratio >= policy.max_neighbor_latency_ratio;
58 return {storage_ratio, latency_ratio, storage_exceeded, latency_exceeded,
59 storage_exceeded || latency_exceeded};
60}
61
62// Stateful steady-state controller layered on top of the raw threshold signal.
63// Storage growth is an immediate safety trigger. Latency is noisier on shared
64// hardware, so latency-driven consolidation requires meaningful patch growth,
65// persistent threshold breaches, and a cooldown after a successful cutover.
71
73 public:
75 : config_(config) {}
76
77 bool observe(const ConsolidationSignal& signal, std::size_t epoch) noexcept {
78 // Do not delay the hard storage bound with latency hysteresis or cooldown.
79 if (signal.storage_limit_exceeded) {
80 latency_breach_streak_ = 0;
81 return true;
82 }
83
84 const bool cooldown_complete = last_consolidation_epoch_ == 0 ||
85 epoch >= last_consolidation_epoch_ + config_.min_epochs_between_consolidations;
86 if (!cooldown_complete) {
87 // Cooldown samples are deliberately excluded from the persistence window.
88 // A latency-driven cutover therefore needs a fresh run of qualifying
89 // samples after the cooldown, not evidence accumulated while cutover was
90 // forbidden.
91 latency_breach_streak_ = 0;
92 return false;
93 }
94
95 const bool meaningful_patch_growth =
96 signal.storage_growth_ratio >= config_.min_storage_growth_for_latency_trigger;
97 if (signal.latency_limit_exceeded && meaningful_patch_growth) {
98 ++latency_breach_streak_;
99 } else {
100 latency_breach_streak_ = 0;
101 }
102
103 return signal.latency_limit_exceeded && meaningful_patch_growth &&
104 latency_breach_streak_ >= config_.latency_breach_samples;
105 }
106
107 void mark_consolidated(std::size_t epoch) noexcept {
108 last_consolidation_epoch_ = epoch;
109 latency_breach_streak_ = 0;
110 }
111
112 std::size_t latency_breach_streak() const noexcept { return latency_breach_streak_; }
113 std::size_t last_consolidation_epoch() const noexcept { return last_consolidation_epoch_; }
114
115 private:
117 std::size_t latency_breach_streak_{0};
118 std::size_t last_consolidation_epoch_{0};
119};
120
121// Rebuild the current logical graph into a canonical segmented-CSR snapshot.
122// This deliberately does not mutate the source graph: callers can validate the
123// snapshot before an application-level cutover. Row patches and delta arenas in
124// the returned graph are empty because bulk_load_edges() constructs fresh CSR.
125//
126// A steady-state benchmark calls compact() before this function. In that common
127// path, compact_neighbors() returns a zero-copy span from either the canonical
128// CSR or a row patch. Avoiding neighbors() there removes one temporary vector
129// allocation/copy per vertex during a 100M+ edge canonicalization. The fallback
130// to neighbors() preserves correctness when callers consolidate a graph that
131// still contains live deltas.
133 std::vector<std::pair<VertexId, VertexId>> edges;
134 edges.reserve(source.directed() ? source.edge_count_directed()
135 : source.edge_count_directed() / 2);
136
137 const bool compact_source = source.is_compact();
138 for (std::size_t u = 0; u < source.vertex_count(); ++u) {
139 const auto vertex = static_cast<VertexId>(u);
140 if (compact_source) {
141 const auto row = source.compact_neighbors(vertex);
142 for (const auto v : row) {
143 if (source.directed() || u < static_cast<std::size_t>(v)) {
144 edges.emplace_back(vertex, v);
145 }
146 }
147 } else {
148 const auto row = source.neighbors(vertex);
149 for (const auto v : row) {
150 if (source.directed() || u < static_cast<std::size_t>(v)) {
151 edges.emplace_back(vertex, v);
152 }
153 }
154 }
155 }
156
157 DynamicGraph consolidated(source.vertex_count(), source.directed());
158 consolidated.bulk_load_edges(edges);
159 const auto source_bytes = source.storage_bytes();
160 const auto consolidated_bytes = consolidated.storage_bytes();
161 const auto directed_edges = source.edge_count_directed();
162 return {std::move(consolidated), source_bytes, consolidated_bytes, directed_edges};
163}
164
165} // namespace velographx
void mark_consolidated(std::size_t epoch) noexcept
std::size_t latency_breach_streak() const noexcept
ConsolidationController(ConsolidationControllerConfig config={})
bool observe(const ConsolidationSignal &signal, std::size_t epoch) noexcept
std::size_t last_consolidation_epoch() const noexcept
bool directed() const noexcept
std::size_t storage_bytes() const noexcept
bool is_compact() const noexcept
std::size_t edge_count_directed() const noexcept
std::span< const VertexId > compact_neighbors(VertexId u) const noexcept
std::vector< VertexId > neighbors(VertexId u) const
void bulk_load_edges(const std::vector< std::pair< VertexId, VertexId > > &edges)
std::size_t vertex_count() const noexcept
ConsolidationSnapshot consolidate_to_csr_snapshot(const DynamicGraph &source)
ConsolidationSignal evaluate_consolidation(std::size_t current_storage_bytes, std::size_t canonical_storage_bytes, double current_neighbor_latency, double canonical_neighbor_latency, ConsolidationPolicy policy={}) noexcept
ConsolidationPolicy scale_aware_consolidation_policy(std::size_t directed_edges, double latency_ratio=1.25) noexcept
std::uint32_t VertexId
Definition frontier.hpp:6