VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
dynamic_graph.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <optional>
8#include <span>
9#include <unordered_map>
10#include <utility>
11#include <vector>
12
13namespace velographx {
14
15using VertexId = std::uint32_t;
16
17struct EdgeUpdate {
20 bool add{true};
21 std::uint64_t timestamp{0};
22};
23
25 std::vector<EdgeUpdate> updates;
26 void add(VertexId u, VertexId v, std::uint64_t ts = 0) { updates.push_back({u, v, true, ts}); }
27 void remove(VertexId u, VertexId v, std::uint64_t ts = 0) { updates.push_back({u, v, false, ts}); }
28 [[nodiscard]] bool empty() const noexcept { return updates.empty(); }
29};
30
31namespace storage_detail {
32
34 public:
35 static constexpr std::size_t kVerticesPerSegment = 1u << 16;
36
37 [[nodiscard]] std::size_t vertex_count() const noexcept { return vertex_count_; }
38 [[nodiscard]] std::size_t edge_count() const noexcept { return edge_count_; }
39 [[nodiscard]] std::size_t segment_count() const noexcept { return segments_.size(); }
40
41 [[nodiscard]] std::size_t segment_begin(std::size_t index) const noexcept {
42 return index < segments_.size() ? segments_[index].first_vertex : vertex_count_;
43 }
44
45 [[nodiscard]] std::size_t segment_end(std::size_t index) const noexcept {
46 if (index >= segments_.size()) return vertex_count_;
47 return segments_[index].first_vertex + segments_[index].vertex_count;
48 }
49
50 [[nodiscard]] std::size_t segment_edge_count(std::size_t index) const noexcept {
51 return index < segments_.size() ? segments_[index].edges.size() : 0;
52 }
53
54 void resize_vertices(std::size_t vertices) {
55 if (vertices <= vertex_count_) return;
56 while (vertex_count_ < vertices) {
57 const auto segment_index = vertex_count_ / kVerticesPerSegment;
58 if (segment_index == segments_.size()) {
59 Segment segment;
60 segment.first_vertex = segment_index * kVerticesPerSegment;
61 segment.offsets.push_back(0);
62 segments_.push_back(std::move(segment));
63 }
64 auto& segment = segments_[segment_index];
65 const auto target_count = std::min(kVerticesPerSegment, vertices - segment.first_vertex);
66 segment.offsets.resize(target_count + 1, segment.edges.size());
67 segment.vertex_count = target_count;
68 vertex_count_ = segment.first_vertex + target_count;
69 }
70 }
71
72 void clear(std::size_t vertices = 0) {
73 segments_.clear();
74 vertex_count_ = 0;
75 edge_count_ = 0;
76 resize_vertices(vertices);
77 }
78
79 void build(std::size_t vertices, std::vector<std::pair<VertexId, VertexId>> arcs) {
80 clear(vertices);
81 arcs.erase(std::remove_if(arcs.begin(), arcs.end(), [vertices](const auto& e) {
82 return e.first == e.second || e.first >= vertices || e.second >= vertices;
83 }),
84 arcs.end());
85 std::sort(arcs.begin(), arcs.end());
86 arcs.erase(std::unique(arcs.begin(), arcs.end()), arcs.end());
87
88 for (const auto& [u, v] : arcs) {
89 (void)v;
90 auto& segment = segment_for(u);
91 const auto local = static_cast<std::size_t>(u) - segment.first_vertex;
92 ++segment.offsets[local + 1];
93 }
94 for (auto& segment : segments_) {
95 for (std::size_t i = 1; i < segment.offsets.size(); ++i) {
96 segment.offsets[i] += segment.offsets[i - 1];
97 }
98 segment.edges.resize(segment.offsets.back());
99 }
100
101 std::vector<std::vector<std::size_t>> cursors;
102 cursors.reserve(segments_.size());
103 for (const auto& segment : segments_) cursors.push_back(segment.offsets);
104 for (const auto& [u, v] : arcs) {
105 const auto segment_index = static_cast<std::size_t>(u) / kVerticesPerSegment;
106 auto& segment = segments_[segment_index];
107 const auto local = static_cast<std::size_t>(u) - segment.first_vertex;
108 segment.edges[cursors[segment_index][local]++] = v;
109 }
110 edge_count_ = arcs.size();
111 }
112
113 template <class RowProvider>
114 void build_from_rows(std::size_t vertices, RowProvider&& rows) {
115 clear(vertices);
116 edge_count_ = 0;
117 for (std::size_t u = 0; u < vertices; ++u) {
118 auto row_values = rows(static_cast<VertexId>(u));
119 auto& segment = segment_for(static_cast<VertexId>(u));
120 const auto local = u - segment.first_vertex;
121 segment.offsets[local] = segment.edges.size();
122 segment.edges.insert(segment.edges.end(), row_values.begin(), row_values.end());
123 segment.offsets[local + 1] = segment.edges.size();
124 edge_count_ += row_values.size();
125 }
126 }
127
128 template <class RowProvider>
129 void rebuild_segment(std::size_t index, RowProvider&& rows) {
130 if (index >= segments_.size()) return;
131 const auto& old = segments_[index];
132 Segment rebuilt;
133 rebuilt.first_vertex = old.first_vertex;
134 rebuilt.vertex_count = old.vertex_count;
135 rebuilt.offsets.reserve(rebuilt.vertex_count + 1);
136 rebuilt.offsets.push_back(0);
137 for (std::size_t local = 0; local < rebuilt.vertex_count; ++local) {
138 const auto u = static_cast<VertexId>(rebuilt.first_vertex + local);
139 auto row_values = rows(u);
140 rebuilt.edges.insert(rebuilt.edges.end(), row_values.begin(), row_values.end());
141 rebuilt.offsets.push_back(rebuilt.edges.size());
142 }
143 edge_count_ = edge_count_ - old.edges.size() + rebuilt.edges.size();
144 segments_[index] = std::move(rebuilt);
145 }
146
147 void build_transpose_from(const SegmentedCsr& source) {
148 clear(source.vertex_count());
149 std::vector<std::size_t> indegree(vertex_count_, 0);
150 for (std::size_t u = 0; u < source.vertex_count(); ++u) {
151 for (auto v : source.row(static_cast<VertexId>(u))) ++indegree[v];
152 }
153
154 for (std::size_t v = 0; v < vertex_count_; ++v) {
155 auto& segment = segment_for(static_cast<VertexId>(v));
156 const auto local = v - segment.first_vertex;
157 segment.offsets[local + 1] = indegree[v];
158 }
159 for (auto& segment : segments_) {
160 for (std::size_t i = 1; i < segment.offsets.size(); ++i) {
161 segment.offsets[i] += segment.offsets[i - 1];
162 }
163 segment.edges.resize(segment.offsets.back());
164 }
165
166 std::vector<std::vector<std::size_t>> cursors;
167 cursors.reserve(segments_.size());
168 for (const auto& segment : segments_) cursors.push_back(segment.offsets);
169 for (std::size_t u = 0; u < source.vertex_count(); ++u) {
170 for (auto v : source.row(static_cast<VertexId>(u))) {
171 const auto segment_index = static_cast<std::size_t>(v) / kVerticesPerSegment;
172 auto& segment = segments_[segment_index];
173 const auto local = static_cast<std::size_t>(v) - segment.first_vertex;
174 segment.edges[cursors[segment_index][local]++] = static_cast<VertexId>(u);
175 }
176 }
177 edge_count_ = source.edge_count();
178 }
179
180 [[nodiscard]] std::span<const VertexId> row(VertexId u) const noexcept {
181 if (u >= vertex_count_) return {};
182 const auto segment_index = static_cast<std::size_t>(u) / kVerticesPerSegment;
183 const auto& segment = segments_[segment_index];
184 const auto local = static_cast<std::size_t>(u) - segment.first_vertex;
185 const auto begin = segment.offsets[local];
186 const auto end = segment.offsets[local + 1];
187 return {segment.edges.data() + begin, end - begin};
188 }
189
190 [[nodiscard]] bool contains(VertexId u, VertexId v) const noexcept {
191 const auto neighbors = row(u);
192 return std::binary_search(neighbors.begin(), neighbors.end(), v);
193 }
194
195 [[nodiscard]] std::size_t storage_bytes() const noexcept {
196 std::size_t bytes = sizeof(*this) + segments_.capacity() * sizeof(Segment);
197 for (const auto& segment : segments_) {
198 bytes += segment.offsets.capacity() * sizeof(std::size_t);
199 bytes += segment.edges.capacity() * sizeof(VertexId);
200 }
201 return bytes;
202 }
203
204 private:
205 struct Segment {
206 std::size_t first_vertex{0};
207 std::size_t vertex_count{0};
208 std::vector<std::size_t> offsets;
209 std::vector<VertexId> edges;
210 };
211
212 Segment& segment_for(VertexId u) {
213 return segments_[static_cast<std::size_t>(u) / kVerticesPerSegment];
214 }
215
216 std::vector<Segment> segments_;
217 std::size_t vertex_count_{0};
218 std::size_t edge_count_{0};
219};
220
222 public:
223 struct Entry {
225 bool present{true};
226 };
227
228 void resize_vertices(std::size_t vertices) {
229 if (vertices > rows_.size()) rows_.resize(vertices);
230 }
231
232 void clear() {
233 arena_.clear();
234 live_entries_ = 0;
235 present_entries_ = 0;
236 absent_entries_ = 0;
237 for (auto& row_meta : rows_) row_meta = {};
238 }
239
240 [[nodiscard]] std::span<const Entry> row(VertexId u) const noexcept {
241 if (u >= rows_.size()) return {};
242 const auto& meta = rows_[u];
243 if (meta.count == 0) return {};
244 return {arena_.data() + meta.offset, meta.count};
245 }
246
247 [[nodiscard]] std::optional<bool> override_for(VertexId u, VertexId v) const noexcept {
248 const auto entries = row(u);
249 const auto it = std::lower_bound(entries.begin(), entries.end(), v,
250 [](const Entry& e, VertexId target) { return e.dst < target; });
251 if (it == entries.end() || it->dst != v) return std::nullopt;
252 return it->present;
253 }
254
255 void set(VertexId u, VertexId v, bool desired_present, bool base_present) {
256 resize_vertices(static_cast<std::size_t>(u) + 1);
257 if (desired_present == base_present) erase(u, v);
258 else upsert(u, v, desired_present);
259 }
260
261 bool set_if_changed(VertexId u, VertexId v, bool desired_present, bool base_present) {
262 resize_vertices(static_cast<std::size_t>(u) + 1);
263 auto& meta = rows_[u];
264 const auto entries = row(u);
265 const auto it = std::lower_bound(entries.begin(), entries.end(), v,
266 [](const Entry& e, VertexId target) { return e.dst < target; });
267 const auto pos = static_cast<std::size_t>(it - entries.begin());
268 const bool found = it != entries.end() && it->dst == v;
269 const bool current = found ? it->present : base_present;
270 if (current == desired_present) return false;
271
272 if (desired_present == base_present) {
273 if (!found) return false;
274 const bool old_present = it->present;
275 if (old_present) --present_entries_;
276 else --absent_entries_;
277 for (std::size_t i = pos + 1; i < meta.count; ++i) {
278 arena_[meta.offset + i - 1] = arena_[meta.offset + i];
279 }
280 --meta.count;
281 --live_entries_;
282 return true;
283 }
284
285 if (found) {
286 if (it->present) {
287 --present_entries_;
288 ++absent_entries_;
289 } else {
290 --absent_entries_;
291 ++present_entries_;
292 }
293 arena_[meta.offset + pos].present = desired_present;
294 return true;
295 }
296
297 ensure_capacity(u, meta.count + 1);
298 auto& refreshed = rows_[u];
299 for (std::size_t i = refreshed.count; i > pos; --i) {
300 arena_[refreshed.offset + i] = arena_[refreshed.offset + i - 1];
301 }
302 arena_[refreshed.offset + pos] = {v, desired_present};
303 ++refreshed.count;
304 ++live_entries_;
305 if (desired_present) ++present_entries_;
306 else ++absent_entries_;
307 return true;
308 }
309
310 [[nodiscard]] std::size_t size() const noexcept { return live_entries_; }
311 [[nodiscard]] std::size_t additions() const noexcept { return present_entries_; }
312 [[nodiscard]] std::size_t deletions() const noexcept { return absent_entries_; }
313 [[nodiscard]] bool empty() const noexcept { return live_entries_ == 0; }
314
315 [[nodiscard]] std::size_t count_range(std::size_t begin, std::size_t end) const noexcept {
316 end = std::min(end, rows_.size());
317 std::size_t total = 0;
318 for (std::size_t u = begin; u < end; ++u) total += rows_[u].count;
319 return total;
320 }
321
322 void clear_range(std::size_t begin, std::size_t end) {
323 end = std::min(end, rows_.size());
324 for (std::size_t u = begin; u < end; ++u) {
325 auto& meta = rows_[u];
326 if (meta.count == 0) continue;
327 for (const auto& entry : row(static_cast<VertexId>(u))) {
328 if (entry.present) --present_entries_;
329 else --absent_entries_;
330 }
331 live_entries_ -= meta.count;
332 meta = {};
333 }
334 }
335
336 [[nodiscard]] std::size_t storage_bytes() const noexcept {
337 return sizeof(*this) + rows_.capacity() * sizeof(Row) + arena_.capacity() * sizeof(Entry);
338 }
339
340 void repack() {
341 if (live_entries_ == 0) {
342 arena_.clear();
343 for (auto& row_meta : rows_) row_meta = {};
344 return;
345 }
346 std::vector<Entry> packed;
347 packed.reserve(live_entries_);
348 for (auto& meta : rows_) {
349 if (meta.count == 0) {
350 meta = {};
351 continue;
352 }
353 const auto old_offset = meta.offset;
354 const auto count = meta.count;
355 const auto new_offset = packed.size();
356 packed.insert(packed.end(), arena_.begin() + old_offset,
357 arena_.begin() + old_offset + count);
358 meta.offset = new_offset;
359 meta.capacity = count;
360 }
361 arena_.swap(packed);
362 }
363
364 [[nodiscard]] double fragmentation_ratio() const noexcept {
365 if (arena_.empty()) return 0.0;
366 return 1.0 - static_cast<double>(live_entries_) / static_cast<double>(arena_.size());
367 }
368
369 private:
370 static constexpr std::size_t kNoOffset = std::numeric_limits<std::size_t>::max();
371
372 struct Row {
373 std::size_t offset{kNoOffset};
374 std::size_t count{0};
375 std::size_t capacity{0};
376 };
377
378 void ensure_capacity(VertexId u, std::size_t required) {
379 auto& meta = rows_[u];
380 if (meta.capacity >= required) return;
381 const auto new_capacity = std::max(required, std::max<std::size_t>(1, meta.capacity * 2));
382 const auto new_offset = arena_.size();
383 arena_.resize(new_offset + new_capacity);
384 if (meta.count != 0) {
385 std::copy_n(arena_.begin() + meta.offset, meta.count, arena_.begin() + new_offset);
386 }
387 meta.offset = new_offset;
388 meta.capacity = new_capacity;
389 }
390
391 void upsert(VertexId u, VertexId v, bool present) {
392 auto& meta = rows_[u];
393 const auto entries = row(u);
394 const auto it = std::lower_bound(entries.begin(), entries.end(), v,
395 [](const Entry& e, VertexId target) { return e.dst < target; });
396 const auto pos = static_cast<std::size_t>(it - entries.begin());
397 if (it != entries.end() && it->dst == v) {
398 if (it->present != present) {
399 if (it->present) {
400 --present_entries_;
401 ++absent_entries_;
402 } else {
403 --absent_entries_;
404 ++present_entries_;
405 }
406 arena_[meta.offset + pos].present = present;
407 }
408 return;
409 }
410
411 ensure_capacity(u, meta.count + 1);
412 auto& refreshed = rows_[u];
413 for (std::size_t i = refreshed.count; i > pos; --i) {
414 arena_[refreshed.offset + i] = arena_[refreshed.offset + i - 1];
415 }
416 arena_[refreshed.offset + pos] = {v, present};
417 ++refreshed.count;
418 ++live_entries_;
419 if (present) ++present_entries_;
420 else ++absent_entries_;
421 }
422
423 void erase(VertexId u, VertexId v) {
424 if (u >= rows_.size()) return;
425 auto& meta = rows_[u];
426 if (meta.count == 0) return;
427 const auto entries = row(u);
428 const auto it = std::lower_bound(entries.begin(), entries.end(), v,
429 [](const Entry& e, VertexId target) { return e.dst < target; });
430 if (it == entries.end() || it->dst != v) return;
431 const auto pos = static_cast<std::size_t>(it - entries.begin());
432 if (it->present) --present_entries_;
433 else --absent_entries_;
434 for (std::size_t i = pos + 1; i < meta.count; ++i) {
435 arena_[meta.offset + i - 1] = arena_[meta.offset + i];
436 }
437 --meta.count;
438 --live_entries_;
439 }
440
441 std::vector<Row> rows_;
442 std::vector<Entry> arena_;
443 std::size_t live_entries_{0};
444 std::size_t present_entries_{0};
445 std::size_t absent_entries_{0};
446};
447
449 public:
450 void clear() { rows_.clear(); }
451 [[nodiscard]] bool empty() const noexcept { return rows_.empty(); }
452
453 [[nodiscard]] const std::vector<VertexId>* find(VertexId u) const noexcept {
454 const auto it = rows_.find(u);
455 return it == rows_.end() ? nullptr : &it->second;
456 }
457
458 void set(VertexId u, std::vector<VertexId> row) {
459 rows_.insert_or_assign(u, std::move(row));
460 }
461
462 [[nodiscard]] std::size_t storage_bytes() const noexcept {
463 std::size_t bytes = sizeof(*this) + rows_.size() * sizeof(typename decltype(rows_)::value_type);
464 for (const auto& [u, row] : rows_) {
465 (void)u;
466 bytes += row.capacity() * sizeof(VertexId);
467 }
468 return bytes;
469 }
470
471 private:
472 std::unordered_map<VertexId, std::vector<VertexId>> rows_;
473};
474
475} // namespace storage_detail
476
477class IncrementalTriangleCount;
478
480 public:
481 explicit DynamicGraph(std::size_t vertices = 0, bool directed = false)
482 : directed_(directed) {
483 base_out_.resize_vertices(vertices);
484 base_in_.resize_vertices(vertices);
485 delta_out_.resize_vertices(vertices);
486 delta_in_.resize_vertices(vertices);
487 }
488
489 [[nodiscard]] std::size_t vertex_count() const noexcept { return base_out_.vertex_count(); }
490 [[nodiscard]] std::uint64_t version() const noexcept { return version_; }
491 [[nodiscard]] bool directed() const noexcept { return directed_; }
492
494 const auto n = static_cast<std::size_t>(v) + 1;
495 if (n <= vertex_count()) return;
496 base_out_.resize_vertices(n);
497 base_in_.resize_vertices(n);
498 delta_out_.resize_vertices(n);
499 delta_in_.resize_vertices(n);
500 }
501
502 void bulk_load_edges(const std::vector<std::pair<VertexId, VertexId>>& edges) {
503 VertexId max_vertex = 0;
504 bool saw_edge = false;
505 std::vector<std::pair<VertexId, VertexId>> arcs;
506 arcs.reserve(directed_ ? edges.size() : edges.size() * 2);
507 for (const auto& [u, v] : edges) {
508 if (u == v) continue;
509 max_vertex = std::max(max_vertex, std::max(u, v));
510 saw_edge = true;
511 arcs.emplace_back(u, v);
512 if (!directed_) arcs.emplace_back(v, u);
513 }
514 if (saw_edge) ensure_vertex(max_vertex);
515
516 base_out_.build(vertex_count(), std::move(arcs));
517 base_in_.build_transpose_from(base_out_);
518 patches_out_.clear();
519 patches_in_.clear();
520 compact_out_edges_ = base_out_.edge_count();
521 compact_in_edges_ = base_in_.edge_count();
522 delta_out_.clear();
523 delta_in_.clear();
524 delta_out_.resize_vertices(vertex_count());
525 delta_in_.resize_vertices(vertex_count());
526 dirty_out_rows_.clear();
527 dirty_in_rows_.clear();
528 ++version_;
529 }
530
532 UpdateBatch batch;
533 batch.add(u, v);
534 apply(batch);
535 }
536
538 UpdateBatch batch;
539 batch.remove(u, v);
540 apply(batch);
541 }
542
543 void apply(const UpdateBatch& batch) {
544 for (const auto& op : batch.updates) apply_unversioned(op);
545 if (!batch.empty()) {
546 ++version_;
547 automatic_storage_maintenance();
548 }
549 }
550
551 [[nodiscard]] std::vector<VertexId> neighbors(VertexId u) const {
552 return materialize_row(base_out_, patches_out_, delta_out_, u);
553 }
554
555 [[nodiscard]] std::vector<VertexId> in_neighbors(VertexId v) const {
556 return materialize_row(base_in_, patches_in_, delta_in_, v);
557 }
558
559 template <class Fn>
560 void for_each_neighbor(VertexId u, Fn&& fn) const {
561 for_each_effective_row(base_out_, patches_out_, delta_out_, u, std::forward<Fn>(fn));
562 }
563
564 template <class Fn>
565 void for_each_in_neighbor(VertexId v, Fn&& fn) const {
566 for_each_effective_row(base_in_, patches_in_, delta_in_, v, std::forward<Fn>(fn));
567 }
568
569 [[nodiscard]] bool is_compact() const noexcept {
570 return delta_out_.empty() && delta_in_.empty();
571 }
572
573 [[nodiscard]] std::span<const VertexId> compact_neighbors(VertexId u) const noexcept {
574 if (const auto* patch = patches_out_.find(u)) return *patch;
575 return base_out_.row(u);
576 }
577
578 [[nodiscard]] std::span<const VertexId> compact_in_neighbors(VertexId v) const noexcept {
579 if (const auto* patch = patches_in_.find(v)) return *patch;
580 return base_in_.row(v);
581 }
582
583 [[nodiscard]] bool has_edge(VertexId u, VertexId v) const {
584 if (u >= vertex_count()) return false;
585 if (const auto overlay = delta_out_.override_for(u, v); overlay.has_value()) return *overlay;
586 return compact_contains(base_out_, patches_out_, u, v);
587 }
588
589 [[nodiscard]] std::size_t edge_count_directed() const noexcept {
590 return compact_out_edges_ + delta_out_.additions() - delta_out_.deletions();
591 }
592
593 [[nodiscard]] std::size_t base_edge_count_directed() const noexcept {
594 return compact_out_edges_;
595 }
596
597 [[nodiscard]] std::size_t delta_edge_count() const noexcept { return delta_out_.size(); }
598
599 [[nodiscard]] std::size_t storage_bytes() const noexcept {
600 return base_out_.storage_bytes() + base_in_.storage_bytes() +
601 patches_out_.storage_bytes() + patches_in_.storage_bytes() +
602 delta_out_.storage_bytes() + delta_in_.storage_bytes() +
603 dirty_out_rows_.capacity() * sizeof(VertexId) +
604 dirty_in_rows_.capacity() * sizeof(VertexId);
605 }
606
607 [[nodiscard]] double delta_ratio() const noexcept {
608 return static_cast<double>(delta_out_.size()) /
609 static_cast<double>(std::max<std::size_t>(1, compact_out_edges_));
610 }
611
612 [[nodiscard]] std::size_t dirty_out_segment_count() const {
613 return unique_dirty_count(dirty_out_rows_);
614 }
615
616 [[nodiscard]] std::size_t dirty_in_segment_count() const {
617 return unique_dirty_count(dirty_in_rows_);
618 }
619
620 bool maybe_compact(double threshold = 0.25) {
621 bool compacted = false;
622 compacted |= compact_dense_rows(base_out_, patches_out_, delta_out_, dirty_out_rows_,
623 compact_out_edges_, threshold);
624 compacted |= compact_dense_rows(base_in_, patches_in_, delta_in_, dirty_in_rows_,
625 compact_in_edges_, threshold);
626 if (compacted) {
627 if (delta_out_.fragmentation_ratio() > 0.25) delta_out_.repack();
628 if (delta_in_.fragmentation_ratio() > 0.25) delta_in_.repack();
629 }
630 return compacted;
631 }
632
633 void compact() {
634 if (is_compact()) return;
635 compact_marked_rows(base_out_, patches_out_, delta_out_, dirty_out_rows_, compact_out_edges_);
636 compact_marked_rows(base_in_, patches_in_, delta_in_, dirty_in_rows_, compact_in_edges_);
637 delta_out_.repack();
638 delta_in_.repack();
639 }
640
641 private:
643
644 static std::span<const VertexId> compact_row(const storage_detail::SegmentedCsr& base,
646 VertexId u) noexcept {
647 if (patches.empty()) return base.row(u);
648 if (const auto* patch = patches.find(u)) return *patch;
649 return base.row(u);
650 }
651
652 static bool compact_contains(const storage_detail::SegmentedCsr& base,
654 VertexId u, VertexId v) noexcept {
655 const auto row = compact_row(base, patches, u);
656 return std::binary_search(row.begin(), row.end(), v);
657 }
658
659 template <class Fn>
660 static void for_each_effective_row(const storage_detail::SegmentedCsr& base,
661 const storage_detail::CompactRowPatches& patches,
662 const storage_detail::PackedDeltaStore& delta,
663 VertexId u,
664 Fn&& fn) {
665 const auto base_row = compact_row(base, patches, u);
666 const auto overlay = delta.row(u);
667 std::size_t i = 0;
668 std::size_t j = 0;
669 while (i < base_row.size() || j < overlay.size()) {
670 if (j == overlay.size() || (i < base_row.size() && base_row[i] < overlay[j].dst)) {
671 fn(base_row[i++]);
672 } else if (i == base_row.size() || overlay[j].dst < base_row[i]) {
673 if (overlay[j].present) fn(overlay[j].dst);
674 ++j;
675 } else {
676 if (overlay[j].present) fn(base_row[i]);
677 ++i;
678 ++j;
679 }
680 }
681 }
682
683 static std::vector<VertexId> materialize_row(const storage_detail::SegmentedCsr& base,
684 const storage_detail::CompactRowPatches& patches,
685 const storage_detail::PackedDeltaStore& delta,
686 VertexId u) {
687 const auto base_row = compact_row(base, patches, u);
688 const auto overlay = delta.row(u);
689 if (overlay.empty()) return {base_row.begin(), base_row.end()};
690
691 std::vector<VertexId> out;
692 out.reserve(base_row.size() + overlay.size());
693 std::size_t i = 0;
694 std::size_t j = 0;
695 while (i < base_row.size() || j < overlay.size()) {
696 if (j == overlay.size() || (i < base_row.size() && base_row[i] < overlay[j].dst)) {
697 out.push_back(base_row[i++]);
698 } else if (i == base_row.size() || overlay[j].dst < base_row[i]) {
699 if (overlay[j].present) out.push_back(overlay[j].dst);
700 ++j;
701 } else {
702 if (overlay[j].present) out.push_back(base_row[i]);
703 ++i;
704 ++j;
705 }
706 }
707 return out;
708 }
709
710 static std::size_t unique_dirty_count(const std::vector<VertexId>& rows) {
711 if (rows.empty()) return 0;
712 std::vector<VertexId> copy = rows;
713 std::sort(copy.begin(), copy.end());
714 return static_cast<std::size_t>(std::unique(copy.begin(), copy.end()) - copy.begin());
715 }
716
717 void apply_unversioned(const EdgeUpdate& op) {
718 // VeloGraphX uses simple-graph semantics consistently across static,
719 // bulk-load, dynamic, weighted, and consolidation paths.
720 if (op.src == op.dst) return;
721 ensure_vertex(std::max(op.src, op.dst));
722 apply_arc(op.src, op.dst, op.add);
723 if (!directed_) apply_arc(op.dst, op.src, op.add);
724 }
725
726 void apply_arc(VertexId u, VertexId v, bool present) {
727 const auto base_present_out = compact_contains(base_out_, patches_out_, u, v);
728 if (!delta_out_.set_if_changed(u, v, present, base_present_out)) return;
729
730 delta_in_.set(v, u, present, base_present_out);
731 dirty_out_rows_.push_back(u);
732 dirty_in_rows_.push_back(v);
733 }
734
735 static void normalize_dirty(std::vector<VertexId>& rows) {
736 std::sort(rows.begin(), rows.end());
737 rows.erase(std::unique(rows.begin(), rows.end()), rows.end());
738 }
739
740 static void compact_one_row(const storage_detail::SegmentedCsr& base,
741 storage_detail::CompactRowPatches& patches,
742 storage_detail::PackedDeltaStore& delta,
743 VertexId u,
744 std::size_t& compact_edges) {
745 if (delta.row(u).empty()) return;
746 const auto old_size = compact_row(base, patches, u).size();
747 auto merged = materialize_row(base, patches, delta, u);
748 const auto new_size = merged.size();
749 patches.set(u, std::move(merged));
750 delta.clear_range(static_cast<std::size_t>(u), static_cast<std::size_t>(u) + 1);
751 compact_edges = compact_edges - old_size + new_size;
752 }
753
754 static void compact_marked_rows(const storage_detail::SegmentedCsr& base,
755 storage_detail::CompactRowPatches& patches,
756 storage_detail::PackedDeltaStore& delta,
757 std::vector<VertexId>& dirty_rows,
758 std::size_t& compact_edges) {
759 normalize_dirty(dirty_rows);
760 for (const auto u : dirty_rows) compact_one_row(base, patches, delta, u, compact_edges);
761 dirty_rows.clear();
762 }
763
764 static bool compact_dense_rows(const storage_detail::SegmentedCsr& base,
765 storage_detail::CompactRowPatches& patches,
766 storage_detail::PackedDeltaStore& delta,
767 std::vector<VertexId>& dirty_rows,
768 std::size_t& compact_edges,
769 double threshold) {
770 normalize_dirty(dirty_rows);
771 bool compacted = false;
772 std::vector<VertexId> pending;
773 pending.reserve(dirty_rows.size());
774 for (const auto u : dirty_rows) {
775 const auto overlay_size = delta.row(u).size();
776 if (overlay_size == 0) continue;
777 const auto base_size = compact_row(base, patches, u).size();
778 const auto work_budget = std::max<std::size_t>(8, base_size);
779 const double density = static_cast<double>(overlay_size) / static_cast<double>(work_budget);
780 if (density < threshold) {
781 pending.push_back(u);
782 continue;
783 }
784 compact_one_row(base, patches, delta, u, compact_edges);
785 compacted = true;
786 }
787 dirty_rows.swap(pending);
788 return compacted;
789 }
790
791 void automatic_storage_maintenance() {
792 constexpr std::size_t kAutomaticMinimumDeltaEntries = 65536;
793 constexpr double kAutomaticGlobalDeltaRatio = 0.01;
794 constexpr double kRowDeltaDensityThreshold = 0.50;
795 constexpr double kFragmentationThreshold = 0.60;
796 if (delta_out_.size() >= kAutomaticMinimumDeltaEntries &&
797 delta_ratio() >= kAutomaticGlobalDeltaRatio) {
798 (void)maybe_compact(kRowDeltaDensityThreshold);
799 }
800 if (delta_out_.fragmentation_ratio() > kFragmentationThreshold) delta_out_.repack();
801 if (delta_in_.fragmentation_ratio() > kFragmentationThreshold) delta_in_.repack();
802 }
803
804 bool directed_{false};
805 storage_detail::SegmentedCsr base_out_;
806 storage_detail::SegmentedCsr base_in_;
807 storage_detail::CompactRowPatches patches_out_;
808 storage_detail::CompactRowPatches patches_in_;
809 storage_detail::PackedDeltaStore delta_out_;
810 storage_detail::PackedDeltaStore delta_in_;
811 std::vector<VertexId> dirty_out_rows_;
812 std::vector<VertexId> dirty_in_rows_;
813 std::size_t compact_out_edges_{0};
814 std::size_t compact_in_edges_{0};
815 std::uint64_t version_{0};
816};
817
818} // namespace velographx
void for_each_neighbor(VertexId u, Fn &&fn) const
DynamicGraph(std::size_t vertices=0, bool directed=false)
std::size_t dirty_out_segment_count() const
bool directed() const noexcept
std::size_t storage_bytes() const noexcept
void ensure_vertex(VertexId v)
std::uint64_t version() const noexcept
bool maybe_compact(double threshold=0.25)
bool is_compact() const noexcept
std::size_t edge_count_directed() const noexcept
std::span< const VertexId > compact_in_neighbors(VertexId v) const noexcept
std::vector< VertexId > in_neighbors(VertexId v) const
void apply(const UpdateBatch &batch)
void remove_edge(VertexId u, VertexId v)
std::size_t base_edge_count_directed() const noexcept
double delta_ratio() const noexcept
std::span< const VertexId > compact_neighbors(VertexId u) const noexcept
bool has_edge(VertexId u, VertexId v) const
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
void add_edge(VertexId u, VertexId v)
std::size_t dirty_in_segment_count() const
void for_each_in_neighbor(VertexId v, Fn &&fn) const
std::size_t delta_edge_count() const noexcept
const std::vector< VertexId > * find(VertexId u) const noexcept
void set(VertexId u, std::vector< VertexId > row)
bool set_if_changed(VertexId u, VertexId v, bool desired_present, bool base_present)
std::size_t count_range(std::size_t begin, std::size_t end) const noexcept
void clear_range(std::size_t begin, std::size_t end)
void set(VertexId u, VertexId v, bool desired_present, bool base_present)
std::span< const Entry > row(VertexId u) const noexcept
std::optional< bool > override_for(VertexId u, VertexId v) const noexcept
bool contains(VertexId u, VertexId v) const noexcept
void build_transpose_from(const SegmentedCsr &source)
void build(std::size_t vertices, std::vector< std::pair< VertexId, VertexId > > arcs)
std::size_t edge_count() const noexcept
void resize_vertices(std::size_t vertices)
void clear(std::size_t vertices=0)
void build_from_rows(std::size_t vertices, RowProvider &&rows)
std::size_t segment_edge_count(std::size_t index) const noexcept
std::size_t segment_count() const noexcept
std::size_t segment_begin(std::size_t index) const noexcept
static constexpr std::size_t kVerticesPerSegment
std::size_t storage_bytes() const noexcept
std::size_t vertex_count() const noexcept
std::span< const VertexId > row(VertexId u) const noexcept
void rebuild_segment(std::size_t index, RowProvider &&rows)
std::size_t segment_end(std::size_t index) const noexcept
std::uint32_t VertexId
Definition frontier.hpp:6
constexpr std::size_t vertex_count(const Graph &graph)
bool empty() const noexcept
void remove(VertexId u, VertexId v, std::uint64_t ts=0)
void add(VertexId u, VertexId v, std::uint64_t ts=0)
std::vector< EdgeUpdate > updates