Skip to main content

kernel_manager/
suspend.rs

1// Copyright 2025 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
5use crate::Kernels;
6use anyhow::Error;
7use fidl::Peered;
8use fidl_fuchsia_starnix_runner as fstarnixrunner;
9use fuchsia_inspect::{self as inspect, UintExponentialHistogramProperty, UintProperty};
10use log::warn;
11use starnix_sync::{
12    CondVar, LockDepMutex, SuspendHandlesLock, SuspendWorkerPoolStateLock,
13    SuspendWorkerResultsLock, SuspendWorkerRunLock, WakeSourcesLock, WakeWatchersLock,
14};
15use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
16use std::sync::{Arc, LazyLock, OnceLock};
17use zx::Task;
18
19/// The signal that the kernel raises to indicate that it's awake.
20pub const AWAKE_SIGNAL: zx::Signals = zx::Signals::USER_0;
21
22/// The signal that the kernel raises to indicate that it's suspended.
23pub const ASLEEP_SIGNAL: zx::Signals = zx::Signals::USER_1;
24
25pub struct WakeSource {
26    handle: zx::NullableHandle,
27    name: String,
28    signals: zx::Signals,
29}
30
31impl WakeSource {
32    pub fn from_counter(counter: zx::Counter, name: String) -> Self {
33        Self { handle: counter.into_handle(), name, signals: zx::Signals::COUNTER_POSITIVE }
34    }
35
36    pub fn from_handle(handle: zx::NullableHandle, name: String, signals: zx::Signals) -> Self {
37        Self { handle, name, signals }
38    }
39
40    fn as_wait_item(&self) -> zx::WaitItem<'_> {
41        self.handle.wait_item(self.signals)
42    }
43}
44
45pub type WakeSources = std::collections::HashMap<zx::Koid, WakeSource>;
46
47pub struct SuspendContext {
48    pub wake_sources: Arc<LockDepMutex<WakeSources, WakeSourcesLock>>,
49    pub wake_watchers: Arc<LockDepMutex<Vec<zx::EventPair>, WakeWatchersLock>>,
50
51    /// Inspect node for suspend-related metrics.
52    pub node: inspect::Node,
53    /// Histogram recording the boot timeline duration (in nanoseconds) of successful container
54    /// suspensions.
55    pub suspend_duration_histogram: UintExponentialHistogramProperty,
56    /// The total number of times the container has attempted to suspend.
57    pub suspend_attempts_count: UintProperty,
58    /// The total number of times the container has successfully suspended.
59    pub suspend_successes_count: UintProperty,
60    /// The total number of times the container has failed to suspend.
61    pub suspend_failures_count: UintProperty,
62}
63
64impl Default for SuspendContext {
65    fn default() -> Self {
66        let inspector = inspect::component::inspector();
67        let node = inspector.root().create_child("suspend");
68        let suspend_duration_histogram = node.create_uint_exponential_histogram(
69            "suspend_duration_boot_ns",
70            inspect::ExponentialHistogramParams {
71                floor: 100_000,
72                initial_step: 100_000,
73                step_multiplier: 2,
74                buckets: 32,
75            },
76        );
77        let suspend_attempts_count = node.create_uint("suspend_attempts_count", 0);
78        let suspend_successes_count = node.create_uint("suspend_successes_count", 0);
79        let suspend_failures_count = node.create_uint("suspend_failures_count", 0);
80        // Create the threads used to suspend and resume containers ahead of the first suspend.
81        LazyLock::force(&SUSPEND_WORKER_POOL);
82        Self {
83            wake_sources: Default::default(),
84            wake_watchers: Default::default(),
85            node,
86            suspend_duration_histogram,
87            suspend_attempts_count,
88            suspend_successes_count,
89            suspend_failures_count,
90        }
91    }
92}
93
94fn signal_wake_watchers(
95    suspend_context: &SuspendContext,
96    clear_mask: zx::Signals,
97    set_mask: zx::Signals,
98) {
99    let mut watchers = suspend_context.wake_watchers.lock();
100    watchers.retain(|event| match event.signal_peer(clear_mask, set_mask) {
101        Err(zx::Status::PEER_CLOSED) => false,
102        Ok(()) => true,
103        Err(e) => {
104            log::warn!("Failed to signal wake watcher ({set_mask:?}): {e:?}");
105            true
106        }
107    });
108}
109
110/// Suspends the container specified by the `payload`.
111pub async fn suspend_container(
112    payload: fstarnixrunner::ManagerSuspendContainerRequest,
113    suspend_context: &Arc<SuspendContext>,
114    kernels: &Kernels,
115) -> Result<
116    Result<fstarnixrunner::ManagerSuspendContainerResponse, fstarnixrunner::SuspendError>,
117    Error,
118> {
119    fuchsia_trace::duration!("power", "starnix-runner:suspending-container");
120    let Some(container_job) = payload.container_job else {
121        warn!(
122            "error suspending container: could not find container job {:?}",
123            payload.container_job
124        );
125        return Ok(Err(fstarnixrunner::SuspendError::SuspendFailure));
126    };
127    let container_job = Arc::new(container_job);
128
129    // Declared before `suspend_handles` so Rust's reverse-declaration (LIFO) drop order always
130    // thaws container processes (`suspend_handles`) *before* signaling `AWAKE_SIGNAL` on wake
131    // watchers, including on early error returns or future cancellation during `drop_wake_lease`.
132    let wake_watchers_awake_guard;
133
134    // These handles keep the container processes frozen until dropped.
135    log::info!("Suspending all container processes.");
136    let suspend_handles = match suspend_job(&container_job) {
137        Ok(handles) => handles,
138        Err(e) => {
139            warn!("error suspending container {:?}", e);
140            fuchsia_trace::instant!(
141                "power",
142                "starnix-runner:suspend-failed-actual",
143                fuchsia_trace::Scope::Process
144            );
145            return Ok(Err(fstarnixrunner::SuspendError::SuspendFailure));
146        }
147    };
148    log::info!("Finished suspending all container processes.");
149
150    let suspend_start = zx::BootInstant::get();
151    if let Some(wake_locks) = payload.wake_locks {
152        match wake_locks
153            .wait_one(zx::Signals::EVENT_SIGNALED, zx::MonotonicInstant::ZERO)
154            .to_result()
155        {
156            Ok(_) => {
157                // There were wake locks active after suspending all processes, resume
158                // and fail the suspend call.
159                warn!("error suspending container: Linux wake locks exist");
160                fuchsia_trace::instant!(
161                    "power",
162                    "starnix-runner:suspend-failed-with-wake-locks",
163                    fuchsia_trace::Scope::Process
164                );
165                return Ok(Err(fstarnixrunner::SuspendError::WakeLocksExist));
166            }
167            Err(_) => {}
168        };
169    }
170
171    log::info!("Notifying wake watchers of container suspend.");
172    signal_wake_watchers(suspend_context, AWAKE_SIGNAL, ASLEEP_SIGNAL);
173    // Restore wake watchers to `AWAKE_SIGNAL` once `start_acquire_wake_lease` has dispatched the
174    // wake-lease request and the container processes are thawed, or if an early error/cancellation
175    // exits `suspend_container` (after `suspend_handles` has already dropped).
176    wake_watchers_awake_guard = scopeguard::guard(suspend_context, |suspend_context| {
177        log::info!("Notifying wake watchers of container wakeup.");
178        signal_wake_watchers(suspend_context, ASLEEP_SIGNAL, AWAKE_SIGNAL);
179    });
180
181    log::info!("Pre-drop wake lease");
182    kernels.drop_wake_lease(&container_job).await?;
183    log::info!("Post-drop wake lease");
184
185    let resume_reason = {
186        // Hold `wake_sources` only while waiting and scanning `wait_items`, so the terminal
187        // `WakeSourcesLock` is released before dropping `suspend_handles` or awaiting.
188        let wake_sources = suspend_context.wake_sources.lock();
189        let mut wait_items: Vec<zx::WaitItem<'_>> =
190            wake_sources.values().map(|w| w.as_wait_item()).collect();
191
192        // TODO: We will likely have to handle a larger number of wake sources in the
193        // future, at which point we may want to consider a Port-based approach. This
194        // would also allow us to unblock this thread.
195        let wait_result = {
196            fuchsia_trace::duration!("power", "starnix-runner:waiting-on-container-wake");
197            if !wait_items.is_empty() {
198                log::info!("Waiting on container to receive incoming message on wake proxies");
199                zx::object_wait_many(
200                    &mut wait_items,
201                    zx::MonotonicInstant::after(zx::Duration::from_seconds(9)),
202                )
203                .inspect_err(|e| {
204                    warn!("error waiting for wake event {:?}", e);
205                })
206                .map(|_| ())
207            } else {
208                Ok(())
209            }
210        };
211        log::info!("Finished waiting on container wake proxies.");
212
213        let mut resume_reasons: Vec<String> = Vec::new();
214        for (wake_source, wait_item) in wake_sources.values().zip(&wait_items) {
215            if (wait_item.pending() & wait_item.waiting_for()) != zx::Signals::NONE {
216                log::info!("Woke container from sleep for: {}", wake_source.name,);
217                resume_reasons.push(wake_source.name.clone());
218            }
219        }
220
221        if resume_reasons.is_empty() {
222            match wait_result {
223                // Expose the suspend timeout injected by Starnix.
224                Err(zx::Status::TIMED_OUT) => Some("starnix-container-timeout".into()),
225                // An error was already printed earlier. Ok(_) was always silent.
226                _ => None,
227            }
228        } else {
229            Some(resume_reasons.join(","))
230        }
231    };
232
233    // Synchronously write `TakeApplicationActivityLease` to SAG's channel before thawing the
234    // container processes so SAG starts resuming drivers immediately at wakeup, while its ~12.5 ms
235    // `AllDrivers` reply completes asynchronously on `Kernels`'s wake-lease scope off the
236    // `suspend_container` critical path.
237    log::info!("Pre-acquire wake lease");
238    let acquire_result = kernels.start_acquire_wake_lease(&container_job);
239    log::info!("Dispatched acquire wake lease");
240
241    // Thaw the container processes on all exit paths (including if `start_acquire_wake_lease`
242    // returned an error) before signaling `AWAKE_SIGNAL` on wake watchers.
243    std::mem::drop(suspend_handles);
244    acquire_result?;
245
246    std::mem::drop(wake_watchers_awake_guard);
247
248    log::info!("Returning successfully from suspend container");
249    Ok(Ok(fstarnixrunner::ManagerSuspendContainerResponse {
250        suspend_time: Some((zx::BootInstant::get() - suspend_start).into_nanos()),
251        resume_reason,
252        ..Default::default()
253    }))
254}
255
256/// The maximum number of worker threads used to suspend and resume the processes of a container in
257/// parallel.
258///
259/// Suspending a container with thousands of threads is dominated by the cost of the
260/// `zx_task_suspend` calls (which have to interrupt every thread of the process) and by the
261/// per-thread syscalls needed to confirm that every thread is suspended, and resuming it by the
262/// cost of closing the suspend tokens (which wakes every thread of the process). These are
263/// independent across processes, so spreading them over a few CPUs reduces the latency of
264/// container suspend and resume.
265const MAX_SUSPEND_WORKERS: usize = 4;
266
267/// The number of items (processes) up to which the suspend and resume work is done on the calling
268/// thread only, as waking up the worker threads would cost more than it saves.
269const PARALLEL_SUSPEND_THRESHOLD: usize = 8;
270
271/// The number of suspend tokens that a worker closes at a time when resuming processes in
272/// parallel.
273///
274/// The number of threads per process varies widely (e.g. system_server has hundreds), and the
275/// workers can contend with freshly woken threads, so the tokens are handed out in small chunks to
276/// balance the work dynamically across the workers.
277const RESUME_CHUNK_SIZE: usize = 4;
278
279/// Thread name assigned to persistent `SUSPEND_WORKER_POOL` threads.
280const SUSPEND_WORKER_THREAD_NAME: &str = "starnix-suspend-worker";
281
282/// Zircon scheduler role applied to `SUSPEND_WORKER_POOL` threads so freshly thawed deadline
283/// threads cannot preempt the workers mid-close.
284const SUSPEND_WORKER_ROLE_NAME: &str = "fuchsia.starnix.runner.suspend_worker";
285
286/// The signals that indicate that a thread will not run again until it is resumed.
287const THREAD_STOPPED_SIGNALS: zx::Signals =
288    zx::Signals::THREAD_SUSPENDED.union(zx::Signals::THREAD_TERMINATED);
289
290/// The suspend tokens of the processes of a suspended container.
291///
292/// The processes are resumed when this is dropped, on all the paths out of `suspend_container`
293/// (including early returns and cancellation), and on the error paths of `suspend_job`.
294struct SuspendedProcesses(Vec<zx::NullableHandle>);
295
296impl SuspendedProcesses {
297    #[cfg(test)]
298    fn len(&self) -> usize {
299        self.0.len()
300    }
301}
302
303impl Drop for SuspendedProcesses {
304    fn drop(&mut self) {
305        resume_processes(std::mem::take(&mut self.0));
306    }
307}
308
309/// Resumes the processes by closing their `suspend_handles`, spreading the work over up to
310/// `MAX_SUSPEND_WORKERS` threads, in chunks of `RESUME_CHUNK_SIZE` handles, if there are more than
311/// `PARALLEL_SUSPEND_THRESHOLD` processes.
312///
313/// Closing the last handle to a suspend token resumes every thread of its process from within the
314/// `zx_handle_close` call, which takes ~100 us for a typical container process, so resuming a
315/// large container one process at a time takes milliseconds.
316///
317/// The handles are closed one at a time with `zx_handle_close` rather than in batches with
318/// `zx_handle_close_many`: `HandleTable::remove_handles` (used by `zx_handle_close_many`) releases
319/// the handles, and thus resumes the processes, while holding the write lock of the handle table
320/// of the runner. Resuming processes with `zx_handle_close_many` would thus serialize the workers,
321/// and block the other threads of the runner (e.g. the pager threads that serve the page faults of
322/// the resumed threads) from using handles for milliseconds. `zx_handle_close` resumes the process
323/// after releasing the lock.
324fn resume_processes(mut suspend_handles: Vec<zx::NullableHandle>) {
325    if suspend_handles.is_empty() {
326        return;
327    }
328    fuchsia_trace::duration!(
329        "power",
330        "starnix-runner:resume_kernel",
331        "num_processes" => suspend_handles.len() as u64
332    );
333    if suspend_handles.len() <= PARALLEL_SUSPEND_THRESHOLD
334        || !SUSPEND_WORKER_POOL.can_run_in_parallel()
335    {
336        // Pop from the tail so critical wake-path processes placed at the end of `suspend_handles`
337        // are still resumed first when closing sequentially.
338        while let Some(suspend_handle) = suspend_handles.pop() {
339            drop(suspend_handle);
340        }
341        return;
342    }
343    let suspend_handles = LockDepMutex::<_, SuspendHandlesLock>::new(suspend_handles);
344    SUSPEND_WORKER_POOL.run(Arc::new(move || {
345        loop {
346            // Take a chunk of handles while holding the lock, but close them after releasing
347            // it, without allocating.
348            let mut chunk = [const { zx::NullableHandle::invalid() }; RESUME_CHUNK_SIZE];
349            let is_empty = {
350                let mut suspend_handles = suspend_handles.lock();
351                if suspend_handles.is_empty() {
352                    break;
353                }
354                for slot in &mut chunk {
355                    let Some(suspend_handle) = suspend_handles.pop() else {
356                        break;
357                    };
358                    *slot = suspend_handle;
359                }
360                suspend_handles.is_empty()
361            };
362            // Dropping the handles closes them one at a time.
363            drop(chunk);
364            if is_empty {
365                break;
366            }
367        }
368    }));
369}
370
371/// Returns the resume priority rank for `process` based on its Zircon process name.
372///
373/// Higher values are placed closer to the tail (`Vec::pop()` end) of `SuspendedProcesses` so they
374/// are thawed first in chunk #0 on wake:
375/// - `4`: `starnix_kernel` (input event relay)
376/// - `3`: `suspend-service` / `android.system.suspend` (serves synchronous
377///   `ISystemSuspend::acquireWakeLock` calls made by `InputReader` under `PMS.mLock`)
378/// - `2`: `system_server` (`InputReader`, `PowerManagerService`, `DisplayPowerController`)
379/// - `1`: `surfaceflinger` (display power mode and frame composition)
380/// - `0`: all other processes
381fn process_resume_priority(process: &zx::Process) -> u8 {
382    let Ok(name) = process.get_name() else {
383        return 0;
384    };
385    process_resume_priority_for_name(name.as_bstr().as_ref())
386}
387
388fn process_resume_priority_for_name(name: &[u8]) -> u8 {
389    if contains_subslice(name, b"starnix_kernel") {
390        4
391    } else if contains_subslice(name, b"suspend-service")
392        || contains_subslice(name, b"android.system.suspend")
393    {
394        3
395    } else if contains_subslice(name, b"system_server") {
396        2
397    } else if contains_subslice(name, b"surfaceflinger") {
398        1
399    } else {
400        0
401    }
402}
403
404fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
405    !needle.is_empty() && haystack.windows(needle.len()).any(|window| window == needle)
406}
407
408/// Suspends the provided `zx::Job` by suspending each process in the job individually.
409///
410/// Returns a `SuspendedProcesses` guard holding the suspend tokens of all the suspended processes,
411/// ordered so critical wake-path processes sit at the `Vec::pop()` end and resume first when
412/// dropped.
413///
414/// Returns an error if any individual suspend failed, in which case the processes that were
415/// suspended are resumed before the error is returned.
416fn suspend_job(kernel_job: &Arc<zx::Job>) -> Result<SuspendedProcesses, Error> {
417    // The processes that were already handled, whether they were suspended or not.
418    let mut known_process_koids = std::collections::HashSet::<zx::Koid>::new();
419    // Resumes the suspended processes if an error is returned.
420    let mut suspended_processes = SuspendedProcesses(vec![]);
421    let mut priorities = Vec::<u8>::new();
422    loop {
423        // Processes that could not be suspended because they no longer exist or are dying are not
424        // retried: a dying process can remain in the job for an arbitrarily long time, but none of
425        // its threads will run user code again.
426        let new_process_koids: Vec<zx::Koid> = kernel_job
427            .processes()?
428            .into_iter()
429            .filter(|process_koid| known_process_koids.insert(*process_koid))
430            .collect();
431
432        // Processes can be created by threads that were not suspended yet. Once every thread of
433        // every known process is suspended, no new processes can appear, so we are done.
434        if new_process_koids.is_empty() {
435            break;
436        }
437
438        // Request the suspension of all the processes before waiting for any of their threads,
439        // so that the kernel suspends the threads of all the processes concurrently.
440        let job = kernel_job.clone();
441        let new_suspended_processes =
442            parallel_try_filter_map(new_process_koids, move |process_koid, _cancelled| {
443                suspend_process(&job, *process_koid)
444            })?;
445
446        let mut processes = Vec::with_capacity(new_suspended_processes.len());
447        for (process, suspend_handle, priority) in new_suspended_processes {
448            suspended_processes.0.push(suspend_handle);
449            priorities.push(priority);
450            processes.push(process);
451        }
452
453        parallel_try_filter_map(processes, |process, cancelled| {
454            wait_for_process_suspended(process, cancelled).map(|()| None::<()>)
455        })?;
456    }
457
458    if priorities.iter().any(|&priority| priority > 0) {
459        let mut handles_with_priority: Vec<(u8, zx::NullableHandle)> =
460            priorities.into_iter().zip(std::mem::take(&mut suspended_processes.0)).collect();
461        handles_with_priority.sort_by_key(|(priority, _)| *priority);
462        suspended_processes.0 =
463            handles_with_priority.into_iter().map(|(_, suspend_handle)| suspend_handle).collect();
464    }
465
466    Ok(suspended_processes)
467}
468
469/// Requests the suspension of the process identified by `process_koid` in `kernel_job`.
470///
471/// Returns the process, its suspend handle, and its resume priority, or `None` if the process no
472/// longer exists or is dying, in which case it does not need to be suspended. Returns any other
473/// error.
474fn suspend_process(
475    kernel_job: &zx::Job,
476    process_koid: zx::Koid,
477) -> Result<Option<(zx::Process, zx::NullableHandle, u8)>, zx::Status> {
478    let process_handle = match kernel_job.get_child(&process_koid, zx::Rights::SAME_RIGHTS) {
479        Ok(process_handle) => process_handle,
480        Err(zx::Status::NOT_FOUND) => {
481            // The process exited after the job was enumerated.
482            return Ok(None);
483        }
484        Err(e) => {
485            log::warn!("Failed to get container process {process_koid:?}: {e:?}");
486            return Err(e);
487        }
488    };
489    let process = zx::Process::from(process_handle);
490    match process.suspend() {
491        Ok(suspend_handle) => {
492            let priority = process_resume_priority(&process);
493            Ok(Some((process, suspend_handle, priority)))
494        }
495        Err(zx::Status::BAD_STATE) => {
496            // The process was already dead or dying, and thus can't be suspended.
497            Ok(None)
498        }
499        Err(e) => {
500            log::warn!("Failed process suspension: {:?}", e);
501            Err(e)
502        }
503    }
504}
505
506/// Waits until every thread of `process`, which must have been suspended, is suspended or
507/// terminated.
508///
509/// Threads that are blocked on an exception are skipped, as they will not be suspended until the
510/// exception is handled.
511///
512/// Gives up waiting without returning an error if `cancelled` becomes true.
513fn wait_for_process_suspended(
514    process: &zx::Process,
515    cancelled: &AtomicBool,
516) -> Result<(), zx::Status> {
517    let threads = process.threads()?;
518    for thread_koid in &threads {
519        if cancelled.load(Ordering::Relaxed) {
520            // Suspending another process failed, so the whole suspension is abandoned.
521            return Ok(());
522        }
523        fuchsia_trace::duration!("power", "starnix-runner:suspend_kernel", "thread_koid" => *thread_koid);
524        let thread_obj = match process.get_child(thread_koid, zx::Rights::SAME_RIGHTS) {
525            Ok(thread_obj) => thread_obj,
526            Err(zx::Status::NOT_FOUND) => {
527                // The thread exited after the process was enumerated.
528                continue;
529            }
530            Err(e) => {
531                log::warn!("Failed to get container thread {thread_koid:?}: {e:?}");
532                return Err(e);
533            }
534        };
535
536        // Most threads are already suspended by the time they are checked, so first check without
537        // blocking to avoid querying the state of each thread.
538        if thread_obj
539            .wait_one(THREAD_STOPPED_SIGNALS, zx::MonotonicInstant::ZERO)
540            .to_result()
541            .is_ok()
542        {
543            continue;
544        }
545
546        let mut watchdog_count = 0;
547        loop {
548            if let Ok(info) = thread_obj.info() {
549                if let zx::ThreadState::Blocked(zx::ThreadBlockType::Exception(_)) = info.state {
550                    let thread_name = thread_obj
551                        .get_name()
552                        .map(|n| n.to_string())
553                        .unwrap_or_else(|_| "unknown".to_string());
554                    log::warn!(
555                        "Thread {} (Koid: {:?}) is blocked on exception, skipping suspend wait.",
556                        thread_name,
557                        thread_koid
558                    );
559                    break;
560                }
561            }
562
563            match thread_obj
564                .wait_one(
565                    THREAD_STOPPED_SIGNALS,
566                    zx::MonotonicInstant::after(zx::Duration::from_millis(100)),
567                )
568                .to_result()
569            {
570                Err(zx::Status::TIMED_OUT) => {
571                    if cancelled.load(Ordering::Relaxed) {
572                        // Suspending another process failed, so the whole suspension is
573                        // abandoned.
574                        return Ok(());
575                    }
576                    watchdog_count += 1;
577                    if watchdog_count == 100 || watchdog_count % 600 == 0 {
578                        let process_name = process
579                            .get_name()
580                            .map(|n| n.to_string())
581                            .unwrap_or_else(|_| "unknown".to_string());
582                        let thread_name = thread_obj
583                            .get_name()
584                            .map(|n| n.to_string())
585                            .unwrap_or_else(|_| "unknown".to_string());
586                        let thread_state = thread_obj
587                            .info()
588                            .map(|info| format!("{:?}", info.state))
589                            .unwrap_or_else(|_| "unknown".to_string());
590                        log::warn!(
591                            "[SUSPEND_WATCHDOG] Timeout waiting for task suspension. Thread Koid: {:?} Name: '{}', Process: '{}', State: {}, continuing to wait...",
592                            thread_koid,
593                            thread_name,
594                            process_name,
595                            thread_state
596                        );
597                    }
598                }
599                Err(e) => {
600                    log::warn!("Error waiting for task suspension: {:?}", e);
601                    return Err(e);
602                }
603                _ => break,
604            }
605        }
606    }
607    Ok(())
608}
609
610/// Calls `f` on every item of `items`, spreading the calls over up to `MAX_SUSPEND_WORKERS`
611/// threads of `SUSPEND_WORKER_POOL` if there are more than `PARALLEL_SUSPEND_THRESHOLD` items, and
612/// returns the `Some` values returned by `f`, in no particular order.
613///
614/// If `f` fails for any item, no further items are processed, the `cancelled` flag passed to the
615/// calls of `f` that are still running is set, and the first error is returned once all the calls
616/// have returned. All the values returned by `f` are dropped in that case, mostly by the threads
617/// that returned them.
618fn parallel_try_filter_map<T, R, F>(items: Vec<T>, f: F) -> Result<Vec<R>, zx::Status>
619where
620    T: Send + Sync + 'static,
621    R: Send + 'static,
622    F: Fn(&T, &AtomicBool) -> Result<Option<R>, zx::Status> + Send + Sync + 'static,
623{
624    struct State<T, R, F> {
625        items: Vec<T>,
626        f: F,
627        next_index: AtomicUsize,
628        cancelled: AtomicBool,
629        first_error: OnceLock<zx::Status>,
630        results: LockDepMutex<Vec<R>, SuspendWorkerResultsLock>,
631    }
632
633    let num_items = items.len();
634    let state = Arc::new(State {
635        items,
636        f,
637        next_index: AtomicUsize::new(0),
638        cancelled: AtomicBool::new(false),
639        first_error: OnceLock::new(),
640        results: LockDepMutex::new(Vec::new()),
641    });
642    let worker_state = state.clone();
643    run_on_workers(num_items, move || {
644        let state = &*worker_state;
645        let mut results = Vec::new();
646        while !state.cancelled.load(Ordering::Relaxed) {
647            let Some(item) = state.items.get(state.next_index.fetch_add(1, Ordering::Relaxed))
648            else {
649                break;
650            };
651            match (state.f)(item, &state.cancelled) {
652                Ok(Some(result)) => results.push(result),
653                Ok(None) => {}
654                Err(e) => {
655                    let _ = state.first_error.set(e);
656                    state.cancelled.store(true, Ordering::Relaxed);
657                }
658            }
659        }
660        // If the work was cancelled, drop the results here, so that the workers release them
661        // (e.g. close the suspend tokens) in parallel.
662        if !results.is_empty() && !state.cancelled.load(Ordering::Relaxed) {
663            state.results.lock().append(&mut results);
664        }
665    });
666
667    if let Some(e) = state.first_error.get() {
668        return Err(*e);
669    }
670    Ok(std::mem::take(&mut *state.results.lock()))
671}
672
673/// Runs `job` across the dedicated worker threads of `SUSPEND_WORKER_POOL` when `num_items` is
674/// more than `PARALLEL_SUSPEND_THRESHOLD` (or on the calling thread otherwise), and returns once
675/// all workers running `job` have returned.
676///
677/// The threads running `job` are expected to share its work dynamically: a worker thread that
678/// doesn't start running `job` before another worker finishes all remaining items doesn't run it
679/// at all.
680fn run_on_workers(num_items: usize, job: impl Fn() + Send + Sync + 'static) {
681    if num_items <= PARALLEL_SUSPEND_THRESHOLD || !SUSPEND_WORKER_POOL.can_run_in_parallel() {
682        job();
683    } else {
684        SUSPEND_WORKER_POOL.run(Arc::new(job));
685    }
686}
687
688/// A job run by the threads of a `SuspendWorkerPool`.
689type SuspendWorkerJob = Arc<dyn Fn() + Send + Sync>;
690
691/// The pool of worker threads used to suspend and resume the processes of a container in parallel.
692///
693/// The worker threads are created once with the `SUSPEND_WORKER_ROLE_NAME` scheduler role and kept
694/// for the lifetime of the runner, so that no thread is created or destroyed while suspending or
695/// resuming a container. It is forced when the `SuspendContext` is created, so that its threads are
696/// not created during the first suspend.
697static SUSPEND_WORKER_POOL: LazyLock<SuspendWorkerPool> = LazyLock::new(SuspendWorkerPool::new);
698
699/// A pool of persistent worker threads that run suspend and resume jobs in parallel.
700struct SuspendWorkerPool {
701    /// Serializes the jobs, as the worker threads run one job at a time.
702    run_lock: LockDepMutex<(), SuspendWorkerRunLock>,
703    shared: Arc<SuspendWorkerPoolShared>,
704    num_workers: usize,
705}
706
707/// The state of a `SuspendWorkerPool` shared with its worker threads.
708struct SuspendWorkerPoolShared {
709    state: LockDepMutex<SuspendWorkerPoolState, SuspendWorkerPoolStateLock>,
710    /// Signaled when a job is available to the worker threads.
711    job_available: CondVar,
712    /// Signaled when the last worker thread running a job returns from it.
713    workers_done: CondVar,
714}
715
716#[derive(Default)]
717struct SuspendWorkerPoolState {
718    /// Monotonically increasing job generation so each worker claims a job at most once.
719    generation: u64,
720    /// The job currently being run.
721    job: Option<SuspendWorkerJob>,
722    /// The number of worker threads that can still start running `job`.
723    unclaimed: usize,
724    /// The number of worker threads running `job`.
725    running: usize,
726}
727
728impl SuspendWorkerPool {
729    fn new() -> Self {
730        let shared = Arc::new(SuspendWorkerPoolShared {
731            state: LockDepMutex::new(SuspendWorkerPoolState::default()),
732            job_available: CondVar::new(),
733            workers_done: CondVar::new(),
734        });
735        let available_workers =
736            std::thread::available_parallelism().map_or(1, |n| n.get()).min(MAX_SUSPEND_WORKERS);
737        let max_workers = if available_workers >= 2 { available_workers } else { 0 };
738        let mut num_workers = 0;
739        for _ in 0..max_workers {
740            let shared = shared.clone();
741            match std::thread::Builder::new().name(SUSPEND_WORKER_THREAD_NAME.to_string()).spawn(
742                move || {
743                    let _ = fuchsia_scheduler::set_role_for_this_thread(SUSPEND_WORKER_ROLE_NAME);
744                    shared.run_worker();
745                },
746            ) {
747                Ok(_) => num_workers += 1,
748                Err(e) => {
749                    // Fewer workers only reduces parallelism; if fewer than two workers spawn, work
750                    // falls back to the calling thread.
751                    log::warn!("Failed to spawn suspend worker: {e:?}");
752                    break;
753                }
754            }
755        }
756        Self { run_lock: LockDepMutex::new(()), shared, num_workers }
757    }
758
759    /// Whether the pool has at least two dedicated worker threads to execute jobs in parallel.
760    fn can_run_in_parallel(&self) -> bool {
761        self.num_workers >= 2
762    }
763
764    /// Runs `job` across the pool's dedicated worker threads (or on the calling thread if fewer
765    /// than two worker threads are available), and returns once all workers running `job` have
766    /// returned.
767    ///
768    /// Must not be called from within a job, as it would deadlock.
769    fn run(&self, job: SuspendWorkerJob) {
770        if !self.can_run_in_parallel() {
771            job();
772            return;
773        }
774        let _run_guard = self.run_lock.lock();
775        {
776            let mut state = self.shared.state.lock();
777            state.generation += 1;
778            state.job = Some(job);
779            state.unclaimed = self.num_workers;
780        }
781        self.shared.job_available.notify_all();
782
783        let mut state = self.shared.state.lock();
784        while state.unclaimed > 0 || state.running > 0 {
785            self.shared.workers_done.wait(&mut state);
786        }
787    }
788}
789
790impl SuspendWorkerPoolShared {
791    /// The body of the worker threads, which run the jobs as they become available.
792    fn run_worker(&self) {
793        let mut last_generation = 0;
794        let mut state = self.state.lock();
795        loop {
796            if state.unclaimed == 0 || state.generation == last_generation {
797                self.job_available.wait(&mut state);
798                continue;
799            }
800            last_generation = state.generation;
801            state.unclaimed -= 1;
802            state.running += 1;
803            let job = state.job.clone().expect("a job is available");
804            drop(state);
805
806            job();
807            // Release the job before reporting that it returned, so that the caller of
808            // `SuspendWorkerPool::run` gets back sole ownership of what the job holds.
809            drop(job);
810
811            state = self.state.lock();
812            // Once a worker finishes `job()`, all remaining items have already been claimed by the
813            // currently running workers or cancelled, so any late workers no longer need to start.
814            state.unclaimed = 0;
815            state.job = None;
816            state.running -= 1;
817            if state.running == 0 {
818                self.workers_done.notify_all();
819            }
820        }
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827    use fidl_fuchsia_power_system as fpower;
828    use futures::StreamExt;
829
830    /// Creates a named process in `job` with `num_threads` started threads, which are kept
831    /// suspended by the returned suspend handles so that they never execute any code.
832    fn create_named_process_with_suspended_threads(
833        job: &zx::Job,
834        name: &[u8],
835        num_threads: usize,
836    ) -> (zx::Process, Vec<zx::Thread>, Vec<zx::NullableHandle>) {
837        let (process, root_vmar) =
838            job.create_child_process(zx::ProcessOptions::empty(), name).unwrap();
839        let addr = root_vmar.info().unwrap().base;
840        let mut threads = vec![];
841        let mut thread_suspend_handles = vec![];
842        for i in 0..num_threads {
843            let thread = process.create_thread(b"test-thread").unwrap();
844            thread_suspend_handles.push(thread.suspend().unwrap());
845            if i == 0 {
846                process.start(&thread, addr, addr, zx::NullableHandle::invalid(), 0).unwrap();
847            } else {
848                thread.start(addr, addr, 0, 0).unwrap();
849            }
850            threads.push(thread);
851        }
852        for thread in &threads {
853            thread
854                .wait_one(zx::Signals::THREAD_SUSPENDED, zx::MonotonicInstant::INFINITE)
855                .to_result()
856                .unwrap();
857        }
858        (process, threads, thread_suspend_handles)
859    }
860
861    /// Creates a process in `job` with `num_threads` started threads, which are kept suspended by
862    /// the returned suspend handles so that they never execute any code.
863    fn create_process_with_suspended_threads(
864        job: &zx::Job,
865        num_threads: usize,
866    ) -> (zx::Process, Vec<zx::Thread>, Vec<zx::NullableHandle>) {
867        create_named_process_with_suspended_threads(job, b"test-process", num_threads)
868    }
869
870    fn is_suspended(thread: &zx::Thread) -> bool {
871        thread
872            .wait_one(zx::Signals::THREAD_SUSPENDED, zx::MonotonicInstant::ZERO)
873            .to_result()
874            .is_ok()
875    }
876
877    fn wait_until_blocked_on_exception(thread: &zx::Thread) {
878        while !matches!(
879            thread.info().unwrap().state,
880            zx::ThreadState::Blocked(zx::ThreadBlockType::Exception(_))
881        ) {
882            std::thread::sleep(std::time::Duration::from_millis(1));
883        }
884    }
885
886    /// Whether `parallel_try_filter_map` and `resume_processes` can run work in parallel when
887    /// given more than `PARALLEL_SUSPEND_THRESHOLD` items.
888    fn can_run_in_parallel() -> bool {
889        SUSPEND_WORKER_POOL.can_run_in_parallel()
890    }
891
892    /// A child job for a test, which is killed (and waited on) when dropped, so that the test
893    /// processes are killed before their suspend tokens are released. Exceptions in the job are
894    /// left unhandled in its exception channel, so they never escape the job.
895    struct TestJob {
896        job: Arc<zx::Job>,
897        _exception_channel: zx::Channel,
898    }
899
900    impl TestJob {
901        fn new() -> Self {
902            let job = fuchsia_runtime::job_default().create_child_job().unwrap();
903            let _exception_channel = job.create_exception_channel().unwrap();
904            Self { job: Arc::new(job), _exception_channel }
905        }
906    }
907
908    impl std::ops::Deref for TestJob {
909        type Target = Arc<zx::Job>;
910
911        fn deref(&self) -> &Arc<zx::Job> {
912            &self.job
913        }
914    }
915
916    impl Drop for TestJob {
917        fn drop(&mut self) {
918            if let Err(e) = self.job.kill() {
919                log::warn!("Failed to kill test job: {e:?}");
920                return;
921            }
922            let _ = self.job.wait_one(zx::Signals::TASK_TERMINATED, zx::MonotonicInstant::INFINITE);
923        }
924    }
925
926    // Note: These tests can't cover waiting for threads that are running when `suspend_job` is
927    // called (the 100 ms wait loop), as the test can't create threads that execute code: there is
928    // no executable code mapped in the test processes. The test threads are thus either already
929    // suspended or blocked on an exception when `suspend_job` checks them.
930
931    #[fuchsia::test]
932    fn suspend_empty_job() {
933        let job = TestJob::new();
934        assert_eq!(suspend_job(&job).unwrap().len(), 0);
935    }
936
937    #[fuchsia::test]
938    fn suspend_job_suspends_all_processes() {
939        // More than `PARALLEL_SUSPEND_THRESHOLD`, so that the processes are suspended in parallel.
940        const NUM_PROCESSES: usize = 16;
941        const NUM_THREADS: usize = 4;
942
943        let job = TestJob::new();
944        let mut processes = vec![];
945        let mut threads = vec![];
946        let mut thread_suspend_handles = vec![];
947        for _ in 0..NUM_PROCESSES {
948            let (process, mut process_threads, mut process_thread_suspend_handles) =
949                create_process_with_suspended_threads(&job, NUM_THREADS);
950            processes.push(process);
951            threads.append(&mut process_threads);
952            thread_suspend_handles.append(&mut process_thread_suspend_handles);
953        }
954
955        let suspend_handles = suspend_job(&job).unwrap();
956        assert_eq!(suspend_handles.len(), NUM_PROCESSES);
957
958        // The threads must stay suspended once only the handles returned by `suspend_job` remain.
959        drop(thread_suspend_handles);
960        for thread in &threads {
961            assert!(is_suspended(thread));
962        }
963
964        // Kill the processes before resuming them, as their threads can't execute any code.
965        drop(job);
966        drop(suspend_handles);
967    }
968
969    #[fuchsia::test]
970    fn suspend_job_skips_threads_blocked_in_exception() {
971        // The test job leaves exceptions unhandled, so the faulting thread stays blocked on the
972        // exception.
973        let job = TestJob::new();
974
975        // A process with a thread that faults immediately, since nothing is mapped in the process.
976        let (faulting_process, root_vmar) =
977            job.create_child_process(zx::ProcessOptions::empty(), b"faulting-process").unwrap();
978        let addr = root_vmar.info().unwrap().base;
979        let faulting_thread = faulting_process.create_thread(b"faulting-thread").unwrap();
980        faulting_process
981            .start(&faulting_thread, addr, addr, zx::NullableHandle::invalid(), 0)
982            .unwrap();
983        while !matches!(
984            faulting_thread.info().unwrap().state,
985            zx::ThreadState::Blocked(zx::ThreadBlockType::Exception(_))
986        ) {
987            std::thread::sleep(std::time::Duration::from_millis(1));
988        }
989
990        let (_process, threads, thread_suspend_handles) =
991            create_process_with_suspended_threads(&job, 2);
992
993        let suspend_handles = suspend_job(&job).unwrap();
994        assert_eq!(suspend_handles.len(), 2);
995        assert!(!is_suspended(&faulting_thread));
996        drop(thread_suspend_handles);
997        for thread in &threads {
998            assert!(is_suspended(thread));
999        }
1000
1001        drop(job);
1002        drop(suspend_handles);
1003    }
1004
1005    /// Checks that dropping the result of `suspend_job` for a job with `num_processes` processes
1006    /// closes all the suspend handles and resumes all the threads.
1007    fn check_dropping_suspended_processes_resumes_threads(num_processes: usize) {
1008        const NUM_THREADS: usize = 2;
1009
1010        // The test job leaves exceptions unhandled, so the resumed threads, which fault since
1011        // nothing is mapped in their processes, block on the exception.
1012        let job = TestJob::new();
1013        let mut processes = vec![];
1014        let mut threads = vec![];
1015        let mut thread_suspend_handles = vec![];
1016        for _ in 0..num_processes {
1017            let (process, mut process_threads, mut process_thread_suspend_handles) =
1018                create_process_with_suspended_threads(&job, NUM_THREADS);
1019            processes.push(process);
1020            threads.append(&mut process_threads);
1021            thread_suspend_handles.append(&mut process_thread_suspend_handles);
1022        }
1023
1024        let suspended_processes = suspend_job(&job).unwrap();
1025        assert_eq!(suspended_processes.len(), num_processes);
1026        let raw_suspend_handles: Vec<_> =
1027            suspended_processes.0.iter().map(|handle| handle.raw_handle()).collect();
1028        drop(thread_suspend_handles);
1029        for thread in &threads {
1030            assert!(is_suspended(thread));
1031        }
1032
1033        drop(suspended_processes);
1034
1035        for raw_suspend_handle in raw_suspend_handles {
1036            assert!(zx::Handle::check_raw_valid(raw_suspend_handle).is_err());
1037        }
1038        for thread in &threads {
1039            wait_until_blocked_on_exception(thread);
1040        }
1041    }
1042
1043    #[fuchsia::test]
1044    fn dropping_few_suspended_processes_resumes_threads() {
1045        check_dropping_suspended_processes_resumes_threads(PARALLEL_SUSPEND_THRESHOLD);
1046    }
1047
1048    #[fuchsia::test]
1049    fn dropping_many_suspended_processes_resumes_threads() {
1050        // More than `PARALLEL_SUSPEND_THRESHOLD`, so that the processes are resumed in parallel.
1051        check_dropping_suspended_processes_resumes_threads(4 * PARALLEL_SUSPEND_THRESHOLD + 1);
1052    }
1053
1054    #[fuchsia::test]
1055    fn resume_processes_closes_all_handles() {
1056        for num_handles in [0, 1, PARALLEL_SUSPEND_THRESHOLD, 4 * PARALLEL_SUSPEND_THRESHOLD + 1] {
1057            let events: Vec<_> = (0..num_handles).map(|_| zx::Event::create()).collect();
1058            let mut handles = vec![];
1059            for event in &events {
1060                handles
1061                    .push(event.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap().into_handle());
1062            }
1063            resume_processes(handles);
1064            // All the duplicates were closed, so only the original handles remain.
1065            for event in &events {
1066                assert_eq!(event.count_info().unwrap().handle_count, 1);
1067            }
1068        }
1069    }
1070
1071    #[fuchsia::test]
1072    fn process_resume_priority_orders_critical_wake_processes() {
1073        let kernel_prio = process_resume_priority_for_name(b"starnix_kernel.cm");
1074        let suspend_service_prio =
1075            process_resume_priority_for_name(b"android.system.suspend-service");
1076        let short_suspend_service_prio = process_resume_priority_for_name(b"suspend-service");
1077        let system_server_prio = process_resume_priority_for_name(b"system_server");
1078        let surfaceflinger_prio = process_resume_priority_for_name(b"surfaceflinger");
1079        let normal_prio = process_resume_priority_for_name(b"com.android.systemui");
1080
1081        assert_eq!(suspend_service_prio, short_suspend_service_prio);
1082        assert!(kernel_prio > suspend_service_prio);
1083        assert!(suspend_service_prio > system_server_prio);
1084        assert!(system_server_prio > surfaceflinger_prio);
1085        assert!(surfaceflinger_prio > normal_prio);
1086        assert_eq!(normal_prio, 0);
1087        assert_eq!(process_resume_priority_for_name(b"starnix_mlock_pins"), 0);
1088        assert_eq!(process_resume_priority_for_name(b"starnix_zx_map_info_cache"), 0);
1089        assert_eq!(process_resume_priority_for_name(b"audioserver"), 0);
1090        assert_eq!(process_resume_priority_for_name(b""), 0);
1091    }
1092
1093    #[fuchsia::test]
1094    fn suspend_job_places_critical_processes_at_pop_end() {
1095        // Spawn more than `PARALLEL_SUSPEND_THRESHOLD` total processes in scrambled order so
1096        // `suspend_job` exercises parallel enumeration while sorting critical wake-path processes
1097        // to the `Vec::pop()` tail.
1098        let job = TestJob::new();
1099        let mut critical_thread_suspend_handles = vec![];
1100        let mut app_thread_suspend_handles = vec![];
1101        let mut app_processes = vec![];
1102        let mut app_threads = vec![];
1103
1104        let mut spawn_app = |name: &[u8]| {
1105            let (process, mut threads, mut suspend_handles) =
1106                create_named_process_with_suspended_threads(&job, name, 2);
1107            app_processes.push(process);
1108            app_threads.append(&mut threads);
1109            app_thread_suspend_handles.append(&mut suspend_handles);
1110        };
1111
1112        spawn_app(b"com.android.settings");
1113        spawn_app(b"audioserver");
1114        let (_sf_proc, sf_threads, mut sf_handles) =
1115            create_named_process_with_suspended_threads(&job, b"surfaceflinger", 2);
1116        critical_thread_suspend_handles.append(&mut sf_handles);
1117
1118        spawn_app(b"zygote64");
1119        spawn_app(b"com.android.systemui");
1120        let (_ss_proc, ss_threads, mut ss_handles) =
1121            create_named_process_with_suspended_threads(&job, b"system_server", 2);
1122        critical_thread_suspend_handles.append(&mut ss_handles);
1123
1124        spawn_app(b"netd");
1125        spawn_app(b"logd");
1126        let (_susp_proc, susp_threads, mut susp_handles) =
1127            create_named_process_with_suspended_threads(&job, b"android.system.suspend-service", 2);
1128        critical_thread_suspend_handles.append(&mut susp_handles);
1129
1130        spawn_app(b"servicemanager");
1131        let (_kernel_proc, kernel_threads, mut kernel_handles) =
1132            create_named_process_with_suspended_threads(&job, b"starnix_kernel.cm", 2);
1133        critical_thread_suspend_handles.append(&mut kernel_handles);
1134
1135        spawn_app(b"vold");
1136        spawn_app(b"lmkd");
1137
1138        let mut suspended_processes = suspend_job(&job).unwrap();
1139        assert_eq!(suspended_processes.len(), 13);
1140        drop(critical_thread_suspend_handles);
1141        drop(app_thread_suspend_handles);
1142
1143        for thread in kernel_threads
1144            .iter()
1145            .chain(&susp_threads)
1146            .chain(&ss_threads)
1147            .chain(&sf_threads)
1148            .chain(&app_threads)
1149        {
1150            assert!(is_suspended(thread));
1151        }
1152
1153        // Pop 1st handle (tail of `Vec` / chunk #0 slot 0): `starnix_kernel.cm` thaws first.
1154        drop(suspended_processes.0.pop().unwrap());
1155        for thread in &kernel_threads {
1156            wait_until_blocked_on_exception(thread);
1157        }
1158        for thread in susp_threads.iter().chain(&ss_threads).chain(&sf_threads).chain(&app_threads)
1159        {
1160            assert!(is_suspended(thread));
1161        }
1162
1163        // Pop 2nd handle (chunk #0 slot 1): `android.system.suspend-service` thaws second.
1164        drop(suspended_processes.0.pop().unwrap());
1165        for thread in &susp_threads {
1166            wait_until_blocked_on_exception(thread);
1167        }
1168        for thread in ss_threads.iter().chain(&sf_threads).chain(&app_threads) {
1169            assert!(is_suspended(thread));
1170        }
1171
1172        // Pop 3rd handle (chunk #0 slot 2): `system_server` thaws third.
1173        drop(suspended_processes.0.pop().unwrap());
1174        for thread in &ss_threads {
1175            wait_until_blocked_on_exception(thread);
1176        }
1177        for thread in sf_threads.iter().chain(&app_threads) {
1178            assert!(is_suspended(thread));
1179        }
1180
1181        // Pop 4th handle (chunk #0 slot 3): `surfaceflinger` thaws fourth.
1182        drop(suspended_processes.0.pop().unwrap());
1183        for thread in &sf_threads {
1184            wait_until_blocked_on_exception(thread);
1185        }
1186        for thread in &app_threads {
1187            assert!(is_suspended(thread));
1188        }
1189
1190        // Dropping `suspended_processes` resumes the remaining 9 (> `PARALLEL_SUSPEND_THRESHOLD`)
1191        // background processes in parallel.
1192        assert_eq!(suspended_processes.len(), 9);
1193        drop(suspended_processes);
1194        for thread in &app_threads {
1195            wait_until_blocked_on_exception(thread);
1196        }
1197    }
1198
1199    #[fuchsia::test]
1200    async fn suspend_container_thaws_processes_before_wake_lease_completes() {
1201        let job = TestJob::new();
1202        let (_process, threads, thread_suspend_handles) =
1203            create_named_process_with_suspended_threads(&job, b"starnix_kernel.cm", 2);
1204
1205        let (activity_governor, mut sag_stream) =
1206            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
1207        let kernels = Arc::new(
1208            Kernels::new_for_test(job.clone(), "test-container", activity_governor).unwrap(),
1209        );
1210
1211        let suspend_context = Arc::new(SuspendContext::default());
1212        let wake_counter = zx::Counter::create();
1213        let wake_koid = wake_counter.koid().unwrap();
1214        // Pre-signal the wake source so `zx::object_wait_many` wakes immediately and attributes
1215        // the wake reason without blocking the executor thread.
1216        wake_counter.add(1).unwrap();
1217        suspend_context
1218            .wake_sources
1219            .lock()
1220            .insert(wake_koid, WakeSource::from_counter(wake_counter, "power-button".to_string()));
1221
1222        let (watcher_runner, watcher_client) = zx::EventPair::create();
1223        watcher_runner.signal_peer(zx::Signals::NONE, AWAKE_SIGNAL).unwrap();
1224        suspend_context.wake_watchers.lock().push(watcher_runner);
1225
1226        let container_job = job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
1227        let suspend_context_clone = suspend_context.clone();
1228        let kernels_clone = kernels.clone();
1229        let suspend_task = fuchsia_async::Task::local(async move {
1230            suspend_container(
1231                fstarnixrunner::ManagerSuspendContainerRequest {
1232                    container_job: Some(container_job),
1233                    wake_locks: None,
1234                    ..Default::default()
1235                },
1236                &suspend_context_clone,
1237                &kernels_clone,
1238            )
1239            .await
1240        });
1241
1242        // `suspend_container` must write `TakeApplicationActivityLease` to SAG's channel, thaw the
1243        // container processes, restore wake watchers to `AWAKE_SIGNAL`, and return its FIDL
1244        // response immediately without waiting for SAG's `AllDrivers` reply.
1245        let response = suspend_task.await.unwrap().unwrap();
1246        assert_eq!(response.resume_reason.as_deref(), Some("power-button"));
1247
1248        // Receive `TakeApplicationActivityLease` and verify it was already queued in SAG's channel
1249        // while `responder` is still unanswered.
1250        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
1251            name,
1252            responder,
1253        })) = sag_stream.next().await
1254        else {
1255            panic!("expected TakeApplicationActivityLease request");
1256        };
1257        assert_eq!(name, "starnix-kernel-test-container");
1258        assert!(!kernels.has_wake_lease(&job));
1259
1260        // Release the test's initial thread suspend handles while `responder` is still unanswered.
1261        // Because `suspend_container` already dropped `suspend_handles`, the container threads
1262        // thaw immediately instead of staying frozen.
1263        drop(thread_suspend_handles);
1264        for thread in &threads {
1265            wait_until_blocked_on_exception(thread);
1266        }
1267        assert!(
1268            watcher_client.wait_one(AWAKE_SIGNAL, zx::MonotonicInstant::ZERO).to_result().is_ok()
1269        );
1270
1271        let (lease_client, lease_server) = zx::EventPair::create();
1272        responder.send(lease_client).unwrap();
1273        kernels.wait_for_in_flight_wake_lease(&job).await;
1274        assert!(kernels.has_wake_lease(&job));
1275        assert_eq!(
1276            lease_server
1277                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
1278                .to_result(),
1279            Err(zx::Status::TIMED_OUT)
1280        );
1281    }
1282
1283    #[fuchsia::test]
1284    async fn suspend_container_rapid_resuspend_waits_for_in_flight_wake_lease() {
1285        let job = TestJob::new();
1286        let (_process, _threads, _thread_suspend_handles) =
1287            create_named_process_with_suspended_threads(&job, b"starnix_kernel.cm", 1);
1288
1289        let (activity_governor, mut sag_stream) =
1290            fidl::endpoints::create_proxy_and_stream::<fpower::ActivityGovernorMarker>();
1291        let kernels = Arc::new(
1292            Kernels::new_for_test(job.clone(), "test-container", activity_governor).unwrap(),
1293        );
1294
1295        let suspend_context = Arc::new(SuspendContext::default());
1296        let wake_counter = zx::Counter::create();
1297        let wake_koid = wake_counter.koid().unwrap();
1298        wake_counter.add(1).unwrap();
1299        suspend_context
1300            .wake_sources
1301            .lock()
1302            .insert(wake_koid, WakeSource::from_counter(wake_counter, "power-button".to_string()));
1303
1304        // First suspend/wake cycle returns immediately while SAG's `TakeApplicationActivityLease`
1305        // reply is still in flight.
1306        let container_job1 = job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
1307        let response1 = suspend_container(
1308            fstarnixrunner::ManagerSuspendContainerRequest {
1309                container_job: Some(container_job1),
1310                wake_locks: None,
1311                ..Default::default()
1312            },
1313            &suspend_context,
1314            &kernels,
1315        )
1316        .await
1317        .unwrap()
1318        .unwrap();
1319        assert_eq!(response1.resume_reason.as_deref(), Some("power-button"));
1320
1321        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
1322            name: name1,
1323            responder: responder1,
1324        })) = sag_stream.next().await
1325        else {
1326            panic!("expected first TakeApplicationActivityLease request");
1327        };
1328        assert_eq!(name1, "starnix-kernel-test-container");
1329        assert!(!kernels.has_wake_lease(&job));
1330
1331        // Immediately enter a second `suspend_container` before SAG answers `responder1`.
1332        // `drop_wake_lease` must wait for the in-flight acquisition and drop its lease before
1333        // proceeding into the sleep wait.
1334        let container_job2 = job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
1335        let mut suspend2_fut = std::pin::pin!(suspend_container(
1336            fstarnixrunner::ManagerSuspendContainerRequest {
1337                container_job: Some(container_job2),
1338                wake_locks: None,
1339                ..Default::default()
1340            },
1341            &suspend_context,
1342            &kernels,
1343        ));
1344        assert!(matches!(futures::poll!(&mut suspend2_fut), std::task::Poll::Pending));
1345        {
1346            let mut next_req = std::pin::pin!(sag_stream.next());
1347            assert!(matches!(futures::poll!(&mut next_req), std::task::Poll::Pending));
1348        }
1349
1350        // Now SAG replies to the first wake's request; `drop_wake_lease` must immediately close
1351        // `lease1` and allow the second suspend/wake cycle to complete and acquire `lease2`.
1352        let (lease1_client, lease1_server) = zx::EventPair::create();
1353        responder1.send(lease1_client).unwrap();
1354
1355        let response2 = suspend2_fut.await.unwrap().unwrap();
1356        assert_eq!(response2.resume_reason.as_deref(), Some("power-button"));
1357        assert!(
1358            lease1_server
1359                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
1360                .to_result()
1361                .is_ok()
1362        );
1363
1364        let Some(Ok(fpower::ActivityGovernorRequest::TakeApplicationActivityLease {
1365            name: name2,
1366            responder: responder2,
1367        })) = sag_stream.next().await
1368        else {
1369            panic!("expected second TakeApplicationActivityLease request");
1370        };
1371        assert_eq!(name2, "starnix-kernel-test-container");
1372        let (lease2_client, lease2_server) = zx::EventPair::create();
1373        responder2.send(lease2_client).unwrap();
1374        kernels.wait_for_in_flight_wake_lease(&job).await;
1375        assert!(kernels.has_wake_lease(&job));
1376        assert_eq!(
1377            lease2_server
1378                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::ZERO)
1379                .to_result(),
1380            Err(zx::Status::TIMED_OUT)
1381        );
1382    }
1383
1384    #[fuchsia::test]
1385    fn parallel_try_filter_map_runs_few_items_on_calling_thread() {
1386        let items: Vec<usize> = (0..PARALLEL_SUSPEND_THRESHOLD).collect();
1387        let calling_thread = std::thread::current().id();
1388        let results = parallel_try_filter_map(items.clone(), move |item, _cancelled| {
1389            assert_eq!(std::thread::current().id(), calling_thread);
1390            Ok(Some(*item))
1391        })
1392        .unwrap();
1393        // A single worker processes the items in order.
1394        assert_eq!(results, items);
1395    }
1396
1397    #[fuchsia::test]
1398    fn parallel_try_filter_map_runs_many_items_on_helper_threads() {
1399        if !can_run_in_parallel() {
1400            return;
1401        }
1402        // Run the function twice, as the worker threads are reused across calls.
1403        for _ in 0..2 {
1404            // More than `PARALLEL_SUSPEND_THRESHOLD` items, so that the items are processed in
1405            // parallel across the dedicated `starnix-suspend-worker` threads.
1406            let items: Vec<usize> = (0..=PARALLEL_SUSPEND_THRESHOLD).collect();
1407            let second_worker_ran = Arc::new(AtomicBool::new(false));
1408            let second_worker_ran_clone = second_worker_ran.clone();
1409            parallel_try_filter_map(items, move |item, _cancelled| {
1410                assert_eq!(std::thread::current().name(), Some(SUSPEND_WORKER_THREAD_NAME));
1411                if *item == 0 {
1412                    // Hold the worker that picked up item 0 until a second worker thread runs a
1413                    // call on one of the remaining items.
1414                    while !second_worker_ran_clone.load(Ordering::Relaxed) {
1415                        std::thread::yield_now();
1416                    }
1417                } else {
1418                    second_worker_ran_clone.store(true, Ordering::Relaxed);
1419                }
1420                Ok(None::<()>)
1421            })
1422            .unwrap();
1423            assert!(second_worker_ran.load(Ordering::Relaxed));
1424        }
1425    }
1426
1427    #[fuchsia::test]
1428    fn parallel_try_filter_map_returns_all_values() {
1429        let items: Vec<usize> = (0..100).collect();
1430        let mut results = parallel_try_filter_map(items, |item, cancelled| {
1431            assert!(!cancelled.load(Ordering::Relaxed));
1432            Ok((item % 2 == 0).then_some(*item))
1433        })
1434        .unwrap();
1435        results.sort();
1436        assert_eq!(results, (0..100).step_by(2).collect::<Vec<_>>());
1437    }
1438
1439    #[fuchsia::test]
1440    fn parallel_try_filter_map_handles_no_items() {
1441        let items: Vec<usize> = vec![];
1442        let results = parallel_try_filter_map(items, |item, _cancelled| Ok(Some(*item))).unwrap();
1443        assert!(results.is_empty());
1444    }
1445
1446    #[fuchsia::test]
1447    fn parallel_try_filter_map_returns_first_error() {
1448        let items: Vec<usize> = (0..100).collect();
1449        let result = parallel_try_filter_map(items, |item, _cancelled| {
1450            if *item == 10 { Err(zx::Status::INTERNAL) } else { Ok(Some(*item)) }
1451        });
1452        assert_eq!(result, Err(zx::Status::INTERNAL));
1453    }
1454
1455    #[fuchsia::test]
1456    fn parallel_try_filter_map_stops_after_error() {
1457        let items: Vec<usize> = (0..100).collect();
1458        let processed = Arc::new(LockDepMutex::<_, SuspendWorkerResultsLock>::new(vec![]));
1459        let processed_clone = processed.clone();
1460        let result = parallel_try_filter_map(items, move |item, _cancelled| {
1461            processed_clone.lock().push(*item);
1462            // Fail every item, so that no worker can process more than one item.
1463            Err::<Option<()>, _>(zx::Status::INTERNAL)
1464        });
1465        assert_eq!(result, Err(zx::Status::INTERNAL));
1466        let processed = processed.lock();
1467        assert!(!processed.is_empty());
1468        assert!(processed.len() <= MAX_SUSPEND_WORKERS);
1469    }
1470
1471    #[fuchsia::test]
1472    fn parallel_try_filter_map_cancels_running_calls_on_error() {
1473        if !can_run_in_parallel() {
1474            return;
1475        }
1476        // Items are handed out in order, so the call for item 0 starts first. It only fails once
1477        // the call for another item is running, which must then be cancelled. There are more than
1478        // `PARALLEL_SUSPEND_THRESHOLD` items so that the items are processed in parallel.
1479        let items: Vec<usize> = (0..=PARALLEL_SUSPEND_THRESHOLD).collect();
1480        let other_call_started = Arc::new(AtomicBool::new(false));
1481        let other_call_cancelled = Arc::new(AtomicBool::new(false));
1482        let (started, cancelled_clone) = (other_call_started.clone(), other_call_cancelled.clone());
1483        let result = parallel_try_filter_map(items, move |item, cancelled| {
1484            if *item == 0 {
1485                while !started.load(Ordering::Relaxed) {
1486                    std::thread::yield_now();
1487                }
1488                return Err(zx::Status::INTERNAL);
1489            }
1490            started.store(true, Ordering::Relaxed);
1491            // Wait until the failure of the call for item 0 cancels this call.
1492            while !cancelled.load(Ordering::Relaxed) {
1493                std::thread::yield_now();
1494            }
1495            cancelled_clone.store(true, Ordering::Relaxed);
1496            Ok(Some(*item))
1497        });
1498        assert_eq!(result, Err(zx::Status::INTERNAL));
1499        assert!(other_call_cancelled.load(Ordering::Relaxed));
1500    }
1501}