xref: /aosp_15_r20/external/crosvm/tools/contrib/vcpu_blocker_analyzer/src/parse.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2024 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use anyhow::anyhow;
6 use anyhow::Context;
7 use anyhow::Result;
8 use once_cell::sync::OnceCell;
9 use regex::Regex;
10 
11 use crate::ProcState;
12 
13 #[derive(Debug)]
14 pub struct Event {
15     pub pid: i32,
16     pub proc_name: String,
17     pub name: String,
18     pub details: String,
19     pub time: f64,
20 }
21 
22 impl PartialEq for Event {
eq(&self, other: &Self) -> bool23     fn eq(&self, other: &Self) -> bool {
24         self.pid == other.pid
25             && self.proc_name == other.proc_name
26             && self.name == other.name
27             && self.details == other.details
28             && self.time.to_bits() == other.time.to_bits()
29     }
30 }
31 
32 static EVENT_PATTERN: OnceCell<Regex> = OnceCell::new();
33 
parse_event(line: &str) -> Option<Event>34 pub fn parse_event(line: &str) -> Option<Event> {
35     let event_pattern = EVENT_PATTERN.get_or_init(|| {
36         Regex::new(
37             r" +(?P<proc>.*)-(?P<pid>\d+) +\[(?P<cpu>\d+)\] +(?P<ts>\d+\.\d+): +(?P<event>\S*): +",
38         )
39         .expect("Failed to compile event pattern")
40     });
41 
42     let event_captures = event_pattern.captures(line)?;
43 
44     Some(Event {
45         pid: event_captures["pid"].parse::<i32>().ok()?,
46         proc_name: event_captures["proc"].to_string(),
47         name: event_captures["event"].to_string(),
48         details: line[event_pattern.find(line)?.end()..].to_string(),
49         time: event_captures["ts"].parse::<f64>().ok()?,
50     })
51 }
52 
53 static VCPU_ID_PATTERN: OnceCell<Regex> = OnceCell::new();
54 
parse_vcpu_id(proc_name: &str) -> Result<usize>55 pub fn parse_vcpu_id(proc_name: &str) -> Result<usize> {
56     let vcpu_id_pattern = VCPU_ID_PATTERN.get_or_init(|| Regex::new(r"crosvm_vcpu(\d+)").unwrap());
57 
58     if let Some(captures) = vcpu_id_pattern.captures(proc_name) {
59         captures
60             .get(1)
61             .and_then(|id_match| id_match.as_str().parse::<usize>().ok())
62             .ok_or_else(|| anyhow::anyhow!("Invalid vCPU ID format in process name: {}", proc_name))
63     } else {
64         Err(anyhow::anyhow!(
65             "VCPU ID not found in process name: {}",
66             proc_name
67         ))
68     }
69 }
70 
71 #[derive(Debug, PartialEq, Eq)]
72 pub struct SchedWaking {
73     pub waked_proc_name: String,
74     pub waked_pid: i32,
75 }
76 
77 static SCHED_WAKING_PATTERN: OnceCell<Regex> = OnceCell::new();
78 
parse_sched_waking(details: &str) -> Result<SchedWaking>79 pub fn parse_sched_waking(details: &str) -> Result<SchedWaking> {
80     let sched_waking_pattern = SCHED_WAKING_PATTERN
81         .get_or_init(|| Regex::new(r"comm=(?P<proc>.*) pid=(?P<pid>\d+)").unwrap());
82 
83     let sched_waking_captures = sched_waking_pattern
84         .captures(details)
85         .ok_or_else(|| anyhow!("Failed to parse sched_waking"))?;
86     Ok(SchedWaking {
87         waked_proc_name: sched_waking_captures["proc"].to_string(),
88         waked_pid: sched_waking_captures["pid"]
89             .parse::<i32>()
90             .with_context(|| format!("Failed to parse pid: {}", &sched_waking_captures["pid"]))?,
91     })
92 }
93 
94 #[derive(Debug, PartialEq, Eq)]
95 pub struct SchedSwitch {
96     pub prev_proc_name: String,
97     pub prev_pid: i32,
98     pub prev_proc_state: ProcState,
99     pub new_proc_name: String,
100     pub new_pid: i32,
101 }
102 
103 static SCHED_SWITCH_PATTERN: OnceCell<Regex> = OnceCell::new();
104 
parse_sched_switch(details: &str) -> Result<SchedSwitch>105 pub fn parse_sched_switch(details: &str) -> Result<SchedSwitch> {
106     let sched_switch_pattern = SCHED_SWITCH_PATTERN.get_or_init(|| Regex::new(r"(?P<prev>.*):(?P<prev_pid>\d+) \[-?\d+\] (?P<state>\S+) ==> (?P<new>.*):(?P<new_pid>\d+) \[-?\d+\]").expect("failed to compile regex"));
107 
108     let sched_switch_captures = sched_switch_pattern
109         .captures(details)
110         .with_context(|| format!("Failed to parse sched_switch: {}", details))?;
111 
112     let prev_state = match &sched_switch_captures["state"] {
113         "R" | "R+" => ProcState::Preempted,
114         "D" | "S" => ProcState::Sleep,
115         "X" => ProcState::Dead,
116         _ => ProcState::Other,
117     };
118 
119     Ok(SchedSwitch {
120         prev_proc_name: sched_switch_captures["prev"].to_string(),
121         prev_pid: sched_switch_captures["prev_pid"]
122             .parse::<i32>()
123             .with_context(|| {
124                 format!(
125                     "Failed to parse pid: {}",
126                     &sched_switch_captures["prev_pid"]
127                 )
128             })?,
129         prev_proc_state: prev_state,
130         new_proc_name: sched_switch_captures["new"].to_string(),
131         new_pid: sched_switch_captures["new_pid"]
132             .parse::<i32>()
133             .with_context(|| {
134                 format!("Failed to parse pid: {}", &sched_switch_captures["new_pid"])
135             })?,
136     })
137 }
138 
139 static TASK_RENAME_PATTERN: OnceCell<Regex> = OnceCell::new();
140 
parse_task_rename(details: &str) -> Result<String>141 pub fn parse_task_rename(details: &str) -> Result<String> {
142     // Match a line like "newcomm=D-Bus Thread oom_score_adj="
143     let task_rename_pattern = TASK_RENAME_PATTERN.get_or_init(|| {
144         Regex::new(r"newcomm=(?P<comm>.*) +oom_score_adj=").expect("failed to compile regex")
145     });
146 
147     Ok(task_rename_pattern
148         .captures(details)
149         .with_context(|| format!("Failed to parse task_rename: {}", details))?
150         .name("comm")
151         .with_context(|| format!("Failed to parse comm: {}", details))?
152         .as_str()
153         .to_owned())
154 }
155 
156 #[cfg(test)]
157 mod tests {
158     use rstest::*;
159 
160     use super::*;
161 
162     #[rstest]
163     #[case(
164         "normal",
165         " trace-cmd-6563 [006] 575400.854473: sched_stat_runtime:  comm=trace-cmd pid=6563 runtime=44314 [ns] vruntime=112263744696292 [ns]",
166         Event {
167             proc_name: "trace-cmd".to_string(),
168             pid: 6563,
169             name: "sched_stat_runtime".to_string(),
170             details: "comm=trace-cmd pid=6563 runtime=44314 [ns] vruntime=112263744696292 [ns]".to_string(),
171             time: 575400.854473,
172         }
173     )]
174     #[case(
175         "proc name with a space",
176         "  D-Bus thread-3284 [002] 575401.489380842: sched_waking:     comm=chrome pid=3269 prio=112 target_cpu=004",
177         Event {
178             proc_name: "D-Bus thread".to_string(),
179             pid: 3284,
180             name: "sched_waking".to_string(),
181             details: "comm=chrome pid=3269 prio=112 target_cpu=004".to_string(),
182             time: 575401.489380842,
183         }
184     )]
test_parse_event(#[case] name: &str, #[case] line: &str, #[case] want: Event)185     fn test_parse_event(#[case] name: &str, #[case] line: &str, #[case] want: Event) {
186         assert_eq!(parse_event(line), Some(want), "Test case: {}", name);
187     }
188 
189     #[rstest]
190     #[case(
191         "normal",
192         "comm=VizCompositorTh pid=3338 prio=112 target_cpu=000",
193         SchedWaking {
194             waked_proc_name: "VizCompositorTh".to_string(),
195             waked_pid: 3338,
196         }
197     )]
198     #[case(
199         "proc name with a space",
200         "comm=D-Bus thread pid=3338 prio=112 target_cpu=000",
201         SchedWaking {
202             waked_proc_name: "D-Bus thread".to_string(),
203             waked_pid: 3338,
204         }
205     )]
test_parse_sched_waking(#[case] name: &str, #[case] line: &str, #[case] want: SchedWaking)206     fn test_parse_sched_waking(#[case] name: &str, #[case] line: &str, #[case] want: SchedWaking) {
207         assert_eq!(
208             parse_sched_waking(line).unwrap(),
209             want,
210             "Test case: {}",
211             name
212         );
213     }
214 
215     #[rstest]
216     #[case(
217         "normal",
218         "trace-cmd:6559 [120] D ==> swapper/2:0 [120]",
219         SchedSwitch {
220             prev_proc_name: "trace-cmd".to_string(),
221             prev_pid: 6559,
222             prev_proc_state: ProcState::Sleep,
223             new_proc_name: "swapper/2".to_string(),
224             new_pid: 0,
225         }
226     )]
227     #[case(
228         "preempted",
229         "trace-cmd:6559 [120] R+ ==> swapper/2:0 [120]",
230         SchedSwitch {
231             prev_proc_name: "trace-cmd".to_string(),
232             prev_pid: 6559,
233             prev_proc_state: ProcState::Preempted,
234             new_proc_name: "swapper/2".to_string(),
235             new_pid: 0,
236         }
237     )]
238     // ... add more test cases as needed
test_parse_sched_switch(#[case] name: &str, #[case] line: &str, #[case] want: SchedSwitch)239     fn test_parse_sched_switch(#[case] name: &str, #[case] line: &str, #[case] want: SchedSwitch) {
240         assert_eq!(
241             parse_sched_switch(line).unwrap(),
242             want,
243             "Test case: {}",
244             name
245         );
246     }
247 
248     #[rstest]
249     #[case("crosvm_vcpu123", Some(123))]
250     #[case("crosvm_vcpu4", Some(4))]
251     #[case("invalid_format", None)]
252     #[case("crosvm_vcpuXYZ", None)]
253     #[case("", None)]
test_parse_vcpu_id(#[case] input: &str, #[case] expected_output: Option<usize>)254     fn test_parse_vcpu_id(#[case] input: &str, #[case] expected_output: Option<usize>) {
255         // Test logic
256         match super::parse_vcpu_id(input) {
257             Ok(id) => assert_eq!(id, expected_output.unwrap()),
258             Err(_) => assert!(expected_output.is_none()), // Assert an error was expected
259         }
260     }
261 }
262