Files
matschi95andClaude Opus 5.5 0d2510d6fa Logs: no more stutter from log writes (OLED flicker, 60 → 50 FPS every few seconds) (#45)
* Logs: buffered and written out by a background thread

On the console a write to a file under /data takes ~25 ms or more, and
stderr was unbuffered: the GL driver's batch summary line, printed every
10000 draws, held up present for ~300 ms, which an OLED shows as
flicker. The log pipe meant to avoid that never worked there
(fcntl(F_DUPFD): EINVAL, dup2: EPERM), so both streams wrote directly.

Both streams are now fully buffered and a background thread writes them
out once a second; the frame statistics and Report no longer flush.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Settings > Diagnostics: write logs at once, to find a crash

Logs are buffered and written out once a second, so the last lines
before a fault that kills the process can be missing. "Write logs at
once" has every line written as it comes, as before (stderr unbuffered,
stdout by lines), at the cost of stutter; from the next start. Texts in
every catalog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 17:33:32 -04:00

71 lines
2.7 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Log streams without storage stalls. On the console a small write to a file under /data takes
// ~25 ms or more, and the GL driver's batch summary line on the unbuffered stderr held up present
// for ~300 ms every 10000 draws. The streams' descriptors cannot be pointed at a pipe there
// (fcntl(F_DUPFD): EINVAL, dup2: EPERM), so both streams are fully buffered and a background
// thread writes them out once a second. A thread that prints only waits for the storage when it
// prints while that write is going on, or when a second's lines overfill the buffer. Lines still
// buffered when the process is killed are lost (a second's worth); the crash handler writes them
// out (crash_report.cpp). To find a fault that kills the process anyway, Settings > Diagnostics >
// "Write logs at once" has every line written as it comes, as before: stderr unbuffered, stdout
// by lines, and games stutter again.
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <mutex>
#include <thread>
namespace Eden {
class LogFlusher {
public:
LogFlusher() = default;
LogFlusher(const LogFlusher&) = delete;
LogFlusher& operator=(const LogFlusher&) = delete;
~LogFlusher() { Stop(); }
// The streams must already write to their log files. At once: every line is written as it
// comes, with no thread.
bool Start(bool at_once = false) {
static char stderr_buffer[64 * 1024], stdout_buffer[64 * 1024];
if (worker.joinable()) return true;
if (at_once)
return std::setvbuf(stderr, nullptr, _IONBF, 0) == 0 &&
(std::setvbuf(stdout, stdout_buffer, _IOLBF, sizeof(stdout_buffer)) == 0 ||
std::setvbuf(stdout, nullptr, _IONBF, 0) == 0);
if (std::setvbuf(stderr, stderr_buffer, _IOFBF, sizeof(stderr_buffer)) != 0 ||
std::setvbuf(stdout, stdout_buffer, _IOFBF, sizeof(stdout_buffer)) != 0) return false;
worker = std::thread([this] { Run(); });
return true;
}
// Writes out what is buffered; the streams stay buffered.
void Stop() {
if (!worker.joinable()) return;
{
std::lock_guard lock{mutex};
stopping = true;
}
wake.notify_all();
worker.join();
}
private:
void Run() {
for (bool stop = false; !stop;) {
{
std::unique_lock lock{mutex};
stop = wake.wait_for(lock, std::chrono::seconds(1), [this] { return stopping; });
}
std::fflush(stderr);
std::fflush(stdout);
}
}
std::mutex mutex;
std::condition_variable wake;
bool stopping = false;
std::thread worker;
};
}