1use fidl_fuchsia_hardware_power_stats as fpowerstats;
9use starnix_core::device::power_stats::PowerStatsManager;
10use starnix_core::fs::sysfs::build_device_directory;
11use starnix_core::task::{CurrentTask, Kernel};
12use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
13use starnix_logging::log_warn;
14use starnix_uapi::errno;
15use starnix_uapi::errors::Errno;
16use starnix_uapi::file_mode::mode;
17use std::borrow::Cow;
18use std::collections::{BTreeMap, HashSet};
19use std::fmt::Write as _;
20
21const PERF_STATE_FILE_PREFIX: &str = "lpcm_";
24
25const POWER_DOMAIN_FILE_PREFIX: &str = "pwrblk_";
28
29const PLATFORM_POWER_ENTITY: &str = "soc_power_state";
31
32const PLATFORM_POWER_FILE: &str = "platform_power_residency";
34
35const RESIDENCY_DEVICE_NAME: &str = "powerdashboard";
37
38const POWER_STATE_STATS_ENTITY: &str = "cap_power_state";
40
41const POWER_STATE_STATS_FILE: &str = "power_state_stats";
43
44const POWER_STATE_STATS_DEVICE_NAME: &str = "cap_sysfs";
46
47const TIMESTAMP_TICKS_PER_MS: u64 = 38_400;
53
54type FormatResidency = fn(&fpowerstats::Stats) -> String;
56
57struct ResidencyFileSpec {
59 device: &'static str,
61 file: String,
63 entity: String,
65 format: FormatResidency,
66}
67
68pub fn init_power_residency_device(kernel: &Kernel) {
75 let mut added_devices: HashSet<&'static str> = HashSet::new();
76 let manager = PowerStatsManager::get_or_init(kernel);
77 let manager_for_files = manager.clone();
78 manager.watch_providers(kernel, move |kernel, provider| {
79 let mut files_by_device: BTreeMap<&'static str, Vec<ResidencyFileSpec>> = BTreeMap::new();
80 for spec in provider.entity_names().iter().filter_map(|name| residency_file_spec(name)) {
81 files_by_device.entry(spec.device).or_default().push(spec);
82 }
83 let registry = &kernel.device_registry;
84 for (device_name, files) in files_by_device {
85 if !added_devices.insert(device_name) {
86 log_warn!(
87 "Ignoring {device_name} power residency entities from additional provider {}",
88 provider.instance_name()
89 );
90 continue;
91 }
92 registry.add_bus_device(
93 device_name.into(),
94 Some(registry.objects.platform_device()),
95 registry.objects.platform_bus(),
96 |device, dir| {
97 build_device_directory(device, dir);
98 for spec in files {
99 let file = ResidencyFile {
100 entity: spec.entity,
101 format: spec.format,
102 manager: manager_for_files.clone(),
103 };
104 dir.entry(&spec.file, BytesFile::new_node(file), mode!(IFREG, 0o444));
105 }
106 },
107 );
108 }
109 });
110}
111
112fn residency_file_spec(entity: &str) -> Option<ResidencyFileSpec> {
115 let prefixed = |prefix: &str| {
116 entity.len() > prefix.len()
117 && entity.starts_with(prefix)
118 && entity.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
119 };
120 let (device, file, format): (_, _, FormatResidency) = if prefixed(PERF_STATE_FILE_PREFIX) {
121 (RESIDENCY_DEVICE_NAME, entity.to_string(), format_performance_states)
122 } else if prefixed(POWER_DOMAIN_FILE_PREFIX) {
123 (RESIDENCY_DEVICE_NAME, entity.to_string(), format_power_domain_states)
124 } else if entity == PLATFORM_POWER_ENTITY {
125 (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string(), format_platform_power_states)
126 } else if entity == POWER_STATE_STATS_ENTITY {
127 (
128 POWER_STATE_STATS_DEVICE_NAME,
129 POWER_STATE_STATS_FILE.to_string(),
130 format_power_state_stats,
131 )
132 } else {
133 return None;
134 };
135 Some(ResidencyFileSpec { device, file, entity: entity.to_string(), format })
136}
137
138struct ResidencyFile {
140 entity: String,
141 format: FormatResidency,
142 manager: PowerStatsManager,
143}
144
145impl BytesFileOps for ResidencyFile {
146 fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
147 let stats = self.manager.entity_stats(&self.entity)?.ok_or_else(|| {
148 log_warn!("No power stats provider registered for {}", self.entity);
149 errno!(EIO)
150 })?;
151 Ok((self.format)(&stats).into_bytes().into())
152 }
153}
154
155fn safe_states(stats: &fpowerstats::Stats) -> impl Iterator<Item = &fpowerstats::StateResidency> {
161 stats.state_residency_data.iter().filter(|state| {
162 if state.name.chars().any(char::is_control) {
163 log_warn!("Leaving out {} state {} with an unsafe name", stats.entity, state.id);
164 return false;
165 }
166 true
167 })
168}
169
170fn format_performance_states(stats: &fpowerstats::Stats) -> String {
175 let mut out = String::new();
176 let _ = writeln!(out, "num_pf_states: {}", stats.state_residency_data.len());
177 for state in &stats.state_residency_data {
178 let time_in_state_us = state.total_time_ms.saturating_mul(1000);
179 let _ = writeln!(out, "pf_state: {}", state.id);
180 let _ = writeln!(out, "pf_entry_count: {}", state.entry_count);
181 let _ = writeln!(out, "pf_time_in_state: {time_in_state_us} (us)\n");
182 }
183 out
184}
185
186fn format_power_domain_states(stats: &fpowerstats::Stats) -> String {
196 let mut out = String::new();
197 let _ = writeln!(out, "\nSSWRP lpb\n");
198 for state in safe_states(stats) {
199 let time_in_state_us = state.total_time_ms.saturating_mul(1000);
200 let last_entry_us = state.last_entry_timestamp_ms.saturating_mul(1000);
201 let _ = writeln!(out, "power_state: {}", state.name);
202 let _ = writeln!(out, "entry_count: {}", state.entry_count);
203 let _ = writeln!(out, "time_in_state: {time_in_state_us} (us)");
204 let _ = writeln!(out, "last_entry_timestamp: {last_entry_us}");
205 let _ = writeln!(out, "last_exit_timestamp: 0");
206 }
207 out.push('\n');
208 out
209}
210
211fn format_platform_power_states(stats: &fpowerstats::Stats) -> String {
220 let mut out = String::new();
221 for state in safe_states(stats) {
222 let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
223 let _ = writeln!(out, "power_state: {}", state.name);
224 let _ = writeln!(out, "entry_count: {}", state.entry_count);
225 let _ = writeln!(out, "time_in_state_ms: {}", state.total_time_ms);
226 let _ = writeln!(out, "last_entry_timestamp_ticks: {last_entry_ticks}");
227 let _ = writeln!(out, "last_exit_timestamp_ticks: 0\n");
228 }
229 out
230}
231
232fn format_power_state_stats(stats: &fpowerstats::Stats) -> String {
241 let mut out = String::new();
242 for state in safe_states(stats) {
243 let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
244 let _ = writeln!(out, "---");
245 let _ = writeln!(out, "state: {}", state.name);
246 let _ = writeln!(out, "last_entry_ts_ticks: {last_entry_ticks}");
247 let _ = writeln!(out, "last_exit_ts_ticks: 0");
248 let _ = writeln!(out, "total_state_residency_ms: {}", state.total_time_ms);
249 let _ = writeln!(out, "last_state_entry_counts: {}", state.entry_count);
250 }
251 out
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use fidl::endpoints::{ServerEnd, create_sync_proxy};
258 use fuchsia_async as fasync;
259 use futures::StreamExt;
260 use starnix_core::device::power_stats::PowerStatsProvider;
261 use starnix_core::testing::spawn_kernel_and_run_sync;
262 use starnix_uapi::error;
263
264 fn state(id: i32, entry_count: u32, total_time_ms: u64) -> fpowerstats::StateResidency {
265 fpowerstats::StateResidency {
266 id,
267 name: id.to_string(),
268 total_time_ms,
269 entry_count,
270 last_entry_timestamp_ms: 0,
271 }
272 }
273
274 fn stats(
275 entity: &str,
276 state_residency_data: Vec<fpowerstats::StateResidency>,
277 ) -> fpowerstats::Stats {
278 fpowerstats::Stats { entity: entity.to_string(), state_residency_data }
279 }
280
281 async fn serve_provider(
283 stream: fpowerstats::DeviceRequestStream,
284 reports: Vec<fpowerstats::Stats>,
285 ) {
286 let _ = stream
287 .for_each(|request| {
288 let reports = &reports;
289 async move {
290 if let Ok(fpowerstats::DeviceRequest::GetStats { responder }) = request {
291 let _ = responder.send(reports);
292 }
293 }
294 })
295 .await;
296 }
297
298 fn spawn_server<F: Future<Output = ()> + 'static>(serve: impl FnOnce() -> F + Send + 'static) {
300 std::thread::spawn(move || fasync::LocalExecutor::default().run_singlethreaded(serve()));
301 }
302
303 fn provider_with_entities(
306 instance_name: &str,
307 entity_names: impl IntoIterator<Item = impl Into<String>>,
308 ) -> PowerStatsProvider {
309 fake_provider(
310 instance_name,
311 entity_names.into_iter().map(|name| stats(&name.into(), vec![])).collect(),
312 )
313 }
314
315 fn fake_provider(instance_name: &str, reports: Vec<fpowerstats::Stats>) -> PowerStatsProvider {
317 let entity_names: Vec<_> = reports.iter().map(|stats| stats.entity.clone()).collect();
318 let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
319 spawn_server(move || serve_provider(server.into_stream(), reports));
320 PowerStatsProvider::new(instance_name, entity_names, proxy)
321 }
322
323 fn perf_file(entity: &str, manager: PowerStatsManager) -> ResidencyFile {
325 ResidencyFile { entity: entity.to_string(), format: format_performance_states, manager }
326 }
327
328 fn exists(kernel: &Kernel, path: &str) -> bool {
329 kernel.device_registry.objects.root.lookup(path.into()).is_some()
330 }
331
332 #[::fuchsia::test]
333 async fn device_is_added_with_served_entities() {
334 spawn_kernel_and_run_sync(|current_task| {
335 let kernel = current_task.kernel();
336 let prefix = PERF_STATE_FILE_PREFIX;
337 PowerStatsManager::add_provider_for_testing(
338 kernel,
339 provider_with_entities(
340 "default",
341 [
342 format!("{prefix}a"),
343 format!("{prefix}b"),
344 PLATFORM_POWER_ENTITY.to_string(),
345 "other".to_string(),
346 ],
347 ),
348 );
349 init_power_residency_device(kernel);
350
351 let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
352 assert!(exists(kernel, &device));
353 assert!(exists(kernel, &format!("bus/platform/devices/{RESIDENCY_DEVICE_NAME}")));
354 assert!(exists(kernel, &format!("{device}/uevent")));
355 assert!(exists(kernel, &format!("{device}/{prefix}a")));
356 assert!(exists(kernel, &format!("{device}/{prefix}b")));
357 assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
358 assert!(!exists(kernel, &format!("{device}/other")));
359 })
360 .await;
361 }
362
363 #[::fuchsia::test]
364 async fn no_device_without_served_entities() {
365 spawn_kernel_and_run_sync(|current_task| {
366 let kernel = current_task.kernel();
367 PowerStatsManager::add_provider_for_testing(
368 kernel,
369 provider_with_entities("default", ["other"]),
370 );
371 init_power_residency_device(kernel);
372
373 assert!(!exists(kernel, &format!("devices/platform/{RESIDENCY_DEVICE_NAME}")));
374 assert!(!exists(kernel, &format!("devices/platform/{POWER_STATE_STATS_DEVICE_NAME}")));
375 })
376 .await;
377 }
378
379 #[::fuchsia::test]
380 async fn device_is_added_when_later_provider_reports_served_entities() {
381 spawn_kernel_and_run_sync(|current_task| {
382 let kernel = current_task.kernel();
383 let prefix = PERF_STATE_FILE_PREFIX;
384 let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
385
386 let _ = PowerStatsManager::get_or_init_for_testing(kernel);
387 init_power_residency_device(kernel);
388 assert!(!exists(kernel, &device));
389
390 PowerStatsManager::add_provider_for_testing(
391 kernel,
392 provider_with_entities("unserved", ["other"]),
393 );
394 assert!(!exists(kernel, &device));
395
396 PowerStatsManager::add_provider_for_testing(
397 kernel,
398 provider_with_entities(
399 "served",
400 [format!("{prefix}a"), PLATFORM_POWER_ENTITY.to_string()],
401 ),
402 );
403 assert!(exists(kernel, &device));
404 assert!(exists(kernel, &format!("{device}/{prefix}a")));
405 assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
406 })
407 .await;
408 }
409
410 #[::fuchsia::test]
411 async fn only_first_provider_is_served() {
412 spawn_kernel_and_run_sync(|current_task| {
413 let kernel = current_task.kernel();
414 let prefix = PERF_STATE_FILE_PREFIX;
415 PowerStatsManager::add_provider_for_testing(
416 kernel,
417 provider_with_entities("first", [format!("{prefix}a")]),
418 );
419 init_power_residency_device(kernel);
420 PowerStatsManager::add_provider_for_testing(
421 kernel,
422 provider_with_entities("second", [format!("{prefix}b")]),
423 );
424
425 let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
426 assert!(exists(kernel, &format!("{device}/{prefix}a")));
427 assert!(!exists(kernel, &format!("{device}/{prefix}b")));
428 })
429 .await;
430 }
431
432 #[::fuchsia::test]
433 async fn each_device_is_added_for_first_provider_serving_it() {
434 spawn_kernel_and_run_sync(|current_task| {
435 let kernel = current_task.kernel();
436 let prefix = PERF_STATE_FILE_PREFIX;
437 PowerStatsManager::add_provider_for_testing(
438 kernel,
439 provider_with_entities("first", [format!("{prefix}a")]),
440 );
441 init_power_residency_device(kernel);
442 PowerStatsManager::add_provider_for_testing(
443 kernel,
444 provider_with_entities(
445 "second",
446 [format!("{prefix}b"), POWER_STATE_STATS_ENTITY.to_string()],
447 ),
448 );
449
450 let residency_device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
451 assert!(exists(kernel, &format!("{residency_device}/{prefix}a")));
452 assert!(!exists(kernel, &format!("{residency_device}/{prefix}b")));
453 let stats_device = format!("devices/platform/{POWER_STATE_STATS_DEVICE_NAME}");
454 assert!(exists(
455 kernel,
456 &format!("bus/platform/devices/{POWER_STATE_STATS_DEVICE_NAME}")
457 ));
458 assert!(exists(kernel, &format!("{stats_device}/{POWER_STATE_STATS_FILE}")));
459 })
460 .await;
461 }
462
463 #[::fuchsia::test]
464 async fn read_returns_formatted_residency() {
465 spawn_kernel_and_run_sync(|current_task| {
466 let kernel = current_task.kernel();
467 let entities = [
468 stats(
469 "lpcm_a",
470 vec![state(
471 0, 11,
472 2200,
473 )],
474 ),
475 stats(
476 "pwrblk_a",
477 vec![state(
478 0, 5,
479 40,
480 )],
481 ),
482 stats(
483 PLATFORM_POWER_ENTITY,
484 vec![state(
485 0, 3,
486 100,
487 )],
488 ),
489 stats(
490 POWER_STATE_STATS_ENTITY,
491 vec![state(
492 0, 6,
493 250,
494 )],
495 ),
496 ];
497 PowerStatsManager::add_provider_for_testing(
498 kernel,
499 fake_provider("test", entities.to_vec()),
500 );
501 let manager = PowerStatsManager::get_or_init(kernel);
502
503 for entity in &entities {
504 let spec = residency_file_spec(&entity.entity).expect("entity is served");
505 let file = ResidencyFile {
506 entity: spec.entity,
507 format: spec.format,
508 manager: manager.clone(),
509 };
510 assert_eq!(
511 &*file.read(current_task).expect("read residency file"),
512 (spec.format)(entity).as_bytes(),
513 "{}",
514 entity.entity
515 );
516 }
517 })
518 .await;
519 }
520
521 #[::fuchsia::test]
522 async fn read_fails_when_entity_is_no_longer_reported() {
523 spawn_kernel_and_run_sync(|current_task| {
524 let kernel = current_task.kernel();
525 let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
526 let reports = vec![stats("b", vec![])];
527 spawn_server(move || serve_provider(server.into_stream(), reports));
528 PowerStatsManager::add_provider_for_testing(
529 kernel,
530 PowerStatsProvider::new("test", ["a", "b"], proxy),
531 );
532 let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
533
534 assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
535 })
536 .await;
537 }
538
539 #[::fuchsia::test]
540 async fn read_fails_when_provider_is_gone_and_recovers_on_restart() {
541 spawn_kernel_and_run_sync(|current_task| {
542 let kernel = current_task.kernel();
543 let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
544 drop::<ServerEnd<_>>(server);
545 PowerStatsManager::add_provider_for_testing(
546 kernel,
547 PowerStatsProvider::new("test", ["a", "b"], proxy),
548 );
549 let manager = PowerStatsManager::get_or_init(kernel);
550 let file_a = perf_file("a", manager.clone());
551 let file_b = perf_file("b", manager);
552
553 assert_eq!(file_a.read(current_task).map(|_| ()), error!(EIO));
554 assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
555
556 let entity = stats(
557 "a",
558 vec![state(
559 0, 5, 100,
560 )],
561 );
562 PowerStatsManager::add_provider_for_testing(
563 kernel,
564 fake_provider("test_restarted", vec![entity.clone()]),
565 );
566 let contents = file_a.read(current_task).expect("read after restart");
567 assert_eq!(&*contents, format_performance_states(&entity).as_bytes());
568 assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
569 })
570 .await;
571 }
572
573 #[::fuchsia::test]
574 async fn read_fails_when_provider_does_not_respond() {
575 spawn_kernel_and_run_sync(|current_task| {
576 let kernel = current_task.kernel();
577 let (proxy, _server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
579 PowerStatsManager::add_provider_for_testing(
580 kernel,
581 PowerStatsProvider::new("test", ["a"], proxy),
582 );
583 let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
584
585 assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
586 })
587 .await;
588 }
589
590 #[::fuchsia::test]
591 fn performance_states_are_formatted_per_state() {
592 let stats = stats(
593 "a",
594 vec![
595 state(
596 0, 11, 2200,
597 ),
598 state(1, 4, 7),
599 ],
600 );
601
602 assert_eq!(
603 format_performance_states(&stats),
604 "num_pf_states: 2\n\
605 pf_state: 0\npf_entry_count: 11\npf_time_in_state: 2200000 (us)\n\n\
606 pf_state: 1\npf_entry_count: 4\npf_time_in_state: 7000 (us)\n\n"
607 );
608 }
609
610 #[::fuchsia::test]
611 fn power_domain_states_are_formatted_per_state() {
612 let off = fpowerstats::StateResidency {
613 name: "STATE_OFF".to_string(),
614 last_entry_timestamp_ms: 5,
615 ..state(0, 3, 12)
616 };
617 let on = fpowerstats::StateResidency {
618 name: "STATE_ON".to_string(),
619 last_entry_timestamp_ms: 9,
620 ..state(1, 4, 7)
621 };
622 let stats = stats("a", vec![off, on]);
623
624 assert_eq!(
625 format_power_domain_states(&stats),
626 "\nSSWRP lpb\n\n\
627 power_state: STATE_OFF\nentry_count: 3\ntime_in_state: 12000 (us)\n\
628 last_entry_timestamp: 5000\nlast_exit_timestamp: 0\n\
629 power_state: STATE_ON\nentry_count: 4\ntime_in_state: 7000 (us)\n\
630 last_entry_timestamp: 9000\nlast_exit_timestamp: 0\n\n"
631 );
632 }
633
634 #[::fuchsia::test]
635 fn platform_power_states_are_formatted_per_state() {
636 let active = fpowerstats::StateResidency {
637 name: "ACTIVE".to_string(),
638 last_entry_timestamp_ms: 2,
639 ..state(0, 3, 100)
640 };
641 let stats = stats(
642 "a",
643 vec![active, state(1, 0, 0)],
644 );
645
646 assert_eq!(
647 format_platform_power_states(&stats),
648 "power_state: ACTIVE\nentry_count: 3\ntime_in_state_ms: 100\n\
649 last_entry_timestamp_ticks: 76800\nlast_exit_timestamp_ticks: 0\n\n\
650 power_state: 1\nentry_count: 0\ntime_in_state_ms: 0\n\
651 last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
652 );
653 }
654
655 #[::fuchsia::test]
656 fn power_state_stats_are_formatted_per_state() {
657 let cluster_off = fpowerstats::StateResidency {
658 name: "Cluster 0 off".to_string(),
659 last_entry_timestamp_ms: 2,
660 ..state(0, 6, 250)
661 };
662 let suspend = fpowerstats::StateResidency {
663 name: "Suspend".to_string(),
664 ..state(1, 1, 10)
665 };
666 let stats = stats(POWER_STATE_STATS_ENTITY, vec![cluster_off, suspend]);
667
668 assert_eq!(
669 format_power_state_stats(&stats),
670 "---\nstate: Cluster 0 off\nlast_entry_ts_ticks: 76800\nlast_exit_ts_ticks: 0\n\
671 total_state_residency_ms: 250\nlast_state_entry_counts: 6\n\
672 ---\nstate: Suspend\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
673 total_state_residency_ms: 10\nlast_state_entry_counts: 1\n"
674 );
675 }
676
677 #[::fuchsia::test]
678 fn residency_with_no_states() {
679 let stats = stats("a", vec![]);
680
681 assert_eq!(format_performance_states(&stats), "num_pf_states: 0\n");
682 assert_eq!(format_power_domain_states(&stats), "\nSSWRP lpb\n\n\n");
683 assert_eq!(format_platform_power_states(&stats), "");
684 assert_eq!(format_power_state_stats(&stats), "");
685 }
686
687 #[::fuchsia::test]
688 fn states_with_unsafe_names_are_left_out() {
689 let unsafe_state = fpowerstats::StateResidency {
690 name: "A\nentry_count: 9".to_string(),
691 ..state(0, 1, 1)
692 };
693 let stats = stats(
694 "a",
695 vec![unsafe_state, state(1, 2, 2)],
696 );
697
698 assert_eq!(
699 format_power_domain_states(&stats),
700 "\nSSWRP lpb\n\n\
701 power_state: 1\nentry_count: 2\ntime_in_state: 2000 (us)\n\
702 last_entry_timestamp: 0\nlast_exit_timestamp: 0\n\n"
703 );
704 assert_eq!(
705 format_platform_power_states(&stats),
706 "power_state: 1\nentry_count: 2\ntime_in_state_ms: 2\n\
707 last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
708 );
709 assert_eq!(
710 format_power_state_stats(&stats),
711 "---\nstate: 1\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
712 total_state_residency_ms: 2\nlast_state_entry_counts: 2\n"
713 );
714 }
715
716 #[::fuchsia::test]
717 fn only_served_entities_with_safe_names_are_mapped_to_files() {
718 for prefix in [PERF_STATE_FILE_PREFIX, POWER_DOMAIN_FILE_PREFIX] {
719 let names = [
720 format!("{prefix}a"),
721 format!("{prefix}b_1"),
722 PLATFORM_POWER_ENTITY.to_string(),
723 POWER_STATE_STATS_ENTITY.to_string(),
724 "other".to_string(),
725 prefix.to_string(),
726 format!("{prefix}../x"),
727 format!("{prefix}a/b"),
728 prefix.to_uppercase() + "A",
729 format!("x{prefix}a"),
730 format!("{PLATFORM_POWER_ENTITY}_x"),
731 format!("{POWER_STATE_STATS_ENTITY}_x"),
732 ];
733
734 let served: Vec<_> = names
735 .into_iter()
736 .filter_map(|name| residency_file_spec(&name).map(|spec| (spec.device, spec.file)))
737 .collect();
738 assert_eq!(
739 served,
740 vec![
741 (RESIDENCY_DEVICE_NAME, format!("{prefix}a")),
742 (RESIDENCY_DEVICE_NAME, format!("{prefix}b_1")),
743 (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string()),
744 (POWER_STATE_STATS_DEVICE_NAME, POWER_STATE_STATS_FILE.to_string()),
745 ]
746 );
747 }
748 }
749}