53 static constexpr unsigned depth = 4;
62 std::filesystem::path
const& path,
64 size_t queued_limit =
size_t{1} << 30
66 : path_(path), queued_limit_(queued_limit), durable_(durable) {
67 int const flags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
68 fd_ =
ablated(
"direct-io") ? -1 : ::open(path.c_str(), flags | O_DIRECT, 0644);
70 if (fd_ == -1) fd_ = ::open(path.c_str(), flags, 0644);
71 if (fd_ == -1) fail(
"cannot write");
72 struct statfs status{};
74 if (direct_ && ::fstatfs(fd_, &status) == 0 &&
75 (status.f_type == 0x6969 || status.f_type == 0x0BD00BD0)) {
76 if (::fcntl(fd_, F_SETFL, ::fcntl(fd_, F_GETFL) & ~O_DIRECT) == -1) {
88 if (direct_ && !durable_ && !
ablated(
"write-probe")) {
89 buffered_fd_ = ::open(path.c_str(), O_WRONLY | O_CLOEXEC);
90 if (buffered_fd_ == -1) fail(
"cannot write");
95 if (::ioctl(fd_, FS_IOC_GETFLAGS, &attributes) == 0) {
96 attributes |= FS_NOCOMP_FL;
97 static_cast<void>(::ioctl(fd_, FS_IOC_SETFLAGS, &attributes));
100 thread_ = std::thread([
this] { work(); });
107 if (fd_ != -1) ::close(fd_);
108 if (buffered_fd_ != -1) ::close(buffered_fd_);
115 std::unique_lock lock(append_mutex_);
116 auto const total = carry_bytes_ + bytes;
117 current_ = take_extent((
total + 4095) / 4096 * 4096);
118 std::memcpy(current_->data.get(), carry_.data(), carry_bytes_);
120 reservation_ = std::move(lock);
121 return {current_->data.get() + carry_bytes_, bytes};
127 queue_current(
false);
128 reservation_.unlock();
133 void append(std::initializer_list<std::span<char const>> chunks) {
135 for (
auto const& chunk : chunks) bytes += chunk.size();
136 auto* out =
reserve(bytes).data();
137 for (
auto const& chunk : chunks) {
138 if (!chunk.empty()) std::memcpy(out, chunk.data(), chunk.size());
144 void append(std::span<char const> bytes) {
155 size = offset_ + carry_bytes_;
157 reservation_.unlock();
160 std::unique_lock lock(mutex_);
161 space_.wait(lock, [&] {
return failed_ != 0 || (queue_.empty() && in_flight_ == 0); });
165 if (error == 0 && direct_ && ::ftruncate(fd_,
static_cast<off_t
>(size)) != 0) error = errno;
166 if (error == 0 && durable_ && ::fsync(fd_) != 0) error = errno;
167 if (::close(fd_) != 0 && error == 0) error = errno;
169 if (buffered_fd_ != -1 && ::close(buffered_fd_) != 0 && error == 0) error = errno;
171 if (error != 0) fail(
"cannot write", error);
175 [[nodiscard]]
bool direct() const noexcept {
181 return ring_ !=
nullptr;
185 struct free_deleter {
186 void operator()(
char* data)
const noexcept {
193 std::unique_ptr<char, free_deleter> data;
195 unsigned pending = 0;
199 struct queued_block {
200 extent* source =
nullptr;
201 char const* data =
nullptr;
206 [[noreturn]]
void fail(
char const* what,
int error = errno)
const {
207 throw std::system_error(
208 error, std::generic_category(), std::string(what) +
" " + path_.string()
215 extent* take_extent(
size_t bytes) {
216 std::unique_lock lock(mutex_);
217 space_.wait(lock, [&] {
218 return failed_ != 0 || extents_.size() - free_.size() < 2 ||
219 queued_ + bytes <= queued_limit_;
221 if (failed_ != 0) fail(
"cannot write", failed_);
223 std::ranges::find_if(free_, [&](extent* e) {
return e->capacity >= bytes; });
224 extent* chosen =
nullptr;
225 if (fits != free_.end()) {
230 if (!free_.empty()) {
231 auto*
const smallest = free_.front();
232 free_.erase(free_.begin());
233 std::erase_if(extents_, [&](
auto const& e) {
return e.get() == smallest; });
235 auto fresh = std::make_unique<extent>();
238 static size_t const alignment =
239 std::max<size_t>(4096,
static_cast<size_t>(::sysconf(_SC_PAGESIZE)));
242 fresh->data.reset(
static_cast<char*
>(std::aligned_alloc(alignment, fresh->capacity)));
243 if (!fresh->data)
throw std::bad_alloc();
247 static_cast<void>(::madvise(fresh->data.get(), fresh->capacity, MADV_NOHUGEPAGE));
248 chosen = extents_.emplace_back(std::move(fresh)).get();
250 queued_ += chosen->capacity;
256 void queue_current(
bool final) {
257 auto const total = carry_bytes_ + reserved_;
258 size_t written =
total;
259 if (direct_) written =
final ? (
total + 4095) / 4096 * 4096 :
total / 4096 * 4096;
260 char*
const data = current_->data.get();
262 carry_bytes_ =
total - std::min(
total, written);
263 std::memcpy(carry_.data(), data + written, carry_bytes_);
265 std::lock_guard lock(mutex_);
266 for (
size_t at = 0; at < written; at +=
block_bytes) {
268 {current_, data + at, std::min(
block_bytes, written - at), offset_ + at}
272 if (current_->pending == 0) release(current_);
281 void release(extent* done) {
282 queued_ -= done->capacity;
283 auto const at = std::ranges::lower_bound(free_, done->capacity, {}, &extent::capacity);
284 free_.insert(at, done);
289 std::lock_guard lock(mutex_);
293 if (thread_.joinable()) thread_.join();
297 static int write_all(
int fd, queued_block
const& piece) {
299 while (done < piece.bytes) {
300 auto const wrote = ::pwrite(
301 fd, piece.data + done, piece.bytes - done,
static_cast<off_t
>(piece.offset + done)
303 if (wrote < 0)
return errno;
304 if (wrote == 0)
return EIO;
305 done +=
static_cast<size_t>(wrote);
310 using clock = std::chrono::steady_clock;
312 static double seconds_since(clock::time_point start) {
313 return std::chrono::duration<double>(clock::now() - start).count();
317 void retire(queued_block
const& written,
int error) {
319 std::lock_guard lock(mutex_);
320 if (error != 0 && failed_ == 0) failed_ = error;
321 if (--written.source->pending == 0) release(written.source);
337 std::vector<queued_block>
slots(ring_ ?
depth : 1U);
338 std::vector<unsigned> idle;
339 for (
unsigned slot = 0; slot <
slots.size(); ++slot) idle.push_back(slot);
340 auto const window = std::min(
probe_window, queued_limit_);
341 bool buffered =
false, settled = buffered_fd_ == -1, tried =
false;
342 uint64_t submitted = 0, window_bytes = 0;
343 double window_seconds = 0, direct_rate = 0;
348 std::unique_lock lock(mutex_);
349 ready_.wait(lock, [&] {
return stopping_ || !queue_.empty(); });
350 if (queue_.empty())
return;
351 next = queue_.front();
355 auto const start = clock::now();
356 int const error = write_all(buffered_fd_, next);
357 window_seconds += seconds_since(start);
358 window_bytes += next.bytes;
360 std::lock_guard lock(mutex_);
364 if (window_bytes >= (tried ? 16 : 1) * window) {
365 auto const pace = tried ? 0.75 * direct_rate : direct_rate;
366 buffered =
static_cast<double>(window_bytes) >= pace * window_seconds;
375 std::unique_lock lock(mutex_);
377 if (idle.size() ==
slots.size()) {
378 if (!settled && submitted >= window) {
379 direct_rate =
static_cast<double>(window_bytes) / window_seconds;
385 ready_.wait(lock, [&] {
return stopping_ || !queue_.empty(); });
386 if (queue_.empty())
return;
388 for (; !idle.empty() && !queue_.empty() && (settled || submitted < window);
390 slots[idle.back()] = queue_.front();
393 submitted +=
slots[idle.back()].bytes;
395 auto const& next =
slots[idle.back()];
399 static_cast<uint32_t
>(next.bytes),
406 if (idle.size() ==
slots.size())
continue;
407 auto const start = clock::now();
409 int const error = write_all(fd_,
slots[0]);
410 window_seconds += seconds_since(start);
411 window_bytes +=
slots[0].bytes;
412 finish_slot(
slots, idle, 0, error);
415 int const error = ring_->submit_and_wait(1, [&](uint64_t slot,
int result) {
416 auto const expected =
static_cast<int64_t
>(
slots[slot].bytes);
417 window_bytes +=
slots[slot].bytes;
422 static_cast<unsigned>(slot),
423 result < 0 ? -result : (result == expected ? 0 : EIO)
426 window_seconds += seconds_since(start);
429 for (
auto& extent : extents_) static_cast<void>(extent->data.release());
430 std::lock_guard lock(mutex_);
439 std::vector<queued_block>&
slots,
440 std::vector<unsigned>& idle,
445 std::lock_guard lock(mutex_);
448 retire(
slots[slot], error);
449 idle.push_back(slot);
452 std::filesystem::path path_;
453 size_t const queued_limit_;
457 int buffered_fd_ = -1;
458 bool direct_ =
false;
459 std::unique_ptr<io_ring> ring_;
462 std::mutex append_mutex_;
463 std::unique_lock<std::mutex> reservation_;
464 extent* current_ =
nullptr;
465 size_t reserved_ = 0;
467 uint64_t offset_ = 0;
469 std::array<char, 4096> carry_{};
470 size_t carry_bytes_ = 0;
473 std::condition_variable ready_, space_;
474 std::deque<queued_block> queue_;
475 std::vector<std::unique_ptr<extent>> extents_;
477 std::vector<extent*> free_;
480 unsigned in_flight_ = 0;
482 bool stopping_ =
false;