1use dispatch2::{DispatchQueue, DispatchQueueGlobalPriority, DispatchTime, GlobalQueueIdentifier};
2use gpui::{
3 GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, RunnableMeta, RunnableVariant, TaskTiming,
4 ThreadTaskTimings, add_task_timing,
5};
6use mach2::{
7 kern_return::KERN_SUCCESS,
8 mach_time::mach_timebase_info_data_t,
9 thread_policy::{
10 THREAD_EXTENDED_POLICY, THREAD_EXTENDED_POLICY_COUNT, THREAD_PRECEDENCE_POLICY,
11 THREAD_PRECEDENCE_POLICY_COUNT, THREAD_TIME_CONSTRAINT_POLICY,
12 THREAD_TIME_CONSTRAINT_POLICY_COUNT, thread_extended_policy_data_t,
13 thread_precedence_policy_data_t, thread_time_constraint_policy_data_t,
14 },
15};
16use util::ResultExt;
17
18use async_task::Runnable;
19use objc::{
20 class, msg_send,
21 runtime::{BOOL, YES},
22 sel, sel_impl,
23};
24use std::{
25 ffi::c_void,
26 ptr::NonNull,
27 time::{Duration, Instant},
28};
29
30pub(crate) struct MacDispatcher;
31
32impl MacDispatcher {
33 pub fn new() -> Self {
34 Self
35 }
36}
37
38impl PlatformDispatcher for MacDispatcher {
39 fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
40 let global_timings = GLOBAL_THREAD_TIMINGS.lock();
41 ThreadTaskTimings::convert(&global_timings)
42 }
43
44 fn get_current_thread_timings(&self) -> ThreadTaskTimings {
45 gpui::profiler::get_current_thread_task_timings()
46 }
47
48 fn is_main_thread(&self) -> bool {
49 let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
50 is_main_thread == YES
51 }
52
53 fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
54 let context = runnable.into_raw().as_ptr() as *mut c_void;
55
56 let queue_priority = match priority {
57 Priority::RealtimeAudio => {
58 panic!("RealtimeAudio priority should use spawn_realtime, not dispatch")
59 }
60 Priority::High => DispatchQueueGlobalPriority::High,
61 Priority::Medium => DispatchQueueGlobalPriority::Default,
62 Priority::Low => DispatchQueueGlobalPriority::Low,
63 };
64
65 unsafe {
66 DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(queue_priority))
67 .exec_async_f(context, trampoline);
68 }
69 }
70
71 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
72 let context = runnable.into_raw().as_ptr() as *mut c_void;
73 unsafe {
74 DispatchQueue::main().exec_async_f(context, trampoline);
75 }
76 }
77
78 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
79 let context = runnable.into_raw().as_ptr() as *mut c_void;
80 let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::Priority(
81 DispatchQueueGlobalPriority::High,
82 ));
83 let when = DispatchTime::NOW.time(duration.as_nanos() as i64);
84 unsafe {
85 DispatchQueue::exec_after_f(when, &queue, context, trampoline);
86 }
87 }
88
89 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
90 std::thread::spawn(move || {
91 set_audio_thread_priority().log_err();
92 f();
93 });
94 }
95}
96
97fn set_audio_thread_priority() -> anyhow::Result<()> {
98 // https://chromium.googlesource.com/chromium/chromium/+/master/base/threading/platform_thread_mac.mm#93
99
100 // SAFETY: always safe to call
101 let thread_id = unsafe { libc::pthread_self() };
102
103 // SAFETY: thread_id is a valid thread id
104 let thread_id = unsafe { libc::pthread_mach_thread_np(thread_id) };
105
106 // Fixed priority thread
107 let mut policy = thread_extended_policy_data_t { timeshare: 0 };
108
109 // SAFETY: thread_id is a valid thread id
110 // SAFETY: thread_extended_policy_data_t is passed as THREAD_EXTENDED_POLICY
111 let result = unsafe {
112 mach2::thread_policy::thread_policy_set(
113 thread_id,
114 THREAD_EXTENDED_POLICY,
115 &mut policy as *mut _ as *mut _,
116 THREAD_EXTENDED_POLICY_COUNT,
117 )
118 };
119
120 if result != KERN_SUCCESS {
121 anyhow::bail!("failed to set thread extended policy");
122 }
123
124 // relatively high priority
125 let mut precedence = thread_precedence_policy_data_t { importance: 63 };
126
127 // SAFETY: thread_id is a valid thread id
128 // SAFETY: thread_precedence_policy_data_t is passed as THREAD_PRECEDENCE_POLICY
129 let result = unsafe {
130 mach2::thread_policy::thread_policy_set(
131 thread_id,
132 THREAD_PRECEDENCE_POLICY,
133 &mut precedence as *mut _ as *mut _,
134 THREAD_PRECEDENCE_POLICY_COUNT,
135 )
136 };
137
138 if result != KERN_SUCCESS {
139 anyhow::bail!("failed to set thread precedence policy");
140 }
141
142 const GUARANTEED_AUDIO_DUTY_CYCLE: f32 = 0.75;
143 const MAX_AUDIO_DUTY_CYCLE: f32 = 0.85;
144
145 // ~128 frames @ 44.1KHz
146 const TIME_QUANTUM: f32 = 2.9;
147
148 const AUDIO_TIME_NEEDED: f32 = GUARANTEED_AUDIO_DUTY_CYCLE * TIME_QUANTUM;
149 const MAX_TIME_ALLOWED: f32 = MAX_AUDIO_DUTY_CYCLE * TIME_QUANTUM;
150
151 let mut timebase_info = mach_timebase_info_data_t { numer: 0, denom: 0 };
152 // SAFETY: timebase_info is a valid pointer to a mach_timebase_info_data_t struct
153 unsafe { mach2::mach_time::mach_timebase_info(&mut timebase_info) };
154
155 let ms_to_abs_time = ((timebase_info.denom as f32) / (timebase_info.numer as f32)) * 1000000f32;
156
157 let mut time_constraints = thread_time_constraint_policy_data_t {
158 period: (TIME_QUANTUM * ms_to_abs_time) as u32,
159 computation: (AUDIO_TIME_NEEDED * ms_to_abs_time) as u32,
160 constraint: (MAX_TIME_ALLOWED * ms_to_abs_time) as u32,
161 preemptible: 0,
162 };
163
164 // SAFETY: thread_id is a valid thread id
165 // SAFETY: thread_precedence_pthread_time_constraint_policy_data_t is passed as THREAD_TIME_CONSTRAINT_POLICY
166 let result = unsafe {
167 mach2::thread_policy::thread_policy_set(
168 thread_id,
169 THREAD_TIME_CONSTRAINT_POLICY,
170 &mut time_constraints as *mut _ as *mut _,
171 THREAD_TIME_CONSTRAINT_POLICY_COUNT,
172 )
173 };
174
175 if result != KERN_SUCCESS {
176 anyhow::bail!("failed to set thread time constraint policy");
177 }
178
179 Ok(())
180}
181
182extern "C" fn trampoline(context: *mut c_void) {
183 let runnable =
184 unsafe { Runnable::<RunnableMeta>::from_raw(NonNull::new_unchecked(context as *mut ())) };
185
186 let location = runnable.metadata().location;
187
188 let start = Instant::now();
189 let mut timing = TaskTiming {
190 location,
191 start,
192 end: None,
193 };
194
195 add_task_timing(timing);
196
197 runnable.run();
198
199 timing.end = Some(Instant::now());
200 add_task_timing(timing);
201}