Wildmeshing Toolkit
Loading...
Searching...
No Matches
LocalizedRetry.hpp
1#pragma once
2
3#include <wmtk/ExecutionScheduler.hpp>
4#include <wmtk/threading/collector.hpp>
5
6#include <algorithm>
7#include <cassert>
8#include <cstdint>
9#include <deque>
10#include <mutex>
11#include <utility>
12#include <vector>
13
14namespace wmtk {
15
49template <class Mesh>
50size_t run_localized_to_convergence(
51 Mesh& m,
52 ExecutePass<Mesh>& executor,
53 OpList<typename Mesh::Tuple> ops,
54 size_t max_passes = 0)
55{
56 using Tuple = typename Mesh::Tuple;
57
58 // vertex_epoch[v] = the last round in which v was in some successful operation's
59 // modified region. Capacity is fixed within a phase (storage is preallocated), and
60 // any vertex created during the phase (splits) has an id below this capacity.
61 std::vector<uint64_t> vertex_epoch(m.vert_capacity(), 0);
62 uint64_t round = 0;
63 // A failure is recorded by its operation's rank among the registered names rather than by
64 // the name itself: a pass can fail on most of its candidates, and a std::string per entry
65 // is 32 of the 72 bytes. edit_operation_maps is a std::map, so its keys are already sorted
66 // and the rank is a binary search; it is read-only while the pass runs.
67 std::vector<Op> op_names;
68 op_names.reserve(executor.edit_operation_maps.size());
69 for (const auto& kv : executor.edit_operation_maps) {
70 op_names.push_back(kv.first);
71 }
72 // A deque rather than a vector-backed collector: most candidates of a pass can fail, and a
73 // vector growing by doubling to that size holds its old and new buffer at once.
74 std::mutex failures_mutex;
75 std::deque<std::pair<uint32_t, Tuple>> failures;
76
77 auto edge_epoch = [&vertex_epoch](const Mesh& m_, const Tuple& t) -> uint64_t {
78 const size_t a = t.vid(m_);
79 const size_t b = t.switch_vertex(m_).vid(m_);
80 const uint64_t ea = a < vertex_epoch.size() ? vertex_epoch[a] : 0;
81 const uint64_t eb = b < vertex_epoch.size() ? vertex_epoch[b] : 0;
82 return std::max(ea, eb);
83 };
84
85 // Wrap the driver-provided renewal: keep its behavior (re-enqueue affected tuples
86 // within the pass) and additionally stamp those tuples' vertices with the current
87 // round so the between-pass filter can find the failures adjacent to them.
88 auto driver_renew = executor.renew_neighbor_tuples;
89 executor.renew_neighbor_tuples =
90 [&, driver_renew](const Mesh& m_, Op op, const std::vector<Tuple>& newts) {
91 auto tups = driver_renew(m_, op, newts);
92 for (const auto& [_, t] : tups) {
93 const size_t a = t.vid(m_);
94 const size_t b = t.switch_vertex(m_).vid(m_);
95 if (a < vertex_epoch.size()) {
96 // this is thread-safe because each vertex is only ever modified by one
97 // operation at a time (the two-ring lock)
98 vertex_epoch[a] = round;
99 }
100 if (b < vertex_epoch.size()) {
101 vertex_epoch[b] = round;
102 }
103 }
104 return tups;
105 };
106 executor.on_fail = [&failures, &failures_mutex, &op_names](const Mesh&, Op op, const Tuple& t) {
107 const auto it = std::lower_bound(op_names.begin(), op_names.end(), op);
108 assert(it != op_names.end() && *it == op);
109 std::lock_guard<std::mutex> lock(failures_mutex);
110 failures.emplace_back(static_cast<uint32_t>(it - op_names.begin()), t);
111 };
112
113 size_t total_success = 0;
114 do {
115 ++round;
116 failures.clear();
117 // Handed over, so the executor frees the list once it has queued it.
118 executor(m, std::move(ops));
119 total_success += static_cast<size_t>(executor.get_cnt_success());
120 ops.clear();
121 for (const auto& pr : failures) {
122 const Tuple& t = pr.second;
123 if (!t.is_valid(m)) {
124 continue;
125 }
126 // retry only if this failure's neighborhood was modified during this round
127 if (edge_epoch(m, t) == round) {
128 ops.emplace_back(op_names[pr.first], t);
129 }
130 }
131 } while (executor.get_cnt_success() > 0 && !ops.empty() &&
132 (max_passes == 0 || round < max_passes));
133 return total_success;
134}
135
136} // namespace wmtk