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