Skip to main content

all_smi/parsing/
macros.rs

1//! Parsing macros for repeated text parsing patterns.
2
3// Copyright 2025 Lablup Inc. and Jeongkyu Shin
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17/// Parse a numeric metric value from a "Key: Value <SUFFIX>" line.
18/// - Extracts substring after the first ':'.
19/// - Takes the first whitespace-separated token.
20/// - Strips the provided suffix (e.g., "MHz", "mW") if present.
21/// - Additionally strips a trailing '%' if present (common in residency values).
22/// - Parses the remainder into the requested numeric type.
23///
24/// Returns Option<T> (None if parsing fails).
25///
26/// # Safety
27/// This macro does not panic. Returns None for invalid input.
28#[macro_export]
29macro_rules! parse_metric {
30    ($line:expr, $suffix:expr, $ty:ty) => {{
31        let opt = $crate::parsing::common::after_colon_trimmed($line)
32            .and_then(|rest| rest.split_whitespace().next())
33            .map(|tok| {
34                let no_suffix = tok.trim_end_matches($suffix);
35                // Common pattern: percentages like "64.29%"
36                no_suffix.trim_end_matches('%').to_string()
37            })
38            .and_then(|num| $crate::parsing::common::parse_number::<$ty>(&num));
39        opt
40    }};
41}
42
43/// Parse a Prometheus-formatted metric line using a regex with 3 capture groups:
44/// 1) metric name without the `all_smi_` prefix
45/// 2) labels content inside braces `{}`
46/// 3) numeric value
47///
48/// Example regex: r"^all_smi_([^\{]+)\{([^}]+)\} ([\d\.]+)$"
49/// Returns Option<(String, String, f64)>
50///
51/// # Safety
52/// This macro does not panic. Returns None for invalid input or regex mismatches.
53#[macro_export]
54macro_rules! parse_prometheus {
55    ($line:expr, $re:expr) => {{
56        if let Some(cap) = $re.captures($line.trim()) {
57            let name = cap.get(1).map(|m| m.as_str().to_string());
58            let labels = cap.get(2).map(|m| m.as_str().to_string());
59            let value = cap
60                .get(3)
61                .and_then(|m| m.as_str().parse::<f64>().ok())
62                .unwrap_or(0.0);
63            if let (Some(name), Some(labels)) = (name, labels) {
64                // Add length validation that was previously enforced by bounded quantifiers
65                if name.len() > 256 || labels.len() > 1024 {
66                    None
67                } else {
68                    Some((name, labels, value))
69                }
70            } else {
71                None
72            }
73        } else {
74            None
75        }
76    }};
77}
78
79/// Extract a label value from a HashMap and insert it into a detail HashMap with a given key.
80/// Useful for processing Prometheus label data.
81///
82/// Example usage:
83/// ```ignore
84/// extract_label_to_detail!(labels, "cuda_version", gpu_info.detail, "cuda_version");
85/// ```
86///
87/// # Safety
88/// This macro does not panic. Silently skips if the label is not found.
89#[macro_export]
90macro_rules! extract_label_to_detail {
91    ($labels:expr, $label_key:expr, $detail_map:expr, $detail_key:expr) => {
92        if let Some(value) = $labels.get($label_key) {
93            $detail_map.insert($detail_key.to_string(), value.clone());
94        }
95    };
96    // Variant that uses the same key for both label and detail
97    ($labels:expr, $key:expr, $detail_map:expr) => {
98        extract_label_to_detail!($labels, $key, $detail_map, $key);
99    };
100}
101
102/// Process multiple label extractions in a batch.
103/// Takes a list of label keys and inserts them into the detail map.
104/// Optimized to perform single HashMap lookup per key.
105///
106/// Example usage:
107/// ```ignore
108/// extract_labels_batch!(
109///     labels, gpu_info.detail,
110///     ["cuda_version", "driver_version", "architecture", "compute_capability"]
111/// );
112/// ```
113#[macro_export]
114macro_rules! extract_labels_batch {
115    ($labels:expr, $detail_map:expr, [$($key:expr),* $(,)?]) => {
116        $(
117            if let Some(value) = $labels.get($key) {
118                $detail_map.insert($key.to_string(), value.clone());
119            }
120        )*
121    };
122}
123
124/// Update a struct field based on a metric name match.
125/// Reduces repetitive match arms to single macro calls.
126/// Uses saturating casts to prevent overflow/underflow.
127///
128/// Example usage:
129/// ```ignore
130/// update_metric_field!(metric_name, value, gpu_info, {
131///     "gpu_utilization" => utilization as f64,
132///     "gpu_memory_used_bytes" => used_memory as u64,
133///     "gpu_temperature_celsius" => temperature as u32
134/// });
135/// ```
136#[macro_export]
137macro_rules! update_metric_field {
138    ($metric_name:expr, $value:expr, $target:expr, {
139        $($name:expr => $field:ident as $type:ty),* $(,)?
140    }) => {
141        match $metric_name {
142            $(
143                $name => {
144                    // Use saturating conversions for integer types to prevent overflow
145                    #[allow(unused_comparisons)]
146                    let safe_value = if $value < 0.0 {
147                        0 as $type
148                    } else if $value > (<$type>::MAX as f64) {
149                        <$type>::MAX
150                    } else {
151                        $value as $type
152                    };
153                    $target.$field = safe_value;
154                },
155            )*
156            _ => {}
157        }
158    };
159}
160
161/// Extract a label value from a HashMap with a default if not present.
162/// Returns the value or a default. Uses efficient borrowing when possible.
163///
164/// Example usage:
165/// ```ignore
166/// let gpu_name = get_label_or_default!(labels, "gpu");
167/// let gpu_index = get_label_or_default!(labels, "index", "0");
168/// ```
169#[macro_export]
170macro_rules! get_label_or_default {
171    ($labels:expr, $key:expr) => {
172        $labels
173            .get($key)
174            .map(|s| s.as_str())
175            .unwrap_or("")
176            .to_string()
177    };
178    ($labels:expr, $key:expr, $default:expr) => {
179        $labels
180            .get($key)
181            .map(|s| s.to_string())
182            .unwrap_or_else(|| $default.to_string())
183    };
184}
185
186/// Update a field within an optional struct field.
187/// Useful for updating fields in optional nested structures like apple_silicon_info.
188///
189/// Example usage:
190/// ```ignore
191/// update_optional_field!(cpu_info, apple_silicon_info, p_core_count, value as u32);
192/// ```
193#[macro_export]
194macro_rules! update_optional_field {
195    ($parent:expr, $optional_field:ident, $field:ident, $value:expr) => {
196        if let Some(ref mut inner) = $parent.$optional_field {
197            inner.$field = $value;
198        }
199    };
200}
201
202/// Extract fields from a struct and insert them into a HashMap.
203/// Useful for populating detail HashMaps from device structs.
204/// Optimized to avoid redundant allocations for static strings.
205///
206/// Example usage:
207/// ```ignore
208/// extract_struct_fields!(detail, device, {
209///     "serial_number" => device_sn,
210///     "firmware_version" => firmware,
211///     "pci_bdf" => pci_bdf
212/// });
213/// ```
214#[macro_export]
215macro_rules! extract_struct_fields {
216    ($detail:expr, $source:expr, {
217        $($key:literal => $field:ident),* $(,)?
218    }) => {
219        $(
220            $detail.insert($key.into(), $source.$field.clone());
221        )*
222    };
223}
224
225/// Insert optional fields from a struct into a HashMap if they exist.
226/// Skips None values automatically.
227/// Optimized to avoid redundant allocations for static strings.
228///
229/// Example usage:
230/// ```ignore
231/// insert_optional_fields!(detail, static_info, {
232///     "PCIe Address" => pcie_address,
233///     "PCIe Vendor ID" => pcie_vendor_id,
234///     "PCIe Device ID" => pcie_device_id
235/// });
236/// ```
237#[macro_export]
238macro_rules! insert_optional_fields {
239    ($detail:expr, $source:expr, {
240        $($key:literal => $field:ident),* $(,)?
241    }) => {
242        $(
243            if let Some(ref value) = $source.$field {
244                $detail.insert($key.into(), value.clone());
245            }
246        )*
247    };
248}
249
250/// Parse a value after a colon with optional type conversion.
251/// Simple utility for "Key: Value" parsing patterns.
252///
253/// Example usage:
254/// ```ignore
255/// let frequency = parse_colon_value!(line, u32);
256/// let temperature = parse_colon_value!(line, f64);
257/// ```
258#[macro_export]
259macro_rules! parse_colon_value {
260    ($line:expr, $type:ty) => {
261        $line
262            .split(':')
263            .nth(1)
264            .and_then(|s| s.split_whitespace().next())
265            .and_then(|s| s.parse::<$type>().ok())
266    };
267}
268
269/// Parse a line starting with a specific prefix and extract the value.
270/// Useful for consistent prefix-based parsing.
271///
272/// Example usage:
273/// ```ignore
274/// if line.starts_with("CPU Temperature:") {
275///     let temp = parse_prefixed_line!(line, "CPU Temperature:", f64);
276/// }
277/// ```
278#[macro_export]
279macro_rules! parse_prefixed_line {
280    ($line:expr, $prefix:expr, $type:ty) => {
281        if $line.starts_with($prefix) {
282            $line
283                .strip_prefix($prefix)
284                .and_then(|s| s.trim().split_whitespace().next())
285                .and_then(|s| s.parse::<$type>().ok())
286        } else {
287            None
288        }
289    };
290}
291
292#[cfg(test)]
293mod tests {
294    use regex::Regex;
295
296    #[test]
297    fn test_parse_metric_frequency() {
298        let line = "GPU HW active frequency: 444 MHz";
299        let v = parse_metric!(line, "MHz", u32);
300        assert_eq!(v, Some(444u32));
301    }
302
303    #[test]
304    fn test_parse_metric_percentage() {
305        let line = "E-Cluster HW active residency:  64.29% (details omitted)";
306        let v = parse_metric!(line, "%", f64);
307        assert!(v.is_some());
308        assert!((v.unwrap() - 64.29).abs() < 1e-6);
309    }
310
311    #[test]
312    fn test_parse_metric_power() {
313        let line = "CPU Power: 475 mW";
314        let v = parse_metric!(line, "mW", f64);
315        assert_eq!(v, Some(475.0));
316    }
317
318    #[test]
319    fn test_parse_metric_invalid() {
320        let line = "Invalid Line";
321        let v = parse_metric!(line, "MHz", u32);
322        assert!(v.is_none());
323    }
324
325    #[test]
326    fn test_parse_prometheus_success() {
327        let re = Regex::new(r"^all_smi_([^\{]+)\{([^}]+)\} ([\d\.]+)$").unwrap();
328        let line = r#"all_smi_gpu_utilization{gpu="RTX", uuid="GPU-1"} 25.5"#;
329        let parsed = parse_prometheus!(line, re);
330        assert!(parsed.is_some());
331        let (name, labels, value) = parsed.unwrap();
332        assert_eq!(name, "gpu_utilization");
333        assert!(labels.contains(r#"gpu="RTX""#));
334        assert_eq!(value, 25.5);
335    }
336
337    #[test]
338    fn test_parse_prometheus_invalid() {
339        let re = Regex::new(r"^all_smi_([^\{]+)\{([^}]+)\} ([\d\.]+)$").unwrap();
340        let line = "bad format";
341        let parsed = parse_prometheus!(line, re);
342        assert!(parsed.is_none());
343    }
344
345    #[test]
346    fn test_extract_label_to_detail() {
347        use std::collections::HashMap;
348
349        let mut labels = HashMap::new();
350        labels.insert("cuda_version".to_string(), "11.8".to_string());
351        labels.insert("driver_version".to_string(), "525.60.13".to_string());
352
353        let mut detail = HashMap::new();
354
355        extract_label_to_detail!(labels, "cuda_version", detail, "cuda_version");
356        assert_eq!(detail.get("cuda_version"), Some(&"11.8".to_string()));
357
358        extract_label_to_detail!(labels, "driver_version", detail);
359        assert_eq!(detail.get("driver_version"), Some(&"525.60.13".to_string()));
360
361        // Test non-existent label
362        extract_label_to_detail!(labels, "non_existent", detail);
363        assert_eq!(detail.get("non_existent"), None);
364    }
365
366    #[test]
367    fn test_extract_labels_batch() {
368        use std::collections::HashMap;
369
370        let mut labels = HashMap::new();
371        labels.insert("cuda_version".to_string(), "11.8".to_string());
372        labels.insert("driver_version".to_string(), "525.60.13".to_string());
373        labels.insert("architecture".to_string(), "Ampere".to_string());
374
375        let mut detail = HashMap::new();
376
377        extract_labels_batch!(
378            labels,
379            detail,
380            [
381                "cuda_version",
382                "driver_version",
383                "architecture",
384                "non_existent"
385            ]
386        );
387
388        assert_eq!(detail.get("cuda_version"), Some(&"11.8".to_string()));
389        assert_eq!(detail.get("driver_version"), Some(&"525.60.13".to_string()));
390        assert_eq!(detail.get("architecture"), Some(&"Ampere".to_string()));
391        assert_eq!(detail.get("non_existent"), None);
392    }
393
394    #[test]
395    fn test_update_metric_field() {
396        struct TestStruct {
397            utilization: f64,
398            memory: u64,
399            temperature: u32,
400        }
401
402        let mut test = TestStruct {
403            utilization: 0.0,
404            memory: 0,
405            temperature: 0,
406        };
407
408        let metric_name = "gpu_utilization";
409        let value = 75.5;
410
411        update_metric_field!(metric_name, value, test, {
412            "gpu_utilization" => utilization as f64,
413            "gpu_memory_used_bytes" => memory as u64,
414            "gpu_temperature_celsius" => temperature as u32
415        });
416
417        assert_eq!(test.utilization, 75.5);
418
419        let metric_name = "gpu_memory_used_bytes";
420        let value = 1024.0;
421
422        update_metric_field!(metric_name, value, test, {
423            "gpu_utilization" => utilization as f64,
424            "gpu_memory_used_bytes" => memory as u64,
425            "gpu_temperature_celsius" => temperature as u32
426        });
427
428        assert_eq!(test.memory, 1024);
429    }
430
431    #[test]
432    fn test_get_label_or_default() {
433        use std::collections::HashMap;
434
435        let mut labels = HashMap::new();
436        labels.insert("gpu".to_string(), "RTX 4090".to_string());
437        labels.insert("index".to_string(), "2".to_string());
438
439        let gpu_name = get_label_or_default!(labels, "gpu");
440        assert_eq!(gpu_name, "RTX 4090");
441
442        let non_existent = get_label_or_default!(labels, "non_existent");
443        assert_eq!(non_existent, "");
444
445        let custom_default = get_label_or_default!(labels, "non_existent", "N/A");
446        assert_eq!(custom_default, "N/A");
447
448        let index = get_label_or_default!(labels, "index", "0");
449        assert_eq!(index, "2");
450    }
451}