Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stdout / stderr performance to FAQ #274

Closed
joshka opened this issue Dec 13, 2023 · 8 comments · Fixed by #365
Closed

Add stdout / stderr performance to FAQ #274

joshka opened this issue Dec 13, 2023 · 8 comments · Fixed by #365

Comments

@joshka
Copy link
Member

joshka commented Dec 13, 2023

https://ratatui.rs/faq/#should-i-use-stdout-or-stderr

See ratatui-org/ratatui#583 (comment)

Valentin271 Dec 13, 2023

Oh that's interesting, stdout is also faster on my machine

from ~1.60 to ~2 fps in fullscreen
from ~15 to ~90 fps in 80x24!

If this is consistent, we might want to keep that in mind. We currently use stderr in the template. And we should mention that in the FAQ

@kdheepak
Copy link
Contributor

I found the same thing happening in a game of life tui, i.e. stdout was ~2.5x faster that stderr. Relevant discussion here with some profiling screenshots: ratatui-org/ratatui#579 (reply in thread)

@Valentin271
Copy link
Member

That's super interesting! And worth officially mentioning somewhere!

I wonder if other TUI libs have seen similar behaviors (mainly bubbletea and ncurses)

@orhun
Copy link
Sponsor Member

orhun commented Dec 14, 2023

I didn't go through all the discussion but do we know why this is happening? Some underlying crossterm implementation difference?

@kdheepak
Copy link
Contributor

kdheepak commented Dec 14, 2023

On MacOS I was finding this was in the kernel.

stderr:

image

stdout:

image

With a profiler, I was seeing 6.6k hits to a call to a kernel write function with stderr but only 2.5k hits when calling stdout, where more hits means more time spent during that call.

@kdheepak
Copy link
Contributor

I believe @orhun is planning submit a PR to ratatui that will make stderr as fast as stdout.

@orhun
Copy link
Sponsor Member

orhun commented Dec 28, 2023

Yes, the trick is to make the internal buffer of the backend buffered via LineWriter or BufWriter:

Diff
diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs
index b35c3af..0162d4e 100644
--- a/src/backend/crossterm.rs
+++ b/src/backend/crossterm.rs
@@ -2,7 +2,7 @@
 //! the [Crossterm] crate to interact with the terminal.
 //!
 //! [Crossterm]: https://crates.io/crates/crossterm
-use std::io::{self, Write};
+use std::io::{self, LineWriter, Write};
 
 #[cfg(feature = "underline-color")]
 use crossterm::style::SetUnderlineColor;
@@ -78,10 +78,10 @@ use crate::{
 /// [`backend`]: crate::backend
 /// [Crossterm]: https://crates.io/crates/crossterm
 /// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#examples
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+#[derive(Debug)]
 pub struct CrosstermBackend<W: Write> {
     /// The writer used to send commands to the terminal.
-    writer: W,
+    writer: LineWriter<W>,
 }
 
 impl<W> CrosstermBackend<W>
@@ -98,7 +98,9 @@ where
     /// let backend = CrosstermBackend::new(stdout());
     /// ```
     pub fn new(writer: W) -> CrosstermBackend<W> {
-        CrosstermBackend { writer }
+        CrosstermBackend {
+            writer: LineWriter::new(writer),
+        }
     }
 }

But the downside is, both LineWriter and BufWriter do not implement any of the Default, Clone, Eq, PartialEq, Hash traits. None of these are used in the codebase though so we can remove them as a breaking change.

Another option is to implement a custom buffered writer:

Diff
diff --git a/src/backend/crossterm.rs b/src/backend/crossterm.rs
index b35c3af..849fb8a 100644
--- a/src/backend/crossterm.rs
+++ b/src/backend/crossterm.rs
@@ -81,7 +81,7 @@ use crate::{
 #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
 pub struct CrosstermBackend<W: Write> {
     /// The writer used to send commands to the terminal.
-    writer: W,
+    writer: CustomWriter<W>,
 }
 
 impl<W> CrosstermBackend<W>
@@ -98,17 +98,37 @@ where
     /// let backend = CrosstermBackend::new(stdout());
     /// ```
     pub fn new(writer: W) -> CrosstermBackend<W> {
-        CrosstermBackend { writer }
+        CrosstermBackend {
+            writer: CustomWriter {
+                writer,
+                buffer: Vec::new(),
+            },
+        }
     }
 }
 
-impl<W> Write for CrosstermBackend<W>
+const BUFFER_SIZE: usize = 8096;
+
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+struct CustomWriter<W: Write> {
+    pub writer: W,
+    pub buffer: Vec<u8>,
+}
+
+impl<W> Write for CustomWriter<W>
 where
     W: Write,
 {
     /// Writes a buffer of bytes to the underlying buffer.
     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        self.writer.write(buf)
+        self.buffer.extend(buf);
+        if self.buffer.len() >= BUFFER_SIZE {
+            let n = self.writer.write(buf)?;
+            self.writer.flush()?;
+            Ok(n)
+        } else {
+            Ok(buf.len())
+        }
     }
 
     /// Flushes the underlying buffer.

But this is suboptimal since additional maintenance burden + needs more optimizations.

@joshka
Copy link
Member Author

joshka commented Dec 29, 2023

I suspect that it might be better to let the user handle this by wrapping their writer outside of Ratatui.

@orhun
Copy link
Sponsor Member

orhun commented Dec 29, 2023

Yes, I think the best thing to do here is to update the documentation about the difference between stdout and stderr & mention how to achieve the same performance.

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 a pull request may close this issue.

4 participants