Skip to content

fix: flush stdout writes in ConsoleLogger::writeDirect - #183

Closed
tx3stn wants to merge 1 commit into
Snapchat:mainfrom
tx3stn:cli-logging
Closed

fix: flush stdout writes in ConsoleLogger::writeDirect#183
tx3stn wants to merge 1 commit into
Snapchat:mainfrom
tx3stn:cli-logging

Conversation

@tx3stn

@tx3stn tx3stn commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Description

Flush stdout writes in ConsoleLogger::writeDirect

writeDirect never flushes. It backs process.stdout.write in CLI apps, and std::cout is fully buffered on anything that isn't a TTY, so piped output arrives in 4 KB bursts and a CLI killed by a signal loses it entirely. SIGTERM and SIGINT aren't handled, so a long-running CLI never flushes at all.

log() already flushes via std::endl so it is reliable but the raw one isn't. log also prefixes lines with text like:

[0.011: INFO] [JS]

which is noise for an end user. Calling process.stdout.write from my logger lets me skip that and keep the formatted lines I actually want to show.

My usecase: a long-running daemon built as a valdi_cli_application, logging through process.stdout.write.
Every short-lived command that just does something and exits worked.
The daemon printed nothing. Same binary, output redirected to a file, killed after five seconds: zero bytes, no log lines. Re-running the identical command on a pty and reading while the process was still alive produced all of it. Terminal fine, redirected silent.

That is line buffering on a TTY against full buffering on a file, with no flush and no normal exit to trigger one. Under systemd it means journalctl stays empty until 4 KB accumulates, and systemctl stop discards whatever is pending.

Example:

import { beginKeepAlive } from 'valdi_core/src/utils/KeepAliveCallback';

declare const process: { stdout: { write(text: string): boolean } };

process.stdout.write('this line never arrives\n');
beginKeepAlive();

steps:

cli_example_app             # on a terminal it prints immediately

cli_example_app > out.txt &
pid=$!
sleep 2
cat out.txt                 # empty, though the write call has returned
kill $pid
cat out.txt                 # still empty, the buffered bytes died with the process

The keep-alive is what makes it reproducible. A CLI that exits on its own flushes stdio on the way out, so every existing example prints fine even when redirected. It needs two things: stdout that isn't a TTY, and a process that doesn't exit normally.

Limitations:

  • One write per call instead of one per 4 KB. A CLI emitting large output a line at a time now pays a syscall per line.
  • A flush on a pipe blocks once the pipe buffer is full, which happens sooner than waiting for a 4 KB buffer to fill. That matters for a process logging from a latency-sensitive thread to a slow reader.

Working around them:

// one syscall per line
for (const line of lines) process.stdout.write(line + '\n');

// one syscall
process.stdout.write(lines.join('\n') + '\n');

Batching in JS trades libc's invisible 4 KB buffer for one the caller sizes and flushes on its own schedule. Every process.stdout.write crosses the JS to native bridge, which costs more than the write it triggers, so anyone doing per-line writes for bulk output already has a performance problem room for improvement. Batching makes them faster than they were before this change.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Documentation improvement
  • Performance optimization
  • Test improvement
  • Other (please describe)

Testing

  • Tests pass locally (bazel test //...)
  • Added/updated tests for changes (if applicable)
  • Tested on multiple platforms (iOS/Android/Web/macOS as applicable)
  • Manual testing performed (describe below)

Testing Details

Checklist

  • Code follows project style guidelines
  • Documentation updated (if needed)
  • No breaking changes (or documented in description)
  • Commit messages follow conventional format
  • No secrets, API keys, or internal URLs included

Related Issues

Additional Context

@github-actions

Copy link
Copy Markdown

🎉 Bazel & CI Test Results

Test Suite Result
macOS: C++ & Platform Tests ✅ success
API Surface Check ✅ success
valdi_web Integration Test ✅ success
Linux: Build Compiler ✅ success
Linux: C++ Tests ✅ success
Test Coverage Delta ✅ success
Snapshot Tests ✅ success
Valdi Smoke Tests ✅ success
Linux: Hotreload Smoke ✅ success
Linux: Registry Validation ✅ success
Linux: Build & Export ✅ success
Linux: Module Tests ✅ success

All Bazel configuration and CI tests passed!

The build system and core tooling are working correctly.

🚀 Bazel remote cache is now enabled - future builds will be faster!

Workflow: Valdi CI

@beaucollins

Copy link
Copy Markdown
Collaborator

Thank you for the contribution, we're accepting and pulling this through our internal system.

@clholgat clholgat closed this in 1c8a722 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants