Skip to content

Commit 6426bbe

Browse files
jankatinsandytom
andauthored
feat: Add timings subcommand (starship#1629)
* 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]>
1 parent bb32483 commit 6426bbe

File tree

7 files changed

+316
-173
lines changed

7 files changed

+316
-173
lines changed

src/main.rs

+83-80
Original file line numberDiff line numberDiff line change
@@ -71,87 +71,89 @@ fn main() {
7171
.long("print-full-init")
7272
.help("Print the main initialization script (as opposed to the init stub)");
7373

74-
let mut app =
75-
App::new("starship")
76-
.about("The cross-shell prompt for astronauts. ☄🌌️")
77-
// pull the version number from Cargo.toml
78-
.version(crate_version!())
79-
// pull the authors from Cargo.toml
80-
.author(crate_authors!())
81-
.after_help("https://github.com/starship/starship")
82-
.setting(AppSettings::SubcommandRequiredElseHelp)
83-
.subcommand(
84-
SubCommand::with_name("init")
85-
.about("Prints the shell function used to execute starship")
86-
.arg(&shell_arg)
87-
.arg(&init_scripts_arg),
88-
)
89-
.subcommand(
90-
SubCommand::with_name("prompt")
91-
.about("Prints the full starship prompt")
92-
.arg(&status_code_arg)
93-
.arg(&path_arg)
94-
.arg(&cmd_duration_arg)
95-
.arg(&keymap_arg)
96-
.arg(&jobs_arg),
97-
)
98-
.subcommand(
99-
SubCommand::with_name("module")
100-
.about("Prints a specific prompt module")
101-
.arg(
102-
Arg::with_name("name")
103-
.help("The name of the module to be printed")
104-
.required(true)
105-
.required_unless("list"),
106-
)
107-
.arg(
108-
Arg::with_name("list")
109-
.short("l")
110-
.long("list")
111-
.help("List out all supported modules"),
112-
)
113-
.arg(&status_code_arg)
114-
.arg(&path_arg)
115-
.arg(&cmd_duration_arg)
116-
.arg(&keymap_arg)
117-
.arg(&jobs_arg),
118-
)
119-
.subcommand(
120-
SubCommand::with_name("config")
121-
.alias("configure")
122-
.about("Edit the starship configuration")
123-
.arg(
124-
Arg::with_name("name")
125-
.help("Configuration key to edit")
126-
.required(false)
127-
.requires("value"),
128-
)
129-
.arg(Arg::with_name("value").help("Value to place into that key")),
130-
)
131-
.subcommand(SubCommand::with_name("bug-report").about(
74+
let mut app = App::new("starship")
75+
.about("The cross-shell prompt for astronauts. ☄🌌️")
76+
// pull the version number from Cargo.toml
77+
.version(crate_version!())
78+
// pull the authors from Cargo.toml
79+
.author(crate_authors!())
80+
.after_help("https://github.com/starship/starship")
81+
.setting(AppSettings::SubcommandRequiredElseHelp)
82+
.subcommand(
83+
SubCommand::with_name("init")
84+
.about("Prints the shell function used to execute starship")
85+
.arg(&shell_arg)
86+
.arg(&init_scripts_arg),
87+
)
88+
.subcommand(
89+
SubCommand::with_name("prompt")
90+
.about("Prints the full starship prompt")
91+
.arg(&status_code_arg)
92+
.arg(&path_arg)
93+
.arg(&cmd_duration_arg)
94+
.arg(&keymap_arg)
95+
.arg(&jobs_arg),
96+
)
97+
.subcommand(
98+
SubCommand::with_name("module")
99+
.about("Prints a specific prompt module")
100+
.arg(
101+
Arg::with_name("name")
102+
.help("The name of the module to be printed")
103+
.required(true)
104+
.required_unless("list"),
105+
)
106+
.arg(
107+
Arg::with_name("list")
108+
.short("l")
109+
.long("list")
110+
.help("List out all supported modules"),
111+
)
112+
.arg(&status_code_arg)
113+
.arg(&path_arg)
114+
.arg(&cmd_duration_arg)
115+
.arg(&keymap_arg)
116+
.arg(&jobs_arg),
117+
)
118+
.subcommand(
119+
SubCommand::with_name("config")
120+
.alias("configure")
121+
.about("Edit the starship configuration")
122+
.arg(
123+
Arg::with_name("name")
124+
.help("Configuration key to edit")
125+
.required(false)
126+
.requires("value"),
127+
)
128+
.arg(Arg::with_name("value").help("Value to place into that key")),
129+
)
130+
.subcommand(
131+
SubCommand::with_name("bug-report").about(
132132
"Create a pre-populated GitHub issue with information about your configuration",
133-
))
134-
.subcommand(
135-
SubCommand::with_name("time")
136-
.about("Prints time in milliseconds")
137-
.settings(&[AppSettings::Hidden]),
138-
)
139-
.subcommand(
140-
SubCommand::with_name("explain").about("Explains the currently showing modules"),
141-
)
142-
.subcommand(
143-
SubCommand::with_name("completions")
144-
.about("Generate starship shell completions for your shell to stdout")
145-
.arg(
146-
Arg::with_name("shell")
147-
.takes_value(true)
148-
.possible_values(&Shell::variants())
149-
.help("the shell to generate completions for")
150-
.value_name("SHELL")
151-
.required(true)
152-
.env("STARSHIP_SHELL"),
153-
),
154-
);
133+
),
134+
)
135+
.subcommand(
136+
SubCommand::with_name("time")
137+
.about("Prints time in milliseconds")
138+
.settings(&[AppSettings::Hidden]),
139+
)
140+
.subcommand(
141+
SubCommand::with_name("explain").about("Explains the currently showing modules"),
142+
)
143+
.subcommand(SubCommand::with_name("timings").about("Prints timings of all active modules"))
144+
.subcommand(
145+
SubCommand::with_name("completions")
146+
.about("Generate starship shell completions for your shell to stdout")
147+
.arg(
148+
Arg::with_name("shell")
149+
.takes_value(true)
150+
.possible_values(&Shell::variants())
151+
.help("the shell to generate completions for")
152+
.value_name("SHELL")
153+
.required(true)
154+
.env("STARSHIP_SHELL"),
155+
),
156+
);
155157

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

@@ -197,6 +199,7 @@ fn main() {
197199
}
198200
}
199201
("explain", Some(sub_m)) => print::explain(sub_m.clone()),
202+
("timings", Some(sub_m)) => print::timings(sub_m.clone()),
200203
("completions", Some(sub_m)) => {
201204
let shell: Shell = sub_m
202205
.value_of("shell")

src/module.rs

+39-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::segment::Segment;
33
use crate::utils::wrap_colorseq_for_shell;
44
use ansi_term::{ANSIString, ANSIStrings};
55
use std::fmt;
6+
use std::time::Duration;
67

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

7475
/// The collection of segments that compose this module.
7576
pub segments: Vec<Segment>,
77+
78+
/// the time it took to compute this module
79+
pub duration: Duration,
7680
}
7781

7882
impl<'a> Module<'a> {
@@ -83,6 +87,7 @@ impl<'a> Module<'a> {
8387
name: name.to_string(),
8488
description: desc.to_string(),
8589
segments: Vec::new(),
90+
duration: Duration::default(),
8691
}
8792
}
8893

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

111117
/// Get values of the module's segments
@@ -167,6 +173,7 @@ mod tests {
167173
name: name.to_string(),
168174
description: desc.to_string(),
169175
segments: Vec::new(),
176+
duration: Duration::default(),
170177
};
171178

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

186194
assert!(module.is_empty());
187195
}
196+
197+
#[test]
198+
fn test_module_is_not_empty_with_linebreak_only() {
199+
let name = "unit_test";
200+
let desc = "This is a unit test";
201+
let module = Module {
202+
config: None,
203+
name: name.to_string(),
204+
description: desc.to_string(),
205+
segments: vec![Segment::new(None, "\n")],
206+
duration: Duration::default(),
207+
};
208+
209+
assert!(!module.is_empty());
210+
}
211+
212+
#[test]
213+
fn test_module_is_not_empty_with_space_only() {
214+
let name = "unit_test";
215+
let desc = "This is a unit test";
216+
let module = Module {
217+
config: None,
218+
name: name.to_string(),
219+
description: desc.to_string(),
220+
segments: vec![Segment::new(None, " ")],
221+
duration: Duration::default(),
222+
};
223+
224+
assert!(!module.is_empty());
225+
}
188226
}

src/modules/custom.rs

+31-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::io::Write;
22
use std::process::{Command, Output, Stdio};
3+
use std::time::Instant;
34

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

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

4951
let trimmed = output.trim();
50-
if trimmed.is_empty() {
51-
return None;
52+
if !trimmed.is_empty() {
53+
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
54+
formatter
55+
.map_meta(|var, _| match var {
56+
"symbol" => Some(config.symbol),
57+
_ => None,
58+
})
59+
.map_style(|variable| match variable {
60+
"style" => Some(Ok(config.style)),
61+
_ => None,
62+
})
63+
.map(|variable| match variable {
64+
// This may result in multiple calls to `get_module_version` when a user have
65+
// multiple `$version` variables defined in `format`.
66+
"output" => Some(Ok(trimmed)),
67+
_ => None,
68+
})
69+
.parse(None)
70+
});
71+
72+
match parsed {
73+
Ok(segments) => module.set_segments(segments),
74+
Err(error) => {
75+
log::warn!("Error in module `custom.{}`:\n{}", name, error);
76+
}
77+
};
5278
}
53-
54-
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
55-
formatter
56-
.map_meta(|var, _| match var {
57-
"symbol" => Some(config.symbol),
58-
_ => None,
59-
})
60-
.map_style(|variable| match variable {
61-
"style" => Some(Ok(config.style)),
62-
_ => None,
63-
})
64-
.map(|variable| match variable {
65-
// This may result in multiple calls to `get_module_version` when a user have
66-
// multiple `$version` variables defined in `format`.
67-
"output" => Some(Ok(trimmed)),
68-
_ => None,
69-
})
70-
.parse(None)
71-
});
72-
73-
module.set_segments(match parsed {
74-
Ok(segments) => segments,
75-
Err(error) => {
76-
log::warn!("Error in module `custom.{}`:\n{}", name, error);
77-
return None;
78-
}
79-
});
80-
79+
let elapsed = start.elapsed();
80+
log::trace!("Took {:?} to compute custom module {:?}", elapsed, name);
81+
module.duration = elapsed;
8182
Some(module)
8283
}
8384

src/modules/line_break.rs

+16
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,19 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
1111

1212
Some(module)
1313
}
14+
15+
#[cfg(test)]
16+
mod test {
17+
use std::io;
18+
19+
use crate::test::ModuleRenderer;
20+
21+
#[test]
22+
fn produces_result() -> io::Result<()> {
23+
let expected = Some(String::from("\n"));
24+
let actual = ModuleRenderer::new("line_break").collect();
25+
assert_eq!(expected, actual);
26+
27+
Ok(())
28+
}
29+
}

0 commit comments

Comments
 (0)