add performance metrics for measuring startup time (#3854)

* add performance metrics for measureing startup time

* removed some comments

* update so tests pass

* update default.toml for tests, merged main

* fix clippy lints

* wording changes
This commit is contained in:
Darren Schroeder 2021-07-29 18:52:40 -05:00 committed by GitHub
parent 653cbe651f
commit 23ba01d89c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 309 additions and 8 deletions

1
Cargo.lock generated
View File

@ -3055,6 +3055,7 @@ version = "0.34.1"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"indexmap", "indexmap",
"lazy_static 1.4.0",
"log", "log",
"nu-ansi-term", "nu-ansi-term",
"nu-command", "nu-command",

View File

@ -31,6 +31,7 @@ ctrlc = { version="3.1.7", optional=true }
shadow-rs = { version="0.6", default-features=false, optional=true } shadow-rs = { version="0.6", default-features=false, optional=true }
serde = { version="1.0.123", features=["derive"] } serde = { version="1.0.123", features=["derive"] }
serde_yaml = "0.8.16" serde_yaml = "0.8.16"
lazy_static = "1.4.0"
[build-dependencies] [build-dependencies]
shadow-rs = "0.6" shadow-rs = "0.6"

View File

@ -1,10 +1,10 @@
mod logger; mod logger;
mod options; mod options;
mod options_parser; mod options_parser;
pub mod stopwatch;
pub use options::{CliOptions, NuScript, Options}; use self::stopwatch::Stopwatch;
use options_parser::{NuParser, OptionsParser}; use lazy_static::lazy_static;
use nu_command::{commands::NuSignature as Nu, utils::test_bins as binaries}; use nu_command::{commands::NuSignature as Nu, utils::test_bins as binaries};
use nu_engine::{get_full_help, EvaluationContext}; use nu_engine::{get_full_help, EvaluationContext};
use nu_errors::ShellError; use nu_errors::ShellError;
@ -12,6 +12,18 @@ use nu_protocol::hir::{Call, Expression, SpannedExpression, Synthetic};
use nu_protocol::{Primitive, UntaggedValue}; use nu_protocol::{Primitive, UntaggedValue};
use nu_source::{Span, Tag}; use nu_source::{Span, Tag};
use nu_stream::InputStream; use nu_stream::InputStream;
pub use options::{CliOptions, NuScript, Options};
use options_parser::{NuParser, OptionsParser};
use std::sync::Mutex;
lazy_static! {
pub static ref STOPWATCH: Mutex<Stopwatch> = {
let mut sw = Stopwatch::default();
sw.start();
sw.stop();
Mutex::new(sw)
};
}
pub struct App { pub struct App {
parser: Box<dyn OptionsParser>, parser: Box<dyn OptionsParser>,
@ -32,6 +44,14 @@ impl App {
} }
pub fn main(&self, argv: &[String]) -> Result<(), ShellError> { pub fn main(&self, argv: &[String]) -> Result<(), ShellError> {
if self.perf() {
// start the stopwatch running
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
let argv = quote_positionals(argv).join(" "); let argv = quote_positionals(argv).join(" ");
if let Err(cause) = self.parse(&argv) { if let Err(cause) = self.parse(&argv) {
@ -52,6 +72,21 @@ impl App {
consume(context, stream)?; consume(context, stream)?;
if self.perf() {
// stop the stopwatch since we're exiting
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
eprintln!(
"help {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed()
);
}
std::process::exit(0); std::process::exit(0);
} }
@ -93,6 +128,22 @@ impl App {
}; };
consume(context, stream)?; consume(context, stream)?;
if self.perf() {
// stop the stopwatch since we're exiting
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
eprintln!(
"version {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed()
);
}
std::process::exit(0); std::process::exit(0);
} }
@ -113,10 +164,10 @@ impl App {
} }
let mut opts = CliOptions::new(); let mut opts = CliOptions::new();
opts.config = self.config().map(std::ffi::OsString::from); opts.config = self.config().map(std::ffi::OsString::from);
opts.stdin = self.takes_stdin(); opts.stdin = self.takes_stdin();
opts.save_history = self.save_history(); opts.save_history = self.save_history();
opts.perf = self.perf();
use logger::{configure, debug_filters, logger, trace_filters}; use logger::{configure, debug_filters, logger, trace_filters};
@ -128,6 +179,14 @@ impl App {
Ok(()) Ok(())
})?; })?;
if self.perf() {
// start a new split
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start()
}
if let Some(commands) = self.commands() { if let Some(commands) = self.commands() {
let commands = commands?; let commands = commands?;
let script = NuScript::code(&commands)?; let script = NuScript::code(&commands)?;
@ -136,6 +195,21 @@ impl App {
return crate::run_script_file(context, opts); return crate::run_script_file(context, opts);
} }
if self.perf() {
// start a new spit
eprintln!(
"commands using -c at launch: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
if let Some(scripts) = self.scripts() { if let Some(scripts) = self.scripts() {
let mut source_files = vec![]; let mut source_files = vec![];
for script in scripts { for script in scripts {
@ -162,12 +236,38 @@ impl App {
return Ok(()); return Ok(());
} }
if self.perf() {
// start a new split
eprintln!(
"script file(s) passed in: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
let context = crate::create_default_context(true)?; let context = crate::create_default_context(true)?;
if !self.skip_plugins() { if !self.skip_plugins() {
let _ = crate::register_plugins(&context); let _ = crate::register_plugins(&context);
} }
if self.perf() {
// start a new split
eprintln!(
"plugins registered: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
}
#[cfg(feature = "rustyline-support")] #[cfg(feature = "rustyline-support")]
{ {
crate::cli(context, opts)?; crate::cli(context, opts)?;
@ -189,6 +289,13 @@ impl App {
}) })
} }
pub fn perf(&self) -> bool {
self.options
.get("perf")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn help(&self) -> bool { pub fn help(&self) -> bool {
self.options self.options
.get("help") .get("help")

View File

@ -10,6 +10,7 @@ pub struct CliOptions {
pub stdin: bool, pub stdin: bool,
pub scripts: Vec<NuScript>, pub scripts: Vec<NuScript>,
pub save_history: bool, pub save_history: bool,
pub perf: bool,
} }
impl Default for CliOptions { impl Default for CliOptions {
@ -25,6 +26,7 @@ impl CliOptions {
stdin: false, stdin: false,
scripts: vec![], scripts: vec![],
save_history: true, save_history: true,
perf: false,
} }
} }
} }

View File

@ -0,0 +1,118 @@
#![allow(dead_code)]
use std::default::Default;
use std::fmt;
use std::time::{Duration, Instant};
#[derive(Clone, Copy)]
pub struct Stopwatch {
/// The time the stopwatch was started last, if ever.
start_time: Option<Instant>,
/// The time the stopwatch was split last, if ever.
split_time: Option<Instant>,
/// The time elapsed while the stopwatch was running (between start() and stop()).
elapsed: Duration,
}
impl Default for Stopwatch {
fn default() -> Stopwatch {
Stopwatch {
start_time: None,
split_time: None,
elapsed: Duration::from_secs(0),
}
}
}
impl fmt::Display for Stopwatch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}ms", self.elapsed_ms());
}
}
impl Stopwatch {
/// Returns a new stopwatch.
pub fn new() -> Stopwatch {
let sw: Stopwatch = Default::default();
sw
}
/// Returns a new stopwatch which will immediately be started.
pub fn start_new() -> Stopwatch {
let mut sw = Stopwatch::new();
sw.start();
sw
}
/// Starts the stopwatch.
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
}
/// Stops the stopwatch.
pub fn stop(&mut self) {
self.elapsed = self.elapsed();
self.start_time = None;
self.split_time = None;
}
/// Resets all counters and stops the stopwatch.
pub fn reset(&mut self) {
self.elapsed = Duration::from_secs(0);
self.start_time = None;
self.split_time = None;
}
/// Resets and starts the stopwatch again.
pub fn restart(&mut self) {
self.reset();
self.start();
}
/// Returns whether the stopwatch is running.
pub fn is_running(&self) -> bool {
self.start_time.is_some()
}
/// Returns the elapsed time since the start of the stopwatch.
pub fn elapsed(&self) -> Duration {
match self.start_time {
// stopwatch is running
Some(t1) => t1.elapsed() + self.elapsed,
// stopwatch is not running
None => self.elapsed,
}
}
/// Returns the elapsed time since the start of the stopwatch in milliseconds.
pub fn elapsed_ms(&self) -> i64 {
let dur = self.elapsed();
(dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64
}
/// Returns the elapsed time since last split or start/restart.
///
/// If the stopwatch is in stopped state this will always return a zero Duration.
pub fn elapsed_split(&mut self) -> Duration {
match self.start_time {
// stopwatch is running
Some(start) => {
let res = match self.split_time {
Some(split) => split.elapsed(),
None => start.elapsed(),
};
self.split_time = Some(Instant::now());
res
}
// stopwatch is not running
None => Duration::from_secs(0),
}
}
/// Returns the elapsed time since last split or start/restart in milliseconds.
///
/// If the stopwatch is in stopped state this will always return zero.
pub fn elapsed_split_ms(&mut self) -> i64 {
let dur = self.elapsed_split();
(dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64
}
}

View File

@ -1,3 +1,4 @@
use crate::app::STOPWATCH;
use crate::line_editor::configure_ctrl_c; use crate::line_editor::configure_ctrl_c;
use nu_ansi_term::Color; use nu_ansi_term::Color;
use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext}; use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext};
@ -97,15 +98,26 @@ pub fn cli(
} else { } else {
load_global_cfg(&context); load_global_cfg(&context);
} }
// Store cmd duration in an env var // Store cmd duration in an env var
context.scope.add_env_var( context.scope.add_env_var(
"CMD_DURATION_MS", "CMD_DURATION_MS",
format!("{}", startup_commands_start_time.elapsed().as_millis()), format!("{}", startup_commands_start_time.elapsed().as_millis()),
); );
trace!(
"startup commands took {:?}", if options.perf {
startup_commands_start_time.elapsed() eprintln!(
"config loaded: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
); );
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//Configure rustyline //Configure rustyline
let mut rl = default_rustyline_editor_configuration(); let mut rl = default_rustyline_editor_configuration();
@ -118,11 +130,39 @@ pub fn cli(
nu_data::config::path::default_history_path() nu_data::config::path::default_history_path()
}; };
if options.perf {
eprintln!(
"rustyline configuration: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
// Don't load history if it's not necessary // Don't load history if it's not necessary
if options.save_history { if options.save_history {
let _ = rl.load_history(&history_path); let _ = rl.load_history(&history_path);
} }
if options.perf {
eprintln!(
"history load: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//set vars from cfg if present //set vars from cfg if present
let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs().lock().global_config let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs().lock().global_config
{ {
@ -136,6 +176,20 @@ pub fn cli(
(false, None) (false, None)
}; };
if options.perf {
eprintln!(
"load custom prompt: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//Check whether dir we start in contains local cfg file and if so load it. //Check whether dir we start in contains local cfg file and if so load it.
load_local_cfg_if_present(&context); load_local_cfg_if_present(&context);
@ -159,6 +213,20 @@ pub fn cli(
let mut ctrlcbreak = false; let mut ctrlcbreak = false;
if options.perf {
eprintln!(
"timing stopped. starting run loop: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
}
loop { loop {
if context.ctrl_c().load(Ordering::SeqCst) { if context.ctrl_c().load(Ordering::SeqCst) {
context.ctrl_c().store(false, Ordering::SeqCst); context.ctrl_c().store(false, Ordering::SeqCst);

View File

@ -14,6 +14,7 @@ impl WholeStreamCommand for Command {
.switch("stdin", "redirect stdin", None) .switch("stdin", "redirect stdin", None)
.switch("skip-plugins", "do not load plugins", None) .switch("skip-plugins", "do not load plugins", None)
.switch("no-history", "don't save history", None) .switch("no-history", "don't save history", None)
.switch("perf", "show startup performance metrics", None)
.named( .named(
"commands", "commands",
SyntaxShape::String, SyntaxShape::String,

View File

@ -67,6 +67,7 @@ impl Director {
config_file.to_str().expect("failed to convert."), config_file.to_str().expect("failed to convert."),
]); ]);
} }
process.arg("--perf");
director.executable = Some(process); director.executable = Some(process);
director director

View File

@ -1 +1,3 @@
skip_welcome_message = true skip_welcome_message = true
filesize_format = "auto"
rm_always_trash = false