1use 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
19pub const AWAKE_SIGNAL: zx::Signals = zx::Signals::USER_0;
21
22pub 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 pub node: inspect::Node,
53 pub suspend_duration_histogram: UintExponentialHistogramProperty,
56 pub suspend_attempts_count: UintProperty,
58 pub suspend_successes_count: UintProperty,
60 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 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
110pub 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 let wake_watchers_awake_guard;
133
134 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 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 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 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 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 Err(zx::Status::TIMED_OUT) => Some("starnix-container-timeout".into()),
225 _ => None,
227 }
228 } else {
229 Some(resume_reasons.join(","))
230 }
231 };
232
233 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 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
256const MAX_SUSPEND_WORKERS: usize = 4;
266
267const PARALLEL_SUSPEND_THRESHOLD: usize = 8;
270
271const RESUME_CHUNK_SIZE: usize = 4;
278
279const SUSPEND_WORKER_THREAD_NAME: &str = "starnix-suspend-worker";
281
282const SUSPEND_WORKER_ROLE_NAME: &str = "fuchsia.starnix.runner.suspend_worker";
285
286const THREAD_STOPPED_SIGNALS: zx::Signals =
288 zx::Signals::THREAD_SUSPENDED.union(zx::Signals::THREAD_TERMINATED);
289
290struct 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
309fn 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 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 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 drop(chunk);
364 if is_empty {
365 break;
366 }
367 }
368 }));
369}
370
371fn 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
408fn suspend_job(kernel_job: &Arc<zx::Job>) -> Result<SuspendedProcesses, Error> {
417 let mut known_process_koids = std::collections::HashSet::<zx::Koid>::new();
419 let mut suspended_processes = SuspendedProcesses(vec![]);
421 let mut priorities = Vec::<u8>::new();
422 loop {
423 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 if new_process_koids.is_empty() {
435 break;
436 }
437
438 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
469fn 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 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 Ok(None)
498 }
499 Err(e) => {
500 log::warn!("Failed process suspension: {:?}", e);
501 Err(e)
502 }
503 }
504}
505
506fn 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 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 continue;
529 }
530 Err(e) => {
531 log::warn!("Failed to get container thread {thread_koid:?}: {e:?}");
532 return Err(e);
533 }
534 };
535
536 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 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
610fn 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 !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
673fn 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
688type SuspendWorkerJob = Arc<dyn Fn() + Send + Sync>;
690
691static SUSPEND_WORKER_POOL: LazyLock<SuspendWorkerPool> = LazyLock::new(SuspendWorkerPool::new);
698
699struct SuspendWorkerPool {
701 run_lock: LockDepMutex<(), SuspendWorkerRunLock>,
703 shared: Arc<SuspendWorkerPoolShared>,
704 num_workers: usize,
705}
706
707struct SuspendWorkerPoolShared {
709 state: LockDepMutex<SuspendWorkerPoolState, SuspendWorkerPoolStateLock>,
710 job_available: CondVar,
712 workers_done: CondVar,
714}
715
716#[derive(Default)]
717struct SuspendWorkerPoolState {
718 generation: u64,
720 job: Option<SuspendWorkerJob>,
722 unclaimed: usize,
724 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 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 fn can_run_in_parallel(&self) -> bool {
761 self.num_workers >= 2
762 }
763
764 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 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 drop(job);
810
811 state = self.state.lock();
812 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 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 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 fn can_run_in_parallel() -> bool {
889 SUSPEND_WORKER_POOL.can_run_in_parallel()
890 }
891
892 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 #[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 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 drop(thread_suspend_handles);
960 for thread in &threads {
961 assert!(is_suspended(thread));
962 }
963
964 drop(job);
966 drop(suspend_handles);
967 }
968
969 #[fuchsia::test]
970 fn suspend_job_skips_threads_blocked_in_exception() {
971 let job = TestJob::new();
974
975 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 fn check_dropping_suspended_processes_resumes_threads(num_processes: usize) {
1008 const NUM_THREADS: usize = 2;
1009
1010 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 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 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 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 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 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 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 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 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 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 let response = suspend_task.await.unwrap().unwrap();
1246 assert_eq!(response.resume_reason.as_deref(), Some("power-button"));
1247
1248 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 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 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 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 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 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 for _ in 0..2 {
1404 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 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 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 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 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}