4#ifndef DASMIG_CORE_BATCH_HPP
5#define DASMIG_CORE_BATCH_HPP
19#include <system_error>
24#include <unordered_set>
48[[nodiscard]]
constexpr std::uint64_t
item_seed(std::uint64_t batch_seed,
49 std::uint64_t index)
noexcept
64[[nodiscard]]
constexpr std::uint64_t
attempt_seed(std::uint64_t batch_seed,
66 std::uint64_t attempt)
noexcept
68 const std::uint64_t base =
item_seed(batch_seed, index);
69 return attempt == 0 ? base :
item_seed(base, attempt);
89 template <
typename T,
typename Key>
93 rule._type = std::type_index{
typeid(T)};
95 rule._key = [key = std::move(key)](
const void* item)
96 {
return std::string{key(*
static_cast<const T*
>(item))}; };
102 [[nodiscard]] std::size_t
max_attempts() const noexcept {
return _max_attempts; }
106 [[nodiscard]] std::type_index
type() const noexcept {
return _type; }
111 [[nodiscard]] std::string
key_of(
const void* item)
const {
return _key(item); }
116 std::type_index _type{
typeid(void)};
117 std::size_t _max_attempts{1};
118 std::function<std::string(
const void*)> _key;
127 std::optional<unique_rule> unique{};
134template <
typename Make>
135using batch_item_t = std::remove_cvref_t<std::invoke_result_t<const Make&, std::uint64_t>>;
138[[nodiscard]]
inline std::size_t resolve_threads(std::size_t requested, std::size_t work)
noexcept
140#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
145 std::size_t threads = requested;
148 threads = std::max<std::size_t>(1, std::thread::hardware_concurrency());
150 return std::max<std::size_t>(1, std::min(threads, work));
156template <
typename Make,
typename Item>
157void fill_window(
const Make& make, std::uint64_t batch_seed, std::uint64_t first,
158 std::size_t n, std::size_t threads, std::vector<std::optional<Item>>& slots)
161 const auto run_chunk = [&](std::size_t lo, std::size_t hi, std::exception_ptr& error)
165 for (std::size_t k = lo; k < hi; ++k)
167 slots[k].emplace(make(
item_seed(batch_seed, first + k)));
172 error = std::current_exception();
178 std::exception_ptr error;
179 run_chunk(0, n, error);
180 if (error) std::rethrow_exception(error);
184 std::vector<std::exception_ptr> errors(threads);
185 const auto bound = [&](std::size_t chunk) {
return n * chunk / threads; };
186 std::vector<std::thread> workers;
187 workers.reserve(threads - 1);
188 for (std::size_t c = 1; c < threads; ++c)
192 workers.emplace_back(run_chunk, bound(c), bound(c + 1), std::ref(errors[c]));
194 catch (
const std::system_error&)
197 run_chunk(bound(c), bound(c + 1), errors[c]);
200 run_chunk(bound(0), bound(1), errors[0]);
201 for (
auto& worker : workers) worker.join();
202 for (
const auto& error : errors)
204 if (error) std::rethrow_exception(error);
208template <
typename Make,
typename Sink>
209void run_batch_windows(
const batch_options& options, std::uint64_t batch_seed,
210 const Make& make, Sink& sink, std::uint64_t window_items)
212 using item = batch_item_t<Make>;
214 if (options.count > std::numeric_limits<std::uint64_t>::max() - options.first)
216 throw std::invalid_argument(
"batch: first + count overflows the item index");
220 if (options.unique->type() != std::type_index{typeid(item)})
222 throw std::invalid_argument(
"batch: the unique rule was made for another item type");
224 if (options.unique->max_attempts() == 0)
226 throw std::invalid_argument(
"batch: a unique rule needs at least one attempt");
230 const std::size_t threads = resolve_threads(
232 static_cast<std::size_t
>(std::min<std::uint64_t>(options.count, std::numeric_limits<std::size_t>::max())));
233 std::unordered_set<std::string> seen;
234 std::vector<std::optional<item>> slots;
236 for (std::uint64_t done = 0; done < options.count;)
238 const auto n =
static_cast<std::size_t
>(std::min(window_items, options.count - done));
239 const std::uint64_t window_first = options.first + done;
242 fill_window(make, batch_seed, window_first, n, threads, slots);
244 for (std::size_t k = 0; k < n; ++k)
246 const std::uint64_t index = window_first + k;
249 const unique_rule& rule = *options.unique;
250 std::string key = rule.key_of(&*slots[k]);
251 for (std::uint64_t attempt = 1; !seen.insert(key).second; ++attempt)
253 if (attempt >= rule.max_attempts())
255 throw std::runtime_error(
256 "batch: item " + std::to_string(index) +
" has no unique key after " +
257 std::to_string(rule.max_attempts()) +
" attempts");
259 slots[k].emplace(make(
attempt_seed(batch_seed, index, attempt)));
260 key = rule.key_of(&*slots[k]);
263 sink(index, *slots[k]);
290template <
typename Make>
291 requires std::invocable<const Make&, std::uint64_t>
293 std::uint64_t batch_seed,
296 using item = detail::batch_item_t<Make>;
297 std::vector<item> out;
298 out.reserve(
static_cast<std::size_t
>(options.
count));
299 auto sink = [&out](std::uint64_t, item& value) { out.push_back(std::move(value)); };
300 detail::run_batch_windows(options, batch_seed, make, sink,
301 std::max<std::uint64_t>(options.
count, 1));
319template <
typename Make,
typename Sink>
320 requires std::invocable<const Make&, std::uint64_t>
324 const std::size_t threads = detail::resolve_threads(options.
threads, std::numeric_limits<std::size_t>::max());
325 detail::run_batch_windows(options, batch_seed, make, sink,
326 static_cast<std::uint64_t
>(threads) * 4096U);
void for_each_batch(const batch_options &options, std::uint64_t batch_seed, const Make &make, Sink &&sink)
Generate a batch and pass each item to sink, in index order, without holding the whole batch.
constexpr std::uint64_t attempt_seed(std::uint64_t batch_seed, std::uint64_t index, std::uint64_t attempt) noexcept
The seed of attempt attempt of item index, for a batch that must be unique.
std::vector< detail::batch_item_t< Make > > run_batch(const batch_options &options, std::uint64_t batch_seed, const Make &make)
Generate a batch and return it.
constexpr std::uint64_t item_seed(std::uint64_t batch_seed, std::uint64_t index) noexcept
The seed of item index in a batch.
static constexpr std::uint64_t child_seed_of(std::uint64_t seed, std::uint64_t index) noexcept
child_seed(index) for a seed, without building an engine.
Makes a batch unique by a key (an email, a username, ...).
static unique_rule by(Key key, std::size_t max_attempts=100)
Unique by key.
std::type_index type() const noexcept
The item type the rule was made for.
std::string key_of(const void *item) const
The key of an item.
std::size_t max_attempts() const noexcept
Attempts per item, including the first.
The random engine every generator library uses: xoshiro256** seeded through SplitMix64,...
How a batch is generated. Requests keep describing one item.
std::size_t threads
Worker threads; 0 means the hardware count, 1 runs on the caller.
std::uint64_t count
Number of items.
std::uint64_t first
Index of the first item; item i is item_seed(seed, first + i).