Repository: tursodatabase/limbo
Pull request #1372: Optimization: only initialize `Rustyline` if we are in a tty
Author: login: pedrocarlo
Created: 2025-04-20T20:40:34Z | Merged: 2025-04-25T20:02:56Z

This is small nitpick, but it will be useful for #1258. If we are testing or just piping some sql through stdin, we can just not initialize `Rustyline` and save some execution time. 

On `Select 1` bench, I got a minor performance bump, but it starts to become less apparent on more complex queries. 
<img width="759" alt="image" src="https://github.com/user-attachments/assets/12e22675-e081-4284-a5ed-15d53a9c5579" />



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 810b9983e1..ca334c0846 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1575,9 +1575,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
-version = "0.2.171"
+version = "0.2.172"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
 
 [[package]]
 name = "libgit2-sys"
@@ -1702,6 +1702,7 @@ dependencies = [
  "ctrlc",
  "dirs 5.0.1",
  "env_logger 0.10.2",
+ "libc",
  "limbo_core",
  "miette",
  "nu-ansi-term 0.50.1",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 2f16254209..253c08b457 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -28,6 +28,7 @@ csv = "1.3.1"
 ctrlc = "3.4.4"
 dirs = "5.0.1"
 env_logger = "0.10.1"
+libc = "0.2.172"
 limbo_core = { path = "../core", default-features = true, features = [
     "completion",
 ] }
@@ -42,7 +43,6 @@ tracing = "0.1.41"
 tracing-appender = "0.2.3"
 tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
 
-
 [features]
 default = ["io_uring"]
 io_uring = ["limbo_core/io_uring"]
diff --git a/cli/app.rs b/cli/app.rs
index bb95156606..16c464b41b 100644
--- a/cli/app.rs
+++ b/cli/app.rs
@@ -7,6 +7,7 @@ use crate::{
     helper::LimboHelper,
     input::{get_io, get_writer, DbLocation, OutputMode, Settings},
     opcodes_dictionary::OPCODE_DESCRIPTIONS,
+    HISTORY_FILE,
 };
 use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
 use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult};
@@ -14,10 +15,10 @@ use tracing_appender::non_blocking::WorkerGuard;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
 
 use clap::Parser;
-use rustyline::{history::DefaultHistory, Editor};
+use rustyline::{error::ReadlineError, history::DefaultHistory, Editor};
 use std::{
     fmt,
-    io::{self, Write},
+    io::{self, BufRead as _, Write},
     path::PathBuf,
     rc::Rc,
     sync::{
@@ -62,7 +63,7 @@ pub struct Opts {
 
 const PROMPT: &str = "limbo> ";
 
-pub struct Limbo<'a> {
+pub struct Limbo {
     pub prompt: String,
     io: Arc<dyn limbo_core::IO>,
     writer: Box<dyn Write>,
@@ -70,7 +71,7 @@ pub struct Limbo<'a> {
     pub interrupt_count: Arc<AtomicUsize>,
     input_buff: String,
     opts: Settings,
-    pub rl: &'a mut Editor<LimboHelper, DefaultHistory>,
+    pub rl: Option<Editor<LimboHelper, DefaultHistory>>,
 }
 
 struct QueryStatistics {
@@ -105,8 +106,8 @@ macro_rules! query_internal {
 
 static COLORS: &[Color] = &[Color::Green, Color::Black, Color::Grey];
 
-impl<'a> Limbo<'a> {
-    pub fn new(rl: &'a mut rustyline::Editor<LimboHelper, DefaultHistory>) -> anyhow::Result<Self> {
+impl Limbo {
+    pub fn new() -> anyhow::Result<Self> {
         let opts = Opts::parse();
         let db_file = opts
             .database
@@ -133,8 +134,6 @@ impl<'a> Limbo<'a> {
             )
         };
         let conn = db.connect()?;
-        let h = LimboHelper::new(conn.clone(), io.clone());
-        rl.set_helper(Some(h));
         let interrupt_count = Arc::new(AtomicUsize::new(0));
         {
             let interrupt_count: Arc<AtomicUsize> = Arc::clone(&interrupt_count);
@@ -154,12 +153,19 @@ impl<'a> Limbo<'a> {
             interrupt_count,
             input_buff: String::new(),
             opts: Settings::from(opts),
-            rl,
+            rl: None,
         };
         app.first_run(sql, quiet)?;
         Ok(app)
     }
 
+    pub fn with_readline(mut self, mut rl: Editor<LimboHelper, DefaultHistory>) -> Self {
+        let h = LimboHelper::new(self.conn.clone(), self.io.clone());
+        rl.set_helper(Some(h));
+        self.rl = Some(rl);
+        self
+    }
+
     fn first_run(&mut self, sql: Option<String>, quiet: bool) -> io::Result<()> {
         if let Some(sql) = sql {
             self.handle_first_input(&sql);
@@ -470,8 +476,9 @@ impl<'a> Limbo<'a> {
         }
     }
 
-    fn reset_line(&mut self, line: &str) -> rustyline::Result<()> {
-        self.rl.add_history_entry(line.to_owned())?;
+    fn reset_line(&mut self, _line: &str) -> rustyline::Result<()> {
+        // Entry is auto added to history
+        // self.rl.add_history_entry(line.to_owned())?;
         self.interrupt_count.store(0, Ordering::SeqCst);
         Ok(())
     }
@@ -973,4 +980,34 @@ impl<'a> Limbo<'a> {
         self.run_query(buff.as_str());
         self.reset_input();
     }
+
+    pub fn readline(&mut self) -> Result<String, ReadlineError> {
+        if let Some(rl) = &mut self.rl {
+            Ok(rl.readline(&self.prompt)?)
+        } else {
+            let mut input = String::new();
+            println!("");
+            let mut reader = std::io::stdin().lock();
+            if reader.read_line(&mut input)? == 0 {
+                return Err(ReadlineError::Eof.into());
+            }
+            // Remove trailing newline
+            if input.ends_with('\n') {
+                input.pop();
+                if input.ends_with('\r') {
+                    input.pop();
+                }
+            }
+
+            Ok(input)
+        }
+    }
+}
+
+impl Drop for Limbo {
+    fn drop(&mut self) {
+        if let Some(rl) = &mut self.rl {
+            let _ = rl.save_history(HISTORY_FILE.as_path());
+        }
+    }
 }
diff --git a/cli/main.rs b/cli/main.rs
index ec81b64afd..82eb649537 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -6,25 +6,39 @@ mod input;
 mod opcodes_dictionary;
 
 use rustyline::{error::ReadlineError, Config, Editor};
-use std::sync::atomic::Ordering;
+use std::{
+    path::PathBuf,
+    sync::{atomic::Ordering, LazyLock},
+};
 
 fn rustyline_config() -> Config {
     Config::builder()
         .completion_type(rustyline::CompletionType::List)
+        .auto_add_history(true)
         .build()
 }
 
+pub static HOME_DIR: LazyLock<PathBuf> =
+    LazyLock::new(|| dirs::home_dir().expect("Could not determine home directory"));
+
+pub static HISTORY_FILE: LazyLock<PathBuf> = LazyLock::new(|| HOME_DIR.join(".limbo_history"));
+
 fn main() -> anyhow::Result<()> {
-    let mut rl = Editor::with_config(rustyline_config())?;
-    let mut app = app::Limbo::new(&mut rl)?;
+    let mut app = app::Limbo::new()?;
     let _guard = app.init_tracing()?;
-    let home = dirs::home_dir().expect("Could not determine home directory");
-    let history_file = home.join(".limbo_history");
-    if history_file.exists() {
-        app.rl.load_history(history_file.as_path())?;
+
+    if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
+        let mut rl = Editor::with_config(rustyline_config())?;
+        if HISTORY_FILE.exists() {
+            rl.load_history(HISTORY_FILE.as_path())?;
+        }
+        app = app.with_readline(rl);
+    } else {
+        tracing::debug!("not in tty");
     }
+
     loop {
-        let readline = app.rl.readline(&app.prompt);
+        let readline = app.readline();
         match readline {
             Ok(line) => match app.handle_input_line(line.trim()) {
                 Ok(_) => {}
@@ -54,6 +68,5 @@ fn main() -> anyhow::Result<()> {
             }
         }
     }
-    rl.save_history(history_file.as_path())?;
     Ok(())
 }


Reviews:
user: login: PThorpe92
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T02:33:00Z
user: login: pedrocarlo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T03:47:49Z

Review comments:
diff_hunk: @@ -5,26 +5,42 @@ mod helper;
 mod input;
 mod opcodes_dictionary;
 
+#[cfg(unix)]
+use nix::unistd::isatty;
 use rustyline::{error::ReadlineError, Config, Editor};
-use std::sync::atomic::Ordering;
+use std::{
+    path::PathBuf,
+    sync::{atomic::Ordering, LazyLock},
+};
 
 fn rustyline_config() -> Config {
     Config::builder()
         .completion_type(rustyline::CompletionType::List)
+        .auto_add_history(true)
         .build()
 }
 
+pub static HOME_DIR: LazyLock<PathBuf> =
+    LazyLock::new(|| dirs::home_dir().expect("Could not determine home directory"));
+
+pub static HISTORY_FILE: LazyLock<PathBuf> = LazyLock::new(|| HOME_DIR.join(".limbo_history"));
+
 fn main() -> anyhow::Result<()> {
-    let mut rl = Editor::with_config(rustyline_config())?;
-    let mut app = app::Limbo::new(&mut rl)?;
+    let mut app = app::Limbo::new()?;
     let _guard = app.init_tracing()?;
-    let home = dirs::home_dir().expect("Could not determine home directory");
-    let history_file = home.join(".limbo_history");
-    if history_file.exists() {
-        app.rl.load_history(history_file.as_path())?;
+
+    if is_a_tty() {
path: cli/main.rs
user: login: PThorpe92
body: We can prob replace this with `std::io::IsTerminal::is_terminal(std::io::stdout())`
created_at: 2025-04-21T02:32:59Z
author_association: COLLABORATOR
diff_hunk: @@ -5,26 +5,42 @@ mod helper;
 mod input;
 mod opcodes_dictionary;
 
+#[cfg(unix)]
+use nix::unistd::isatty;
 use rustyline::{error::ReadlineError, Config, Editor};
-use std::sync::atomic::Ordering;
+use std::{
+    path::PathBuf,
+    sync::{atomic::Ordering, LazyLock},
+};
 
 fn rustyline_config() -> Config {
     Config::builder()
         .completion_type(rustyline::CompletionType::List)
+        .auto_add_history(true)
         .build()
 }
 
+pub static HOME_DIR: LazyLock<PathBuf> =
+    LazyLock::new(|| dirs::home_dir().expect("Could not determine home directory"));
+
+pub static HISTORY_FILE: LazyLock<PathBuf> = LazyLock::new(|| HOME_DIR.join(".limbo_history"));
+
 fn main() -> anyhow::Result<()> {
-    let mut rl = Editor::with_config(rustyline_config())?;
-    let mut app = app::Limbo::new(&mut rl)?;
+    let mut app = app::Limbo::new()?;
     let _guard = app.init_tracing()?;
-    let home = dirs::home_dir().expect("Could not determine home directory");
-    let history_file = home.join(".limbo_history");
-    if history_file.exists() {
-        app.rl.load_history(history_file.as_path())?;
+
+    if is_a_tty() {
path: cli/main.rs
user: login: pedrocarlo
body: Yeah, I will have a look at the source code for this. I tried to fit the implementation from `Rustyline` for this. Maybe they had to implement this first before the Standard Library created this function. Let me check if this has the same result as what I have done
created_at: 2025-04-21T03:47:48Z
author_association: COLLABORATOR

PR comments:
user: login: penberg
created_at: 2025-04-22T07:29:59Z
body: Hey @pedrocarlo! The `SELECT 1` benchmark bypasses the CLI altogether so that performance change is just variance.
author_association: COLLABORATOR
user: login: pedrocarlo
created_at: 2025-04-22T14:10:00Z
body: Hey @penberg, you are right haha 🤦‍♂️. I forgot we don't run the binary. But, either way I think this makes it clear for the developer where code for the cli should be located for the interactive session and makes tests slightly faster. I say this, because in the #1258 I introduced a CLI Config file and, without noticing,  just the Deserialization of the file takes like 10 ms. So, I just slowed down our test suite by (10 ms * number of tests), which felt to me unacceptable.
author_association: COLLABORATOR