Unravel Engine C++ Reference
Loading...
Searching...
No Matches
file_benchmarks.cpp
Go to the documentation of this file.
1#include "file_benchmarks.h"
2#include "file_istream.h"
3
5#include <logging/logging.h>
6
7#include <algorithm>
8#include <array>
9#include <chrono>
10#include <cstdio>
11#include <cstdint>
12#include <fstream>
13#include <string>
14#include <vector>
15
16#if UNRAVEL_PLATFORM_WINDOWS && (UNRAVEL_COMPILER_MSVC || UNRAVEL_COMPILER_CLANG)
17#define WIN32_LEAN_AND_MEAN
18#include <Windows.h>
19#elif UNRAVEL_PLATFORM_LINUX || UNRAVEL_PLATFORM_OSX
20#include <unistd.h>
21#endif
22
23namespace fs
24{
25namespace
26{
27
28constexpr size_t read_chunk_size = 64 * 1024;
29
30enum class read_backend : std::uint8_t
31{
32 file_stdio,
33 file_istream,
34 fstream,
35};
36
37enum class read_mode : std::uint8_t
38{
39 chunked,
40 whole_file,
41};
42
43struct backend_timing
44{
45 read_backend backend = read_backend::file_stdio;
46 double cold_seconds = 0.0;
47 double hot_seconds = 0.0;
48 std::uint64_t cold_checksum = 0;
49 std::uint64_t hot_checksum = 0;
50 size_t bytes_read = 0;
51};
52
53auto backend_name(read_backend backend) -> const char*
54{
55 switch(backend)
56 {
57 case read_backend::file_stdio:
58 return "FILE*";
59 case read_backend::file_istream:
60 return "file_istream";
61 case read_backend::fstream:
62 return "fstream";
63 }
64 return "unknown";
65}
66
67auto read_mode_name(read_mode mode) -> const char*
68{
69 switch(mode)
70 {
71 case read_mode::chunked:
72 return "chunked (64 KiB)";
73 case read_mode::whole_file:
74 return "whole file";
75 }
76 return "unknown";
77}
78
79auto add_chunk_checksum(std::uint64_t checksum, const std::uint8_t* data, size_t size) -> std::uint64_t
80{
81 for(size_t i = 0; i < size; ++i)
82 {
83 checksum += static_cast<std::uint64_t>(data[i]);
84 }
85 return checksum;
86}
87
88auto add_chunk_checksum(std::uint64_t checksum, const char* data, size_t size) -> std::uint64_t
89{
90 return add_chunk_checksum(checksum, reinterpret_cast<const std::uint8_t*>(data), size);
91}
92
93auto collect_regular_files(const path& directory) -> std::vector<path>
94{
95 std::vector<path> files;
96 std::error_code ec;
97 if(!fs::exists(directory, ec) || !fs::is_directory(directory, ec))
98 {
99 return files;
100 }
101 for(const auto& entry : fs::recursive_directory_iterator(directory, ec))
102 {
103 if(ec)
104 {
105 break;
106 }
107 if(entry.is_regular_file(ec))
108 {
109 files.push_back(entry.path());
110 }
111 }
112 std::sort(files.begin(), files.end());
113 return files;
114}
115
116auto file_byte_size(const path& file_path) -> size_t
117{
118 std::error_code ec;
119 const auto size = fs::file_size(file_path, ec);
120 if(ec || size == static_cast<std::uintmax_t>(-1))
121 {
122 return 0;
123 }
124 return static_cast<size_t>(size);
125}
126
127auto total_bytes_for_files(const std::vector<path>& files) -> size_t
128{
129 size_t total = 0;
130 for(const auto& file_path : files)
131 {
132 total += file_byte_size(file_path);
133 }
134 return total;
135}
136
137auto format_bytes_summary(size_t bytes) -> std::string
138{
139 constexpr double kb = 1024.0;
140 constexpr double mb = 1024.0 * 1024.0;
141 constexpr double gb = 1024.0 * 1024.0 * 1024.0;
142 const double val = static_cast<double>(bytes);
143 if(val >= gb)
144 {
145 return fmt::format("{:.2f} GiB", val / gb);
146 }
147 if(val >= mb)
148 {
149 return fmt::format("{:.1f} MiB", val / mb);
150 }
151 if(val >= kb)
152 {
153 return fmt::format("{:.1f} KiB", val / kb);
154 }
155 return fmt::format("{} B", bytes);
156}
157
158auto try_drop_os_page_cache() -> bool
159{
160#if UNRAVEL_PLATFORM_LINUX
161 sync();
162 FILE* drop_caches = std::fopen("/proc/sys/vm/drop_caches", "we");
163 if(drop_caches == nullptr)
164 {
165 return false;
166 }
167 const int wrote = std::fputs("3", drop_caches);
168 const int closed = std::fclose(drop_caches);
169 return wrote != EOF && closed == 0;
170#elif UNRAVEL_PLATFORM_WINDOWS && (UNRAVEL_COMPILER_MSVC || UNRAVEL_COMPILER_CLANG)
171 HANDLE token = nullptr;
172 if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token))
173 {
174 return false;
175 }
176 TOKEN_PRIVILEGES privileges{};
177 privileges.PrivilegeCount = 1;
178 if(!LookupPrivilegeValueA(nullptr, SE_INCREASE_QUOTA_NAME, &privileges.Privileges[0].Luid))
179 {
180 CloseHandle(token);
181 return false;
182 }
183 privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
184 AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr);
185 CloseHandle(token);
186 return SetSystemFileCacheSize(static_cast<SIZE_T>(-1), static_cast<SIZE_T>(-1), 0) != FALSE;
187#elif UNRAVEL_PLATFORM_OSX
188 sync();
189 return std::system("/usr/sbin/purge >/dev/null 2>&1") == 0;
190#else
191 return false;
192#endif
193}
194
195auto checksum_file_stdio_chunked(const path& file_path) -> std::uint64_t
196{
197 std::uint64_t checksum = 0;
198 FILE* file = std::fopen(file_path.string().c_str(), "rb");
199 if(file == nullptr)
200 {
201 return checksum;
202 }
203 std::array<char, read_chunk_size> buffer{};
204 size_t bytes_read = 0;
205 while((bytes_read = std::fread(buffer.data(), 1, buffer.size(), file)) > 0)
206 {
207 checksum = add_chunk_checksum(checksum, buffer.data(), bytes_read);
208 }
209 std::fclose(file);
210 return checksum;
211}
212
213auto checksum_file_stdio_whole(const path& file_path) -> std::uint64_t
214{
215 const size_t file_size = file_byte_size(file_path);
216 if(file_size == 0)
217 {
218 return 0;
219 }
220 FILE* file = std::fopen(file_path.string().c_str(), "rb");
221 if(file == nullptr)
222 {
223 return 0;
224 }
225 std::vector<char> buffer(file_size);
226 const size_t bytes_read = std::fread(buffer.data(), 1, file_size, file);
227 std::fclose(file);
228 if(bytes_read == 0)
229 {
230 return 0;
231 }
232 return add_chunk_checksum(0, buffer.data(), bytes_read);
233}
234
235auto checksum_file_istream_chunked(const path& file_path) -> std::uint64_t
236{
237 std::uint64_t checksum = 0;
238 file_istream stream(file_path, default_file_read_mode);
239 if(!stream.is_open())
240 {
241 return checksum;
242 }
243 std::array<char, read_chunk_size> buffer{};
244 while(stream)
245 {
246 stream.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
247 const auto bytes_read = static_cast<size_t>(stream.gcount());
248 if(bytes_read == 0)
249 {
250 break;
251 }
252 checksum = add_chunk_checksum(checksum, buffer.data(), bytes_read);
253 }
254 return checksum;
255}
256
257auto checksum_file_istream_whole(const path& file_path) -> std::uint64_t
258{
259 const size_t file_size = file_byte_size(file_path);
260 if(file_size == 0)
261 {
262 return 0;
263 }
264 file_istream stream(file_path, default_file_read_mode);
265 if(!stream.is_open())
266 {
267 return 0;
268 }
269 std::vector<char> buffer(file_size);
270 stream.read(buffer.data(), static_cast<std::streamsize>(file_size));
271 const auto bytes_read = static_cast<size_t>(stream.gcount());
272 if(bytes_read == 0)
273 {
274 return 0;
275 }
276 return add_chunk_checksum(0, buffer.data(), bytes_read);
277}
278
279auto checksum_file_fstream_chunked(const path& file_path) -> std::uint64_t
280{
281 std::uint64_t checksum = 0;
282 std::ifstream stream(file_path, std::ios::binary);
283 if(!stream)
284 {
285 return checksum;
286 }
287 std::array<char, read_chunk_size> buffer{};
288 while(stream)
289 {
290 stream.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
291 const auto bytes_read = static_cast<size_t>(stream.gcount());
292 if(bytes_read == 0)
293 {
294 break;
295 }
296 checksum = add_chunk_checksum(checksum, buffer.data(), bytes_read);
297 }
298 return checksum;
299}
300
301auto checksum_file_fstream_whole(const path& file_path) -> std::uint64_t
302{
303 const size_t file_size = file_byte_size(file_path);
304 if(file_size == 0)
305 {
306 return 0;
307 }
308 std::ifstream stream(file_path, std::ios::binary);
309 if(!stream)
310 {
311 return 0;
312 }
313 std::vector<char> buffer(file_size);
314 stream.read(buffer.data(), static_cast<std::streamsize>(file_size));
315 const auto bytes_read = static_cast<size_t>(stream.gcount());
316 if(bytes_read == 0)
317 {
318 return 0;
319 }
320 return add_chunk_checksum(0, buffer.data(), bytes_read);
321}
322
323auto checksum_file(read_backend backend, read_mode mode, const path& file_path) -> std::uint64_t
324{
325 switch(backend)
326 {
327 case read_backend::file_stdio:
328 return mode == read_mode::whole_file ? checksum_file_stdio_whole(file_path)
329 : checksum_file_stdio_chunked(file_path);
330 case read_backend::file_istream:
331 return mode == read_mode::whole_file ? checksum_file_istream_whole(file_path)
332 : checksum_file_istream_chunked(file_path);
333 case read_backend::fstream:
334 return mode == read_mode::whole_file ? checksum_file_fstream_whole(file_path)
335 : checksum_file_fstream_chunked(file_path);
336 }
337 return 0;
338}
339
340auto checksum_files(read_backend backend, read_mode mode, const std::vector<path>& files) -> std::uint64_t
341{
342 std::uint64_t checksum = 0;
343 for(const auto& file_path : files)
344 {
345 checksum += checksum_file(backend, mode, file_path);
346 }
347 return checksum;
348}
349
350auto run_backend_pass(read_backend backend,
351 read_mode mode,
352 const std::vector<path>& files,
353 bool drop_cache_first) -> std::pair<double, std::uint64_t>
354{
355 if(drop_cache_first)
356 {
357 const bool dropped = try_drop_os_page_cache();
358 if(!dropped)
359 {
360 APPLOG_WARNING("benchmark_directory_reads: page cache drop failed for {} / {} (cold timing may be warm)",
361 backend_name(backend),
362 read_mode_name(mode));
363 }
364 }
365 using clock_t = std::chrono::steady_clock;
366 const auto start = clock_t::now();
367 const std::uint64_t checksum = checksum_files(backend, mode, files);
368 const double seconds = std::chrono::duration<double>(clock_t::now() - start).count();
369 return {seconds, checksum};
370}
371
372auto run_all_backends(read_mode mode, const std::vector<path>& files) -> std::array<backend_timing, 3>
373{
374 std::array<backend_timing, 3> results{};
375 const std::array<read_backend, 3> backends = {
376 read_backend::file_stdio,
377 read_backend::file_istream,
378 read_backend::fstream,
379 };
380 const size_t total_bytes = total_bytes_for_files(files);
381 for(size_t i = 0; i < backends.size(); ++i)
382 {
383 results[i].backend = backends[i];
384 results[i].bytes_read = total_bytes;
385 const auto [cold_seconds, cold_checksum] = run_backend_pass(backends[i], mode, files, true);
386 results[i].cold_seconds = cold_seconds;
387 results[i].cold_checksum = cold_checksum;
388 }
389 for(size_t i = 0; i < backends.size(); ++i)
390 {
391 const auto [hot_seconds, hot_checksum] = run_backend_pass(backends[i], mode, files, false);
392 results[i].hot_seconds = hot_seconds;
393 results[i].hot_checksum = hot_checksum;
394 }
395 return results;
396}
397
398void log_benchmark_results(read_mode mode, const std::array<backend_timing, 3>& results)
399{
400 APPLOG_INFO("benchmark_directory_reads: --- {} ---", read_mode_name(mode));
401 APPLOG_INFO("benchmark_directory_reads: backend cold(s) hot(s) hot speedup checksum ok");
402 for(const backend_timing& result : results)
403 {
404 const double speedup =
405 result.hot_seconds > 0.0 ? result.cold_seconds / result.hot_seconds : 0.0;
406 const bool checksum_ok = result.cold_checksum == result.hot_checksum;
407 APPLOG_INFO("benchmark_directory_reads: {:16} {:8.3f} {:8.3f} {:8.2f}x {}",
408 backend_name(result.backend),
409 result.cold_seconds,
410 result.hot_seconds,
411 speedup,
412 checksum_ok ? "yes" : "no");
413 }
414}
415
416} // namespace
417
418void benchmark_directory_reads(const path& directory)
419{
420 const auto files = collect_regular_files(directory);
421 if(files.empty())
422 {
423 APPLOG_WARNING("benchmark_directory_reads: no regular files under '{}'", directory.string());
424 return;
425 }
426 const size_t total_bytes = total_bytes_for_files(files);
427 APPLOG_INFO("benchmark_directory_reads: directory='{}', files={}, total={}",
428 directory.string(),
429 files.size(),
430 format_bytes_summary(total_bytes));
431 log_benchmark_results(read_mode::chunked, run_all_backends(read_mode::chunked, files));
432 log_benchmark_results(read_mode::whole_file, run_all_backends(read_mode::whole_file, files));
433}
434
435} // namespace fs
std::uint64_t bytes
Definition eviction.cpp:823
#define APPLOG_WARNING(...)
Definition logging.h:19
#define APPLOG_INFO(...)
Definition logging.h:18
Definition cache.hpp:11
constexpr std::ios_base::openmode default_file_read_mode
void benchmark_directory_reads(const path &directory)
std::chrono::steady_clock clock_t
Alias for the steady clock used for timing actions.
Definition seq_common.h:23
std::vector< math::vec3 > start
std::string token
float size