Auto merge of #14859 - lunacookies:qos, r=lunacookies
Specify thread types using Quality of Service API
<details>
<summary>Some background (in case you haven’t heard of QoS before)</summary>
Heterogenous multi-core CPUs are increasingly found in laptops and desktops (e.g. Alder Lake, Snapdragon 8cx Gen 3, M1). To maximize efficiency on this kind of hardware, it is important to provide the operating system with more information so threads can be scheduled on different core types appropriately.
The approach that XNU (the kernel of macOS, iOS, etc) and Windows have taken is to provide a high-level semantic API – quality of service, or QoS – which informs the OS of the program’s intent. For instance, you might specify that a thread is running a render loop for a game. This makes the OS provide this thread with as large a share of the system’s resources as possible. Specifying a thread is running an unimportant background task, on the other hand, is cause for it to be scheduled exclusively on high-efficiency cores instead of high-performance cores.
QoS APIs allows for easy configuration of many different parameters at once; for instance, setting QoS on XNU affects scheduling, timer latency, I/O priorities, and of course what core type the thread in question should run on. I don’t know any details on how QoS works on Windows, but I would guess it’s similar.
Hypothetically, taking advantage of these APIs would improve power consumption, thermals, battery life if applicable, etc.
</details>
# Relevance to rust-analyzer
From what I can tell the philosophy behind both the XNU and Windows QoS APIs is that _user interfaces should never stutter under any circumstances._ You can see this in the array of QoS classes which are available: the highest QoS class in both APIs is one intended explicitly for UI render loops.
Imagine rust-analyzer is performing CPU-intensive background work – maybe you just invoked Find Usages on `usize` or opened a large project – in this scenario the editor’s render loop should absolutely get higher priority than rust-analyzer, no matter what. You could view it in terms of “realtime-ness”: flight control software is hard realtime, audio software is soft realtime, GUIs are softer realtime, and rust-analyzer is not realtime at all. Of course, maximizing responsiveness is important, but respecting the rest of the system is more important.
# Implementation
I’ve tried my best to unify thread creation in `stdx`, where the new API I’ve introduced _requires_ specifying a QoS class. Different points along the performance/efficiency curve can make a great difference; the M1’s e-cores use around three times less power than the p-cores, so putting in this effort is worthwhile IMO.
It’s worth mentioning that Linux does not [yet](https://youtu.be/RfgPWpTwTQo) have a QoS API. Maybe translating QoS into regular thread priorities would be acceptable? From what I can tell the only scheduling-related code in rust-analyzer is Windows-specific, so ignoring QoS entirely on Linux shouldn’t cause any new issues. Also, I haven’t implemented support for the Windows QoS APIs because I don’t have a Windows machine to test on, and because I’m completely unfamiliar with Windows APIs :)
I noticed that rust-analyzer handles some requests on the main thread (using `.on_sync()`) and others on a threadpool (using `.on()`). I think it would make sense to run the main thread at the User Initiated QoS and the threadpool at Utility, but only if all requests that are caused by typing use `.on_sync()` and all that don’t use `.on()`. I don’t understand how the `.on_sync()`/`.on()` split that’s currently present was chosen, so I’ve let this code be for the moment. Let me know if changing this to what I proposed makes any sense.
To avoid having to change everything back in case I’ve misunderstood something, I’ve left all threads at the Utility QoS for now. Of course, this isn’t what I hope the code will look like in the end, but I figured I have to start somewhere :P
# References
<ul>
<li><a href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html">Apple documentation related to QoS</a></li>
<li><a href="67e155c940/include/pthread/qos.h
">pthread API for setting QoS on XNU</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows/win32/procthread/quality-of-service">Windows’s QoS classes</a></li>
<li>
<details>
<summary>Full documentation of XNU QoS classes. This documentation is only available as a huge not-very-readable comment in a header file, so I’ve reformatted it and put it here for reference.</summary>
<ul>
<li><p><strong><code>QOS_CLASS_USER_INTERACTIVE</code>: A QOS class which indicates work performed by this thread is interactive with the user.</strong></p><p>Such work is requested to run at high priority relative to other work on the system. Specifying this QOS class is a request to run with nearly all available system CPU and I/O bandwidth even under contention. This is not an energy-efficient QOS class to use for large tasks. The use of this QOS class should be limited to critical interaction with the user such as handling events on the main event loop, view drawing, animation, etc.</p></li>
<li><p><strong><code>QOS_CLASS_USER_INITIATED</code>: A QOS class which indicates work performed by this thread was initiated by the user and that the user is likely waiting for the results.</strong></p><p>Such work is requested to run at a priority below critical user-interactive work, but relatively higher than other work on the system. This is not an energy-efficient QOS class to use for large tasks. Its use should be limited to operations of short enough duration that the user is unlikely to switch tasks while waiting for the results. Typical user-initiated work will have progress indicated by the display of placeholder content or modal user interface.</p></li>
<li><p><strong><code>QOS_CLASS_DEFAULT</code>: A default QOS class used by the system in cases where more specific QOS class information is not available.</strong></p><p>Such work is requested to run at a priority below critical user-interactive and user-initiated work, but relatively higher than utility and background tasks. Threads created by <code>pthread_create()</code> without an attribute specifying a QOS class will default to <code>QOS_CLASS_DEFAULT</code>. This QOS class value is not intended to be used as a work classification, it should only be set when propagating or restoring QOS class values provided by the system.</p></li>
<li><p><strong><code>QOS_CLASS_UTILITY</code>: A QOS class which indicates work performed by this thread may or may not be initiated by the user and that the user is unlikely to be immediately waiting for the results.</strong></p><p>Such work is requested to run at a priority below critical user-interactive and user-initiated work, but relatively higher than low-level system maintenance tasks. The use of this QOS class indicates the work should be run in an energy and thermally-efficient manner. The progress of utility work may or may not be indicated to the user, but the effect of such work is user-visible.</p></li>
<li><p><strong><code>QOS_CLASS_BACKGROUND</code>: A QOS class which indicates work performed by this thread was not initiated by the user and that the user may be unaware of the results.</strong></p><p>Such work is requested to run at a priority below other work. The use of this QOS class indicates the work should be run in the most energy and thermally-efficient manner.</p></li>
<li><p><strong><code>QOS_CLASS_UNSPECIFIED</code>: A QOS class value which indicates the absence or removal of QOS class information.</strong></p><p>As an API return value, may indicate that threads or pthread attributes were configured with legacy API incompatible or in conflict with the QOS class system.</p></li>
</ul>
</details>
</li>
</ul>
This commit is contained in:
commit
6bca9f2aac
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -408,7 +408,6 @@ dependencies = [
|
||||
"cargo_metadata",
|
||||
"command-group",
|
||||
"crossbeam-channel",
|
||||
"jod-thread",
|
||||
"paths",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
@ -1278,6 +1277,7 @@ dependencies = [
|
||||
"paths",
|
||||
"proc-macro-api",
|
||||
"proc-macro-test",
|
||||
"stdx",
|
||||
"tt",
|
||||
]
|
||||
|
||||
@ -1493,7 +1493,6 @@ dependencies = [
|
||||
"ide-db",
|
||||
"ide-ssr",
|
||||
"itertools",
|
||||
"jod-thread",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"mbe",
|
||||
@ -1712,6 +1711,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"always-assert",
|
||||
"backtrace",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
"miow",
|
||||
"winapi",
|
||||
@ -2123,9 +2123,9 @@ name = "vfs-notify"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"jod-thread",
|
||||
"notify",
|
||||
"paths",
|
||||
"stdx",
|
||||
"tracing",
|
||||
"vfs",
|
||||
"walkdir",
|
||||
|
@ -18,7 +18,6 @@ cargo_metadata = "0.15.0"
|
||||
rustc-hash = "1.1.0"
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
jod-thread = "0.1.2"
|
||||
command-group = "2.0.1"
|
||||
|
||||
# local deps
|
||||
|
@ -77,7 +77,7 @@ impl fmt::Display for FlycheckConfig {
|
||||
pub struct FlycheckHandle {
|
||||
// XXX: drop order is significant
|
||||
sender: Sender<StateChange>,
|
||||
_thread: jod_thread::JoinHandle,
|
||||
_thread: stdx::thread::JoinHandle,
|
||||
id: usize,
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ impl FlycheckHandle {
|
||||
) -> FlycheckHandle {
|
||||
let actor = FlycheckActor::new(id, sender, config, workspace_root);
|
||||
let (sender, receiver) = unbounded::<StateChange>();
|
||||
let thread = jod_thread::Builder::new()
|
||||
let thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
||||
.name("Flycheck".to_owned())
|
||||
.spawn(move || actor.run(receiver))
|
||||
.expect("failed to spawn thread");
|
||||
@ -395,7 +395,7 @@ struct CargoHandle {
|
||||
/// The handle to the actual cargo process. As we cannot cancel directly from with
|
||||
/// a read syscall dropping and therefore terminating the process is our best option.
|
||||
child: JodGroupChild,
|
||||
thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
|
||||
thread: stdx::thread::JoinHandle<io::Result<(bool, String)>>,
|
||||
receiver: Receiver<CargoMessage>,
|
||||
}
|
||||
|
||||
@ -409,7 +409,7 @@ impl CargoHandle {
|
||||
|
||||
let (sender, receiver) = unbounded();
|
||||
let actor = CargoActor::new(sender, stdout, stderr);
|
||||
let thread = jod_thread::Builder::new()
|
||||
let thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
||||
.name("CargoHandle".to_owned())
|
||||
.spawn(move || actor.run())
|
||||
.expect("failed to spawn thread");
|
||||
|
@ -80,7 +80,11 @@ pub(crate) fn parallel_prime_caches(
|
||||
for _ in 0..num_worker_threads {
|
||||
let worker = prime_caches_worker.clone();
|
||||
let db = db.snapshot();
|
||||
std::thread::spawn(move || Cancelled::catch(|| worker(db)));
|
||||
|
||||
stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
||||
.allow_leak(true)
|
||||
.spawn(move || Cancelled::catch(|| worker(db)))
|
||||
.expect("failed to spawn thread");
|
||||
}
|
||||
|
||||
(work_sender, progress_receiver)
|
||||
|
@ -22,6 +22,7 @@ object = { version = "0.30.2", default-features = false, features = [
|
||||
libloading = "0.7.3"
|
||||
memmap2 = "0.5.4"
|
||||
|
||||
stdx.workspace = true
|
||||
tt.workspace = true
|
||||
mbe.workspace = true
|
||||
paths.workspace = true
|
||||
|
@ -86,7 +86,6 @@ jemallocator = { version = "0.5.0", package = "tikv-jemallocator", optional = tr
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.4.0"
|
||||
jod-thread = "0.1.2"
|
||||
xshell = "0.2.2"
|
||||
|
||||
test-utils.workspace = true
|
||||
|
@ -78,7 +78,14 @@ fn try_main(flags: flags::RustAnalyzer) -> Result<()> {
|
||||
println!("rust-analyzer {}", rust_analyzer::version());
|
||||
return Ok(());
|
||||
}
|
||||
with_extra_thread("LspServer", run_server)?;
|
||||
|
||||
// rust-analyzer’s “main thread” is actually a secondary thread
|
||||
// with an increased stack size at the User Initiated QoS class.
|
||||
// We use this QoS class because any delay in the main loop
|
||||
// will make actions like hitting enter in the editor slow.
|
||||
// rust-analyzer does not block the editor’s render loop,
|
||||
// so we don’t use User Interactive.
|
||||
with_extra_thread("LspServer", stdx::thread::QoSClass::UserInitiated, run_server)?;
|
||||
}
|
||||
flags::RustAnalyzerCmd::Parse(cmd) => cmd.run()?,
|
||||
flags::RustAnalyzerCmd::Symbols(cmd) => cmd.run()?,
|
||||
@ -136,14 +143,17 @@ const STACK_SIZE: usize = 1024 * 1024 * 8;
|
||||
/// space.
|
||||
fn with_extra_thread(
|
||||
thread_name: impl Into<String>,
|
||||
qos_class: stdx::thread::QoSClass,
|
||||
f: impl FnOnce() -> Result<()> + Send + 'static,
|
||||
) -> Result<()> {
|
||||
let handle =
|
||||
std::thread::Builder::new().name(thread_name.into()).stack_size(STACK_SIZE).spawn(f)?;
|
||||
match handle.join() {
|
||||
Ok(res) => res,
|
||||
Err(panic) => std::panic::resume_unwind(panic),
|
||||
}
|
||||
let handle = stdx::thread::Builder::new(qos_class)
|
||||
.name(thread_name.into())
|
||||
.stack_size(STACK_SIZE)
|
||||
.spawn(f)?;
|
||||
|
||||
handle.join()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_server() -> Result<()> {
|
||||
|
@ -665,14 +665,20 @@ impl GlobalState {
|
||||
use crate::handlers::request as handlers;
|
||||
|
||||
dispatcher
|
||||
// Request handlers that must run on the main thread
|
||||
// because they mutate GlobalState:
|
||||
.on_sync_mut::<lsp_ext::ReloadWorkspace>(handlers::handle_workspace_reload)
|
||||
.on_sync_mut::<lsp_ext::RebuildProcMacros>(handlers::handle_proc_macros_rebuild)
|
||||
.on_sync_mut::<lsp_ext::MemoryUsage>(handlers::handle_memory_usage)
|
||||
.on_sync_mut::<lsp_ext::ShuffleCrateGraph>(handlers::handle_shuffle_crate_graph)
|
||||
// Request handlers which are related to the user typing
|
||||
// are run on the main thread to reduce latency:
|
||||
.on_sync::<lsp_ext::JoinLines>(handlers::handle_join_lines)
|
||||
.on_sync::<lsp_ext::OnEnter>(handlers::handle_on_enter)
|
||||
.on_sync::<lsp_types::request::SelectionRangeRequest>(handlers::handle_selection_range)
|
||||
.on_sync::<lsp_ext::MatchingBrace>(handlers::handle_matching_brace)
|
||||
.on_sync::<lsp_ext::OnTypeFormatting>(handlers::handle_on_type_formatting)
|
||||
// All other request handlers:
|
||||
.on::<lsp_ext::FetchDependencyList>(handlers::fetch_dependency_list)
|
||||
.on::<lsp_ext::AnalyzerStatus>(handlers::handle_analyzer_status)
|
||||
.on::<lsp_ext::SyntaxTree>(handlers::handle_syntax_tree)
|
||||
@ -693,7 +699,6 @@ impl GlobalState {
|
||||
.on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml)
|
||||
.on::<lsp_ext::MoveItem>(handlers::handle_move_item)
|
||||
.on::<lsp_ext::WorkspaceSymbol>(handlers::handle_workspace_symbol)
|
||||
.on::<lsp_ext::OnTypeFormatting>(handlers::handle_on_type_formatting)
|
||||
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)
|
||||
.on::<lsp_types::request::GotoDefinition>(handlers::handle_goto_definition)
|
||||
.on::<lsp_types::request::GotoDeclaration>(handlers::handle_goto_declaration)
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! A thin wrapper around `ThreadPool` to make sure that we join all things
|
||||
//! properly.
|
||||
use std::sync::{Arc, Barrier};
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
|
||||
pub(crate) struct TaskPool<T> {
|
||||
@ -16,6 +18,18 @@ impl<T> TaskPool<T> {
|
||||
.thread_stack_size(STACK_SIZE)
|
||||
.num_threads(threads)
|
||||
.build();
|
||||
|
||||
// Set QoS of all threads in threadpool.
|
||||
let barrier = Arc::new(Barrier::new(threads + 1));
|
||||
for _ in 0..threads {
|
||||
let barrier = barrier.clone();
|
||||
inner.execute(move || {
|
||||
stdx::thread::set_current_thread_qos_class(stdx::thread::QoSClass::Utility);
|
||||
barrier.wait();
|
||||
});
|
||||
}
|
||||
barrier.wait();
|
||||
|
||||
TaskPool { sender, inner }
|
||||
}
|
||||
|
||||
@ -26,7 +40,16 @@ impl<T> TaskPool<T> {
|
||||
{
|
||||
self.inner.execute({
|
||||
let sender = self.sender.clone();
|
||||
move || sender.send(task()).unwrap()
|
||||
move || {
|
||||
if stdx::thread::IS_QOS_AVAILABLE {
|
||||
debug_assert_eq!(
|
||||
stdx::thread::get_current_thread_qos_class(),
|
||||
Some(stdx::thread::QoSClass::Utility)
|
||||
);
|
||||
}
|
||||
|
||||
sender.send(task()).unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,7 @@ pub(crate) fn project(fixture: &str) -> Server {
|
||||
pub(crate) struct Server {
|
||||
req_id: Cell<i32>,
|
||||
messages: RefCell<Vec<Message>>,
|
||||
_thread: jod_thread::JoinHandle<()>,
|
||||
_thread: stdx::thread::JoinHandle,
|
||||
client: Connection,
|
||||
/// XXX: remove the tempdir last
|
||||
dir: TestDir,
|
||||
@ -165,7 +165,7 @@ impl Server {
|
||||
fn new(dir: TestDir, config: Config) -> Server {
|
||||
let (connection, client) = Connection::memory();
|
||||
|
||||
let _thread = jod_thread::Builder::new()
|
||||
let _thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
||||
.name("test server".to_string())
|
||||
.spawn(move || main_loop(config, connection).unwrap())
|
||||
.expect("failed to spawn a thread");
|
||||
|
@ -15,6 +15,7 @@ doctest = false
|
||||
libc = "0.2.135"
|
||||
backtrace = { version = "0.3.65", optional = true }
|
||||
always-assert = { version = "0.1.2", features = ["log"] }
|
||||
jod-thread = "0.1.2"
|
||||
# Think twice before adding anything here
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
@ -11,6 +11,7 @@ pub mod process;
|
||||
pub mod panic_context;
|
||||
pub mod non_empty_vec;
|
||||
pub mod rand;
|
||||
pub mod thread;
|
||||
|
||||
pub use always_assert::{always, never};
|
||||
|
||||
|
326
crates/stdx/src/thread.rs
Normal file
326
crates/stdx/src/thread.rs
Normal file
@ -0,0 +1,326 @@
|
||||
//! A utility module for working with threads that automatically joins threads upon drop
|
||||
//! and provides functionality for interfacing with operating system quality of service (QoS) APIs.
|
||||
//!
|
||||
//! As a system, rust-analyzer should have the property that
|
||||
//! old manual scheduling APIs are replaced entirely by QoS.
|
||||
//! To maintain this invariant, we panic when it is clear that
|
||||
//! old scheduling APIs have been used.
|
||||
//!
|
||||
//! Moreover, we also want to ensure that every thread has a QoS set explicitly
|
||||
//! to force a decision about its importance to the system.
|
||||
//! Thus, [`QoSClass`] has no default value
|
||||
//! and every entry point to creating a thread requires a [`QoSClass`] upfront.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub fn spawn<F, T>(qos_class: QoSClass, f: F) -> JoinHandle<T>
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
F: Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
Builder::new(qos_class).spawn(f).expect("failed to spawn thread")
|
||||
}
|
||||
|
||||
pub struct Builder {
|
||||
qos_class: QoSClass,
|
||||
inner: jod_thread::Builder,
|
||||
allow_leak: bool,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn new(qos_class: QoSClass) -> Builder {
|
||||
Builder { qos_class, inner: jod_thread::Builder::new(), allow_leak: false }
|
||||
}
|
||||
|
||||
pub fn name(self, name: String) -> Builder {
|
||||
Builder { inner: self.inner.name(name), ..self }
|
||||
}
|
||||
|
||||
pub fn stack_size(self, size: usize) -> Builder {
|
||||
Builder { inner: self.inner.stack_size(size), ..self }
|
||||
}
|
||||
|
||||
pub fn allow_leak(self, b: bool) -> Builder {
|
||||
Builder { allow_leak: b, ..self }
|
||||
}
|
||||
|
||||
pub fn spawn<F, T>(self, f: F) -> std::io::Result<JoinHandle<T>>
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
F: Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let inner_handle = self.inner.spawn(move || {
|
||||
set_current_thread_qos_class(self.qos_class);
|
||||
f()
|
||||
})?;
|
||||
|
||||
Ok(JoinHandle { inner: Some(inner_handle), allow_leak: self.allow_leak })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JoinHandle<T = ()> {
|
||||
// `inner` is an `Option` so that we can
|
||||
// take ownership of the contained `JoinHandle`.
|
||||
inner: Option<jod_thread::JoinHandle<T>>,
|
||||
allow_leak: bool,
|
||||
}
|
||||
|
||||
impl<T> JoinHandle<T> {
|
||||
pub fn join(mut self) -> T {
|
||||
self.inner.take().unwrap().join()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for JoinHandle<T> {
|
||||
fn drop(&mut self) {
|
||||
if !self.allow_leak {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(join_handle) = self.inner.take() {
|
||||
join_handle.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for JoinHandle<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.pad("JoinHandle { .. }")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
// Please maintain order from least to most priority for the derived `Ord` impl.
|
||||
pub enum QoSClass {
|
||||
// Documentation adapted from https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/include/sys/qos.h#L55
|
||||
//
|
||||
/// TLDR: invisible maintenance tasks
|
||||
///
|
||||
/// Contract:
|
||||
///
|
||||
/// * **You do not care about how long it takes for work to finish.**
|
||||
/// * **You do not care about work being deferred temporarily.**
|
||||
/// (e.g. if the device’s battery is in a critical state)
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// * in a video editor:
|
||||
/// creating periodic backups of project files
|
||||
/// * in a browser:
|
||||
/// cleaning up cached sites which have not been accessed in a long time
|
||||
/// * in a collaborative word processor:
|
||||
/// creating a searchable index of all documents
|
||||
///
|
||||
/// Use this QoS class for background tasks
|
||||
/// which the user did not initiate themselves
|
||||
/// and which are invisible to the user.
|
||||
/// It is expected that this work will take significant time to complete:
|
||||
/// minutes or even hours.
|
||||
///
|
||||
/// This QoS class provides the most energy and thermally-efficient execution possible.
|
||||
/// All other work is prioritized over background tasks.
|
||||
Background,
|
||||
|
||||
/// TLDR: tasks that don’t block using your app
|
||||
///
|
||||
/// Contract:
|
||||
///
|
||||
/// * **Your app remains useful even as the task is executing.**
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// * in a video editor:
|
||||
/// exporting a video to disk –
|
||||
/// the user can still work on the timeline
|
||||
/// * in a browser:
|
||||
/// automatically extracting a downloaded zip file –
|
||||
/// the user can still switch tabs
|
||||
/// * in a collaborative word processor:
|
||||
/// downloading images embedded in a document –
|
||||
/// the user can still make edits
|
||||
///
|
||||
/// Use this QoS class for tasks which
|
||||
/// may or may not be initiated by the user,
|
||||
/// but whose result is visible.
|
||||
/// It is expected that this work will take a few seconds to a few minutes.
|
||||
/// Typically your app will include a progress bar
|
||||
/// for tasks using this class.
|
||||
///
|
||||
/// This QoS class provides a balance between
|
||||
/// performance, responsiveness and efficiency.
|
||||
Utility,
|
||||
|
||||
/// TLDR: tasks that block using your app
|
||||
///
|
||||
/// Contract:
|
||||
///
|
||||
/// * **You need this work to complete
|
||||
/// before the user can keep interacting with your app.**
|
||||
/// * **Your work will not take more than a few seconds to complete.**
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// * in a video editor:
|
||||
/// opening a saved project
|
||||
/// * in a browser:
|
||||
/// loading a list of the user’s bookmarks and top sites
|
||||
/// when a new tab is created
|
||||
/// * in a collaborative word processor:
|
||||
/// running a search on the document’s content
|
||||
///
|
||||
/// Use this QoS class for tasks which were initiated by the user
|
||||
/// and block the usage of your app while they are in progress.
|
||||
/// It is expected that this work will take a few seconds or less to complete;
|
||||
/// not long enough to cause the user to switch to something else.
|
||||
/// Your app will likely indicate progress on these tasks
|
||||
/// through the display of placeholder content or modals.
|
||||
///
|
||||
/// This QoS class is not energy-efficient.
|
||||
/// Rather, it provides responsiveness
|
||||
/// by prioritizing work above other tasks on the system
|
||||
/// except for critical user-interactive work.
|
||||
UserInitiated,
|
||||
|
||||
/// TLDR: render loops and nothing else
|
||||
///
|
||||
/// Contract:
|
||||
///
|
||||
/// * **You absolutely need this work to complete immediately
|
||||
/// or your app will appear to freeze.**
|
||||
/// * **Your work will always complete virtually instantaneously.**
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// * the main thread in a GUI application
|
||||
/// * the update & render loop in a game
|
||||
/// * a secondary thread which progresses an animation
|
||||
///
|
||||
/// Use this QoS class for any work which, if delayed,
|
||||
/// will make your user interface unresponsive.
|
||||
/// It is expected that this work will be virtually instantaneous.
|
||||
///
|
||||
/// This QoS class is not energy-efficient.
|
||||
/// Specifying this class is a request to run with
|
||||
/// nearly all available system CPU and I/O bandwidth even under contention.
|
||||
UserInteractive,
|
||||
}
|
||||
|
||||
pub const IS_QOS_AVAILABLE: bool = imp::IS_QOS_AVAILABLE;
|
||||
|
||||
pub fn set_current_thread_qos_class(class: QoSClass) {
|
||||
imp::set_current_thread_qos_class(class)
|
||||
}
|
||||
|
||||
pub fn get_current_thread_qos_class() -> Option<QoSClass> {
|
||||
imp::get_current_thread_qos_class()
|
||||
}
|
||||
|
||||
// All Apple platforms use XNU as their kernel
|
||||
// and thus have the concept of QoS.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
mod imp {
|
||||
use super::QoSClass;
|
||||
|
||||
pub(super) const IS_QOS_AVAILABLE: bool = true;
|
||||
|
||||
pub(super) fn set_current_thread_qos_class(class: QoSClass) {
|
||||
let c = match class {
|
||||
QoSClass::UserInteractive => libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE,
|
||||
QoSClass::UserInitiated => libc::qos_class_t::QOS_CLASS_USER_INITIATED,
|
||||
QoSClass::Utility => libc::qos_class_t::QOS_CLASS_UTILITY,
|
||||
QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND,
|
||||
};
|
||||
|
||||
let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) };
|
||||
|
||||
if code == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let errno = unsafe { *libc::__error() };
|
||||
|
||||
match errno {
|
||||
libc::EPERM => {
|
||||
// This thread has been excluded from the QoS system
|
||||
// due to a previous call to a function such as `pthread_setschedparam`
|
||||
// which is incompatible with QoS.
|
||||
//
|
||||
// Panic instead of returning an error
|
||||
// to maintain the invariant that we only use QoS APIs.
|
||||
panic!("tried to set QoS of thread which has opted out of QoS (os error {errno})")
|
||||
}
|
||||
|
||||
libc::EINVAL => {
|
||||
// This is returned if we pass something other than a qos_class_t
|
||||
// to `pthread_set_qos_class_self_np`.
|
||||
//
|
||||
// This is impossible, so again panic.
|
||||
unreachable!(
|
||||
"invalid qos_class_t value was passed to pthread_set_qos_class_self_np"
|
||||
)
|
||||
}
|
||||
|
||||
_ => {
|
||||
// `pthread_set_qos_class_self_np`’s documentation
|
||||
// does not mention any other errors.
|
||||
unreachable!("`pthread_set_qos_class_self_np` returned unexpected error {errno}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_current_thread_qos_class() -> Option<QoSClass> {
|
||||
let current_thread = unsafe { libc::pthread_self() };
|
||||
let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED;
|
||||
let code = unsafe {
|
||||
libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut())
|
||||
};
|
||||
|
||||
if code != 0 {
|
||||
// `pthread_get_qos_class_np`’s documentation states that
|
||||
// an error value is placed into errno if the return code is not zero.
|
||||
// However, it never states what errors are possible.
|
||||
// Inspecting the source[0] shows that, as of this writing, it always returns zero.
|
||||
//
|
||||
// Whatever errors the function could report in future are likely to be
|
||||
// ones which we cannot handle anyway
|
||||
//
|
||||
// 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177
|
||||
let errno = unsafe { *libc::__error() };
|
||||
unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})");
|
||||
}
|
||||
|
||||
match qos_class_raw {
|
||||
libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE => Some(QoSClass::UserInteractive),
|
||||
libc::qos_class_t::QOS_CLASS_USER_INITIATED => Some(QoSClass::UserInitiated),
|
||||
libc::qos_class_t::QOS_CLASS_DEFAULT => None, // QoS has never been set
|
||||
libc::qos_class_t::QOS_CLASS_UTILITY => Some(QoSClass::Utility),
|
||||
libc::qos_class_t::QOS_CLASS_BACKGROUND => Some(QoSClass::Background),
|
||||
|
||||
libc::qos_class_t::QOS_CLASS_UNSPECIFIED => {
|
||||
// Using manual scheduling APIs causes threads to “opt out” of QoS.
|
||||
// At this point they become incompatible with QoS,
|
||||
// and as such have the “unspecified” QoS class.
|
||||
//
|
||||
// Panic instead of returning an error
|
||||
// to maintain the invariant that we only use QoS APIs.
|
||||
panic!("tried to get QoS of thread which has opted out of QoS")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Windows has QoS APIs, we should use them!
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
mod imp {
|
||||
use super::QoSClass;
|
||||
|
||||
pub(super) const IS_QOS_AVAILABLE: bool = false;
|
||||
|
||||
pub(super) fn set_current_thread_qos_class(_: QoSClass) {}
|
||||
|
||||
pub(super) fn get_current_thread_qos_class() -> Option<QoSClass> {
|
||||
None
|
||||
}
|
||||
}
|
@ -13,10 +13,10 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.35"
|
||||
jod-thread = "0.1.2"
|
||||
walkdir = "2.3.2"
|
||||
crossbeam-channel = "0.5.5"
|
||||
notify = "5.0"
|
||||
|
||||
stdx.workspace = true
|
||||
vfs.workspace = true
|
||||
paths.workspace = true
|
||||
|
@ -21,7 +21,7 @@ use walkdir::WalkDir;
|
||||
pub struct NotifyHandle {
|
||||
// Relative order of fields below is significant.
|
||||
sender: Sender<Message>,
|
||||
_thread: jod_thread::JoinHandle,
|
||||
_thread: stdx::thread::JoinHandle,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -34,7 +34,7 @@ impl loader::Handle for NotifyHandle {
|
||||
fn spawn(sender: loader::Sender) -> NotifyHandle {
|
||||
let actor = NotifyActor::new(sender);
|
||||
let (sender, receiver) = unbounded::<Message>();
|
||||
let thread = jod_thread::Builder::new()
|
||||
let thread = stdx::thread::Builder::new(stdx::thread::QoSClass::Utility)
|
||||
.name("VfsLoader".to_owned())
|
||||
.spawn(move || actor.run(receiver))
|
||||
.expect("failed to spawn thread");
|
||||
|
Loading…
x
Reference in New Issue
Block a user