Detailed changes
@@ -6248,7 +6248,7 @@ dependencies = [
"futures-core",
"futures-sink",
"nanorand",
- "spin",
+ "spin 0.9.8",
]
[[package]]
@@ -7287,6 +7287,7 @@ dependencies = [
"calloop",
"calloop-wayland-source",
"cbindgen",
+ "circular-buffer",
"cocoa 0.26.0",
"cocoa-foundation 0.2.0",
"collections",
@@ -7342,6 +7343,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
+ "spin 0.10.0",
"stacksafe",
"strum 0.27.2",
"sum_tree",
@@ -9072,7 +9074,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
- "spin",
+ "spin 0.9.8",
]
[[package]]
@@ -10014,6 +10016,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "miniprofiler_ui"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "serde_json",
+ "smol",
+ "util",
+ "workspace",
+ "zed_actions",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -15854,6 +15868,15 @@ dependencies = [
"lock_api",
]
+[[package]]
+name = "spin"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
+dependencies = [
+ "lock_api",
+]
+
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -21165,6 +21188,7 @@ dependencies = [
"breadcrumbs",
"call",
"channel",
+ "chrono",
"clap",
"cli",
"client",
@@ -21222,6 +21246,7 @@ dependencies = [
"menu",
"migrator",
"mimalloc",
+ "miniprofiler_ui",
"nc",
"nix 0.29.0",
"node_runtime",
@@ -110,6 +110,7 @@ members = [
"crates/menu",
"crates/migrator",
"crates/mistral",
+ "crates/miniprofiler_ui",
"crates/multi_buffer",
"crates/nc",
"crates/net",
@@ -341,6 +342,7 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
+miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
@@ -138,6 +138,8 @@ waker-fn = "1.2.0"
lyon = "1.0"
libc.workspace = true
pin-project = "1.1.10"
+circular-buffer.workspace = true
+spin = "0.10.0"
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
@@ -1,4 +1,4 @@
-use crate::{App, PlatformDispatcher};
+use crate::{App, PlatformDispatcher, RunnableMeta, RunnableVariant};
use async_task::Runnable;
use futures::channel::mpsc;
use smol::prelude::*;
@@ -62,7 +62,7 @@ enum TaskState<T> {
Ready(Option<T>),
/// A task that is currently running.
- Spawned(async_task::Task<T>),
+ Spawned(async_task::Task<T, RunnableMeta>),
}
impl<T> Task<T> {
@@ -146,6 +146,7 @@ impl BackgroundExecutor {
}
/// Enqueues the given future to be run to completion on a background thread.
+ #[track_caller]
pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
@@ -155,6 +156,7 @@ impl BackgroundExecutor {
/// Enqueues the given future to be run to completion on a background thread.
/// The given label can be used to control the priority of the task in tests.
+ #[track_caller]
pub fn spawn_labeled<R>(
&self,
label: TaskLabel,
@@ -166,14 +168,20 @@ impl BackgroundExecutor {
self.spawn_internal::<R>(Box::pin(future), Some(label))
}
+ #[track_caller]
fn spawn_internal<R: Send + 'static>(
&self,
future: AnyFuture<R>,
label: Option<TaskLabel>,
) -> Task<R> {
let dispatcher = self.dispatcher.clone();
- let (runnable, task) =
- async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
+ let location = core::panic::Location::caller();
+ let (runnable, task) = async_task::Builder::new()
+ .metadata(RunnableMeta { location })
+ .spawn(
+ move |_| future,
+ move |runnable| dispatcher.dispatch(RunnableVariant::Meta(runnable), label),
+ );
runnable.schedule();
Task(TaskState::Spawned(task))
}
@@ -374,10 +382,13 @@ impl BackgroundExecutor {
if duration.is_zero() {
return Task::ready(());
}
- let (runnable, task) = async_task::spawn(async move {}, {
- let dispatcher = self.dispatcher.clone();
- move |runnable| dispatcher.dispatch_after(duration, runnable)
- });
+ let location = core::panic::Location::caller();
+ let (runnable, task) = async_task::Builder::new()
+ .metadata(RunnableMeta { location })
+ .spawn(move |_| async move {}, {
+ let dispatcher = self.dispatcher.clone();
+ move |runnable| dispatcher.dispatch_after(duration, RunnableVariant::Meta(runnable))
+ });
runnable.schedule();
Task(TaskState::Spawned(task))
}
@@ -483,24 +494,29 @@ impl ForegroundExecutor {
}
/// Enqueues the given Task to run on the main thread at some point in the future.
+ #[track_caller]
pub fn spawn<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
where
R: 'static,
{
let dispatcher = self.dispatcher.clone();
+ let location = core::panic::Location::caller();
#[track_caller]
fn inner<R: 'static>(
dispatcher: Arc<dyn PlatformDispatcher>,
future: AnyLocalFuture<R>,
+ location: &'static core::panic::Location<'static>,
) -> Task<R> {
- let (runnable, task) = spawn_local_with_source_location(future, move |runnable| {
- dispatcher.dispatch_on_main_thread(runnable)
- });
+ let (runnable, task) = spawn_local_with_source_location(
+ future,
+ move |runnable| dispatcher.dispatch_on_main_thread(RunnableVariant::Meta(runnable)),
+ RunnableMeta { location },
+ );
runnable.schedule();
Task(TaskState::Spawned(task))
}
- inner::<R>(dispatcher, Box::pin(future))
+ inner::<R>(dispatcher, Box::pin(future), location)
}
}
@@ -509,14 +525,16 @@ impl ForegroundExecutor {
/// Copy-modified from:
/// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
#[track_caller]
-fn spawn_local_with_source_location<Fut, S>(
+fn spawn_local_with_source_location<Fut, S, M>(
future: Fut,
schedule: S,
-) -> (Runnable<()>, async_task::Task<Fut::Output, ()>)
+ metadata: M,
+) -> (Runnable<M>, async_task::Task<Fut::Output, M>)
where
Fut: Future + 'static,
Fut::Output: 'static,
- S: async_task::Schedule<()> + Send + Sync + 'static,
+ S: async_task::Schedule<M> + Send + Sync + 'static,
+ M: 'static,
{
#[inline]
fn thread_id() -> ThreadId {
@@ -564,7 +582,11 @@ where
location: Location::caller(),
};
- unsafe { async_task::spawn_unchecked(future, schedule) }
+ unsafe {
+ async_task::Builder::new()
+ .metadata(metadata)
+ .spawn_unchecked(move |_| future, schedule)
+ }
}
/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`].
@@ -594,6 +616,7 @@ impl<'a> Scope<'a> {
}
/// Spawn a future into this scope.
+ #[track_caller]
pub fn spawn<F>(&mut self, f: F)
where
F: Future<Output = ()> + Send + 'a,
@@ -30,6 +30,7 @@ mod keymap;
mod path_builder;
mod platform;
pub mod prelude;
+mod profiler;
mod scene;
mod shared_string;
mod shared_uri;
@@ -87,6 +88,7 @@ use key_dispatch::*;
pub use keymap::*;
pub use path_builder::*;
pub use platform::*;
+pub use profiler::*;
pub use refineable::*;
pub use scene::*;
pub use shared_string::*;
@@ -40,8 +40,8 @@ use crate::{
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
- ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TaskLabel, Window,
- WindowControlArea, hash, point, px, size,
+ ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TaskLabel, TaskTiming,
+ ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
@@ -559,14 +559,32 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
}
}
+/// This type is public so that our test macro can generate and use it, but it should not
+/// be considered part of our public API.
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct RunnableMeta {
+ /// Location of the runnable
+ pub location: &'static core::panic::Location<'static>,
+}
+
+#[doc(hidden)]
+pub enum RunnableVariant {
+ Meta(Runnable<RunnableMeta>),
+ Compat(Runnable),
+}
+
/// This type is public so that our test macro can generate and use it, but it should not
/// be considered part of our public API.
#[doc(hidden)]
pub trait PlatformDispatcher: Send + Sync {
+ fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
+ fn get_current_thread_timings(&self) -> Vec<TaskTiming>;
fn is_main_thread(&self) -> bool;
- fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
- fn dispatch_on_main_thread(&self, runnable: Runnable);
- fn dispatch_after(&self, duration: Duration, runnable: Runnable);
+ fn dispatch(&self, runnable: RunnableVariant, label: Option<TaskLabel>);
+ fn dispatch_on_main_thread(&self, runnable: RunnableVariant);
+ fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
+
fn now(&self) -> Instant {
Instant::now()
}
@@ -1,5 +1,7 @@
-use crate::{PlatformDispatcher, TaskLabel};
-use async_task::Runnable;
+use crate::{
+ GLOBAL_THREAD_TIMINGS, PlatformDispatcher, RunnableVariant, THREAD_TIMINGS, TaskLabel,
+ TaskTiming, ThreadTaskTimings,
+};
use calloop::{
EventLoop,
channel::{self, Sender},
@@ -13,20 +15,20 @@ use util::ResultExt;
struct TimerAfter {
duration: Duration,
- runnable: Runnable,
+ runnable: RunnableVariant,
}
pub(crate) struct LinuxDispatcher {
- main_sender: Sender<Runnable>,
+ main_sender: Sender<RunnableVariant>,
timer_sender: Sender<TimerAfter>,
- background_sender: flume::Sender<Runnable>,
+ background_sender: flume::Sender<RunnableVariant>,
_background_threads: Vec<thread::JoinHandle<()>>,
main_thread_id: thread::ThreadId,
}
impl LinuxDispatcher {
- pub fn new(main_sender: Sender<Runnable>) -> Self {
- let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
+ pub fn new(main_sender: Sender<RunnableVariant>) -> Self {
+ let (background_sender, background_receiver) = flume::unbounded::<RunnableVariant>();
let thread_count = std::thread::available_parallelism()
.map(|i| i.get())
.unwrap_or(1);
@@ -40,7 +42,36 @@ impl LinuxDispatcher {
for runnable in receiver {
let start = Instant::now();
- runnable.run();
+ let mut location = match runnable {
+ RunnableVariant::Meta(runnable) => {
+ let location = runnable.metadata().location;
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ RunnableVariant::Compat(runnable) => {
+ let location = core::panic::Location::caller();
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ };
+
+ let end = Instant::now();
+ location.end = Some(end);
+ Self::add_task_timing(location);
log::trace!(
"background thread {}: ran runnable. took: {:?}",
@@ -72,7 +103,36 @@ impl LinuxDispatcher {
calloop::timer::Timer::from_duration(timer.duration),
move |_, _, _| {
if let Some(runnable) = runnable.take() {
- runnable.run();
+ let start = Instant::now();
+ let mut timing = match runnable {
+ RunnableVariant::Meta(runnable) => {
+ let location = runnable.metadata().location;
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ RunnableVariant::Compat(runnable) => {
+ let timing = TaskTiming {
+ location: core::panic::Location::caller(),
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ };
+ let end = Instant::now();
+
+ timing.end = Some(end);
+ Self::add_task_timing(timing);
}
TimeoutAction::Drop
},
@@ -96,18 +156,53 @@ impl LinuxDispatcher {
main_thread_id: thread::current().id(),
}
}
+
+ pub(crate) fn add_task_timing(timing: TaskTiming) {
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+
+ if let Some(last_timing) = timings.iter_mut().rev().next() {
+ if last_timing.location == timing.location {
+ last_timing.end = timing.end;
+ return;
+ }
+ }
+
+ timings.push_back(timing);
+ });
+ }
}
impl PlatformDispatcher for LinuxDispatcher {
+ fn get_all_timings(&self) -> Vec<crate::ThreadTaskTimings> {
+ let global_timings = GLOBAL_THREAD_TIMINGS.lock();
+ ThreadTaskTimings::convert(&global_timings)
+ }
+
+ fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
+ THREAD_TIMINGS.with(|timings| {
+ let timings = timings.lock();
+ let timings = &timings.timings;
+
+ let mut vec = Vec::with_capacity(timings.len());
+
+ let (s1, s2) = timings.as_slices();
+ vec.extend_from_slice(s1);
+ vec.extend_from_slice(s2);
+ vec
+ })
+ }
+
fn is_main_thread(&self) -> bool {
thread::current().id() == self.main_thread_id
}
- fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
+ fn dispatch(&self, runnable: RunnableVariant, _: Option<TaskLabel>) {
self.background_sender.send(runnable).unwrap();
}
- fn dispatch_on_main_thread(&self, runnable: Runnable) {
+ fn dispatch_on_main_thread(&self, runnable: RunnableVariant) {
self.main_sender.send(runnable).unwrap_or_else(|runnable| {
// NOTE: Runnable may wrap a Future that is !Send.
//
@@ -121,7 +216,7 @@ impl PlatformDispatcher for LinuxDispatcher {
});
}
- fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
+ fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
self.timer_sender
.send(TimerAfter { duration, runnable })
.ok();
@@ -31,7 +31,10 @@ impl HeadlessClient {
handle
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
- runnable.run();
+ match runnable {
+ crate::RunnableVariant::Meta(runnable) => runnable.run(),
+ crate::RunnableVariant::Compat(runnable) => runnable.run(),
+ };
}
})
.ok();
@@ -15,7 +15,6 @@ use std::{
};
use anyhow::{Context as _, anyhow};
-use async_task::Runnable;
use calloop::{LoopSignal, channel::Channel};
use futures::channel::oneshot;
use util::ResultExt as _;
@@ -26,7 +25,8 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
- PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
+ PlatformTextSystem, PlatformWindow, Point, Result, RunnableVariant, Task, WindowAppearance,
+ WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -105,8 +105,8 @@ pub(crate) struct LinuxCommon {
}
impl LinuxCommon {
- pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
- let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
+ pub fn new(signal: LoopSignal) -> (Self, Channel<RunnableVariant>) {
+ let (main_sender, main_receiver) = calloop::channel::channel::<RunnableVariant>();
#[cfg(any(feature = "wayland", feature = "x11"))]
let text_system = Arc::new(crate::CosmicTextSystem::new());
@@ -71,7 +71,6 @@ use super::{
window::{ImeInput, WaylandWindowStatePtr},
};
-use crate::platform::{PlatformWindow, blade::BladeContext};
use crate::{
AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
@@ -80,6 +79,10 @@ use crate::{
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent,
Size, TouchPhase, WindowParams, point, px, size,
};
+use crate::{
+ LinuxDispatcher, RunnableVariant, TaskTiming,
+ platform::{PlatformWindow, blade::BladeContext},
+};
use crate::{
SharedString,
platform::linux::{
@@ -491,7 +494,37 @@ impl WaylandClient {
move |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
handle.insert_idle(|_| {
- runnable.run();
+ let start = Instant::now();
+ let mut timing = match runnable {
+ RunnableVariant::Meta(runnable) => {
+ let location = runnable.metadata().location;
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ LinuxDispatcher::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ RunnableVariant::Compat(runnable) => {
+ let location = core::panic::Location::caller();
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ LinuxDispatcher::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ };
+
+ let end = Instant::now();
+ timing.end = Some(end);
+ LinuxDispatcher::add_task_timing(timing);
});
}
}
@@ -1,4 +1,4 @@
-use crate::{Capslock, xcb_flush};
+use crate::{Capslock, LinuxDispatcher, RunnableVariant, TaskTiming, xcb_flush};
use anyhow::{Context as _, anyhow};
use ashpd::WindowIdentifier;
use calloop::{
@@ -313,7 +313,37 @@ impl X11Client {
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
- runnable.run();
+ let start = Instant::now();
+ let mut timing = match runnable {
+ RunnableVariant::Meta(runnable) => {
+ let location = runnable.metadata().location;
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ LinuxDispatcher::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ RunnableVariant::Compat(runnable) => {
+ let location = core::panic::Location::caller();
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ LinuxDispatcher::add_task_timing(timing);
+
+ runnable.run();
+ timing
+ }
+ };
+
+ let end = Instant::now();
+ timing.end = Some(end);
+ LinuxDispatcher::add_task_timing(timing);
});
}
}
@@ -2,7 +2,11 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
-use crate::{PlatformDispatcher, TaskLabel};
+use crate::{
+ GLOBAL_THREAD_TIMINGS, PlatformDispatcher, RunnableMeta, RunnableVariant, THREAD_TIMINGS,
+ TaskLabel, TaskTiming, ThreadTaskTimings,
+};
+
use async_task::Runnable;
use objc::{
class, msg_send,
@@ -12,7 +16,7 @@ use objc::{
use std::{
ffi::c_void,
ptr::{NonNull, addr_of},
- time::Duration,
+ time::{Duration, Instant},
};
/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent
@@ -29,47 +33,155 @@ pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t {
pub(crate) struct MacDispatcher;
impl PlatformDispatcher for MacDispatcher {
+ fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
+ let global_timings = GLOBAL_THREAD_TIMINGS.lock();
+ ThreadTaskTimings::convert(&global_timings)
+ }
+
+ fn get_current_thread_timings(&self) -> Vec<TaskTiming> {
+ THREAD_TIMINGS.with(|timings| {
+ let timings = &timings.lock().timings;
+
+ let mut vec = Vec::with_capacity(timings.len());
+
+ let (s1, s2) = timings.as_slices();
+ vec.extend_from_slice(s1);
+ vec.extend_from_slice(s2);
+ vec
+ })
+ }
+
fn is_main_thread(&self) -> bool {
let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
is_main_thread == YES
}
- fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
+ fn dispatch(&self, runnable: RunnableVariant, _: Option<TaskLabel>) {
+ let (context, trampoline) = match runnable {
+ RunnableVariant::Meta(runnable) => (
+ runnable.into_raw().as_ptr() as *mut c_void,
+ Some(trampoline as unsafe extern "C" fn(*mut c_void)),
+ ),
+ RunnableVariant::Compat(runnable) => (
+ runnable.into_raw().as_ptr() as *mut c_void,
+ Some(trampoline_compat as unsafe extern "C" fn(*mut c_void)),
+ ),
+ };
unsafe {
dispatch_async_f(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0),
- runnable.into_raw().as_ptr() as *mut c_void,
- Some(trampoline),
+ context,
+ trampoline,
);
}
}
- fn dispatch_on_main_thread(&self, runnable: Runnable) {
- unsafe {
- dispatch_async_f(
- dispatch_get_main_queue(),
+ fn dispatch_on_main_thread(&self, runnable: RunnableVariant) {
+ let (context, trampoline) = match runnable {
+ RunnableVariant::Meta(runnable) => (
runnable.into_raw().as_ptr() as *mut c_void,
- Some(trampoline),
- );
+ Some(trampoline as unsafe extern "C" fn(*mut c_void)),
+ ),
+ RunnableVariant::Compat(runnable) => (
+ runnable.into_raw().as_ptr() as *mut c_void,
+ Some(trampoline_compat as unsafe extern "C" fn(*mut c_void)),
+ ),
+ };
+ unsafe {
+ dispatch_async_f(dispatch_get_main_queue(), context, trampoline);
}
}
- fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
+ fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
+ let (context, trampoline) = match runnable {
+ RunnableVariant::Meta(runnable) => (
+ runnable.into_raw().as_ptr() as *mut c_void,
+ Some(trampoline as unsafe extern "C" fn(*mut c_void)),
+ ),
+ RunnableVariant::Compat(runnable) => (
+ runnable.into_raw().as_ptr() as *mut c_void,
+ Some(trampoline_compat as unsafe extern "C" fn(*mut c_void)),
+ ),
+ };
unsafe {
let queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH.try_into().unwrap(), 0);
let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
- dispatch_after_f(
- when,
- queue,
- runnable.into_raw().as_ptr() as *mut c_void,
- Some(trampoline),
- );
+ dispatch_after_f(when, queue, context, trampoline);
}
}
}
extern "C" fn trampoline(runnable: *mut c_void) {
+ let task =
+ unsafe { Runnable::<RunnableMeta>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
+
+ let location = task.metadata().location;
+
+ let start = Instant::now();
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+ if let Some(last_timing) = timings.iter_mut().rev().next() {
+ if last_timing.location == timing.location {
+ return;
+ }
+ }
+
+ timings.push_back(timing);
+ });
+
+ task.run();
+ let end = Instant::now();
+
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+ let Some(last_timing) = timings.iter_mut().rev().next() else {
+ return;
+ };
+ last_timing.end = Some(end);
+ });
+}
+
+extern "C" fn trampoline_compat(runnable: *mut c_void) {
let task = unsafe { Runnable::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
+
+ let location = core::panic::Location::caller();
+
+ let start = Instant::now();
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+ if let Some(last_timing) = timings.iter_mut().rev().next() {
+ if last_timing.location == timing.location {
+ return;
+ }
+ }
+
+ timings.push_back(timing);
+ });
+
task.run();
+ let end = Instant::now();
+
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+ let Some(last_timing) = timings.iter_mut().rev().next() else {
+ return;
+ };
+ last_timing.end = Some(end);
+ });
}
@@ -1,5 +1,4 @@
-use crate::{PlatformDispatcher, TaskLabel};
-use async_task::Runnable;
+use crate::{PlatformDispatcher, RunnableVariant, TaskLabel};
use backtrace::Backtrace;
use collections::{HashMap, HashSet, VecDeque};
use parking::Unparker;
@@ -26,10 +25,10 @@ pub struct TestDispatcher {
struct TestDispatcherState {
random: StdRng,
- foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
- background: Vec<Runnable>,
- deprioritized_background: Vec<Runnable>,
- delayed: Vec<(Duration, Runnable)>,
+ foreground: HashMap<TestDispatcherId, VecDeque<RunnableVariant>>,
+ background: Vec<RunnableVariant>,
+ deprioritized_background: Vec<RunnableVariant>,
+ delayed: Vec<(Duration, RunnableVariant)>,
start_time: Instant,
time: Duration,
is_main_thread: bool,
@@ -175,7 +174,13 @@ impl TestDispatcher {
let was_main_thread = state.is_main_thread;
state.is_main_thread = main_thread;
drop(state);
- runnable.run();
+
+ // todo(localcc): add timings to tests
+ match runnable {
+ RunnableVariant::Meta(runnable) => runnable.run(),
+ RunnableVariant::Compat(runnable) => runnable.run(),
+ };
+
self.state.lock().is_main_thread = was_main_thread;
true
@@ -268,6 +273,14 @@ impl Clone for TestDispatcher {
}
impl PlatformDispatcher for TestDispatcher {
+ fn get_all_timings(&self) -> Vec<crate::ThreadTaskTimings> {
+ Vec::new()
+ }
+
+ fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
+ Vec::new()
+ }
+
fn is_main_thread(&self) -> bool {
self.state.lock().is_main_thread
}
@@ -277,7 +290,7 @@ impl PlatformDispatcher for TestDispatcher {
state.start_time + state.time
}
- fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+ fn dispatch(&self, runnable: RunnableVariant, label: Option<TaskLabel>) {
{
let mut state = self.state.lock();
if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) {
@@ -289,7 +302,7 @@ impl PlatformDispatcher for TestDispatcher {
self.unpark_last();
}
- fn dispatch_on_main_thread(&self, runnable: Runnable) {
+ fn dispatch_on_main_thread(&self, runnable: RunnableVariant) {
self.state
.lock()
.foreground
@@ -299,7 +312,7 @@ impl PlatformDispatcher for TestDispatcher {
self.unpark_last();
}
- fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
+ fn dispatch_after(&self, duration: std::time::Duration, runnable: RunnableVariant) {
let mut state = self.state.lock();
let next_time = state.time + duration;
let ix = match state.delayed.binary_search_by_key(&next_time, |e| e.0) {
@@ -1,10 +1,9 @@
use std::{
sync::atomic::{AtomicBool, Ordering},
thread::{ThreadId, current},
- time::Duration,
+ time::{Duration, Instant},
};
-use async_task::Runnable;
use flume::Sender;
use util::ResultExt;
use windows::{
@@ -18,12 +17,13 @@ use windows::{
};
use crate::{
- HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD,
+ GLOBAL_THREAD_TIMINGS, HWND, PlatformDispatcher, RunnableVariant, SafeHwnd, THREAD_TIMINGS,
+ TaskLabel, TaskTiming, ThreadTaskTimings, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD,
};
pub(crate) struct WindowsDispatcher {
pub(crate) wake_posted: AtomicBool,
- main_sender: Sender<Runnable>,
+ main_sender: Sender<RunnableVariant>,
main_thread_id: ThreadId,
platform_window_handle: SafeHwnd,
validation_number: usize,
@@ -31,7 +31,7 @@ pub(crate) struct WindowsDispatcher {
impl WindowsDispatcher {
pub(crate) fn new(
- main_sender: Sender<Runnable>,
+ main_sender: Sender<RunnableVariant>,
platform_window_handle: HWND,
validation_number: usize,
) -> Self {
@@ -47,42 +47,115 @@ impl WindowsDispatcher {
}
}
- fn dispatch_on_threadpool(&self, runnable: Runnable) {
+ fn dispatch_on_threadpool(&self, runnable: RunnableVariant) {
let handler = {
let mut task_wrapper = Some(runnable);
WorkItemHandler::new(move |_| {
- task_wrapper.take().unwrap().run();
+ Self::execute_runnable(task_wrapper.take().unwrap());
Ok(())
})
};
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
}
- fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
+ fn dispatch_on_threadpool_after(&self, runnable: RunnableVariant, duration: Duration) {
let handler = {
let mut task_wrapper = Some(runnable);
TimerElapsedHandler::new(move |_| {
- task_wrapper.take().unwrap().run();
+ Self::execute_runnable(task_wrapper.take().unwrap());
Ok(())
})
};
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
}
+
+ #[inline(always)]
+ pub(crate) fn execute_runnable(runnable: RunnableVariant) {
+ let start = Instant::now();
+
+ let mut timing = match runnable {
+ RunnableVariant::Meta(runnable) => {
+ let location = runnable.metadata().location;
+ let timing = TaskTiming {
+ location,
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+
+ timing
+ }
+ RunnableVariant::Compat(runnable) => {
+ let timing = TaskTiming {
+ location: core::panic::Location::caller(),
+ start,
+ end: None,
+ };
+ Self::add_task_timing(timing);
+
+ runnable.run();
+
+ timing
+ }
+ };
+
+ let end = Instant::now();
+ timing.end = Some(end);
+
+ Self::add_task_timing(timing);
+ }
+
+ pub(crate) fn add_task_timing(timing: TaskTiming) {
+ THREAD_TIMINGS.with(|timings| {
+ let mut timings = timings.lock();
+ let timings = &mut timings.timings;
+
+ if let Some(last_timing) = timings.iter_mut().rev().next() {
+ if last_timing.location == timing.location {
+ last_timing.end = timing.end;
+ return;
+ }
+ }
+
+ timings.push_back(timing);
+ });
+ }
}
impl PlatformDispatcher for WindowsDispatcher {
+ fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
+ let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock();
+ ThreadTaskTimings::convert(&global_thread_timings)
+ }
+
+ fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
+ THREAD_TIMINGS.with(|timings| {
+ let timings = timings.lock();
+ let timings = &timings.timings;
+
+ let mut vec = Vec::with_capacity(timings.len());
+
+ let (s1, s2) = timings.as_slices();
+ vec.extend_from_slice(s1);
+ vec.extend_from_slice(s2);
+ vec
+ })
+ }
+
fn is_main_thread(&self) -> bool {
current().id() == self.main_thread_id
}
- fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+ fn dispatch(&self, runnable: RunnableVariant, label: Option<TaskLabel>) {
self.dispatch_on_threadpool(runnable);
if let Some(label) = label {
log::debug!("TaskLabel: {label:?}");
}
}
- fn dispatch_on_main_thread(&self, runnable: Runnable) {
+ fn dispatch_on_main_thread(&self, runnable: RunnableVariant) {
match self.main_sender.send(runnable) {
Ok(_) => {
if !self.wake_posted.swap(true, Ordering::AcqRel) {
@@ -111,7 +184,7 @@ impl PlatformDispatcher for WindowsDispatcher {
}
}
- fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
+ fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
self.dispatch_on_threadpool_after(runnable, duration);
}
}
@@ -239,7 +239,7 @@ impl WindowsWindowInner {
fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID {
for runnable in self.main_receiver.drain() {
- runnable.run();
+ WindowsDispatcher::execute_runnable(runnable);
}
self.handle_paint_msg(handle)
} else {
@@ -1142,8 +1142,10 @@ impl WindowsWindowInner {
require_presentation: false,
force_render,
});
+
self.state.borrow_mut().callbacks.request_frame = Some(request_frame);
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
+
Some(0)
}
@@ -8,7 +8,6 @@ use std::{
use ::util::{ResultExt, paths::SanitizedPath};
use anyhow::{Context as _, Result, anyhow};
-use async_task::Runnable;
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
use parking_lot::RwLock;
@@ -46,7 +45,7 @@ struct WindowsPlatformInner {
raw_window_handles: std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
// The below members will never change throughout the entire lifecycle of the app.
validation_number: usize,
- main_receiver: flume::Receiver<Runnable>,
+ main_receiver: flume::Receiver<RunnableVariant>,
dispatcher: Arc<WindowsDispatcher>,
}
@@ -93,7 +92,7 @@ impl WindowsPlatform {
OleInitialize(None).context("unable to initialize Windows OLE")?;
}
let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?;
- let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
+ let (main_sender, main_receiver) = flume::unbounded::<RunnableVariant>();
let validation_number = if usize::BITS == 64 {
rand::random::<u64>() as usize
} else {
@@ -794,7 +793,7 @@ impl WindowsPlatformInner {
fn run_foreground_task(&self) -> Option<isize> {
loop {
for runnable in self.main_receiver.drain() {
- runnable.run();
+ WindowsDispatcher::execute_runnable(runnable);
}
// Someone could enqueue a Runnable here. The flag is still true, so they will not PostMessage.
@@ -805,7 +804,8 @@ impl WindowsPlatformInner {
match self.main_receiver.try_recv() {
Ok(runnable) => {
let _ = dispatcher.wake_posted.swap(true, Ordering::AcqRel);
- runnable.run();
+
+ WindowsDispatcher::execute_runnable(runnable);
continue;
}
_ => {
@@ -873,7 +873,7 @@ pub(crate) struct WindowCreationInfo {
pub(crate) windows_version: WindowsVersion,
pub(crate) drop_target_helper: IDropTargetHelper,
pub(crate) validation_number: usize,
- pub(crate) main_receiver: flume::Receiver<Runnable>,
+ pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
pub(crate) platform_window_handle: HWND,
pub(crate) disable_direct_composition: bool,
pub(crate) directx_devices: DirectXDevices,
@@ -883,8 +883,8 @@ struct PlatformWindowCreateContext {
inner: Option<Result<Rc<WindowsPlatformInner>>>,
raw_window_handles: std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
validation_number: usize,
- main_sender: Option<flume::Sender<Runnable>>,
- main_receiver: Option<flume::Receiver<Runnable>>,
+ main_sender: Option<flume::Sender<RunnableVariant>>,
+ main_receiver: Option<flume::Receiver<RunnableVariant>>,
directx_devices: Option<DirectXDevices>,
dispatcher: Option<Arc<WindowsDispatcher>>,
}
@@ -12,7 +12,6 @@ use std::{
use ::util::ResultExt;
use anyhow::{Context as _, Result};
-use async_task::Runnable;
use futures::channel::oneshot::{self, Receiver};
use raw_window_handle as rwh;
use smallvec::SmallVec;
@@ -70,7 +69,7 @@ pub(crate) struct WindowsWindowInner {
pub(crate) executor: ForegroundExecutor,
pub(crate) windows_version: WindowsVersion,
pub(crate) validation_number: usize,
- pub(crate) main_receiver: flume::Receiver<Runnable>,
+ pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
pub(crate) platform_window_handle: HWND,
}
@@ -357,7 +356,7 @@ struct WindowCreateContext {
windows_version: WindowsVersion,
drop_target_helper: IDropTargetHelper,
validation_number: usize,
- main_receiver: flume::Receiver<Runnable>,
+ main_receiver: flume::Receiver<RunnableVariant>,
platform_window_handle: HWND,
appearance: WindowAppearance,
disable_direct_composition: bool,
@@ -0,0 +1,218 @@
+use std::{
+ cell::LazyCell,
+ hash::Hasher,
+ hash::{DefaultHasher, Hash},
+ sync::Arc,
+ thread::ThreadId,
+ time::Instant,
+};
+
+use serde::{Deserialize, Serialize};
+
+#[doc(hidden)]
+#[derive(Debug, Copy, Clone)]
+pub struct TaskTiming {
+ pub location: &'static core::panic::Location<'static>,
+ pub start: Instant,
+ pub end: Option<Instant>,
+}
+
+#[doc(hidden)]
+#[derive(Debug, Clone)]
+pub struct ThreadTaskTimings {
+ pub thread_name: Option<String>,
+ pub thread_id: ThreadId,
+ pub timings: Vec<TaskTiming>,
+}
+
+impl ThreadTaskTimings {
+ pub(crate) fn convert(timings: &[GlobalThreadTimings]) -> Vec<Self> {
+ timings
+ .iter()
+ .filter_map(|t| match t.timings.upgrade() {
+ Some(timings) => Some((t.thread_id, timings)),
+ _ => None,
+ })
+ .map(|(thread_id, timings)| {
+ let timings = timings.lock();
+ let thread_name = timings.thread_name.clone();
+ let timings = &timings.timings;
+
+ let mut vec = Vec::with_capacity(timings.len());
+
+ let (s1, s2) = timings.as_slices();
+ vec.extend_from_slice(s1);
+ vec.extend_from_slice(s2);
+
+ ThreadTaskTimings {
+ thread_name,
+ thread_id,
+ timings: vec,
+ }
+ })
+ .collect()
+ }
+}
+
+/// Serializable variant of [`core::panic::Location`]
+#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+pub struct SerializedLocation<'a> {
+ /// Name of the source file
+ pub file: &'a str,
+ /// Line in the source file
+ pub line: u32,
+ /// Column in the source file
+ pub column: u32,
+}
+
+impl<'a> From<&'a core::panic::Location<'a>> for SerializedLocation<'a> {
+ fn from(value: &'a core::panic::Location<'a>) -> Self {
+ SerializedLocation {
+ file: value.file(),
+ line: value.line(),
+ column: value.column(),
+ }
+ }
+}
+
+/// Serializable variant of [`TaskTiming`]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SerializedTaskTiming<'a> {
+ /// Location of the timing
+ #[serde(borrow)]
+ pub location: SerializedLocation<'a>,
+ /// Time at which the measurement was reported in nanoseconds
+ pub start: u128,
+ /// Duration of the measurement in nanoseconds
+ pub duration: u128,
+}
+
+impl<'a> SerializedTaskTiming<'a> {
+ /// Convert an array of [`TaskTiming`] into their serializable format
+ ///
+ /// # Params
+ ///
+ /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
+ pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec<SerializedTaskTiming<'static>> {
+ let serialized = timings
+ .iter()
+ .map(|timing| {
+ let start = timing.start.duration_since(anchor).as_nanos();
+ let duration = timing
+ .end
+ .unwrap_or_else(|| Instant::now())
+ .duration_since(timing.start)
+ .as_nanos();
+ SerializedTaskTiming {
+ location: timing.location.into(),
+ start,
+ duration,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ serialized
+ }
+}
+
+/// Serializable variant of [`ThreadTaskTimings`]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SerializedThreadTaskTimings<'a> {
+ /// Thread name
+ pub thread_name: Option<String>,
+ /// Hash of the thread id
+ pub thread_id: u64,
+ /// Timing records for this thread
+ #[serde(borrow)]
+ pub timings: Vec<SerializedTaskTiming<'a>>,
+}
+
+impl<'a> SerializedThreadTaskTimings<'a> {
+ /// Convert [`ThreadTaskTimings`] into their serializable format
+ ///
+ /// # Params
+ ///
+ /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor
+ pub fn convert(
+ anchor: Instant,
+ timings: ThreadTaskTimings,
+ ) -> SerializedThreadTaskTimings<'static> {
+ let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings);
+
+ let mut hasher = DefaultHasher::new();
+ timings.thread_id.hash(&mut hasher);
+ let thread_id = hasher.finish();
+
+ SerializedThreadTaskTimings {
+ thread_name: timings.thread_name,
+ thread_id,
+ timings: serialized_timings,
+ }
+ }
+}
+
+// Allow 20mb of task timing entries
+const MAX_TASK_TIMINGS: usize = (20 * 1024 * 1024) / core::mem::size_of::<TaskTiming>();
+
+pub(crate) type TaskTimings = circular_buffer::CircularBuffer<MAX_TASK_TIMINGS, TaskTiming>;
+pub(crate) type GuardedTaskTimings = spin::Mutex<ThreadTimings>;
+
+pub(crate) struct GlobalThreadTimings {
+ pub thread_id: ThreadId,
+ pub timings: std::sync::Weak<GuardedTaskTimings>,
+}
+
+pub(crate) static GLOBAL_THREAD_TIMINGS: spin::Mutex<Vec<GlobalThreadTimings>> =
+ spin::Mutex::new(Vec::new());
+
+thread_local! {
+ pub(crate) static THREAD_TIMINGS: LazyCell<Arc<GuardedTaskTimings>> = LazyCell::new(|| {
+ let current_thread = std::thread::current();
+ let thread_name = current_thread.name();
+ let thread_id = current_thread.id();
+ let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id);
+ let timings = Arc::new(spin::Mutex::new(timings));
+
+ {
+ let timings = Arc::downgrade(&timings);
+ let global_timings = GlobalThreadTimings {
+ thread_id: std::thread::current().id(),
+ timings,
+ };
+ GLOBAL_THREAD_TIMINGS.lock().push(global_timings);
+ }
+
+ timings
+ });
+}
+
+pub(crate) struct ThreadTimings {
+ pub thread_name: Option<String>,
+ pub thread_id: ThreadId,
+ pub timings: Box<TaskTimings>,
+}
+
+impl ThreadTimings {
+ pub(crate) fn new(thread_name: Option<String>, thread_id: ThreadId) -> Self {
+ ThreadTimings {
+ thread_name,
+ thread_id,
+ timings: TaskTimings::boxed(),
+ }
+ }
+}
+
+impl Drop for ThreadTimings {
+ fn drop(&mut self) {
+ let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock();
+
+ let Some((index, _)) = thread_timings
+ .iter()
+ .enumerate()
+ .find(|(_, t)| t.thread_id == self.thread_id)
+ else {
+ return;
+ };
+ thread_timings.swap_remove(index);
+ }
+}
@@ -0,0 +1,23 @@
+[package]
+name = "miniprofiler_ui"
+version = "0.1.0"
+publish.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/miniprofiler_ui.rs"
+
+[dependencies]
+gpui.workspace = true
+zed_actions.workspace = true
+workspace.workspace = true
+util.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,393 @@
+use std::{
+ ops::Range,
+ path::PathBuf,
+ time::{Duration, Instant},
+};
+
+use gpui::{
+ App, AppContext, Context, Entity, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
+ ScrollHandle, SerializedTaskTiming, StatefulInteractiveElement, Styled, Task, TaskTiming,
+ TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, div, prelude::FluentBuilder, px,
+ relative, size,
+};
+use util::ResultExt;
+use workspace::{
+ Workspace,
+ ui::{
+ ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, ToggleState,
+ WithScrollbar, h_flex, v_flex,
+ },
+};
+use zed_actions::OpenPerformanceProfiler;
+
+pub fn init(startup_time: Instant, cx: &mut App) {
+ cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| {
+ workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| {
+ let window_handle = window
+ .window_handle()
+ .downcast::<Workspace>()
+ .expect("Workspaces are root Windows");
+ open_performance_profiler(startup_time, workspace, window_handle, cx);
+ });
+ })
+ .detach();
+}
+
+fn open_performance_profiler(
+ startup_time: Instant,
+ _workspace: &mut workspace::Workspace,
+ workspace_handle: WindowHandle<Workspace>,
+ cx: &mut App,
+) {
+ let existing_window = cx
+ .windows()
+ .into_iter()
+ .find_map(|window| window.downcast::<ProfilerWindow>());
+
+ if let Some(existing_window) = existing_window {
+ existing_window
+ .update(cx, |profiler_window, window, _cx| {
+ profiler_window.workspace = Some(workspace_handle);
+ window.activate_window();
+ })
+ .log_err();
+ return;
+ }
+
+ let default_bounds = size(px(1280.), px(720.)); // 16:9
+
+ cx.open_window(
+ WindowOptions {
+ titlebar: Some(TitlebarOptions {
+ title: Some("Profiler Window".into()),
+ appears_transparent: false,
+ traffic_light_position: None,
+ }),
+ focus: true,
+ show: true,
+ is_movable: true,
+ kind: gpui::WindowKind::Normal,
+ window_background: cx.theme().window_background_appearance(),
+ window_decorations: None,
+ window_min_size: Some(default_bounds),
+ window_bounds: Some(WindowBounds::centered(default_bounds, cx)),
+ ..Default::default()
+ },
+ |_window, cx| ProfilerWindow::new(startup_time, Some(workspace_handle), cx),
+ )
+ .log_err();
+}
+
+enum DataMode {
+ Realtime(Option<Vec<TaskTiming>>),
+ Snapshot(Vec<TaskTiming>),
+}
+
+struct TimingBar {
+ location: &'static core::panic::Location<'static>,
+ start: Instant,
+ end: Instant,
+ color: Hsla,
+}
+
+pub struct ProfilerWindow {
+ startup_time: Instant,
+ data: DataMode,
+ include_self_timings: ToggleState,
+ autoscroll: bool,
+ scroll_handle: ScrollHandle,
+ workspace: Option<WindowHandle<Workspace>>,
+ _refresh: Option<Task<()>>,
+}
+
+impl ProfilerWindow {
+ pub fn new(
+ startup_time: Instant,
+ workspace_handle: Option<WindowHandle<Workspace>>,
+ cx: &mut App,
+ ) -> Entity<Self> {
+ let entity = cx.new(|cx| ProfilerWindow {
+ startup_time,
+ data: DataMode::Realtime(None),
+ include_self_timings: ToggleState::Unselected,
+ autoscroll: true,
+ scroll_handle: ScrollHandle::new(),
+ workspace: workspace_handle,
+ _refresh: Some(Self::begin_listen(cx)),
+ });
+
+ entity
+ }
+
+ fn begin_listen(cx: &mut Context<Self>) -> Task<()> {
+ cx.spawn(async move |this, cx| {
+ loop {
+ let data = cx
+ .foreground_executor()
+ .dispatcher
+ .get_current_thread_timings();
+
+ this.update(cx, |this: &mut ProfilerWindow, cx| {
+ let scroll_offset = this.scroll_handle.offset();
+ let max_offset = this.scroll_handle.max_offset();
+ this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
+
+ this.data = DataMode::Realtime(Some(data));
+
+ if this.autoscroll {
+ this.scroll_handle.scroll_to_bottom();
+ }
+
+ cx.notify();
+ })
+ .ok();
+
+ // yield to the executor
+ cx.background_executor()
+ .timer(Duration::from_micros(1))
+ .await;
+ }
+ })
+ }
+
+ fn get_timings(&self) -> Option<&Vec<TaskTiming>> {
+ match &self.data {
+ DataMode::Realtime(data) => data.as_ref(),
+ DataMode::Snapshot(data) => Some(data),
+ }
+ }
+
+ fn render_timing(
+ &self,
+ value_range: Range<Instant>,
+ item: TimingBar,
+ cx: &App,
+ ) -> impl IntoElement {
+ let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32;
+
+ let remap = value_range
+ .end
+ .duration_since(value_range.start)
+ .as_secs_f32()
+ * 1000f32;
+
+ let start = (item.start.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap;
+ let end = (item.end.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap;
+
+ let bar_width = end - start.abs();
+
+ let location = item
+ .location
+ .file()
+ .rsplit_once("/")
+ .unwrap_or(("", item.location.file()))
+ .1;
+ let location = location.rsplit_once("\\").unwrap_or(("", location)).1;
+
+ let label = format!(
+ "{}:{}:{}",
+ location,
+ item.location.line(),
+ item.location.column()
+ );
+
+ h_flex()
+ .gap_2()
+ .w_full()
+ .h(px(32.0))
+ .child(
+ div()
+ .w(px(200.0))
+ .flex_shrink_0()
+ .overflow_hidden()
+ .child(div().text_ellipsis().child(label)),
+ )
+ .child(
+ div()
+ .flex_1()
+ .h(px(24.0))
+ .bg(cx.theme().colors().background)
+ .rounded_md()
+ .p(px(2.0))
+ .relative()
+ .child(
+ div()
+ .absolute()
+ .h_full()
+ .rounded_sm()
+ .bg(item.color)
+ .left(relative(start.max(0f32)))
+ .w(relative(bar_width)),
+ ),
+ )
+ .child(
+ div()
+ .min_w(px(60.0))
+ .flex_shrink_0()
+ .text_right()
+ .child(format!("{:.1}ms", time_ms)),
+ )
+ }
+}
+
+impl Render for ProfilerWindow {
+ fn render(
+ &mut self,
+ window: &mut gpui::Window,
+ cx: &mut gpui::Context<Self>,
+ ) -> impl gpui::IntoElement {
+ v_flex()
+ .id("profiler")
+ .w_full()
+ .h_full()
+ .gap_2()
+ .bg(cx.theme().colors().surface_background)
+ .text_color(cx.theme().colors().text)
+ .child(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Button::new(
+ "switch-mode",
+ match self.data {
+ DataMode::Snapshot { .. } => "Resume",
+ DataMode::Realtime(_) => "Pause",
+ },
+ )
+ .style(ButtonStyle::Filled)
+ .on_click(cx.listener(
+ |this, _, _window, cx| {
+ match &this.data {
+ DataMode::Realtime(Some(data)) => {
+ this._refresh = None;
+ this.data = DataMode::Snapshot(data.clone());
+ }
+ DataMode::Snapshot { .. } => {
+ this._refresh = Some(Self::begin_listen(cx));
+ this.data = DataMode::Realtime(None);
+ }
+ _ => {}
+ };
+ cx.notify();
+ },
+ )),
+ )
+ .child(
+ Button::new("export-data", "Save")
+ .style(ButtonStyle::Filled)
+ .on_click(cx.listener(|this, _, _window, cx| {
+ let Some(workspace) = this.workspace else {
+ return;
+ };
+
+ let Some(data) = this.get_timings() else {
+ return;
+ };
+ let timings =
+ SerializedTaskTiming::convert(this.startup_time, &data);
+
+ let active_path = workspace
+ .read_with(cx, |workspace, cx| {
+ workspace.most_recent_active_path(cx)
+ })
+ .log_err()
+ .flatten()
+ .and_then(|p| p.parent().map(|p| p.to_owned()))
+ .unwrap_or_else(|| PathBuf::default());
+
+ let path = cx.prompt_for_new_path(
+ &active_path,
+ Some("performance_profile.miniprof"),
+ );
+
+ cx.background_spawn(async move {
+ let path = path.await;
+ let path =
+ path.log_err().and_then(|p| p.log_err()).flatten();
+
+ let Some(path) = path else {
+ return;
+ };
+
+ let Some(timings) =
+ serde_json::to_string(&timings).log_err()
+ else {
+ return;
+ };
+
+ smol::fs::write(path, &timings).await.log_err();
+ })
+ .detach();
+ })),
+ ),
+ )
+ .child(
+ Checkbox::new("include-self", self.include_self_timings)
+ .label("Include profiler timings")
+ .on_click(cx.listener(|this, checked, _window, cx| {
+ this.include_self_timings = *checked;
+ cx.notify();
+ })),
+ ),
+ )
+ .when_some(self.get_timings(), |div, e| {
+ if e.len() == 0 {
+ return div;
+ }
+
+ let min = e[0].start;
+ let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now());
+ div.child(
+ v_flex()
+ .id("timings.bars")
+ .overflow_scroll()
+ .w_full()
+ .h_full()
+ .gap_2()
+ .track_scroll(&self.scroll_handle)
+ .on_scroll_wheel(cx.listener(|this, _, _, _cx| {
+ let scroll_offset = this.scroll_handle.offset();
+ let max_offset = this.scroll_handle.max_offset();
+ this.autoscroll = -scroll_offset.y >= (max_offset.height - px(5.0));
+ }))
+ .children(
+ e.iter()
+ .filter(|timing| {
+ timing
+ .end
+ .unwrap_or_else(|| Instant::now())
+ .duration_since(timing.start)
+ .as_millis()
+ >= 1
+ })
+ .filter(|timing| {
+ if self.include_self_timings.selected() {
+ true
+ } else {
+ !timing.location.file().ends_with("miniprofiler_ui.rs")
+ }
+ })
+ .enumerate()
+ .map(|(i, timing)| {
+ self.render_timing(
+ max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
+ ..max,
+ TimingBar {
+ location: timing.location,
+ start: timing.start,
+ end: timing.end.unwrap_or_else(|| Instant::now()),
+ color: cx.theme().accents().color_for_index(i as u32),
+ },
+ cx,
+ )
+ }),
+ ),
+ )
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+ })
+ }
+}
@@ -155,6 +155,12 @@ pub fn temp_dir() -> &'static PathBuf {
})
}
+/// Returns the path to the hang traces directory.
+pub fn hang_traces_dir() -> &'static PathBuf {
+ static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
+ LOGS_DIR.get_or_init(|| data_dir().join("hang_traces"))
+}
+
/// Returns the path to the logs directory.
pub fn logs_dir() -> &'static PathBuf {
static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -321,6 +321,7 @@ fn init_paths() -> anyhow::Result<()> {
paths::languages_dir(),
paths::logs_dir(),
paths::temp_dir(),
+ paths::hang_traces_dir(),
paths::remote_extensions_dir(),
paths::remote_extensions_uploads_dir(),
]
@@ -12,7 +12,7 @@ mod session;
use std::{sync::Arc, time::Duration};
use async_dispatcher::{Dispatcher, Runnable, set_dispatcher};
-use gpui::{App, PlatformDispatcher};
+use gpui::{App, PlatformDispatcher, RunnableVariant};
use project::Fs;
pub use runtimelib::ExecutionState;
@@ -45,11 +45,13 @@ fn zed_dispatcher(cx: &mut App) -> impl Dispatcher {
// other crates in Zed.
impl Dispatcher for ZedDispatcher {
fn dispatch(&self, runnable: Runnable) {
- self.dispatcher.dispatch(runnable, None)
+ self.dispatcher
+ .dispatch(RunnableVariant::Compat(runnable), None);
}
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
- self.dispatcher.dispatch_after(duration, runnable);
+ self.dispatcher
+ .dispatch_after(duration, RunnableVariant::Compat(runnable));
}
}
@@ -97,6 +97,7 @@ markdown.workspace = true
markdown_preview.workspace = true
menu.workspace = true
migrator.workspace = true
+miniprofiler_ui.workspace = true
mimalloc = { version = "0.1", optional = true }
nc.workspace = true
nix = { workspace = true, features = ["pthread", "signal"] }
@@ -166,6 +167,7 @@ zeta.workspace = true
zeta2.workspace = true
zlog.workspace = true
zlog_settings.workspace = true
+chrono.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
@@ -37,7 +37,8 @@ use std::{
io::{self, IsTerminal},
path::{Path, PathBuf},
process,
- sync::Arc,
+ sync::{Arc, OnceLock},
+ time::Instant,
};
use theme::{ActiveTheme, GlobalTheme, ThemeRegistry};
use util::{ResultExt, TryFutureExt, maybe};
@@ -162,7 +163,11 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
}
}
+pub static STARTUP_TIME: OnceLock<Instant> = OnceLock::new();
+
pub fn main() {
+ STARTUP_TIME.get_or_init(|| Instant::now());
+
#[cfg(unix)]
util::prevent_root_execution();
@@ -637,6 +642,7 @@ pub fn main() {
zeta::init(cx);
inspector_ui::init(app_state.clone(), cx);
json_schema_store::init(cx);
+ miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx);
cx.observe_global::<SettingsStore>({
let http = app_state.client.http_client();
@@ -1226,6 +1232,7 @@ fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
paths::database_dir(),
paths::logs_dir(),
paths::temp_dir(),
+ paths::hang_traces_dir(),
]
.into_iter()
.fold(HashMap::default(), |mut errors, path| {
@@ -1,17 +1,22 @@
use anyhow::{Context as _, Result};
use client::{TelemetrySettings, telemetry::MINIDUMP_ENDPOINT};
use futures::AsyncReadExt;
-use gpui::{App, AppContext as _};
+use gpui::{App, AppContext as _, SerializedThreadTaskTimings};
use http_client::{self, HttpClient, HttpClientWithUrl};
+use log::info;
use project::Project;
use proto::{CrashReport, GetCrashFilesResponse};
use reqwest::multipart::{Form, Part};
use settings::Settings;
use smol::stream::StreamExt;
-use std::{ffi::OsStr, fs, sync::Arc};
+use std::{ffi::OsStr, fs, sync::Arc, thread::ThreadId, time::Duration};
use util::ResultExt;
+use crate::STARTUP_TIME;
+
pub fn init(http_client: Arc<HttpClientWithUrl>, installation_id: Option<String>, cx: &mut App) {
+ monitor_hangs(cx);
+
#[cfg(target_os = "macos")]
monitor_main_thread_hangs(http_client.clone(), installation_id.clone(), cx);
@@ -272,6 +277,94 @@ pub fn monitor_main_thread_hangs(
.detach()
}
+fn monitor_hangs(cx: &App) {
+ let main_thread_id = std::thread::current().id();
+
+ let foreground_executor = cx.foreground_executor();
+ let background_executor = cx.background_executor();
+
+ // 3 seconds hang
+ let (mut tx, mut rx) = futures::channel::mpsc::channel(3);
+ foreground_executor
+ .spawn(async move { while (rx.next().await).is_some() {} })
+ .detach();
+
+ background_executor
+ .spawn({
+ let background_executor = background_executor.clone();
+ async move {
+ let mut hang_time = None;
+
+ let mut hanging = false;
+ loop {
+ background_executor.timer(Duration::from_secs(1)).await;
+ match tx.try_send(()) {
+ Ok(_) => {
+ hang_time = None;
+ hanging = false;
+ continue;
+ }
+ Err(e) => {
+ let is_full = e.into_send_error().is_full();
+ if is_full && !hanging {
+ hanging = true;
+ hang_time = Some(chrono::Local::now());
+ }
+
+ if is_full {
+ save_hang_trace(
+ main_thread_id,
+ &background_executor,
+ hang_time.unwrap(),
+ );
+ }
+ }
+ }
+ }
+ }
+ })
+ .detach();
+}
+
+fn save_hang_trace(
+ main_thread_id: ThreadId,
+ background_executor: &gpui::BackgroundExecutor,
+ hang_time: chrono::DateTime<chrono::Local>,
+) {
+ let thread_timings = background_executor.dispatcher.get_all_timings();
+ let thread_timings = thread_timings
+ .into_iter()
+ .map(|mut timings| {
+ if timings.thread_id == main_thread_id {
+ timings.thread_name = Some("main".to_string());
+ }
+
+ SerializedThreadTaskTimings::convert(*STARTUP_TIME.get().unwrap(), timings)
+ })
+ .collect::<Vec<_>>();
+
+ let trace_path = paths::hang_traces_dir().join(&format!(
+ "hang-{}.miniprof",
+ hang_time.format("%Y-%m-%d_%H-%M-%S")
+ ));
+
+ let Some(timings) = serde_json::to_string(&thread_timings)
+ .context("hang timings serialization")
+ .log_err()
+ else {
+ return;
+ };
+
+ std::fs::write(&trace_path, timings)
+ .context("hang trace file writing")
+ .log_err();
+
+ info!(
+ "hang detected, trace file saved at: {}",
+ trace_path.display()
+ );
+}
+
pub async fn upload_previous_minidumps(
http: Arc<HttpClientWithUrl>,
installation_id: Option<String>,
@@ -65,6 +65,8 @@ actions!(
OpenLicenses,
/// Opens the telemetry log.
OpenTelemetryLog,
+ /// Opens the performance profiler.
+ OpenPerformanceProfiler,
]
);