Skip to content

Commit

Permalink
feat: Add timings subcommand (starship#1629)
Browse files Browse the repository at this point in the history
* feat: Add computational duration to all computed modules

This also means that in case we do some computations and these end up empty, we submit an empty module

* feat: Add timings subcommand

This outputs the timings of all computed modules, sorted by the duration it took to compute the module.

Useful for debugging why the prompt takes so long.

* feat: Add timings to explain output

* fix: Ensure that even empty custom modules get timings

* format main.rs

* feat: Only show interesting timings

* fix(tests): Change tests to look for empty string instead of None

* Use proper wording in timings help

* Revert "fix(tests): Change tests to look for empty string instead of None"

This reverts commit aca5bd1.

* fix(tests): Returning None in case the module produced an empty string

* fix: Ensure that linebreaks (and space) make a module not-empty

* Make cargo clippy happy

* Make Module.duration a proper Duration

* Only return a module if we would report it

* Change to cleaner way to return None for empty modules

* Avoid unnecessary module creation

* Simplify a string comparison

* Add timings to trace

Co-authored-by: Thomas O'Donnell <[email protected]>

Co-authored-by: Thomas O'Donnell <[email protected]>
  • Loading branch information
jankatins and andytom authored Sep 21, 2020
1 parent bb32483 commit 6426bbe
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 173 deletions.
163 changes: 83 additions & 80 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,87 +71,89 @@ fn main() {
.long("print-full-init")
.help("Print the main initialization script (as opposed to the init stub)");

let mut app =
App::new("starship")
.about("The cross-shell prompt for astronauts. ☄🌌️")
// pull the version number from Cargo.toml
.version(crate_version!())
// pull the authors from Cargo.toml
.author(crate_authors!())
.after_help("https://github.com/starship/starship")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("init")
.about("Prints the shell function used to execute starship")
.arg(&shell_arg)
.arg(&init_scripts_arg),
)
.subcommand(
SubCommand::with_name("prompt")
.about("Prints the full starship prompt")
.arg(&status_code_arg)
.arg(&path_arg)
.arg(&cmd_duration_arg)
.arg(&keymap_arg)
.arg(&jobs_arg),
)
.subcommand(
SubCommand::with_name("module")
.about("Prints a specific prompt module")
.arg(
Arg::with_name("name")
.help("The name of the module to be printed")
.required(true)
.required_unless("list"),
)
.arg(
Arg::with_name("list")
.short("l")
.long("list")
.help("List out all supported modules"),
)
.arg(&status_code_arg)
.arg(&path_arg)
.arg(&cmd_duration_arg)
.arg(&keymap_arg)
.arg(&jobs_arg),
)
.subcommand(
SubCommand::with_name("config")
.alias("configure")
.about("Edit the starship configuration")
.arg(
Arg::with_name("name")
.help("Configuration key to edit")
.required(false)
.requires("value"),
)
.arg(Arg::with_name("value").help("Value to place into that key")),
)
.subcommand(SubCommand::with_name("bug-report").about(
let mut app = App::new("starship")
.about("The cross-shell prompt for astronauts. ☄🌌️")
// pull the version number from Cargo.toml
.version(crate_version!())
// pull the authors from Cargo.toml
.author(crate_authors!())
.after_help("https://github.com/starship/starship")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("init")
.about("Prints the shell function used to execute starship")
.arg(&shell_arg)
.arg(&init_scripts_arg),
)
.subcommand(
SubCommand::with_name("prompt")
.about("Prints the full starship prompt")
.arg(&status_code_arg)
.arg(&path_arg)
.arg(&cmd_duration_arg)
.arg(&keymap_arg)
.arg(&jobs_arg),
)
.subcommand(
SubCommand::with_name("module")
.about("Prints a specific prompt module")
.arg(
Arg::with_name("name")
.help("The name of the module to be printed")
.required(true)
.required_unless("list"),
)
.arg(
Arg::with_name("list")
.short("l")
.long("list")
.help("List out all supported modules"),
)
.arg(&status_code_arg)
.arg(&path_arg)
.arg(&cmd_duration_arg)
.arg(&keymap_arg)
.arg(&jobs_arg),
)
.subcommand(
SubCommand::with_name("config")
.alias("configure")
.about("Edit the starship configuration")
.arg(
Arg::with_name("name")
.help("Configuration key to edit")
.required(false)
.requires("value"),
)
.arg(Arg::with_name("value").help("Value to place into that key")),
)
.subcommand(
SubCommand::with_name("bug-report").about(
"Create a pre-populated GitHub issue with information about your configuration",
))
.subcommand(
SubCommand::with_name("time")
.about("Prints time in milliseconds")
.settings(&[AppSettings::Hidden]),
)
.subcommand(
SubCommand::with_name("explain").about("Explains the currently showing modules"),
)
.subcommand(
SubCommand::with_name("completions")
.about("Generate starship shell completions for your shell to stdout")
.arg(
Arg::with_name("shell")
.takes_value(true)
.possible_values(&Shell::variants())
.help("the shell to generate completions for")
.value_name("SHELL")
.required(true)
.env("STARSHIP_SHELL"),
),
);
),
)
.subcommand(
SubCommand::with_name("time")
.about("Prints time in milliseconds")
.settings(&[AppSettings::Hidden]),
)
.subcommand(
SubCommand::with_name("explain").about("Explains the currently showing modules"),
)
.subcommand(SubCommand::with_name("timings").about("Prints timings of all active modules"))
.subcommand(
SubCommand::with_name("completions")
.about("Generate starship shell completions for your shell to stdout")
.arg(
Arg::with_name("shell")
.takes_value(true)
.possible_values(&Shell::variants())
.help("the shell to generate completions for")
.value_name("SHELL")
.required(true)
.env("STARSHIP_SHELL"),
),
);

let matches = app.clone().get_matches();

Expand Down Expand Up @@ -197,6 +199,7 @@ fn main() {
}
}
("explain", Some(sub_m)) => print::explain(sub_m.clone()),
("timings", Some(sub_m)) => print::timings(sub_m.clone()),
("completions", Some(sub_m)) => {
let shell: Shell = sub_m
.value_of("shell")
Expand Down
40 changes: 39 additions & 1 deletion src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::segment::Segment;
use crate::utils::wrap_colorseq_for_shell;
use ansi_term::{ANSIString, ANSIStrings};
use std::fmt;
use std::time::Duration;

// List of all modules
// Keep these ordered alphabetically.
Expand Down Expand Up @@ -73,6 +74,9 @@ pub struct Module<'a> {

/// The collection of segments that compose this module.
pub segments: Vec<Segment>,

/// the time it took to compute this module
pub duration: Duration,
}

impl<'a> Module<'a> {
Expand All @@ -83,6 +87,7 @@ impl<'a> Module<'a> {
name: name.to_string(),
description: desc.to_string(),
segments: Vec::new(),
duration: Duration::default(),
}
}

Expand All @@ -105,7 +110,8 @@ impl<'a> Module<'a> {
pub fn is_empty(&self) -> bool {
self.segments
.iter()
.all(|segment| segment.value.trim().is_empty())
// no trim: if we add spaces/linebreaks it's not "empty" as we change the final output
.all(|segment| segment.value.is_empty())
}

/// Get values of the module's segments
Expand Down Expand Up @@ -167,6 +173,7 @@ mod tests {
name: name.to_string(),
description: desc.to_string(),
segments: Vec::new(),
duration: Duration::default(),
};

assert!(module.is_empty());
Expand All @@ -181,8 +188,39 @@ mod tests {
name: name.to_string(),
description: desc.to_string(),
segments: vec![Segment::new(None, "")],
duration: Duration::default(),
};

assert!(module.is_empty());
}

#[test]
fn test_module_is_not_empty_with_linebreak_only() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: vec![Segment::new(None, "\n")],
duration: Duration::default(),
};

assert!(!module.is_empty());
}

#[test]
fn test_module_is_not_empty_with_space_only() {
let name = "unit_test";
let desc = "This is a unit test";
let module = Module {
config: None,
name: name.to_string(),
description: desc.to_string(),
segments: vec![Segment::new(None, " ")],
duration: Duration::default(),
};

assert!(!module.is_empty());
}
}
61 changes: 31 additions & 30 deletions src/modules/custom.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::io::Write;
use std::process::{Command, Output, Stdio};
use std::time::Instant;

use super::{Context, Module, RootModuleConfig};

Expand All @@ -13,6 +14,7 @@ use crate::{configs::custom::CustomConfig, formatter::StringFormatter};
///
/// Finally, the content of the module itself is also set by a command.
pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
let start: Instant = Instant::now();
let toml_config = context.config.get_custom_module_config(name).expect(
"modules::custom::module should only be called after ensuring that the module exists",
);
Expand Down Expand Up @@ -47,37 +49,36 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
let output = exec_command(config.command, &config.shell.0)?;

let trimmed = output.trim();
if trimmed.is_empty() {
return None;
if !trimmed.is_empty() {
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_meta(|var, _| match var {
"symbol" => Some(config.symbol),
_ => None,
})
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|variable| match variable {
// This may result in multiple calls to `get_module_version` when a user have
// multiple `$version` variables defined in `format`.
"output" => Some(Ok(trimmed)),
_ => None,
})
.parse(None)
});

match parsed {
Ok(segments) => module.set_segments(segments),
Err(error) => {
log::warn!("Error in module `custom.{}`:\n{}", name, error);
}
};
}

let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_meta(|var, _| match var {
"symbol" => Some(config.symbol),
_ => None,
})
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|variable| match variable {
// This may result in multiple calls to `get_module_version` when a user have
// multiple `$version` variables defined in `format`.
"output" => Some(Ok(trimmed)),
_ => None,
})
.parse(None)
});

module.set_segments(match parsed {
Ok(segments) => segments,
Err(error) => {
log::warn!("Error in module `custom.{}`:\n{}", name, error);
return None;
}
});

let elapsed = start.elapsed();
log::trace!("Took {:?} to compute custom module {:?}", elapsed, name);
module.duration = elapsed;
Some(module)
}

Expand Down
16 changes: 16 additions & 0 deletions src/modules/line_break.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,19 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {

Some(module)
}

#[cfg(test)]
mod test {
use std::io;

use crate::test::ModuleRenderer;

#[test]
fn produces_result() -> io::Result<()> {
let expected = Some(String::from("\n"));
let actual = ModuleRenderer::new("line_break").collect();
assert_eq!(expected, actual);

Ok(())
}
}
Loading

0 comments on commit 6426bbe

Please sign in to comment.