cuDDL
Loading...
Searching...
No Matches
Classes | Public Member Functions | Static Public Attributes | List of all members
cuddl::detail::file_writer Class Reference

Writes a file from a thread of its own, so the caller produces the next bytes while earlier ones reach storage. More...

#include <file_writer.hpp>

Public Member Functions

 file_writer (std::filesystem::path const &path, bool durable, size_t queued_limit=size_t{1}<< 30)
 
 file_writer (file_writer const &)=delete
 
file_writer & operator= (file_writer const &)=delete
 
 ~file_writer ()
 Stops writing without flushing; finish reports whether every byte arrived.
 
std::span< char > reserve (size_t bytes)
 Space for the next bytes of the file, for the caller to fill and then hand back with commit, sparing append's copy.
 
void commit ()
 Queues the bytes of the last reserve.
 
void append (std::initializer_list< std::span< char const > > chunks)
 Copies chunks, which reach the file together and in order; waits while the queued bytes reach the limit.
 
void append (std::span< char const > bytes)
 
void finish ()
 Writes everything appended and closes the file, flushing it to storage first if durable.
 
bool direct () const noexcept
 Whether writes bypass the page cache.
 
bool asynchronous () const noexcept
 Whether writes go through io_uring.
 

Static Public Attributes

static constexpr size_t block_bytes = size_t{8} << 20
 
static constexpr unsigned depth = 4
 Writes in flight with io_uring.
 
static constexpr size_t probe_window = size_t{64} << 20
 Bytes timed per route before a writer that need not be durable picks one; it leaves the page cache only after 16 such windows through it ran too slow.
 

Detailed Description

Writes a file from a thread of its own, so the caller produces the next bytes while earlier ones reach storage.

Bytes are copied into page-aligned memory by append, or produced there in place through reserve, and written with O_DIRECT in requests of up to 8 MiB, bypassing the page cache. Buffered writes stall in page-cache copies and writeback: on a Samsung 9100 PRO, one buffered stream plus fsync wrote 2.4-3.1 GB/s and O_DIRECT 8 MiB writes 12.9-13.6 GB/s, the drive's rated 13.4 GB/s; 1 MiB writes reached only 9.1-9.7 GB/s. There the copy itself, 11 GB/s on one core, held a 159 GB all-to-all output below the drive. Where io_uring is available, four requests are in flight at once: on a btrfs file system that wrote 2.9-4.4 GB/s against 1.9-2.6 GB/s for synchronous writes. Elsewhere one request is written at a time, which on ext4 already reached the drive's rate.

File systems without O_DIRECT (tmpfs) and network file systems get buffered writes: direct writes to Lustre wait for the server on every request (8 streams reached 28 MB/s).

Definition at line 49 of file file_writer.hpp.

Constructor & Destructor Documentation

◆ file_writer() [1/2]

cuddl::detail::file_writer::file_writer ( std::filesystem::path const &  path,
bool  durable,
size_t  queued_limit = size_t{1} << 30 
)
inline
Parameters
durableWhether finish flushes the file to storage before returning.
queued_limitBytes reserved or appended but not yet written before reserve and append wait; a larger reservation waits only for all but one other.

Definition at line 61 of file file_writer.hpp.

64 {1} << 30
65 )
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);
69 direct_ = fd_ != -1;
70 if (fd_ == -1) fd_ = ::open(path.c_str(), flags, 0644);
71 if (fd_ == -1) fail("cannot write");
72 struct statfs status{};
73 // shortcut: NFS and Lustre only, the network file systems we run on; add others as met.
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) {
77 ::close(fd_);
78 fail("cannot write");
79 }
80 direct_ = false;
81 }
82 // Buffered writes through io_uring would run concurrently on kernel workers, and several
83 // writers into one Lustre file were far slower than one.
84 if (direct_ && !ablated("io-uring")) ring_ = io_ring::make(depth);
85 // A drive slower than the page cache leaves a run that need not be durable waiting on
86 // direct writes the cache could absorb: 159 GB to a PM9A3 took 108-158 s direct and
87 // 60-97 s buffered. Such writers time both routes and keep the faster; see work().
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");
91 // btrfs compresses page-cache writes during writeback, never direct ones; with zstd
92 // a buffered 120 GB run took 58.5 s compressed and 45.8 s not. Other file systems
93 // refuse or ignore the flag.
94 int attributes = 0;
95 if (::ioctl(fd_, FS_IOC_GETFLAGS, &attributes) == 0) {
96 attributes |= FS_NOCOMP_FL;
97 static_cast<void>(::ioctl(fd_, FS_IOC_SETFLAGS, &attributes));
98 }
99 }
100 thread_ = std::thread([this] { work(); });
101 }
static constexpr unsigned depth
Writes in flight with io_uring.
static std::unique_ptr< io_ring > make(unsigned)
Definition io_ring.hpp:140
constexpr bool ablated(std::string_view) noexcept
Always false: this build compiles the ablation switches out.
Definition ablation.hpp:82

◆ file_writer() [2/2]

cuddl::detail::file_writer::file_writer ( file_writer const &  )
delete

◆ ~file_writer()

cuddl::detail::file_writer::~file_writer ( )
inline

Stops writing without flushing; finish reports whether every byte arrived.

Definition at line 105 of file file_writer.hpp.

105 {
106 stop();
107 if (fd_ != -1) ::close(fd_);
108 if (buffered_fd_ != -1) ::close(buffered_fd_);
109 }

Member Function Documentation

◆ append() [1/2]

void cuddl::detail::file_writer::append ( std::initializer_list< std::span< char const > >  chunks)
inline

Copies chunks, which reach the file together and in order; waits while the queued bytes reach the limit.

Throws if an earlier write failed.

Definition at line 133 of file file_writer.hpp.

133 {
134 size_t bytes = 0;
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());
139 out += chunk.size();
140 }
141 commit();
142 }
std::span< char > reserve(size_t bytes)
Space for the next bytes of the file, for the caller to fill and then hand back with commit,...
void commit()
Queues the bytes of the last reserve.
Here is the call graph for this function:
Here is the caller graph for this function:

◆ append() [2/2]

void cuddl::detail::file_writer::append ( std::span< char const >  bytes)
inline

Definition at line 144 of file file_writer.hpp.

144 {
145 append({bytes});
146 }
void append(std::initializer_list< std::span< char const > > chunks)
Copies chunks, which reach the file together and in order; waits while the queued bytes reach the lim...
Here is the call graph for this function:

◆ asynchronous()

bool cuddl::detail::file_writer::asynchronous ( ) const
inlinenoexcept

Whether writes go through io_uring.

Definition at line 180 of file file_writer.hpp.

180 {
181 return ring_ != nullptr;
182 }

◆ commit()

void cuddl::detail::file_writer::commit ( )
inline

Queues the bytes of the last reserve.

A direct write covers whole sectors, so the last partial sector waits for the next bytes.

Definition at line 126 of file file_writer.hpp.

126 {
127 queue_current(false);
128 reservation_.unlock();
129 }
Here is the caller graph for this function:

◆ direct()

bool cuddl::detail::file_writer::direct ( ) const
inlinenoexcept

Whether writes bypass the page cache.

Definition at line 175 of file file_writer.hpp.

175 {
176 return direct_;
177 }

◆ finish()

void cuddl::detail::file_writer::finish ( )
inline

Writes everything appended and closes the file, flushing it to storage first if durable.

Bytes left in the page cache otherwise reach storage after the caller moves on, as with any buffered write.

Definition at line 151 of file file_writer.hpp.

151 {
152 uint64_t size = 0;
153 {
154 reserve(0);
155 size = offset_ + carry_bytes_;
156 queue_current(true);
157 reservation_.unlock();
158 }
159 {
160 std::unique_lock lock(mutex_);
161 space_.wait(lock, [&] { return failed_ != 0 || (queue_.empty() && in_flight_ == 0); });
162 }
163 stop();
164 int error = failed_;
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;
168 fd_ = -1;
169 if (buffered_fd_ != -1 && ::close(buffered_fd_) != 0 && error == 0) error = errno;
170 buffered_fd_ = -1;
171 if (error != 0) fail("cannot write", error);
172 }
Here is the call graph for this function:

◆ operator=()

file_writer & cuddl::detail::file_writer::operator= ( file_writer const &  )
delete

◆ reserve()

std::span< char > cuddl::detail::file_writer::reserve ( size_t  bytes)
inline

Space for the next bytes of the file, for the caller to fill and then hand back with commit, sparing append's copy.

Waits while the queued bytes reach the limit; throws if an earlier write failed. One reservation at a time.

Definition at line 114 of file file_writer.hpp.

114 {
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_);
119 reserved_ = bytes;
120 reservation_ = std::move(lock);
121 return {current_->data.get() + carry_bytes_, bytes};
122 }
auto const total
Definition kernels.cuh:443
Here is the caller graph for this function:

Member Data Documentation

◆ block_bytes

constexpr size_t cuddl::detail::file_writer::block_bytes = size_t{8} << 20
staticconstexpr

Definition at line 51 of file file_writer.hpp.

51{8} << 20;

◆ depth

constexpr unsigned cuddl::detail::file_writer::depth = 4
staticconstexpr

Writes in flight with io_uring.

Definition at line 53 of file file_writer.hpp.

◆ probe_window

constexpr size_t cuddl::detail::file_writer::probe_window = size_t{64} << 20
staticconstexpr

Bytes timed per route before a writer that need not be durable picks one; it leaves the page cache only after 16 such windows through it ran too slow.

Definition at line 56 of file file_writer.hpp.

56{64} << 20;

The documentation for this class was generated from the following file: