Skip to content

Commit ca9a8e5

Browse files
authored
feat: add FeProfilerTool for flame graph generation (#11)
* feat: add FeProfilerTool for flame graph generation
1 parent 0e3d448 commit ca9a8e5

File tree

4 files changed

+113
-6
lines changed

4 files changed

+113
-6
lines changed

src/lib.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,13 @@ fn execute_tool_enhanced(config: &Config, tool: &dyn Tool, _service_name: &str)
271271
match tool.execute(config, pid) {
272272
Ok(result) => {
273273
print_success(&result.message);
274-
if result.output_path.to_str() != Some("console_output") {
275-
print_info(&format!(
276-
"Output saved to: {}",
277-
result.output_path.display()
278-
));
274+
if let Some(path) = result.output_path.to_str() {
275+
if !path.is_empty() && path != "console_output" {
276+
print_info(&format!(
277+
"Output saved to: {}",
278+
result.output_path.display()
279+
));
280+
}
279281
}
280282
Ok(())
281283
}

src/tools/fe/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod jmap;
22
mod jstack;
3+
mod profiler;
34

45
pub use jmap::{JmapDumpTool, JmapHistoTool};
56
pub use jstack::JstackTool;
7+
pub use profiler::FeProfilerTool;

src/tools/fe/profiler.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::config::Config;
2+
use crate::error::{CliError, Result};
3+
use crate::executor;
4+
use crate::tools::{ExecutionResult, Tool};
5+
use crate::ui;
6+
use dialoguer::Input;
7+
use std::env;
8+
use std::process::Command;
9+
10+
pub struct FeProfilerTool;
11+
12+
impl FeProfilerTool {
13+
/// Prompt user for profile duration and return the duration value
14+
/// This method can be called before tool execution to get user input
15+
pub fn prompt_duration() -> Result<u32> {
16+
let input: String = Input::with_theme(&dialoguer::theme::ColorfulTheme::default())
17+
.with_prompt("Enter collection duration in seconds")
18+
.with_initial_text("10")
19+
.interact_text()
20+
.map_err(|e| CliError::InvalidInput(format!("Duration input failed: {e}")))?;
21+
22+
let duration_str = if input.trim().is_empty() {
23+
"10"
24+
} else {
25+
input.trim()
26+
};
27+
28+
match duration_str.parse::<u32>() {
29+
Ok(val) if val > 0 && val <= 300 => Ok(val),
30+
Ok(_) => {
31+
ui::print_warning("Duration must be between 1 and 300 seconds!");
32+
ui::print_info("Hint: Enter a number between 1-300, e.g., 25");
33+
Err(CliError::GracefulExit)
34+
}
35+
Err(_) => {
36+
ui::print_warning("Please enter a valid number!");
37+
ui::print_info("Hint: Enter a number between 1-300, e.g., 25");
38+
Err(CliError::GracefulExit)
39+
}
40+
}
41+
}
42+
43+
/// Execute the profiler with a specific duration
44+
pub fn execute_with_duration(&self, config: &Config, duration: u32) -> Result<ExecutionResult> {
45+
let doris_config = crate::config_loader::load_config()?;
46+
47+
let fe_install_dir = doris_config
48+
.fe_install_dir
49+
.as_ref()
50+
.or(Some(&doris_config.install_dir))
51+
.ok_or_else(|| CliError::ConfigError("FE install directory not found".to_string()))?;
52+
53+
let profile_script = fe_install_dir.join("bin").join("profile_fe.sh");
54+
55+
if !profile_script.exists() {
56+
return Err(CliError::ConfigError(format!(
57+
"profile_fe.sh not found at {}. Please ensure Doris version is 2.1.4+",
58+
profile_script.display()
59+
)));
60+
}
61+
62+
let mut command = Command::new("bash");
63+
command.arg(&profile_script);
64+
command.env("PROFILE_SECONDS", duration.to_string());
65+
66+
executor::execute_command_with_timeout(&mut command, self.name(), config)?;
67+
68+
let message = format!("Flame graph generated successfully (duration: {duration}s).");
69+
70+
Ok(ExecutionResult {
71+
output_path: std::path::PathBuf::new(),
72+
message,
73+
})
74+
}
75+
}
76+
77+
impl Tool for FeProfilerTool {
78+
fn name(&self) -> &str {
79+
"fe-profiler"
80+
}
81+
82+
fn description(&self) -> &str {
83+
"Generate flame graph for FE performance analysis using async-profiler"
84+
}
85+
86+
fn execute(&self, config: &Config, _pid: u32) -> Result<ExecutionResult> {
87+
let profile_seconds = if env::var("PROFILE_SECONDS").is_ok() {
88+
env::var("PROFILE_SECONDS")
89+
.unwrap()
90+
.parse::<u32>()
91+
.unwrap_or(10)
92+
} else {
93+
Self::prompt_duration()?
94+
};
95+
96+
self.execute_with_duration(config, profile_seconds)
97+
}
98+
99+
fn requires_pid(&self) -> bool {
100+
false // FE profiler doesn't need PID as it uses profile_fe.sh script
101+
}
102+
}

src/tools/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl ToolRegistry {
5050
BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
5151
};
5252
use crate::tools::be::{JmapDumpTool as BeJmapDumpTool, JmapHistoTool as BeJmapHistoTool};
53-
use crate::tools::fe::{JmapDumpTool, JmapHistoTool, JstackTool};
53+
use crate::tools::fe::{FeProfilerTool, JmapDumpTool, JmapHistoTool, JstackTool};
5454

5555
let mut registry = Self {
5656
fe_tools: Vec::new(),
@@ -61,6 +61,7 @@ impl ToolRegistry {
6161
registry.fe_tools.push(Box::new(JmapDumpTool));
6262
registry.fe_tools.push(Box::new(JmapHistoTool));
6363
registry.fe_tools.push(Box::new(JstackTool));
64+
registry.fe_tools.push(Box::new(FeProfilerTool));
6465

6566
// Register BE tools
6667
registry.be_tools.push(Box::new(PstackTool));

0 commit comments

Comments
 (0)