Miniprofiler (#42385)
Release Notes: - Added hang detection and a built in performance profiler
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -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) {
|
||||
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 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(),
|
||||
runnable.into_raw().as_ptr() as *mut c_void,
|
||||
Some(trampoline),
|
||||
);
|
||||
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::<()>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
|
||||
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,
|
||||
|
||||
218
crates/gpui/src/profiler.rs
Normal file
218
crates/gpui/src/profiler.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
23
crates/miniprofiler_ui/Cargo.toml
Normal file
23
crates/miniprofiler_ui/Cargo.toml
Normal file
@@ -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"] }
|
||||
1
crates/miniprofiler_ui/LICENSE-GPL
Symbolic link
1
crates/miniprofiler_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
393
crates/miniprofiler_ui/src/miniprofiler_ui.rs
Normal file
393
crates/miniprofiler_ui/src/miniprofiler_ui.rs
Normal file
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user