Mercurial > public > mercurial-scm > hg-stable
changeset 52740:65839176cea9
rhg: buffer the output of `rhg status`
Before this commit, `hg status` was issuing multiple `write` syscalls per
line printed, separately writing out the path and the status fragments.
This change makes hg status on large number of files significantly faster,
going from 1.8s to 1.2s in one case.
This requires adding the color information to `StdoutBuffer`,
and moving the formatting functions from ui to it.
I made `StdoutBuffer` generic over the underlying writer,
without insisting on BufWriter, because I anticipated the need to use
it with both full-buffered and line-buffered writers.
That didn't end up being necessary, but I think the code is still better
this way.
author | Arseniy Alekseyev <aalekseyev@janestreet.com> |
---|---|
date | Fri, 31 Jan 2025 15:04:13 +0000 |
parents | 0f2268783c11 |
children | 48572371d478 |
files | rust/rhg/src/commands/status.rs rust/rhg/src/ui.rs |
diffstat | 2 files changed, 39 insertions(+), 37 deletions(-) [+] |
line wrap: on
line diff
--- a/rust/rhg/src/commands/status.rs Thu Jan 30 09:23:16 2025 +0100 +++ b/rust/rhg/src/commands/status.rs Fri Jan 31 15:04:13 2025 +0000 @@ -687,8 +687,7 @@ mut paths: Vec<StatusPath<'_>>, ) -> Result<(), CommandError> { paths.sort_unstable(); - // TODO: get the stdout lock once for the whole loop - // instead of in each write + let mut stdout = self.ui.stdout_buffer(); for StatusPath { path, copy_source } in paths { let relative_path; let relative_source; @@ -702,25 +701,27 @@ } else { (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes())) }; - // TODO: Add a way to use `write_bytes!` instead of `format_bytes!` - // in order to stream to stdout instead of allocating an - // itermediate `Vec<u8>`. + // TODO: Add a way to use `write_bytes!` instead of + // `format_bytes!` in order to stream to stdout + // instead of allocating an itermediate + // `Vec<u8>`. if !self.no_status { - self.ui.write_stdout_labelled(status_prefix, label)? + stdout.write_stdout_labelled(status_prefix, label)? } let linebreak = if self.print0 { b"\x00" } else { b"\n" }; - self.ui.write_stdout_labelled( + stdout.write_stdout_labelled( &format_bytes!(b"{}{}", path, linebreak), label, )?; if let Some(source) = copy_source { let label = "status.copied"; - self.ui.write_stdout_labelled( + stdout.write_stdout_labelled( &format_bytes!(b" {}{}", source, linebreak), label, )? } } + stdout.flush()?; Ok(()) }
--- a/rust/rhg/src/ui.rs Thu Jan 30 09:23:16 2025 +0100 +++ b/rust/rhg/src/ui.rs Fri Jan 31 15:04:13 2025 +0000 @@ -12,7 +12,9 @@ use hg::utils::files::get_bytes_from_path; use std::borrow::Cow; use std::io; +use std::io::BufWriter; use std::io::IsTerminal; +use std::io::StdoutLock; use std::io::{ErrorKind, Write}; pub struct Ui { @@ -56,8 +58,11 @@ /// Returns a buffered handle on stdout for faster batch printing /// operations. - pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> { - StdoutBuffer::new(self.stdout.lock()) + pub fn stdout_buffer(&self) -> StdoutBuffer<'_, BufWriter<StdoutLock>> { + StdoutBuffer { + stdout: BufWriter::new(self.stdout.lock()), + colors: &self.colors, + } } /// Write bytes to stdout @@ -77,13 +82,21 @@ stderr.flush().or_else(handle_stderr_error) } +} +/// A buffered stdout writer for faster batch printing operations. +pub struct StdoutBuffer<'a, W> { + colors: &'a Option<ColorConfig>, + stdout: W, +} + +impl<'a, W: Write> StdoutBuffer<'a, W> { /// Write bytes to stdout with the given label /// /// Like the optional `label` parameter in `mercurial/ui.py`, /// this label influences the color used for this output. pub fn write_stdout_labelled( - &self, + &mut self, bytes: &[u8], label: &str, ) -> Result<(), UiError> { @@ -96,15 +109,15 @@ } } } - self.write_stdout(bytes) + self.write_all(bytes) } fn write_stdout_with_effects( - &self, + &mut self, bytes: &[u8], effects: &[Effect], ) -> io::Result<()> { - let stdout = &mut self.stdout.lock(); + let stdout = &mut self.stdout; let mut write_line = |line: &[u8], first: bool| { // `line` does not include the newline delimiter if !first { @@ -130,7 +143,17 @@ write_line(line, false)? } } - stdout.flush() + Ok(()) + } + + /// Write bytes to stdout buffer + pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> { + self.stdout.write_all(bytes).or_else(handle_stdout_error) + } + + /// Flush bytes to stdout + pub fn flush(&mut self) -> Result<(), UiError> { + self.stdout.flush().or_else(handle_stdout_error) } } @@ -144,28 +167,6 @@ } } -/// A buffered stdout writer for faster batch printing operations. -pub struct StdoutBuffer<W: Write> { - buf: io::BufWriter<W>, -} - -impl<W: Write> StdoutBuffer<W> { - pub fn new(writer: W) -> Self { - let buf = io::BufWriter::new(writer); - Self { buf } - } - - /// Write bytes to stdout buffer - pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> { - self.buf.write_all(bytes).or_else(handle_stdout_error) - } - - /// Flush bytes to stdout - pub fn flush(&mut self) -> Result<(), UiError> { - self.buf.flush().or_else(handle_stdout_error) - } -} - /// Sometimes writing to stdout is not possible, try writing to stderr to /// signal that failure, otherwise just bail. fn handle_stdout_error(error: io::Error) -> Result<(), UiError> {