Багато розробників помічали, що під час роботи з терміналом вивід у stdout відбувається помітно швидше, ніж у stderr. У першій частині статті ми розглянемо, чому виникає така різниця, як влаштовані стандартні потоки в UNIX-системах та яку роль відіграє буферизація. На прикладі застосунків на Rust автор демонструє, що використання stderr може знизити FPS майже вдвічі. Профілювання за допомогою інструмента samply підтверджує: stdout робить значно менше системних викликів завдяки рядковій буферизації, тоді як stderr працює без неї. Саме ця відмінність пояснює, чому інтерфейси TUI краще реагують при виводі через stdout.
Нещодавно я зрозумів, що stdout набагато швидший за stderr для Rust. Ось мої висновки після глибокого занурення в цю кролячу нору.
Я вже деякий час використовую термінал (тобто командний рядок) для більшості своїх повсякденних справ. Мене завжди захоплювало те, наскільки швидким і зручним може бути командний рядок, і саме тому я прихильник використання програм CLI (командного рядка) або TUI (інтерфейс користувача термінала) замість програм GUI (графічний інтерфейс користувача), коли це можливо. Окрім моїх уже існуючих уподобань, я почав щиро вірити, щотермінал – це майбутнєпісля того, як я побачив нещодавні розробки у сфері взаємодії з терміналами за допомогою таких інструментів, як Zellij, та емуляторів терміналів на базі графічних процесорів, таких як Alacritty/ Wezterm/ Rio. Коли цей величезний потенціал поєднується з потужною мовою системного програмування, такою як Rust, результатом часто є дуже плавна робота з терміналом та розробка, яку, на мою думку, кожен розробник цінує, коли йдеться про ефективність, швидкість та безпеку.
Найімовірніше, саме тому мене взагалі зацікавило створення термінальних додатків для користувача інтерфейсу на Rust. Коли я створив свій перший проєкт на Rust/TUI, kmon, я був здивований тим, як межі такої простої речі, як термінал, можна розширити для створення додатків, що ще більше звикає до використання терміналів. Кілька років потому я став одним із розробників Ratatui, бібліотеки Rust для створення TUI, і мені пощастило бути одним із основних членів команди, яка минулого року відродила непідтримувану бібліотеку tui-rs під назвою Ratatui.
Якщо врахувати все це, то як щоденний користувач терміналу та розробник командного рядка, я щодня стикаюся з новими проблемами, пов’язаними з терміналом. Іноді я стикаюся з справді цікавими питаннями та проблемами. Як і слід було очікувати, ця публікація в блозі є результатом одного з цих питань.
Добре, тепер давайте зробимо крок назад і спробуємо спочатку зрозуміти питання. Нам потрібно зрозуміти деякі концепції UNIX, перш ніж розпочнемо все.
Операційна система UNIX принесла багато новаторських досягнень у світ комп’ютерів, і одним з них, безсумнівно, були стандартні потоки. Згідно з UNIX, кожен процес має три відкриті потоки під час запуску:
0. Стандартний ввід (
stdin): для читання вхідних даних.
1. Стандартний вивід (
stdout): для запису звичайного виводу.
2. Стандартний вивід помилок (
stderr): для друку діагностичних повідомлень або повідомлень про помилки.
Ось спрощений приклад для демонстрації цих потоків:
# read the value of foo from stdin $ read -r foo test # print the value of foo to stdout $ echo "value of foo is '$foo'" value of foo is 'test' # "echoo" command does not exist so an error message will be printed to stderr $ echoo "$foo" bash: echoo: command not found
Ці потоки вводу/виводу (введення/виведення) зазвичай підключаються до терміналу користувача через tty (TeleTYpe), який можна описати як інтерфейс, що забезпечує доступ до терміналу.
Як ви, можливо, чули неодноразово, згідно з філософією UNIX, «все є файлом». Це означає, що потоки вводу/виводу також повинні бути файлом, і це насправді правда:
$ file /dev/stdin /dev/stdout /dev/stderr /dev/stdin: symbolic link to /proc/self/fd/0 /dev/stdout: symbolic link to /proc/self/fd/1 /dev/stderr: symbolic link to /proc/self/fd/2
Якщо ви зрозуміли, файловий дескриптор (унікальний ідентифікатор) цих абстрактних файлів такий самий, як і у початковому списку, наведеному вище (починається з 0).
Тож якщо це файли, ми повинні мати змогу їх прочитати, чи не так?Власне, ні.
*швидко друкує*
$ cat /dev/stdout
Чому нічого не відбувається?
Це тому, що це не справжні файли, а лише файлові дескриптори, пов’язані з TTY або PTY (тобто емульований TTY, також відомий як PseudoTeletType).
$ ls -l /proc/self/fd/ lrwx------ - orhun 0 -> /dev/pts/20 lrwx------ - orhun 1 -> /dev/pts/20 lrwx------ - orhun 2 -> /dev/pts/20
Як бачите, стандартні потоки приєднані до PTY (тобто псевдотерміналів) у розділі /dev/pts.
Зачекайте, тож /dev/stdoutсимволічне посилання на файловий дескриптор за адресою /proc/self/fd/1, а цей файловий дескриптор є символічним посиланням на /dev/pts/20!?
Що ж таке взагалі /dev/pts/20в цьому випадку?
$ file /dev/pts/20 /dev/pts/20: character special (136/20)
У Unix спеціальні символьні файли – це файли, що забезпечують доступ до пристроїв вводу/виводу, таких як NULL-файл ( /dev/null) та файлові дескриптори. У нашому випадку stdout– це файловий дескриптор (1), тому це спеціальний символьний файл. (Назва «спеціальний символ» насправді походить від того, що кожен символ обробляється окремо.)
Кожен спеціальний файл символів має головний номер пристрою, який визначає тип пристрою, та додатковий номер пристрою, який ідентифікує конкретний пристрій заданого типу. Отже, те, що ви бачите праворуч (136/20), означає:
$ rg '136' /proc/devices 136 pts
Майор 136: пристрій PTS (
/dev/pts)
Мінор 20: пристрій 20 (
/dev/pts/20)
Гаразд, круто, але чому я не можу читати з
/dev/stdout?
О, так. Коли ви намагаєтеся прочитати дані зі stdout, він продовжує виконуватися, бо очікує на зчитування даних з файлового дескриптора. Тож, якщо ви введете його, ви зможете прочитати його назад:
$ cat /dev/stdout foo # input foo bar #input bar
Тепер, коли ми маємо загальне розуміння потоків вводу/виводу, ми можемо перейти до реальних прикладів і просунутися до відповіді на наше запитання.
У наступному розділі використовується мова програмування Rust, але загальні концепції, як правило, застосовні до будь-якої мови програмування, навіть до HolyC.
Інтерфейси користувача терміналу використовують термінал, малюючи на ньому віджети/компоненти, такі як текстові поля, спінери та стилізований текст, подібно до традиційних графічних інтерфейсів користувача. Термінал здатний відображати такі елементи завдяки налаштовуваній обробці ANSI-кодів керування. Ці ANSI- послідовності використовуються для керування розташуванням курсора, кольором та стилем терміналу.
Отже, для створення інтерфейсу користувача терміналу нам потрібна низькорівнева бібліотека для керування як терміналом, так і потоками вводу/виводу, а також для рендерингу компонентів інтерфейсу. Зазвичай цей двоетапний процес розділяється між різними бібліотеками для зручності використання та дотримання принципу єдиної відповідальності.
Наприклад, у той час як ncurses (одна з найстаріших бібліотек TUI, написаних на C) займається низькорівневим інтерфейсом до терміналу, CDK (набір розробки curses) надає набір віджетів для створення графічно-подібних програм у терміналі.
Аналогічно, в екосистемі Rust, на сьогодні для цього завдання найбільше користуються такі бібліотеки:
crossterm: бібліотека керування терміналами на рівні чистого Rust, що працює на різних платформах.
ratatui: легка бібліотека, яка надає набір віджетів та утиліт — підтримує різні серверні частини, включаючи
crossterm.
Отже, давайте створимо дуже простий TUI, використовуючи ці бібліотеки:
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! crossterm = "0.27.0"
//! ratatui = "0.25.0"
//! ```
use std::io::{self, stdout};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
/// Handle key events.
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
// Quit when 'q' is pressed.
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
/// Render the widgets.
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("__QQ\n(_)_\">")
.alignment(Alignment::Center)
.block(
Block::default()
.title("blog.orhun.dev")
.borders(Borders::ALL),
),
frame.size(),
);
}
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
Ви можете запустити це за допомогою rust-скрипта наступним чином:
$ cargo install rust-script # [...] $ chmod +x simple-tui.rs $ ./simple-tui.rs
Залежно від розміру вашого терміналу, ви отримаєте результат, подібний до цього:
┌blog.orhun.dev──────────────────────────┐ │ __QQ │ │ (_)_"> │ │ │ └────────────────────────────────────────┘
Це круто! Як це можливо?
Давайте розберемо код на кілька кроків, щоб зрозуміти, що відбувається:
1. Ініціалізація терміналу
У mainфункції ми використовуємо crossterm, щоб налаштувати наш термінал на stdout та вказати ratatuiвикористовувати crosstermбекенд.
use std::io::stdout;
use ratatui::{Terminal, backend::CrosstermBackend};
crossterm::terminal::enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Режим RAW та альтернативний екран?
Увімкнення режиму “сирого” означає, що ми не використовуватимемо стандартну поведінку терміналу (без обробки вводу та спеціальних клавіш) і хочемо мати повний контроль над ним. Ми також переходимо на альтернативний екран (тобто новий буфер) у терміналі, оскільки не хочемо втрачати наш фактичний термінал/командний рядок і повертатися до нього після натискання клавіші “q”.
Говорячи про обробку вхідних даних, ось як ми це робимо:
2. Обробка ключових подій
Ми просто опитуємо події з crosstermфункції handle_eventsта завершуємо роботу, коли натискаємо ‘q’:
use crossterm::event::{self, Event, KeyCode};
if crossterm::event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
3. Рендеринг віджетів
І нарешті, ось де ratatuiсяє:
use ratatui::{prelude::*, widgets::*};
frame.render_widget(
Paragraph::new("__QQ\n(_)_\">")
.alignment(Alignment::Center)
.block(
Block::default()
.title("blog.orhun.dev")
.borders(Borders::ALL),
),
frame.size(),
);
Тут ми створюємо два віджети:
ratatuiнадає багато інших віджетів і робить напрочуд простим створення складних інтерфейсів. Також, як видно з прикладу, вирівняти текст div по центру насправді легко .
Повертаючись до нашої початкової теми, якщо ще раз подивитися на mainфункцію, все це відбувається на stdout :
use std::io::stdout; let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Зрозуміло, що ми можемо спробувати змінити всі посилання на std::io::stdoutto std::io::stderrта спробувати запустити ще раз.
-let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; +let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
…що тоді відбувається? Ну, візуально нічого не змінюється, і ми не можемо помітити різниці. Результат той самий. Чи це так?
Нам потрібен спосіб вимірювання продуктивності візуалізації TUI та побачити різницю між використанням stdout та stderr. Для цього я розробив програму лічильника FPS (кадрів за секунду) разом із деякими монохромними кольорами для візуалізації (на основі шаблону colors_rgbratatui ) .
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! anyhow = "1.0.76"
//! crossterm = "0.27.0"
//! palette = "0.7.3"
//! rand = "0.8.5"
//! ratatui = "0.25.0"
//! ```
//!
use anyhow::Result;
use std::{
fmt,
io::{stderr, stdout, Write},
time::{Duration, Instant},
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use palette::{convert::FromColorUnclamped, Hsv, Srgb};
use ratatui::{prelude::*, widgets::*};
#[derive(Copy, Clone, Debug, Default)]
enum IoStream {
#[default]
Stdout,
Stderr,
}
impl fmt::Display for IoStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}
impl IoStream {
fn as_stream(&self) -> Box<dyn Write> {
match self {
IoStream::Stdout => Box::new(stdout()),
IoStream::Stderr => Box::new(stderr()),
}
}
}
#[derive(Debug)]
struct Fps {
frame_count: usize,
last_instant: Instant,
fps: Option<f32>,
}
impl Default for Fps {
fn default() -> Self {
Self {
frame_count: 0,
last_instant: Instant::now(),
fps: None,
}
}
}
impl Fps {
fn tick(&mut self) {
self.frame_count += 1;
let elapsed = self.last_instant.elapsed();
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
// noise in the fps calculation)
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
self.frame_count = 0;
self.last_instant = Instant::now();
}
}
}
struct FpsWidget<'a> {
fps: &'a Fps,
}
impl<'a> Widget for FpsWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(fps) = self.fps.fps {
let text = format!("{:.1} fps", fps);
Paragraph::new(text).render(area, buf);
}
}
}
struct RgbColorsWidget<'a> {
/// the colors to render - should be double the height of the area
colors: &'a Vec<Vec<Color>>,
/// the number of elapsed frames that have passed - used to animate the colors
frame_count: usize,
}
impl Widget for RgbColorsWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = self.colors;
for (xi, x) in (area.left()..area.right()).enumerate() {
// animate the colors by shifting the x index by the frame number
let xi = (xi + self.frame_count) % (area.width as usize);
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
struct AppWidget<'a> {
title: Paragraph<'a>,
fps_widget: FpsWidget<'a>,
rgb_colors_widget: RgbColorsWidget<'a>,
}
impl<'a> AppWidget<'a> {
fn new(app: &'a App) -> Self {
let title = Paragraph::new(vec![Line::styled(
app.current_stream.to_string(),
Style::new().bold(),
)])
.alignment(Alignment::Center);
Self {
title,
fps_widget: FpsWidget { fps: &app.fps },
rgb_colors_widget: RgbColorsWidget {
colors: &app.colors,
frame_count: app.frame_count,
},
}
}
}
impl Widget for AppWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(8)])
.split(main_layout[0]);
self.title.render(title_layout[0], buf);
self.fps_widget.render(title_layout[1], buf);
self.rgb_colors_widget.render(main_layout[1], buf);
}
}
#[derive(Debug, Default)]
struct App {
should_quit: bool,
switch_stream: bool,
current_stream: IoStream,
// a 2D vector of the colors to render
// calculated when the size changes as this is expensive to calculate every frame
colors: Vec<Vec<Color>>,
last_size: Rect,
fps: Fps,
frame_count: usize,
}
impl App {
pub fn run(io_stream: IoStream) -> Result<bool> {
let mut terminal = init_terminal(io_stream.as_stream())?;
let mut app = App {
current_stream: io_stream,
..Default::default()
};
while !app.should_quit && !app.switch_stream {
app.tick();
terminal.draw(|frame| {
let size = frame.size();
app.setup_colors(size);
frame.render_widget(AppWidget::new(&app), size);
})?;
app.handle_events()?;
}
restore_terminal(io_stream.as_stream())?;
Ok(app.should_quit)
}
fn tick(&mut self) {
self.frame_count += 1;
self.fps.tick();
}
fn handle_events(&mut self) -> Result<()> {
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_quit = true;
}
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char(' ') {
self.switch_stream = true;
};
}
}
Ok(())
}
fn setup_colors(&mut self, size: Rect) {
// only update the colors if the size has changed since the last time we rendered
if self.last_size.width == size.width && self.last_size.height == size.height {
return;
}
self.last_size = size;
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height * 2;
self.colors.clear();
use rand::Rng;
let mut rng = rand::thread_rng();
for y in 0..height {
let mut row = Vec::new();
// more randomness towards the bottom
let randomness_factor = (height - y) as f32 / height as f32;
for _ in 0..width {
let base_value = randomness_factor * ((height - y) as f32 / height as f32);
// adjust the range as needed
let random_offset: f32 = rng.gen_range(-0.1..0.1);
let value = base_value + random_offset;
// clamp the value to ensure it stays within the valid range [0.0, 1.0]
let value = value.max(0.0).min(1.0);
// set hue to 0 for grayscale
let color = Hsv::new(0.0, 0.0, value);
let color = Srgb::<f32>::from_color_unclamped(color);
let color: Srgb<u8> = color.into_format();
let color = Color::Rgb(color.red, color.green, color.blue);
row.push(color);
}
self.colors.push(row);
}
}
}
fn init_terminal<W>(mut stream: W) -> Result<Terminal<CrosstermBackend<W>>>
where
W: Write,
{
enable_raw_mode()?;
stream.execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stream))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal<W>(mut stream: W) -> Result<()>
where
W: Write,
{
disable_raw_mode()?;
stream.execute(LeaveAlternateScreen)?;
Ok(())
}
fn main() -> Result<()> {
let mut io_stream = IoStream::Stderr;
loop {
match io_stream {
IoStream::Stdout => {
io_stream = IoStream::Stderr;
}
IoStream::Stderr => {
io_stream = IoStream::Stdout;
}
};
if App::run(io_stream)? {
break;
}
}
Ok(())
}
Натисніть пробіл , щоб перемикатися між stdout та stderr:
Ви помітили падіння FPS? stdout це~2 рази швидшеніж stderr на терміналі 550×360!
Перш ніж заглибитися в код Rust, давайте поглянемо на зовнішнє середовище виконання, спостерігаючи за викликами процесора та системи, щоб зрозуміти, що відбувається. Для цього завдання я використовуватиму інструмент профілювання під назвою samply.
samplyце профайлер процесора командного рядка, який використовує Firefox Profiler як інтерфейс користувача.
Він записує профіль виконання заданої команди, а потім відкриває profiler.firefox.com у браузері, де ми можемо перевірити купу інформації, наприклад, які функції виконувалися і скільки часу, графіки flame та часові шкали. Ми навіть можемо побачити вихідний код для викликів і які рядки скільки разів вибірково перевірялися.
Почнемо з встановлення samply:
$ cargo install sampl
(Нещодавно я упакував його для Arch Linux, тож він також доступний через pacman -S samplybtw)
А потім нам потрібно внести деякі зміни до коду, який ми збираємося профілювати. Для проектів Rust рекомендується збирати в режимі випуску з налагоджувальною інформацією для отримання вбудованих стеків та перегляду вихідного коду. Тож ми можемо додати наступний профіль до нашого Cargo.toml:
[profile.profiling] inherits = "release" debug = true
Також я вніс деякі зміни до попереднього stdout-vs-stderr.rs:
Видалено такі частини, як віджет FPS, які не мають значення для профілювання.
Додано
STREAMзмінну середовища для запуску TUI із зазначеним потоком вводу/виводу. Приймає або “stdout”, або “stderr”
Додано
DURATIONзмінну середовища для виходу з TUI через певну кількість секунд.
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! anyhow = "1.0.76"
//! crossterm = "0.27.0"
//! palette = "0.7.3"
//! rand = "0.8.5"
//! ratatui = "0.25.0"
//! ```
//!
use anyhow::Result;
use std::{
env,
io::{stderr, stdout, Write},
time::{Duration, Instant},
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use palette::{convert::FromColorUnclamped, Hsv, Srgb};
use ratatui::{prelude::*, widgets::*};
#[derive(Copy, Clone, Debug, Default)]
enum IoStream {
#[default]
Stdout,
Stderr,
}
impl From<String> for IoStream {
fn from(value: String) -> Self {
match value.to_lowercase().as_str() {
"stdout" => Self::Stdout,
"stderr" => Self::Stderr,
_ => Self::default(),
}
}
}
struct RgbColorsWidget<'a> {
/// The colors to render - should be double the height of the area
colors: &'a Vec<Vec<Color>>,
/// the number of elapsed frames that have passed - used to animate the colors
frame_count: usize,
}
impl Widget for RgbColorsWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = self.colors;
for (xi, x) in (area.left()..area.right()).enumerate() {
// animate the colors by shifting the x index by the frame number
let xi = (xi + self.frame_count) % (area.width as usize);
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
struct AppWidget<'a> {
rgb_colors_widget: RgbColorsWidget<'a>,
}
impl<'a> AppWidget<'a> {
fn new(app: &'a App) -> Self {
Self {
rgb_colors_widget: RgbColorsWidget {
colors: &app.colors,
frame_count: app.frame_count,
},
}
}
}
impl Widget for AppWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
self.rgb_colors_widget.render(main_layout[1], buf);
}
}
#[derive(Debug, Default)]
struct App {
should_quit: bool,
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
// to calculate every frame
colors: Vec<Vec<Color>>,
last_size: Rect,
frame_count: usize,
}
impl App {
pub fn run<W>(io_stream: IoStream, stream: W, exit_after: Duration) -> Result<()>
where
W: Write,
{
let mut terminal = init_terminal(stream)?;
let mut app = Self::default();
let start_time = Instant::now();
while !app.should_quit {
app.tick();
terminal.draw(|frame| {
let size = frame.size();
app.setup_colors(size);
frame.render_widget(AppWidget::new(&app), size);
})?;
app.handle_events()?;
if start_time.elapsed() >= exit_after {
break;
}
}
match io_stream {
IoStream::Stdout => restore_terminal(stdout())?,
IoStream::Stderr => restore_terminal(stderr())?,
}
Ok(())
}
fn tick(&mut self) {
self.frame_count += 1;
}
fn handle_events(&mut self) -> Result<()> {
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_quit = true;
}
}
}
Ok(())
}
fn setup_colors(&mut self, size: Rect) {
// only update the colors if the size has changed since the last time we rendered
if self.last_size.width == size.width && self.last_size.height == size.height {
return;
}
self.last_size = size;
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height * 2;
self.colors.clear();
use rand::Rng;
let mut rng = rand::thread_rng();
for y in 0..height {
let mut row = Vec::new();
let randomness_factor = (height - y) as f32 / height as f32; // More randomness towards the bottom
for _ in 0..width {
let base_value = randomness_factor * ((height - y) as f32 / height as f32);
let random_offset: f32 = rng.gen_range(-0.1..0.1); // Adjust the range as needed
let value = base_value + random_offset;
// Clamp the value to ensure it stays within the valid range [0.0, 1.0]
let value = value.max(0.0).min(1.0);
let color = Hsv::new(0.0, 0.0, value); // Set hue to 0 for grayscale
let color = Srgb::<f32>::from_color_unclamped(color);
let color: Srgb<u8> = color.into_format();
let color = Color::Rgb(color.red, color.green, color.blue);
row.push(color);
}
self.colors.push(row);
}
}
}
fn init_terminal<W>(mut stream: W) -> Result<Terminal<CrosstermBackend<W>>>
where
W: Write,
{
enable_raw_mode()?;
stream.execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stream))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal<W>(mut stream: W) -> Result<()>
where
W: Write,
{
disable_raw_mode()?;
stream.execute(LeaveAlternateScreen)?;
Ok(())
}
fn main() -> Result<()> {
let stream = env::var("STREAM").map(IoStream::from).unwrap_or_default();
let duration = env::var("DURATION")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or_else(|| Duration::from_secs(5));
match stream {
IoStream::Stdout => App::run(stream, stdout(), duration)?,
IoStream::Stderr => App::run(stream, stderr(), duration)?,
}
Ok(())
}
Тепер ми можемо зібрати бінарний файл з profilingпрофілем:
$ cargo build --profile profiling
Щоб записати профіль, просто виконайте команду samply:
$ export STREAM="stdout" $ export DURATION=5 $ samply record target/profiling/stdout-vs-stderr-profiler
(Або ви можете скористатися моїм run-profiler.shскриптом, який зробить все за вас.)
Після завершення роботи TUI через 5 секунд samplyнас зустріне сторінка profiler.firefox.com:
Після того, як ми зробимо те саме для stderr через STREAM=”stderr”, ми можемо почати порівнювати, що пішло по-іншому для потоків вводу/виводу.
Ці профілі також доступні для перегляду онлайн, якщо ви хочете самостійно поекспериментувати:
Відразу ж ви можете побачити різницю на сторінці процесора (перша з них — stdout):
stdout має 708 семплів порівняно з stderr, який має 3315, що означає, що stdout зробивУ 4 рази менше дзвінківніж stderr протягом 5 секунд! Ми точно натрапили на щось важливе.
Далі ми можемо з’ясувати характеристики кожного виклику. Можна сміливо припустити, що для кожного рендерингу будуть деякі виклики запису до терміналу. Ми можемо збільшити зображення процесора, щоб чіткіше побачити кожен рендер:
Як бачите, stdout відобразивсябільше кадріву тому ж часовому проміжку, що й stderr. Також для stderr (під час кожного рендерингу) відбувається більше світліших жовтих піків порівняно зі stdout (іноді). Ми скоро з’ясуємо, що це за сплески.
Ми можемо ще більше збільшити масштаб і подивитися на виклики, які відбулися для кожного рендерингу:

Повне трасування стека:
<std::io::stdio::Stdout as std::io::Write>::write_all [library/std/src/io/stdio.rs]
std::io::impls::<impl std::io::Write for &mut W>::write_all [library/std/src/io/impls.rs]
<crossterm::command::write_command_ansi::Adapter<T> as core::fmt::Write>::write_str [crossterm-0.27.0/src/command.rs]
core::fmt::num::imp::fmt_u64 [library/core/src/fmt/num.rs]
core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt [library/core/src/fmt/num.rs]
core::fmt::rt::Argument::fmt [library/core/src/fmt/rt.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<crossterm::style::types::colored::Colored as core::fmt::Display>::fmt [crossterm-0.27.0/src/style/types/colored.rs]
core::fmt::rt::Argument::fmt [library/core/src/fmt/rt.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<&mut W as core::fmt::Write::write_fmt::SpecWriteFmt>::spec_write_fmt [library/core/src/fmt/mod.rs]
core::fmt::Write::write_fmt [library/core/src/fmt/mod.rs]
<crossterm::style::SetForegroundColor as crossterm::command::Command>::write_ansi [crossterm-0.27.0/src/style.rs]
crossterm::command::write_command_ansi [crossterm-0.27.0/src/command.rs]
<T as crossterm::command::QueueableCommand>::queue [crossterm-0.27.0/src/command.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw::{{closure}} [ratatui-0.25.0/src/backend/crossterm.rs]
core::result::Result<T,E>::and_then [library/core/src/result.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw [crossterm-0.27.0/src/macros.rs]
ratatui::terminal::Terminal<B>::flush [ratatui-0.25.0/src/terminal.rs]
ratatui::terminal::Terminal<B>::draw [ratatui-0.25.0/src/terminal.rs]
stdout_vs_stderr::App::run [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
stdout_vs_stderr::main [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
core::ops::function::FnOnce::call_once [library/core/src/ops/function.rs]
std::sys_common::backtrace::__rust_begin_short_backtrace [library/std/src/sys_common/backtrace.rs]
std::rt::lang_start::{{closure}} [library/std/src/rt.rs]
core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once [library/core/src/ops/function.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal::{{closure}} [library/std/src/rt.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal [library/std/src/rt.rs]
std::rt::lang_start [library/std/src/rt.rs]
_libc_start_call_main [/usr/src/debug/glibc/glibc/csu/../sysdeps/nptl/libc_start_call_main.h]
_libc_start_main_impl [/usr/src/debug/glibc/glibc/csu/../csu/libc-start.c]
start [stdout-vs-stderr]
0x7fff18a00137

Повне трасування стека:
<std::io::stdio::Stdout as std::io::Write>::write_all [library/std/src/io/stdio.rs]
std::io::impls::<impl std::io::Write for &mut W>::write_all [library/std/src/io/impls.rs]
<crossterm::command::write_command_ansi::Adapter<T> as core::fmt::Write>::write_str [crossterm-0.27.0/src/command.rs]
core::fmt::num::imp::fmt_u64 [library/core/src/fmt/num.rs]
core::fmt::num::imp::<impl core::fmt::Display for u8>::fmt [library/core/src/fmt/num.rs]
core::fmt::rt::Argument::fmt [library/core/src/fmt/rt.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<crossterm::style::types::colored::Colored as core::fmt::Display>::fmt [crossterm-0.27.0/src/style/types/colored.rs]
core::fmt::rt::Argument::fmt [library/core/src/fmt/rt.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<&mut W as core::fmt::Write::write_fmt::SpecWriteFmt>::spec_write_fmt [library/core/src/fmt/mod.rs]
core::fmt::Write::write_fmt [library/core/src/fmt/mod.rs]
<crossterm::style::SetForegroundColor as crossterm::command::Command>::write_ansi [crossterm-0.27.0/src/style.rs]
crossterm::command::write_command_ansi [crossterm-0.27.0/src/command.rs]
<T as crossterm::command::QueueableCommand>::queue [crossterm-0.27.0/src/command.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw::{{closure}} [ratatui-0.25.0/src/backend/crossterm.rs]
core::result::Result<T,E>::and_then [library/core/src/result.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw [crossterm-0.27.0/src/macros.rs]
ratatui::terminal::Terminal<B>::flush [ratatui-0.25.0/src/terminal.rs]
ratatui::terminal::Terminal<B>::draw [ratatui-0.25.0/src/terminal.rs]
stdout_vs_stderr::App::run [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
stdout_vs_stderr::main [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
core::ops::function::FnOnce::call_once [library/core/src/ops/function.rs]
std::sys_common::backtrace::__rust_begin_short_backtrace [library/std/src/sys_common/backtrace.rs]
std::rt::lang_start::{{closure}} [library/std/src/rt.rs]
core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once [library/core/src/ops/function.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal::{{closure}} [library/std/src/rt.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal [library/std/src/rt.rs]
std::rt::lang_start [library/std/src/rt.rs]
_libc_start_call_main [/usr/src/debug/glibc/glibc/csu/../sysdeps/nptl/libc_start_call_main.h]
_libc_start_main_impl [/usr/src/debug/glibc/glibc/csu/../csu/libc-start.c]
start [stdout-vs-stderr]
0x7fff18a00137
<std::io::stdio::StderrLock as std::io::Write>::write_all
Повне трасування стека:
<std::io::stdio::StderrLock as std::io::Write>::write_all [library/std/src/io/stdio.rs]
<&std::io::stdio::Stderr as std::io::Write>::write_all [library/std/src/io/stdio.rs]
<std::io::stdio::Stderr as std::io::Write>::write_all [library/std/src/io/stdio.rs]
std::io::impls::<impl std::io::Write for &mut W>::write_all [library/std/src/io/impls.rs]
<crossterm::command::write_command_ansi::Adapter<T> as core::fmt::Write>::write_str [crossterm-0.27.0/src/command.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<crossterm::style::types::colored::Colored as core::fmt::Display>::fmt [crossterm-0.27.0/src/style/types/colored.rs]
core::fmt::rt::Argument::fmt [library/core/src/fmt/rt.rs]
core::fmt::write [library/core/src/fmt/mod.rs]
<&mut W as core::fmt::Write::write_fmt::SpecWriteFmt>::spec_write_fmt [library/core/src/fmt/mod.rs]
core::fmt::Write::write_fmt [library/core/src/fmt/mod.rs]
<crossterm::style::SetForegroundColor as crossterm::command::Command>::write_ansi [crossterm-0.27.0/src/style.rs]
crossterm::command::write_command_ansi [crossterm-0.27.0/src/command.rs]
<T as crossterm::command::QueueableCommand>::queue [crossterm-0.27.0/src/command.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw::{{closure}} [ratatui-0.25.0/src/backend/crossterm.rs]
core::result::Result<T,E>::and_then [library/core/src/result.rs]
<ratatui::backend::crossterm::CrosstermBackend<W> as ratatui::backend::Backend>::draw [crossterm-0.27.0/src/macros.rs]
ratatui::terminal::Terminal<B>::flush [ratatui-0.25.0/src/terminal.rs]
ratatui::terminal::Terminal<B>::draw [ratatui-0.25.0/src/terminal.rs]
stdout_vs_stderr::App::run [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
stdout_vs_stderr::main [/home/orhun/gh/ratatui-stdout-vs-stderr/src/bin/2.rs]
core::ops::function::FnOnce::call_once [library/core/src/ops/function.rs]
std::sys_common::backtrace::__rust_begin_short_backtrace [library/std/src/sys_common/backtrace.rs]
std::rt::lang_start::{{closure}} [library/std/src/rt.rs]
core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once [library/core/src/ops/function.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal::{{closure}} [library/std/src/rt.rs]
std::panicking::try::do_call [library/std/src/panicking.rs]
std::panicking::try [library/std/src/panicking.rs]
std::panic::catch_unwind [library/std/src/panic.rs]
std::rt::lang_start_internal [library/std/src/rt.rs]
std::rt::lang_start [library/std/src/rt.rs]
_libc_start_call_main [/usr/src/debug/glibc/glibc/csu/../sysdeps/nptl/libc_start_call_main.h]
_libc_start_main_impl [/usr/src/debug/glibc/glibc/csu/../csu/libc-start.c]
start [stdout-vs-stderr]
0x7ffe88c759d7
Тепер зрозуміло, що ці сплески були
write_allвикликані очікуваннями. Приємно це дізнатися, але що це взагалі означає?
Це означає, що:
викликається stout
write_allодин раз на 5,2 мсі це трапляється час від часу.
викликано stderr
write_all5 разів за 66 мсі він викликається кілька разів майже для кожного рендерингу.
Ми можемо побачити це більш чітко зі стекової діаграми (перша з них — stdout):
Зрештою, ми можемо перевірити код цього виклику, але це не дуже корисно, оскільки він абстрактний:
Отже, який висновок з усього цього можна зробити?
Висновок полягає в тому, що stdout виконує менше викликів запису, таким чином здатний відобразити більше кадрів за той самий час. Іншими словами, stderr блокується, доки функція запису для кожного кадру не буде відображена в терміналі, тоді як stdout повертає дані швидше.
Має бути дещобуферизаціявідбувається для stdout.