1use std::collections::HashMap;
11use std::sync::Mutex;
12
13use spark_connect_core::error::Result;
14use spark_connect_proto as proto;
15
16fn format_literal(literal: &proto::expression::Literal) -> String {
18 use proto::expression::literal::LiteralType;
19
20 match &literal.literal_type {
21 Some(LiteralType::Null(_)) => "null".to_string(),
22 Some(LiteralType::Boolean(b)) => b.to_string(),
23 Some(LiteralType::Byte(b)) => b.to_string(),
24 Some(LiteralType::Short(s)) => s.to_string(),
25 Some(LiteralType::Integer(i)) => i.to_string(),
26 Some(LiteralType::Long(l)) => l.to_string(),
27 Some(LiteralType::Float(f)) => f.to_string(),
28 Some(LiteralType::Double(d)) => d.to_string(),
29 Some(LiteralType::Decimal(_)) => "decimal".to_string(),
30 Some(LiteralType::String(s)) => s.clone(),
31 Some(LiteralType::Binary(b)) => format!("<binary: {} bytes>", b.len()),
32 Some(LiteralType::CalendarInterval(_)) => "calendar_interval".to_string(),
33 Some(LiteralType::YearMonthInterval(_)) => "year_month_interval".to_string(),
34 Some(LiteralType::DayTimeInterval(_)) => "day_time_interval".to_string(),
35 Some(LiteralType::Date(_)) => "date".to_string(),
36 Some(LiteralType::Timestamp(_)) => "timestamp".to_string(),
37 Some(LiteralType::TimestampNtz(_)) => "timestamp_ntz".to_string(),
38 Some(LiteralType::Time(_)) => "time".to_string(),
39 Some(LiteralType::TimestampNtzNanos(_)) => "timestamp_ntz_nanos".to_string(),
40 Some(LiteralType::TimestampLtzNanos(_)) => "timestamp_ltz_nanos".to_string(),
41 Some(LiteralType::Map(_)) => "map".to_string(),
42 Some(LiteralType::Array(_)) => "array".to_string(),
43 Some(LiteralType::Struct(_)) => "struct".to_string(),
44 Some(LiteralType::SpecializedArray(_)) => "specialized_array".to_string(),
45 None => "unknown".to_string(),
46 }
47}
48
49#[derive(Debug, Clone)]
51struct ProfileResult {
52 pub data: String,
54 #[allow(dead_code)]
58 pub metadata: HashMap<String, String>,
59}
60
61#[derive(Debug, Clone)]
66pub struct ProfilerCollector {
67 profiles: std::sync::Arc<Mutex<HashMap<i64, ProfileResult>>>,
69}
70
71impl ProfilerCollector {
72 pub fn new() -> Self {
74 ProfilerCollector {
75 profiles: std::sync::Arc::new(Mutex::new(HashMap::new())),
76 }
77 }
78
79 pub(crate) fn accumulate_profile(
81 &self,
82 id: i64,
83 data: String,
84 metadata: HashMap<String, String>,
85 ) {
86 let mut profiles = self.profiles.lock().unwrap();
87 profiles.insert(id, ProfileResult { data, metadata });
88 }
89
90 pub(crate) fn accumulate_observed_metrics(
92 &self,
93 metrics: &[proto::execute_plan_response::ObservedMetrics],
94 ) {
95 for metric in metrics {
96 let id = metric.plan_id;
98 let mut data = String::new();
99 let mut metadata = HashMap::new();
100
101 let num_values = metric.values.len();
103 for i in 0..num_values {
104 let key = if i < metric.keys.len() {
105 metric.keys[i].clone()
106 } else {
107 format!("value_{}", i)
108 };
109
110 let value_str = format_literal(&metric.values[i]);
112
113 metadata.insert(key.clone(), value_str.clone());
114 if !data.is_empty() {
116 data.push('\n');
117 }
118 data.push_str(&format!("{}: {}", key, value_str));
119 }
120
121 if !data.is_empty() {
123 data.insert_str(0, &format!("name: {}, plan_id: {}\n", metric.name, id));
124 }
125
126 if !data.is_empty() || !metadata.is_empty() {
128 self.accumulate_profile(id, data, metadata);
129 }
130 }
131 }
132
133 pub fn show(&self, id: Option<i64>) -> String {
137 let profiles = self.profiles.lock().unwrap();
138
139 match id {
140 Some(profile_id) => profiles
141 .get(&profile_id)
142 .map(|p| p.data.clone())
143 .unwrap_or_else(|| format!("No profile data for id: {}", profile_id)),
144 None => {
145 if profiles.is_empty() {
147 "No profile data collected".to_string()
148 } else {
149 let mut result = String::new();
150 for (id, profile) in profiles.iter() {
151 result.push_str(&format!("=== Profile {} ===\n", id));
152 result.push_str(&profile.data);
153 result.push('\n');
154 }
155 result
156 }
157 }
158 }
159 }
160
161 pub fn dump(&self, id: Option<i64>, path: &str) -> Result<()> {
166 let data = self.show(id);
167
168 std::fs::write(path, data).map_err(|e| {
169 spark_connect_core::error::SparkError::connect_msg(format!(
170 "Failed to write profile to {}: {}",
171 path, e
172 ))
173 })?;
174
175 Ok(())
176 }
177
178 pub fn clear(&self, id: Option<i64>) {
180 let mut profiles = self.profiles.lock().unwrap();
181
182 match id {
183 Some(profile_id) => {
184 profiles.remove(&profile_id);
185 }
186 None => {
187 profiles.clear();
188 }
189 }
190 }
191
192 #[cfg(test)]
194 pub(crate) fn count(&self) -> usize {
195 self.profiles.lock().unwrap().len()
196 }
197}
198
199impl Default for ProfilerCollector {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_profiler_collector_accumulate_and_show() {
211 let collector = ProfilerCollector::new();
212
213 let mut metadata = HashMap::new();
215 metadata.insert("type".to_string(), "udf".to_string());
216 collector.accumulate_profile(1, "profile data 1".to_string(), metadata);
217
218 let result = collector.show(Some(1));
220 assert_eq!(result, "profile data 1");
221
222 let all = collector.show(None);
224 assert!(all.contains("profile data 1"));
225 assert!(all.contains("=== Profile 1 ==="));
226 }
227
228 #[test]
229 fn test_profiler_collector_show_empty() {
230 let collector = ProfilerCollector::new();
231
232 let result = collector.show(None);
233 assert_eq!(result, "No profile data collected");
234
235 let result = collector.show(Some(99));
236 assert_eq!(result, "No profile data for id: 99");
237 }
238
239 #[test]
240 fn test_profiler_collector_clear() {
241 let collector = ProfilerCollector::new();
242
243 let metadata = HashMap::new();
244 collector.accumulate_profile(1, "profile 1".to_string(), metadata.clone());
245 collector.accumulate_profile(2, "profile 2".to_string(), metadata);
246
247 assert_eq!(collector.count(), 2);
248
249 collector.clear(Some(1));
251 assert_eq!(collector.count(), 1);
252
253 collector.clear(None);
255 assert_eq!(collector.count(), 0);
256 }
257
258 #[test]
259 fn test_profiler_collector_dump_and_read() {
260 let collector = ProfilerCollector::new();
261 let temp_dir = std::env::temp_dir();
262 let path = temp_dir.join("test_profile_dump.txt");
263
264 let metadata = HashMap::new();
265 collector.accumulate_profile(1, "test profile data".to_string(), metadata);
266
267 let result = collector.dump(Some(1), path.to_str().unwrap());
269 assert!(result.is_ok());
270
271 let contents = std::fs::read_to_string(&path).unwrap();
273 assert_eq!(contents, "test profile data");
274
275 let _ = std::fs::remove_file(path);
277 }
278
279 #[test]
280 fn test_profiler_collector_accumulate_observed_metrics() {
281 let collector = ProfilerCollector::new();
282
283 let mut metric = proto::execute_plan_response::ObservedMetrics::default();
285 metric.name = "udf_profile".to_string();
286 metric.plan_id = 1;
287 metric.keys = vec!["udf_time_ms".to_string(), "python_calls".to_string()];
288
289 metric.values.push(proto::expression::Literal {
291 literal_type: Some(proto::expression::literal::LiteralType::Long(150)),
292 data_type: None,
293 });
294 metric.values.push(proto::expression::Literal {
295 literal_type: Some(proto::expression::literal::LiteralType::Long(42)),
296 data_type: None,
297 });
298
299 collector.accumulate_observed_metrics(&[metric]);
300
301 let result = collector.show(Some(1));
303 assert!(result.contains("udf_time_ms: 150"));
304 assert!(result.contains("python_calls: 42"));
305 }
306
307 #[test]
308 fn format_literal_covers_all_variants() {
309 use proto::expression::literal::{self, LiteralType};
310 let lit = |lt: LiteralType| proto::expression::Literal {
311 literal_type: Some(lt),
312 data_type: None,
313 };
314 assert_eq!(
315 format_literal(&lit(LiteralType::Null(proto::DataType::default()))),
316 "null"
317 );
318 assert_eq!(format_literal(&lit(LiteralType::Boolean(true))), "true");
319 assert_eq!(format_literal(&lit(LiteralType::Byte(1))), "1");
320 assert_eq!(format_literal(&lit(LiteralType::Short(2))), "2");
321 assert_eq!(format_literal(&lit(LiteralType::Integer(3))), "3");
322 assert_eq!(format_literal(&lit(LiteralType::Long(4))), "4");
323 assert_eq!(format_literal(&lit(LiteralType::Float(1.5))), "1.5");
324 assert_eq!(format_literal(&lit(LiteralType::Double(2.5))), "2.5");
325 assert_eq!(
326 format_literal(&lit(LiteralType::Decimal(literal::Decimal::default()))),
327 "decimal"
328 );
329 assert_eq!(
330 format_literal(&lit(LiteralType::String("hi".to_string()))),
331 "hi"
332 );
333 assert!(
334 format_literal(&lit(LiteralType::Binary(vec![1u8, 2, 3].into()))).contains("3 bytes")
335 );
336 assert_eq!(
337 format_literal(&lit(LiteralType::CalendarInterval(
338 literal::CalendarInterval::default()
339 ))),
340 "calendar_interval"
341 );
342 assert_eq!(
343 format_literal(&lit(LiteralType::YearMonthInterval(0))),
344 "year_month_interval"
345 );
346 assert_eq!(
347 format_literal(&lit(LiteralType::DayTimeInterval(0))),
348 "day_time_interval"
349 );
350 assert_eq!(format_literal(&lit(LiteralType::Date(0))), "date");
351 assert_eq!(format_literal(&lit(LiteralType::Timestamp(0))), "timestamp");
352 assert_eq!(
353 format_literal(&lit(LiteralType::TimestampNtz(0))),
354 "timestamp_ntz"
355 );
356 assert_eq!(
357 format_literal(&lit(LiteralType::Time(literal::Time::default()))),
358 "time"
359 );
360 assert_eq!(
361 format_literal(&lit(LiteralType::TimestampNtzNanos(
362 literal::TimestampNtzNanos::default()
363 ))),
364 "timestamp_ntz_nanos"
365 );
366 assert_eq!(
367 format_literal(&lit(LiteralType::TimestampLtzNanos(
368 literal::TimestampLtzNanos::default()
369 ))),
370 "timestamp_ltz_nanos"
371 );
372 assert_eq!(
373 format_literal(&lit(LiteralType::Map(literal::Map::default()))),
374 "map"
375 );
376 assert_eq!(
377 format_literal(&lit(LiteralType::Array(literal::Array::default()))),
378 "array"
379 );
380 assert_eq!(
381 format_literal(&lit(LiteralType::Struct(literal::Struct::default()))),
382 "struct"
383 );
384 assert_eq!(
385 format_literal(&lit(LiteralType::SpecializedArray(
386 literal::SpecializedArray::default()
387 ))),
388 "specialized_array"
389 );
390 assert_eq!(
392 format_literal(&proto::expression::Literal {
393 literal_type: None,
394 data_type: None,
395 }),
396 "unknown"
397 );
398 }
399
400 #[test]
401 fn accumulate_observed_metrics_more_values_than_keys() {
402 let collector = ProfilerCollector::new();
404 let mut metric = proto::execute_plan_response::ObservedMetrics::default();
405 metric.name = "m".to_string();
406 metric.plan_id = 7;
407 metric.keys = vec!["a".to_string()];
408 metric.values.push(proto::expression::Literal {
409 literal_type: Some(proto::expression::literal::LiteralType::Long(1)),
410 data_type: None,
411 });
412 metric.values.push(proto::expression::Literal {
413 literal_type: Some(proto::expression::literal::LiteralType::Long(2)),
414 data_type: None,
415 });
416 collector.accumulate_observed_metrics(&[metric]);
417 let shown = collector.show(Some(7));
418 assert!(shown.contains("a: 1"));
419 assert!(shown.contains("value_1: 2"));
420 }
421
422 #[test]
423 fn dump_to_unwritable_path_errors() {
424 let collector = ProfilerCollector::new();
425 collector.accumulate_profile(1, "data".to_string(), HashMap::new());
426 let res = collector.dump(Some(1), "/nonexistent_dir_zzz_12345/profile.txt");
428 assert!(res.is_err());
429 }
430
431 #[test]
432 fn default_constructs_empty_collector() {
433 let collector = ProfilerCollector::default();
434 assert_eq!(collector.count(), 0);
435 }
436}