Skip to main content

starnix_core/device/
power_stats.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Per-[`Kernel`] discovery and querying of `fuchsia.hardware.power.stats.Service` providers.
6
7use crate::device::service_watcher::{ServiceBindingScope, ServiceWatcher};
8use crate::task::Kernel;
9use fidl::endpoints::{Proxy as _, SynchronousProxy as _};
10use fidl_fuchsia_hardware_power_stats as fpowerstats;
11use fuchsia_async::TimeoutExt as _;
12use starnix_logging::log_warn;
13use starnix_sync::{LockDepMutex, PowerStatsManagerLock};
14use starnix_uapi::errors::Errno;
15use starnix_uapi::{errno, error};
16use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19
20/// How long to wait for a power stats provider to respond to a FIDL request before failing.
21const PROVIDER_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(1);
22
23/// Connected `fuchsia.hardware.power.stats.Service` provider instance and its reported entities.
24#[derive(Debug)]
25pub struct PowerStatsProvider {
26    instance_name: String,
27    entity_names: HashSet<String>,
28    proxy: Arc<fpowerstats::DeviceSynchronousProxy>,
29}
30
31impl PowerStatsProvider {
32    /// Creates a [`PowerStatsProvider`] for `instance_name` serving `entity_names` over `proxy`.
33    pub fn new(
34        instance_name: impl Into<String>,
35        entity_names: impl IntoIterator<Item = impl Into<String>>,
36        proxy: fpowerstats::DeviceSynchronousProxy,
37    ) -> Self {
38        Self {
39            instance_name: instance_name.into(),
40            entity_names: entity_names.into_iter().map(Into::into).collect(),
41            proxy: Arc::new(proxy),
42        }
43    }
44
45    /// Name of the service instance within `fuchsia.hardware.power.stats.Service`.
46    pub fn instance_name(&self) -> &str {
47        &self.instance_name
48    }
49
50    /// Names of the newly discovered power entities announced by this provider.
51    ///
52    /// Entities that were already announced by an earlier provider (whether still active or
53    /// disconnected) are excluded so listeners observe each discovered entity name only once.
54    pub fn entity_names(&self) -> &HashSet<String> {
55        &self.entity_names
56    }
57
58    /// Returns whether the underlying provider channel has observed `PEER_CLOSED`.
59    fn is_closed(&self) -> bool {
60        self.proxy.is_closed().unwrap_or(true)
61    }
62
63    /// Queries the provider for the current residency [`fpowerstats::Stats`] of `entity_name`.
64    fn entity_stats(&self, entity_name: &str) -> Result<fpowerstats::Stats, Errno> {
65        let deadline = zx::MonotonicInstant::after(PROVIDER_TIMEOUT);
66        let reports = self.proxy.get_stats(deadline).map_err(|e| {
67            log_warn!(
68                "Failed to read power stats for {entity_name} from {}: {e}",
69                self.instance_name
70            );
71            errno!(EIO)
72        })?;
73        let Some(stats) = reports.into_iter().find(|stats| stats.entity == entity_name) else {
74            log_warn!(
75                "Power stats provider {} no longer reports {entity_name}",
76                self.instance_name
77            );
78            return error!(EIO);
79        };
80        Ok(stats)
81    }
82}
83
84type ProviderListener = dyn FnMut(&Kernel, &Arc<PowerStatsProvider>) + Send;
85
86#[derive(Default)]
87struct PowerStatsManagerState {
88    /// Discovered provider instances that announced new entities, in discovery order.
89    providers: Vec<Arc<PowerStatsProvider>>,
90    /// Active provider serving each discovered entity name, or `None` when the provider that was
91    /// serving that entity has disconnected and has not yet been replaced.
92    entity_providers: HashMap<String, Option<Arc<PowerStatsProvider>>>,
93    /// Listeners notified whenever a provider instance announces new entities.
94    listeners: Vec<Box<ProviderListener>>,
95}
96
97impl std::fmt::Debug for PowerStatsManagerState {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("PowerStatsManagerState")
100            .field("providers", &self.providers)
101            .field("entity_providers", &self.entity_providers)
102            .field("listeners_count", &self.listeners.len())
103            .finish()
104    }
105}
106
107/// Internal state stored in [`Kernel::expando`] so callers must access [`PowerStatsManager`] via
108/// [`PowerStatsManager::get_or_init`].
109#[derive(Debug, Default)]
110struct PowerStatsManagerInner {
111    driver_registered: AtomicBool,
112    state: LockDepMutex<PowerStatsManagerState, PowerStatsManagerLock>,
113}
114
115/// Handle to the per-[`Kernel`] singleton that discovers
116/// `fuchsia.hardware.power.stats.Service` providers and queries per-entity residency statistics.
117#[derive(Clone, Debug)]
118pub struct PowerStatsManager {
119    inner: Arc<PowerStatsManagerInner>,
120}
121
122impl PowerStatsManager {
123    /// Returns the per-[`Kernel`] [`PowerStatsManager`], starting the asynchronous watcher for
124    /// `/svc/fuchsia.hardware.power.stats.Service` on first access.
125    pub fn get_or_init(kernel: &Kernel) -> Self {
126        let inner = kernel.expando.get::<PowerStatsManagerInner>();
127        let manager = Self { inner };
128        if !manager.inner.driver_registered.swap(true, Ordering::AcqRel) {
129            let manager_for_bind = manager.clone();
130            ServiceWatcher::register_with_probe(
131                kernel,
132                fpowerstats::ServiceMarker,
133                Self::probe_service_instance,
134                move |current_task, provider, scope| {
135                    manager_for_bind.bind_provider(current_task.kernel(), provider, scope);
136                },
137            );
138        }
139        manager
140    }
141
142    /// Returns the per-[`Kernel`] [`PowerStatsManager`] for unit testing, suppressing the
143    /// background `/svc` watcher if it has not been started yet.
144    pub fn get_or_init_for_testing(kernel: &Kernel) -> Self {
145        let inner = kernel.expando.get::<PowerStatsManagerInner>();
146        inner.driver_registered.store(true, Ordering::Release);
147        Self { inner }
148    }
149
150    /// Registers `provider` with the per-[`Kernel`] [`PowerStatsManager`] for unit testing,
151    /// suppressing the background `/svc` watcher if it has not been started yet.
152    pub fn add_provider_for_testing(kernel: &Kernel, provider: PowerStatsProvider) {
153        let _ = Self::get_or_init_for_testing(kernel).register_provider(kernel, provider);
154    }
155
156    /// Registers `listener` to be invoked for every [`PowerStatsProvider`] that announces newly
157    /// discovered power entities, including any providers discovered prior to this call.
158    ///
159    /// Each discovered entity name is announced at most once across all provider notifications:
160    /// if a provider disconnects and a restarted provider re-registers the same entities,
161    /// [`Self::entity_stats`] switches to the new provider while listeners are only invoked for
162    /// any previously unannounced entities.
163    ///
164    /// Listeners are invoked while holding [`PowerStatsManagerLock`], which is ordered before
165    /// [`DeviceRegistryStateLevel`](starnix_sync::DeviceRegistryStateLevel) and
166    /// [`SimpleDirectoryEntriesLock`](starnix_sync::SimpleDirectoryEntriesLock) so listeners may
167    /// register devices and sysfs nodes without race windows between existing and newly discovered
168    /// providers. Because [`PowerStatsManagerLock`] is held during the callback, `listener` must
169    /// not synchronously re-enter [`PowerStatsManager`] methods.
170    pub fn watch_providers<F>(&self, kernel: &Kernel, mut listener: F)
171    where
172        F: FnMut(&Kernel, &Arc<PowerStatsProvider>) + Send + 'static,
173    {
174        let mut state = self.inner.state.lock();
175        for provider in &state.providers {
176            listener(kernel, provider);
177        }
178        state.listeners.push(Box::new(listener));
179    }
180
181    /// Queries the current residency [`fpowerstats::Stats`] for `entity_name`.
182    ///
183    /// Returns `Ok(None)` if no provider reporting `entity_name` has been discovered yet, or
184    /// `Err(EIO)` if the provider serving `entity_name` has disconnected, timed out, or omitted
185    /// `entity_name` from its report.
186    pub fn entity_stats(&self, entity_name: &str) -> Result<Option<fpowerstats::Stats>, Errno> {
187        let provider = match self.inner.state.lock().entity_providers.get(entity_name).cloned() {
188            None => return Ok(None),
189            Some(None) => return error!(EIO),
190            Some(Some(provider)) => provider,
191        };
192        provider.entity_stats(entity_name).map(Some)
193    }
194
195    async fn probe_service_instance(
196        instance: Arc<fpowerstats::ServiceProxy>,
197    ) -> Option<PowerStatsProvider> {
198        let instance_name = instance.instance_name().to_string();
199        let device_proxy = match instance.connect_to_device() {
200            Ok(proxy) => proxy,
201            Err(e) => {
202                log_warn!("Failed to connect to power stats provider {instance_name}: {e:#}");
203                return None;
204            }
205        };
206        let entities = match async { Some(device_proxy.get_entities().await) }
207            .on_timeout(PROVIDER_TIMEOUT, || None)
208            .await
209        {
210            Some(Ok(entities)) => entities,
211            Some(Err(e)) => {
212                log_warn!("Failed to get power entities from {instance_name}: {e:#}");
213                return None;
214            }
215            None => {
216                log_warn!("Timed out getting power entities from {instance_name}");
217                return None;
218            }
219        };
220        let client_end = match device_proxy.into_client_end() {
221            Ok(client_end) => client_end,
222            Err(_) => {
223                log_warn!("Failed to unwrap device proxy channel for {instance_name}");
224                return None;
225            }
226        };
227        let entity_names = entities.into_iter().map(|entity| entity.name);
228        let sync_proxy = fpowerstats::DeviceSynchronousProxy::new(client_end.into_channel());
229        Some(PowerStatsProvider::new(instance_name, entity_names, sync_proxy))
230    }
231
232    fn bind_provider(
233        &self,
234        kernel: &Kernel,
235        provider: PowerStatsProvider,
236        scope: &mut ServiceBindingScope,
237    ) {
238        scope.monitor_channel(provider.proxy.clone());
239        let provider = self.register_provider(kernel, provider);
240        let manager = self.clone();
241        scope.on_disconnect(move |_current_task| {
242            manager.disconnect_provider(&provider);
243        });
244    }
245
246    fn register_provider(
247        &self,
248        kernel: &Kernel,
249        mut provider: PowerStatsProvider,
250    ) -> Arc<PowerStatsProvider> {
251        let mut state = self.inner.state.lock();
252        let instance_name = provider.instance_name.clone();
253        let served_entities: Vec<String> = provider
254            .entity_names
255            .iter()
256            .filter(|&entity_name| {
257                if let Some(existing) = state
258                    .entity_providers
259                    .get(entity_name)
260                    .and_then(|slot| slot.as_ref())
261                    .filter(|existing| !existing.is_closed())
262                {
263                    log_warn!(
264                        "Ignoring duplicate power entity {entity_name} from provider {instance_name} (already served by {})",
265                        existing.instance_name()
266                    );
267                    false
268                } else {
269                    true
270                }
271            })
272            .cloned()
273            .collect();
274        provider
275            .entity_names
276            .retain(|entity_name| !state.entity_providers.contains_key(entity_name));
277        let provider = Arc::new(provider);
278        for entity_name in served_entities {
279            state.entity_providers.insert(entity_name, Some(provider.clone()));
280        }
281        if !provider.entity_names.is_empty() {
282            state.providers.push(provider.clone());
283            for listener in &mut state.listeners {
284                listener(kernel, &provider);
285            }
286        }
287        provider
288    }
289
290    fn disconnect_provider(&self, disconnected: &Arc<PowerStatsProvider>) {
291        let mut state = self.inner.state.lock();
292        for slot in state.entity_providers.values_mut() {
293            if slot.as_ref().is_some_and(|current| Arc::ptr_eq(current, disconnected)) {
294                *slot = None;
295            }
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::device::service_watcher::watch_service_dir_for_testing;
304    use crate::testing::{spawn_kernel_and_run, spawn_kernel_and_run_sync};
305    use fidl::endpoints::{ServerEnd, create_sync_proxy};
306    use fuchsia_async as fasync;
307    use futures::StreamExt;
308    use futures::channel::{mpsc, oneshot};
309    use futures::future::select;
310    use starnix_sync::Mutex;
311    use std::pin::pin;
312    use std::sync::atomic::AtomicUsize;
313    use vfs::directory::helper::DirectlyMutable as _;
314    use vfs::directory::immutable::Simple;
315
316    fn state(id: i32, entry_count: u32, total_time_ms: u64) -> fpowerstats::StateResidency {
317        fpowerstats::StateResidency {
318            id,
319            name: id.to_string(),
320            total_time_ms,
321            entry_count,
322            last_entry_timestamp_ms: 0,
323        }
324    }
325
326    fn stats(
327        entity: &str,
328        state_residency_data: Vec<fpowerstats::StateResidency>,
329    ) -> fpowerstats::Stats {
330        fpowerstats::Stats { entity: entity.to_string(), state_residency_data }
331    }
332
333    async fn serve_provider(
334        stream: fpowerstats::DeviceRequestStream,
335        reports: Vec<fpowerstats::Stats>,
336    ) {
337        let _ = stream
338            .for_each(|request| {
339                let reports = &reports;
340                async move {
341                    match request {
342                        Ok(fpowerstats::DeviceRequest::GetEntities { responder }) => {
343                            let entities: Vec<_> = reports
344                                .iter()
345                                .map(|stats| fpowerstats::Entity {
346                                    name: stats.entity.clone(),
347                                    states: vec![],
348                                })
349                                .collect();
350                            let _ = responder.send(&entities);
351                        }
352                        Ok(fpowerstats::DeviceRequest::GetStats { responder }) => {
353                            let _ = responder.send(reports);
354                        }
355                        _ => {}
356                    }
357                }
358            })
359            .await;
360    }
361
362    fn spawn_server<F: Future<Output = ()> + 'static>(serve: impl FnOnce() -> F + Send + 'static) {
363        std::thread::spawn(move || fasync::LocalExecutor::default().run_singlethreaded(serve()));
364    }
365
366    fn start_test_service_dir(kernel: &Kernel) -> Arc<Simple> {
367        let dir = vfs::directory::immutable::simple();
368        let proxy = vfs::directory::serve_read_only(dir.clone(), vfs::ExecutionScope::new());
369        watch_service_dir_for_testing::<fpowerstats::ServiceMarker>(kernel, proxy);
370        dir
371    }
372
373    fn add_instance(
374        dir: &Arc<Simple>,
375        name: &str,
376        on_open: impl Fn(zx::Channel) + Send + Sync + 'static,
377    ) {
378        let instance_dir = vfs::pseudo_directory! {
379            "device" => vfs::service::endpoint(move |_scope, channel| {
380                on_open(channel.into_zx_channel());
381            }),
382        };
383        dir.add_entry_may_overwrite(name, instance_dir, /* overwrite = */ true)
384            .expect("add service instance");
385    }
386
387    #[::fuchsia::test]
388    async fn discovers_entities_and_queries_stats() {
389        spawn_kernel_and_run(async |current_task| {
390            let kernel = current_task.kernel();
391            let dir = start_test_service_dir(kernel);
392            let manager = PowerStatsManager::get_or_init(kernel);
393            assert!(Arc::ptr_eq(&manager.inner, &PowerStatsManager::get_or_init(kernel).inner));
394            assert_eq!(manager.entity_stats("cpu_c2"), Ok(None));
395
396            let (notified_tx, mut notified_rx) = mpsc::unbounded();
397            manager.watch_providers(kernel, move |_, provider| {
398                let _ = notified_tx.unbounded_send(provider.instance_name().to_string());
399            });
400
401            add_instance(&dir, "default", move |server_end| {
402                spawn_server(move || {
403                    let stream =
404                        ServerEnd::<fpowerstats::DeviceMarker>::new(server_end).into_stream();
405                    serve_provider(
406                        stream,
407                        vec![stats("cpu_c2", vec![state(0, 5, 120)]), stats("lpcm_soc", vec![])],
408                    )
409                });
410            });
411
412            assert_eq!(notified_rx.next().await.as_deref(), Some("default"));
413            let cpu_c2 = manager
414                .entity_stats("cpu_c2")
415                .expect("GetStats succeeded")
416                .expect("cpu_c2 entity discovered");
417            assert_eq!(cpu_c2.state_residency_data.len(), 1);
418            assert_eq!(cpu_c2.state_residency_data[0].entry_count, 5);
419            assert_eq!(cpu_c2.state_residency_data[0].total_time_ms, 120);
420            assert_eq!(manager.entity_stats("unknown"), Ok(None));
421        })
422        .await;
423    }
424
425    #[::fuchsia::test]
426    async fn watch_providers_notifies_for_existing_and_new_providers() {
427        spawn_kernel_and_run_sync(|current_task| {
428            let kernel = current_task.kernel();
429            let manager = PowerStatsManager::get_or_init_for_testing(kernel);
430
431            let (proxy1, _server1) = create_sync_proxy::<fpowerstats::DeviceMarker>();
432            PowerStatsManager::add_provider_for_testing(
433                kernel,
434                PowerStatsProvider::new("first", ["first_entity"], proxy1),
435            );
436
437            let seen = Arc::new(AtomicUsize::new(0));
438            let seen_for_listener = seen.clone();
439            manager.watch_providers(kernel, move |_, _| {
440                seen_for_listener.fetch_add(1, Ordering::SeqCst);
441            });
442            assert_eq!(seen.load(Ordering::SeqCst), 1);
443
444            let (proxy2, _server2) = create_sync_proxy::<fpowerstats::DeviceMarker>();
445            PowerStatsManager::add_provider_for_testing(
446                kernel,
447                PowerStatsProvider::new("second", ["second_entity"], proxy2),
448            );
449            assert_eq!(seen.load(Ordering::SeqCst), 2);
450        })
451        .await;
452    }
453
454    #[::fuchsia::test]
455    async fn entity_stats_returns_eio_when_entity_omitted_from_stats_report() {
456        spawn_kernel_and_run_sync(|current_task| {
457            let kernel = current_task.kernel();
458            let manager = PowerStatsManager::get_or_init_for_testing(kernel);
459
460            let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
461            spawn_server(move || {
462                serve_provider(server.into_stream(), vec![stats("b", vec![state(0, 1, 10)])])
463            });
464            PowerStatsManager::add_provider_for_testing(
465                kernel,
466                PowerStatsProvider::new("test", ["a", "b"], proxy),
467            );
468
469            assert!(manager.entity_stats("b").expect("GetStats succeeded").is_some());
470            assert_eq!(manager.entity_stats("a"), error!(EIO));
471        })
472        .await;
473    }
474
475    #[::fuchsia::test]
476    async fn entity_stats_returns_eio_when_provider_times_out() {
477        spawn_kernel_and_run_sync(|current_task| {
478            let kernel = current_task.kernel();
479            let manager = PowerStatsManager::get_or_init_for_testing(kernel);
480
481            // Keep `_server` open without servicing requests so `GetStats` times out.
482            let (proxy, _server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
483            PowerStatsManager::add_provider_for_testing(
484                kernel,
485                PowerStatsProvider::new("hung", ["a"], proxy),
486            );
487
488            assert_eq!(manager.entity_stats("a"), error!(EIO));
489        })
490        .await;
491    }
492
493    #[::fuchsia::test]
494    async fn probe_service_instance_skips_failing_provider_and_discovers_subsequent_instances() {
495        spawn_kernel_and_run(async |current_task| {
496            let kernel = current_task.kernel();
497            let dir = start_test_service_dir(kernel);
498            let manager = PowerStatsManager::get_or_init(kernel);
499
500            let (notified_tx, mut notified_rx) = mpsc::unbounded();
501            manager.watch_providers(kernel, move |_, provider| {
502                let _ = notified_tx.unbounded_send(provider.instance_name().to_string());
503            });
504
505            add_instance(&dir, "broken_closed", |_| {});
506            add_instance(&dir, "healthy", move |server_end| {
507                spawn_server(move || {
508                    let stream =
509                        ServerEnd::<fpowerstats::DeviceMarker>::new(server_end).into_stream();
510                    serve_provider(stream, vec![stats("cpu_c2", vec![state(0, 4, 80)])])
511                });
512            });
513
514            assert_eq!(notified_rx.next().await.as_deref(), Some("healthy"));
515            let cpu_c2 = manager
516                .entity_stats("cpu_c2")
517                .expect("GetStats succeeded")
518                .expect("cpu_c2 entity discovered from healthy provider");
519            assert_eq!(cpu_c2.state_residency_data[0].total_time_ms, 80);
520        })
521        .await;
522    }
523
524    #[::fuchsia::test]
525    async fn duplicate_entities_across_live_providers_keep_first_provider_and_serve_disjoint_entities()
526     {
527        spawn_kernel_and_run_sync(|current_task| {
528            let kernel = current_task.kernel();
529            let manager = PowerStatsManager::get_or_init_for_testing(kernel);
530
531            let (proxy1, server1) = create_sync_proxy::<fpowerstats::DeviceMarker>();
532            spawn_server(move || {
533                serve_provider(
534                    server1.into_stream(),
535                    vec![
536                        stats("shared", vec![state(0, 1, 10)]),
537                        stats("only_first", vec![state(0, 2, 20)]),
538                    ],
539                )
540            });
541            PowerStatsManager::add_provider_for_testing(
542                kernel,
543                PowerStatsProvider::new("first", ["shared", "only_first"], proxy1),
544            );
545
546            let (proxy2, server2) = create_sync_proxy::<fpowerstats::DeviceMarker>();
547            spawn_server(move || {
548                serve_provider(
549                    server2.into_stream(),
550                    vec![
551                        stats("shared", vec![state(0, 9, 999)]),
552                        stats("only_second", vec![state(0, 3, 30)]),
553                    ],
554                )
555            });
556            PowerStatsManager::add_provider_for_testing(
557                kernel,
558                PowerStatsProvider::new("second", ["shared", "only_second"], proxy2),
559            );
560
561            let announcements = Arc::new(Mutex::new(Vec::new()));
562            let announcements_for_listener = announcements.clone();
563            manager.watch_providers(kernel, move |_, provider| {
564                announcements_for_listener
565                    .lock()
566                    .push((provider.instance_name().to_string(), provider.entity_names().clone()));
567            });
568            assert_eq!(
569                *announcements.lock(),
570                vec![
571                    (
572                        "first".to_string(),
573                        HashSet::from(["shared".to_string(), "only_first".to_string()])
574                    ),
575                    ("second".to_string(), HashSet::from(["only_second".to_string()])),
576                ]
577            );
578
579            let shared = manager.entity_stats("shared").unwrap().unwrap();
580            assert_eq!(shared.state_residency_data[0].total_time_ms, 10);
581
582            let only_first = manager.entity_stats("only_first").unwrap().unwrap();
583            assert_eq!(only_first.state_residency_data[0].total_time_ms, 20);
584
585            let only_second = manager.entity_stats("only_second").unwrap().unwrap();
586            assert_eq!(only_second.state_residency_data[0].total_time_ms, 30);
587        })
588        .await;
589    }
590
591    #[::fuchsia::test]
592    async fn watcher_disconnect_clears_provider_and_restarted_instance_recovers() {
593        spawn_kernel_and_run(async |current_task| {
594            let kernel = current_task.kernel();
595            let dir = start_test_service_dir(kernel);
596            let manager = PowerStatsManager::get_or_init(kernel);
597
598            let (announced_tx, mut announced_rx) = mpsc::unbounded();
599            manager.watch_providers(kernel, move |_, provider| {
600                let _ = announced_tx.unbounded_send((
601                    provider.instance_name().to_string(),
602                    provider.entity_names().clone(),
603                ));
604            });
605
606            let (stop_tx, stop_rx) = oneshot::channel::<()>();
607            let stop_rx = Arc::new(Mutex::new(Some(stop_rx)));
608            let (stopped_tx, stopped_rx) = oneshot::channel::<()>();
609            let stopped_tx = Arc::new(Mutex::new(Some(stopped_tx)));
610            add_instance(&dir, "first", move |server_end| {
611                let stop_rx = stop_rx.lock().take().expect("stop_rx taken once");
612                let stopped_tx = stopped_tx.lock().take().expect("stopped_tx taken once");
613                spawn_server(move || async move {
614                    {
615                        let stream =
616                            ServerEnd::<fpowerstats::DeviceMarker>::new(server_end).into_stream();
617                        let serve_fut = pin!(serve_provider(
618                            stream,
619                            vec![
620                                stats("cpu_c2", vec![state(0, 5, 100)]),
621                                stats("unrecovered_entity", vec![state(0, 1, 10)]),
622                            ],
623                        ));
624                        let _ = select(serve_fut, stop_rx).await;
625                    }
626                    let _ = stopped_tx.send(());
627                });
628            });
629
630            assert_eq!(
631                announced_rx.next().await,
632                Some((
633                    "first".to_string(),
634                    HashSet::from(["cpu_c2".to_string(), "unrecovered_entity".to_string()]),
635                ))
636            );
637            let cpu_c2 = manager.entity_stats("cpu_c2").unwrap().unwrap();
638            assert_eq!(cpu_c2.state_residency_data[0].total_time_ms, 100);
639
640            // Stop `"first"`; queries for its entities now return `EIO`.
641            let _ = stop_tx.send(());
642            let _ = stopped_rx.await;
643            assert_eq!(manager.entity_stats("cpu_c2"), error!(EIO));
644            assert_eq!(manager.entity_stats("unrecovered_entity"), error!(EIO));
645
646            // Inject a restarted instance serving `cpu_c2` plus `new_entity`; `cpu_c2` recovers
647            // without being re-announced to listeners, while `unrecovered_entity` still returns
648            // `EIO`.
649            add_instance(&dir, "first_restarted", move |server_end| {
650                spawn_server(move || {
651                    let stream =
652                        ServerEnd::<fpowerstats::DeviceMarker>::new(server_end).into_stream();
653                    serve_provider(
654                        stream,
655                        vec![
656                            stats("cpu_c2", vec![state(0, 7, 250)]),
657                            stats("new_entity", vec![state(0, 1, 20)]),
658                        ],
659                    )
660                });
661            });
662
663            assert_eq!(
664                announced_rx.next().await,
665                Some(("first_restarted".to_string(), HashSet::from(["new_entity".to_string()])))
666            );
667            let recovered = manager.entity_stats("cpu_c2").unwrap().unwrap();
668            assert_eq!(recovered.state_residency_data[0].total_time_ms, 250);
669            assert_eq!(manager.entity_stats("unrecovered_entity"), error!(EIO));
670        })
671        .await;
672    }
673}