mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
Support profiling remote server in the miniprofiler (#49582)
Release Notes: - The `zed: open performance profiler` action can now display profiling data from the remote server.
This commit is contained in:
parent
fc79a6fbe7
commit
1ad5ec6db2
24 changed files with 740 additions and 192 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -10188,8 +10188,10 @@ name = "miniprofiler_ui"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"rpc",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ async fn test_sharing_an_ssh_remote_project(
|
|||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -261,6 +262,7 @@ async fn test_ssh_collaboration_git_branches(
|
|||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -466,6 +468,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
|||
node_runtime: NodeRuntime::unavailable(),
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -628,6 +631,7 @@ async fn test_remote_server_debugger(
|
|||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -740,6 +744,7 @@ async fn test_slow_adapter_startup_retries(
|
|||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -946,6 +951,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
|||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ impl AppContext for AsyncApp {
|
|||
lock.read_window(window, read)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
|
|
@ -407,6 +408,7 @@ impl AppContext for AsyncWindowContext {
|
|||
self.app.read_window(window, read)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ impl BackgroundExecutor {
|
|||
/// Returns a task that will complete after the given duration.
|
||||
/// Depending on other concurrent tasks the elapsed duration may be longer
|
||||
/// than requested.
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Task<()> {
|
||||
if duration.is_zero() {
|
||||
return Task::ready(());
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ use crate::{
|
|||
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
|
||||
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
|
||||
Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TaskTiming,
|
||||
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task,
|
||||
ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
|
@ -620,7 +620,7 @@ impl Drop for TimerResolutionGuard {
|
|||
#[doc(hidden)]
|
||||
pub trait PlatformDispatcher: Send + Sync {
|
||||
fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
|
||||
fn get_current_thread_timings(&self) -> Vec<TaskTiming>;
|
||||
fn get_current_thread_timings(&self) -> ThreadTaskTimings;
|
||||
fn is_main_thread(&self) -> bool;
|
||||
fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
|
||||
|
|
|
|||
|
|
@ -145,9 +145,11 @@ impl PlatformDispatcher for LinuxDispatcher {
|
|||
ThreadTaskTimings::convert(&global_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
|
||||
fn get_current_thread_timings(&self) -> crate::ThreadTaskTimings {
|
||||
THREAD_TIMINGS.with(|timings| {
|
||||
let timings = timings.lock();
|
||||
let thread_name = timings.thread_name.clone();
|
||||
let total_pushed = timings.total_pushed;
|
||||
let timings = &timings.timings;
|
||||
|
||||
let mut vec = Vec::with_capacity(timings.len());
|
||||
|
|
@ -155,7 +157,13 @@ impl PlatformDispatcher for LinuxDispatcher {
|
|||
let (s1, s2) = timings.as_slices();
|
||||
vec.extend_from_slice(s1);
|
||||
vec.extend_from_slice(s2);
|
||||
vec
|
||||
|
||||
crate::ThreadTaskTimings {
|
||||
thread_name,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: vec,
|
||||
total_pushed,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,16 +55,25 @@ impl PlatformDispatcher for MacDispatcher {
|
|||
ThreadTaskTimings::convert(&global_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> Vec<TaskTiming> {
|
||||
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
|
||||
THREAD_TIMINGS.with(|timings| {
|
||||
let timings = &timings.lock().timings;
|
||||
let timings = timings.lock();
|
||||
let thread_name = timings.thread_name.clone();
|
||||
let total_pushed = timings.total_pushed;
|
||||
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
|
||||
|
||||
ThreadTaskTimings {
|
||||
thread_name,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: vec,
|
||||
total_pushed,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,8 +102,13 @@ impl PlatformDispatcher for TestDispatcher {
|
|||
Vec::new()
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
|
||||
Vec::new()
|
||||
fn get_current_thread_timings(&self) -> crate::ThreadTaskTimings {
|
||||
crate::ThreadTaskTimings {
|
||||
thread_name: None,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: Vec::new(),
|
||||
total_pushed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_main_thread(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -113,9 +113,11 @@ impl PlatformDispatcher for WindowsDispatcher {
|
|||
ThreadTaskTimings::convert(&global_thread_timings)
|
||||
}
|
||||
|
||||
fn get_current_thread_timings(&self) -> Vec<crate::TaskTiming> {
|
||||
fn get_current_thread_timings(&self) -> crate::ThreadTaskTimings {
|
||||
THREAD_TIMINGS.with(|timings| {
|
||||
let timings = timings.lock();
|
||||
let thread_name = timings.thread_name.clone();
|
||||
let total_pushed = timings.total_pushed;
|
||||
let timings = &timings.timings;
|
||||
|
||||
let mut vec = Vec::with_capacity(timings.len());
|
||||
|
|
@ -123,7 +125,13 @@ impl PlatformDispatcher for WindowsDispatcher {
|
|||
let (s1, s2) = timings.as_slices();
|
||||
vec.extend_from_slice(s1);
|
||||
vec.extend_from_slice(s2);
|
||||
vec
|
||||
|
||||
crate::ThreadTaskTimings {
|
||||
thread_name,
|
||||
thread_id: std::thread::current().id(),
|
||||
timings: vec,
|
||||
total_pushed,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ impl Scheduler for PlatformScheduler {
|
|||
self.dispatcher.spawn_realtime(f);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn timer(&self, duration: Duration) -> Timer {
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
cell::LazyCell,
|
||||
collections::HashMap,
|
||||
hash::Hasher,
|
||||
hash::{DefaultHasher, Hash},
|
||||
sync::Arc,
|
||||
|
|
@ -9,6 +10,8 @@ use std::{
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::SharedString;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TaskTiming {
|
||||
|
|
@ -23,6 +26,7 @@ pub struct ThreadTaskTimings {
|
|||
pub thread_name: Option<String>,
|
||||
pub thread_id: ThreadId,
|
||||
pub timings: Vec<TaskTiming>,
|
||||
pub total_pushed: u64,
|
||||
}
|
||||
|
||||
impl ThreadTaskTimings {
|
||||
|
|
@ -36,6 +40,7 @@ impl ThreadTaskTimings {
|
|||
.map(|(thread_id, timings)| {
|
||||
let timings = timings.lock();
|
||||
let thread_name = timings.thread_name.clone();
|
||||
let total_pushed = timings.total_pushed;
|
||||
let timings = &timings.timings;
|
||||
|
||||
let mut vec = Vec::with_capacity(timings.len());
|
||||
|
|
@ -48,6 +53,7 @@ impl ThreadTaskTimings {
|
|||
thread_name,
|
||||
thread_id,
|
||||
timings: vec,
|
||||
total_pushed,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -55,20 +61,20 @@ impl ThreadTaskTimings {
|
|||
}
|
||||
|
||||
/// Serializable variant of [`core::panic::Location`]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedLocation<'a> {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedLocation {
|
||||
/// Name of the source file
|
||||
pub file: &'a str,
|
||||
pub file: SharedString,
|
||||
/// 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 {
|
||||
impl From<&core::panic::Location<'static>> for SerializedLocation {
|
||||
fn from(value: &core::panic::Location<'static>) -> Self {
|
||||
SerializedLocation {
|
||||
file: value.file(),
|
||||
file: value.file().into(),
|
||||
line: value.line(),
|
||||
column: value.column(),
|
||||
}
|
||||
|
|
@ -77,23 +83,22 @@ impl<'a> From<&'a core::panic::Location<'a>> for SerializedLocation<'a> {
|
|||
|
||||
/// Serializable variant of [`TaskTiming`]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedTaskTiming<'a> {
|
||||
pub struct SerializedTaskTiming {
|
||||
/// Location of the timing
|
||||
#[serde(borrow)]
|
||||
pub location: SerializedLocation<'a>,
|
||||
pub location: SerializedLocation,
|
||||
/// 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> {
|
||||
impl SerializedTaskTiming {
|
||||
/// 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>> {
|
||||
pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec<SerializedTaskTiming> {
|
||||
let serialized = timings
|
||||
.iter()
|
||||
.map(|timing| {
|
||||
|
|
@ -117,26 +122,22 @@ impl<'a> SerializedTaskTiming<'a> {
|
|||
|
||||
/// Serializable variant of [`ThreadTaskTimings`]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedThreadTaskTimings<'a> {
|
||||
pub struct SerializedThreadTaskTimings {
|
||||
/// 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>>,
|
||||
pub timings: Vec<SerializedTaskTiming>,
|
||||
}
|
||||
|
||||
impl<'a> SerializedThreadTaskTimings<'a> {
|
||||
impl SerializedThreadTaskTimings {
|
||||
/// 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> {
|
||||
pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings {
|
||||
let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings);
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
|
@ -151,6 +152,96 @@ impl<'a> SerializedThreadTaskTimings<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadTimingsDelta {
|
||||
/// Hashed thread id
|
||||
pub thread_id: u64,
|
||||
/// Thread name, if known
|
||||
pub thread_name: Option<String>,
|
||||
/// New timings since the last call. If the circular buffer wrapped around
|
||||
/// since the previous poll, some entries may have been lost.
|
||||
pub new_timings: Vec<SerializedTaskTiming>,
|
||||
}
|
||||
|
||||
/// Tracks which timing events have already been seen so that callers can request only unseen events.
|
||||
#[doc(hidden)]
|
||||
pub struct ProfilingCollector {
|
||||
startup_time: Instant,
|
||||
cursors: HashMap<u64, u64>,
|
||||
}
|
||||
|
||||
impl ProfilingCollector {
|
||||
pub fn new(startup_time: Instant) -> Self {
|
||||
Self {
|
||||
startup_time,
|
||||
cursors: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn startup_time(&self) -> Instant {
|
||||
self.startup_time
|
||||
}
|
||||
|
||||
pub fn collect_unseen(
|
||||
&mut self,
|
||||
all_timings: Vec<ThreadTaskTimings>,
|
||||
) -> Vec<ThreadTimingsDelta> {
|
||||
let mut deltas = Vec::with_capacity(all_timings.len());
|
||||
|
||||
for thread in all_timings {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
thread.thread_id.hash(&mut hasher);
|
||||
let hashed_id = hasher.finish();
|
||||
|
||||
let prev_cursor = self.cursors.get(&hashed_id).copied().unwrap_or(0);
|
||||
let buffer_len = thread.timings.len() as u64;
|
||||
let buffer_start = thread.total_pushed.saturating_sub(buffer_len);
|
||||
|
||||
let mut slice = if prev_cursor < buffer_start {
|
||||
// Cursor fell behind the buffer — some entries were evicted.
|
||||
// Return everything still in the buffer.
|
||||
thread.timings.as_slice()
|
||||
} else {
|
||||
let skip = (prev_cursor - buffer_start) as usize;
|
||||
&thread.timings[skip..]
|
||||
};
|
||||
|
||||
// Don't emit the last entry if it's still in-progress (end: None).
|
||||
let incomplete_at_end = slice.last().is_some_and(|t| t.end.is_none());
|
||||
if incomplete_at_end {
|
||||
slice = &slice[..slice.len() - 1];
|
||||
}
|
||||
|
||||
let cursor_advance = if incomplete_at_end {
|
||||
thread.total_pushed - 1
|
||||
} else {
|
||||
thread.total_pushed
|
||||
};
|
||||
|
||||
self.cursors.insert(hashed_id, cursor_advance);
|
||||
|
||||
if slice.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_timings = SerializedTaskTiming::convert(self.startup_time, slice);
|
||||
|
||||
deltas.push(ThreadTimingsDelta {
|
||||
thread_id: hashed_id,
|
||||
thread_name: thread.thread_name,
|
||||
new_timings,
|
||||
});
|
||||
}
|
||||
|
||||
deltas
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.cursors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Allow 20mb of task timing entries
|
||||
const MAX_TASK_TIMINGS: usize = (20 * 1024 * 1024) / core::mem::size_of::<TaskTiming>();
|
||||
|
||||
|
|
@ -190,6 +281,7 @@ pub(crate) struct ThreadTimings {
|
|||
pub thread_name: Option<String>,
|
||||
pub thread_id: ThreadId,
|
||||
pub timings: Box<TaskTimings>,
|
||||
pub total_pushed: u64,
|
||||
}
|
||||
|
||||
impl ThreadTimings {
|
||||
|
|
@ -198,6 +290,7 @@ impl ThreadTimings {
|
|||
thread_name,
|
||||
thread_id,
|
||||
timings: TaskTimings::boxed(),
|
||||
total_pushed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,15 +314,15 @@ impl Drop for ThreadTimings {
|
|||
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 {
|
||||
if let Some(last_timing) = timings.timings.back_mut() {
|
||||
if last_timing.location == timing.location && last_timing.start == timing.start {
|
||||
last_timing.end = timing.end;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
timings.push_back(timing);
|
||||
timings.timings.push_back(timing);
|
||||
timings.total_pushed += 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ path = "src/miniprofiler_ui.rs"
|
|||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
rpc.workspace = true
|
||||
theme.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace.workspace = true
|
||||
util.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
ops::Range,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
|
|
@ -7,20 +7,60 @@ use std::{
|
|||
|
||||
use gpui::{
|
||||
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
|
||||
ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement,
|
||||
Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
|
||||
ParentElement as _, ProfilingCollector, Render, SerializedLocation, SerializedTaskTiming,
|
||||
SerializedThreadTaskTimings, SharedString, StatefulInteractiveElement, Styled, Task,
|
||||
ThreadTimingsDelta, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
|
||||
WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
|
||||
};
|
||||
use rpc::{AnyProtoClient, proto};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Divider,
|
||||
ScrollableHandle as _, ToggleState, Tooltip, WithScrollbar, h_flex, v_flex,
|
||||
ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, ContextMenu, Divider,
|
||||
DropdownMenu, ScrollAxes, ScrollableHandle as _, Scrollbars, ToggleState, Tooltip,
|
||||
WithScrollbar, h_flex, v_flex,
|
||||
},
|
||||
};
|
||||
use zed_actions::OpenPerformanceProfiler;
|
||||
|
||||
const NANOS_PER_MS: u128 = 1_000_000;
|
||||
const VISIBLE_WINDOW_NANOS: u128 = 10 * 1_000_000_000;
|
||||
const REMOTE_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ProfileSource {
|
||||
Foreground,
|
||||
AllThreads,
|
||||
RemoteForeground,
|
||||
RemoteAllThreads,
|
||||
}
|
||||
|
||||
impl ProfileSource {
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
ProfileSource::Foreground => "Foreground",
|
||||
ProfileSource::AllThreads => "All threads",
|
||||
ProfileSource::RemoteForeground => "Remote: Foreground",
|
||||
ProfileSource::RemoteAllThreads => "Remote: All threads",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_remote(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ProfileSource::RemoteForeground | ProfileSource::RemoteAllThreads
|
||||
)
|
||||
}
|
||||
|
||||
fn foreground_only(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ProfileSource::Foreground | ProfileSource::RemoteForeground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(startup_time: Instant, cx: &mut App) {
|
||||
cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
|
|
@ -52,50 +92,54 @@ fn open_performance_profiler(
|
|||
return;
|
||||
}
|
||||
|
||||
let default_bounds = size(px(1280.), px(720.)); // 16:9
|
||||
let window_background = cx.theme().window_background_appearance();
|
||||
let default_bounds = size(px(1280.), px(720.));
|
||||
|
||||
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>),
|
||||
cx.defer(move |cx| {
|
||||
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,
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
struct TimingBar {
|
||||
location: &'static core::panic::Location<'static>,
|
||||
start: Instant,
|
||||
end: Instant,
|
||||
location: SerializedLocation,
|
||||
start_nanos: u128,
|
||||
duration_nanos: u128,
|
||||
color: Hsla,
|
||||
}
|
||||
|
||||
pub struct ProfilerWindow {
|
||||
startup_time: Instant,
|
||||
data: DataMode,
|
||||
collector: ProfilingCollector,
|
||||
source: ProfileSource,
|
||||
timings: Vec<SerializedThreadTaskTimings>,
|
||||
paused: bool,
|
||||
display_timings: Rc<Vec<SerializedTaskTiming>>,
|
||||
include_self_timings: ToggleState,
|
||||
autoscroll: bool,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
workspace: Option<WeakEntity<Workspace>>,
|
||||
_refresh: Option<Task<()>>,
|
||||
has_remote: bool,
|
||||
remote_now_nanos: u128,
|
||||
remote_received_at: Option<Instant>,
|
||||
_remote_poll_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ProfilerWindow {
|
||||
|
|
@ -104,75 +148,262 @@ impl ProfilerWindow {
|
|||
workspace_handle: Option<WeakEntity<Workspace>>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let entity = cx.new(|cx| ProfilerWindow {
|
||||
startup_time,
|
||||
data: DataMode::Realtime(None),
|
||||
cx.new(|_cx| ProfilerWindow {
|
||||
collector: ProfilingCollector::new(startup_time),
|
||||
source: ProfileSource::Foreground,
|
||||
timings: Vec::new(),
|
||||
paused: false,
|
||||
display_timings: Rc::new(Vec::new()),
|
||||
include_self_timings: ToggleState::Unselected,
|
||||
autoscroll: true,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
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| {
|
||||
this.data = DataMode::Realtime(Some(data));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
// yield to the executor
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_micros(1))
|
||||
.await;
|
||||
}
|
||||
has_remote: false,
|
||||
remote_now_nanos: 0,
|
||||
remote_received_at: None,
|
||||
_remote_poll_task: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_timings(&self) -> Option<&Vec<TaskTiming>> {
|
||||
match &self.data {
|
||||
DataMode::Realtime(data) => data.as_ref(),
|
||||
DataMode::Snapshot(data) => Some(data),
|
||||
fn poll_timings(&mut self, cx: &App) {
|
||||
self.has_remote = self.remote_proto_client(cx).is_some();
|
||||
match self.source {
|
||||
ProfileSource::Foreground => {
|
||||
let dispatcher = cx.foreground_executor().dispatcher();
|
||||
let current_thread = dispatcher.get_current_thread_timings();
|
||||
let deltas = self.collector.collect_unseen(vec![current_thread]);
|
||||
self.apply_deltas(deltas);
|
||||
}
|
||||
ProfileSource::AllThreads => {
|
||||
let dispatcher = cx.foreground_executor().dispatcher();
|
||||
let all_timings = dispatcher.get_all_timings();
|
||||
let deltas = self.collector.collect_unseen(all_timings);
|
||||
self.apply_deltas(deltas);
|
||||
}
|
||||
ProfileSource::RemoteForeground | ProfileSource::RemoteAllThreads => {
|
||||
// Remote timings arrive asynchronously via apply_remote_response.
|
||||
}
|
||||
}
|
||||
self.rebuild_display_timings();
|
||||
}
|
||||
|
||||
fn rebuild_display_timings(&mut self) {
|
||||
let include_self = self.include_self_timings.selected();
|
||||
let cutoff_nanos = self.now_nanos().saturating_sub(VISIBLE_WINDOW_NANOS);
|
||||
|
||||
let per_thread: Vec<Vec<SerializedTaskTiming>> = self
|
||||
.timings
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
let visible = visible_tail(&thread.timings, cutoff_nanos);
|
||||
filter_timings(visible.iter().cloned(), include_self)
|
||||
})
|
||||
.collect();
|
||||
self.display_timings = Rc::new(kway_merge(per_thread));
|
||||
}
|
||||
|
||||
fn now_nanos(&self) -> u128 {
|
||||
if self.source.is_remote() {
|
||||
let elapsed_since_poll = self
|
||||
.remote_received_at
|
||||
.map(|at| Instant::now().duration_since(at).as_nanos())
|
||||
.unwrap_or(0);
|
||||
self.remote_now_nanos + elapsed_since_poll
|
||||
} else {
|
||||
Instant::now()
|
||||
.duration_since(self.collector.startup_time())
|
||||
.as_nanos()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_timing(value_range: Range<Instant>, item: TimingBar, cx: &App) -> Div {
|
||||
let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32;
|
||||
fn set_source(&mut self, source: ProfileSource, cx: &mut Context<Self>) {
|
||||
if self.source == source {
|
||||
return;
|
||||
}
|
||||
|
||||
let remap = value_range
|
||||
.end
|
||||
.duration_since(value_range.start)
|
||||
.as_secs_f32()
|
||||
* 1000f32;
|
||||
self.source = source;
|
||||
|
||||
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;
|
||||
self.timings.clear();
|
||||
self.collector.reset();
|
||||
self.display_timings = Rc::new(Vec::new());
|
||||
self.remote_now_nanos = 0;
|
||||
self.remote_received_at = None;
|
||||
self.has_remote = self.remote_proto_client(cx).is_some();
|
||||
|
||||
let bar_width = end - start.abs();
|
||||
if source.is_remote() {
|
||||
self.start_remote_polling(cx);
|
||||
} else {
|
||||
self._remote_poll_task = None;
|
||||
}
|
||||
}
|
||||
|
||||
let location = item
|
||||
.location
|
||||
.file()
|
||||
.rsplit_once("/")
|
||||
.unwrap_or(("", item.location.file()))
|
||||
.1;
|
||||
let location = location.rsplit_once("\\").unwrap_or(("", location)).1;
|
||||
fn remote_proto_client(&self, cx: &App) -> Option<AnyProtoClient> {
|
||||
let workspace = self.workspace.as_ref()?;
|
||||
workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
let remote_client = project.remote_client()?;
|
||||
Some(remote_client.read(cx).proto_client())
|
||||
})
|
||||
.log_err()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn start_remote_polling(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(proto_client) = self.remote_proto_client(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let source_foreground_only = self.source.foreground_only();
|
||||
let weak = cx.weak_entity();
|
||||
self._remote_poll_task = Some(cx.spawn(async move |_this, cx| {
|
||||
loop {
|
||||
let response = proto_client
|
||||
.request(proto::GetRemoteProfilingData {
|
||||
project_id: proto::REMOTE_SERVER_PROJECT_ID,
|
||||
foreground_only: source_foreground_only,
|
||||
})
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
let ok = weak.update(&mut cx.clone(), |this, cx| {
|
||||
this.apply_remote_response(response);
|
||||
cx.notify();
|
||||
});
|
||||
if ok.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
Err::<(), _>(error).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_executor().timer(REMOTE_POLL_INTERVAL).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn apply_remote_response(&mut self, response: proto::GetRemoteProfilingDataResponse) {
|
||||
self.has_remote = true;
|
||||
self.remote_now_nanos = response.now_nanos as u128;
|
||||
self.remote_received_at = Some(Instant::now());
|
||||
let deltas = response
|
||||
.threads
|
||||
.into_iter()
|
||||
.map(|thread| {
|
||||
let new_timings = thread
|
||||
.timings
|
||||
.into_iter()
|
||||
.map(|t| {
|
||||
let location = t.location.unwrap_or_default();
|
||||
SerializedTaskTiming {
|
||||
location: SerializedLocation {
|
||||
file: SharedString::from(location.file),
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
},
|
||||
start: t.start_nanos as u128,
|
||||
duration: t.duration_nanos as u128,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
ThreadTimingsDelta {
|
||||
thread_id: thread.thread_id,
|
||||
thread_name: thread.thread_name,
|
||||
new_timings,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.apply_deltas(deltas);
|
||||
self.rebuild_display_timings();
|
||||
}
|
||||
|
||||
fn apply_deltas(&mut self, deltas: Vec<ThreadTimingsDelta>) {
|
||||
for delta in deltas {
|
||||
append_to_thread(
|
||||
&mut self.timings,
|
||||
delta.thread_id,
|
||||
delta.thread_name,
|
||||
delta.new_timings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_source_dropdown(
|
||||
&self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> DropdownMenu {
|
||||
let weak = cx.weak_entity();
|
||||
let current_source = self.source;
|
||||
let has_remote = self.has_remote;
|
||||
|
||||
let mut sources = vec![ProfileSource::Foreground, ProfileSource::AllThreads];
|
||||
if has_remote {
|
||||
sources.push(ProfileSource::RemoteForeground);
|
||||
sources.push(ProfileSource::RemoteAllThreads);
|
||||
}
|
||||
|
||||
DropdownMenu::new(
|
||||
"profile-source",
|
||||
current_source.label(),
|
||||
ContextMenu::build(window, cx, move |mut menu, window, cx| {
|
||||
for source in &sources {
|
||||
let source = *source;
|
||||
let weak = weak.clone();
|
||||
menu = menu.entry(source.label(), None, move |_, cx| {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.set_source(source, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
if let Some(index) = sources.iter().position(|s| *s == current_source) {
|
||||
for _ in 0..=index {
|
||||
menu.select_next(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
menu
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_timing(
|
||||
window_start_nanos: u128,
|
||||
window_duration_nanos: u128,
|
||||
item: TimingBar,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
let time_ms = item.duration_nanos as f32 / NANOS_PER_MS as f32;
|
||||
|
||||
let start_fraction = if item.start_nanos >= window_start_nanos {
|
||||
(item.start_nanos - window_start_nanos) as f32 / window_duration_nanos as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let end_nanos = item.start_nanos + item.duration_nanos;
|
||||
let end_fraction = if end_nanos >= window_start_nanos {
|
||||
(end_nanos - window_start_nanos) as f32 / window_duration_nanos as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let start_fraction = start_fraction.clamp(0.0, 1.0);
|
||||
let end_fraction = end_fraction.clamp(0.0, 1.0);
|
||||
let bar_width = (end_fraction - start_fraction).max(0.0);
|
||||
|
||||
let file_str: &str = &item.location.file;
|
||||
let basename = file_str.rsplit_once("/").unwrap_or(("", file_str)).1;
|
||||
let basename = basename.rsplit_once("\\").unwrap_or(("", basename)).1;
|
||||
|
||||
let label = SharedString::from(format!(
|
||||
"{}:{}:{}",
|
||||
location,
|
||||
item.location.line(),
|
||||
item.location.column()
|
||||
basename, item.location.line, item.location.column
|
||||
));
|
||||
|
||||
h_flex()
|
||||
|
|
@ -205,7 +436,7 @@ impl ProfilerWindow {
|
|||
.h_full()
|
||||
.rounded_sm()
|
||||
.bg(item.color)
|
||||
.left(relative(start.max(0f32)))
|
||||
.left(relative(start_fraction.max(0.0)))
|
||||
.w(relative(bar_width)),
|
||||
),
|
||||
)
|
||||
|
|
@ -225,6 +456,12 @@ impl Render for ProfilerWindow {
|
|||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
if !self.paused {
|
||||
self.poll_timings(cx);
|
||||
window.request_animation_frame();
|
||||
}
|
||||
|
||||
let scroll_offset = self.scroll_handle.offset();
|
||||
let max_offset = self.scroll_handle.max_offset();
|
||||
self.autoscroll = -scroll_offset.y >= (max_offset.height - px(24.));
|
||||
|
|
@ -232,8 +469,11 @@ impl Render for ProfilerWindow {
|
|||
self.scroll_handle.scroll_to_bottom();
|
||||
}
|
||||
|
||||
let display_timings = self.display_timings.clone();
|
||||
|
||||
v_flex()
|
||||
.id("profiler")
|
||||
.font(ui_font)
|
||||
.w_full()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
|
|
@ -247,28 +487,21 @@ impl Render for ProfilerWindow {
|
|||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_source_dropdown(window, cx))
|
||||
.child(
|
||||
Button::new(
|
||||
"switch-mode",
|
||||
match self.data {
|
||||
DataMode::Snapshot { .. } => "Resume",
|
||||
DataMode::Realtime(_) => "Pause",
|
||||
},
|
||||
if self.paused { "Resume" } else { "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);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
this.paused = !this.paused;
|
||||
if !this.paused && this.source.is_remote() {
|
||||
this.start_remote_polling(cx);
|
||||
} else if this.paused && this.source.is_remote() {
|
||||
this._remote_poll_task = None;
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
|
|
@ -281,11 +514,24 @@ impl Render for ProfilerWindow {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(data) = this.get_timings() else {
|
||||
if this.timings.iter().all(|t| t.timings.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let serialized = if this.source.foreground_only() {
|
||||
let flat: Vec<&SerializedTaskTiming> = this
|
||||
.timings
|
||||
.iter()
|
||||
.flat_map(|t| &t.timings)
|
||||
.collect();
|
||||
serde_json::to_string(&flat)
|
||||
} else {
|
||||
serde_json::to_string(&this.timings)
|
||||
};
|
||||
|
||||
let Some(serialized) = serialized.log_err() else {
|
||||
return;
|
||||
};
|
||||
let timings =
|
||||
SerializedTaskTiming::convert(this.startup_time, &data);
|
||||
|
||||
let active_path = workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
|
|
@ -310,13 +556,7 @@ impl Render for ProfilerWindow {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(timings) =
|
||||
serde_json::to_string(&timings).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
smol::fs::write(path, &timings).await.log_err();
|
||||
smol::fs::write(path, &serialized).await.log_err();
|
||||
})
|
||||
.detach();
|
||||
})),
|
||||
|
|
@ -331,33 +571,11 @@ impl Render for ProfilerWindow {
|
|||
})),
|
||||
),
|
||||
)
|
||||
.when_some(self.get_timings(), |div, e| {
|
||||
if e.len() == 0 {
|
||||
return div;
|
||||
}
|
||||
.when(!display_timings.is_empty(), |div| {
|
||||
let now_nanos = self.now_nanos();
|
||||
|
||||
let min = e[0].start;
|
||||
let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now());
|
||||
let timings = Rc::new(
|
||||
e.into_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")
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
let window_start_nanos = now_nanos.saturating_sub(VISIBLE_WINDOW_NANOS);
|
||||
let window_duration_nanos = VISIBLE_WINDOW_NANOS;
|
||||
|
||||
div.child(Divider::horizontal()).child(
|
||||
v_flex()
|
||||
|
|
@ -366,25 +584,22 @@ impl Render for ProfilerWindow {
|
|||
.h_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
uniform_list("list", timings.len(), {
|
||||
let timings = timings.clone();
|
||||
uniform_list("list", display_timings.len(), {
|
||||
let timings = display_timings.clone();
|
||||
move |visible_range, _, cx| {
|
||||
let mut items = vec![];
|
||||
for i in visible_range {
|
||||
let timing = &timings[i];
|
||||
let value_range =
|
||||
max.checked_sub(Duration::from_secs(10)).unwrap_or(min)
|
||||
..max;
|
||||
items.push(Self::render_timing(
|
||||
value_range,
|
||||
window_start_nanos,
|
||||
window_duration_nanos,
|
||||
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),
|
||||
location: timing.location.clone(),
|
||||
start_nanos: timing.start,
|
||||
duration_nanos: timing.duration,
|
||||
color: cx.theme().accents().color_for_index(
|
||||
location_color_index(&timing.location),
|
||||
),
|
||||
},
|
||||
cx,
|
||||
));
|
||||
|
|
@ -400,8 +615,102 @@ impl Render for ProfilerWindow {
|
|||
.track_scroll(&self.scroll_handle)
|
||||
.size_full(),
|
||||
)
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx),
|
||||
.custom_scrollbars(
|
||||
Scrollbars::always_visible(ScrollAxes::Vertical)
|
||||
.tracked_scroll_handle(&self.scroll_handle),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_PER_THREAD: usize = 10_000;
|
||||
|
||||
fn visible_tail(timings: &[SerializedTaskTiming], cutoff_nanos: u128) -> &[SerializedTaskTiming] {
|
||||
let len = timings.len();
|
||||
let limit = len.min(MAX_VISIBLE_PER_THREAD);
|
||||
let search_start = len - limit;
|
||||
let tail = &timings[search_start..];
|
||||
|
||||
let mut first_visible = 0;
|
||||
for (i, timing) in tail.iter().enumerate().rev() {
|
||||
if timing.start + timing.duration < cutoff_nanos {
|
||||
first_visible = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
&tail[first_visible..]
|
||||
}
|
||||
|
||||
fn filter_timings(
|
||||
timings: impl Iterator<Item = SerializedTaskTiming>,
|
||||
include_self: bool,
|
||||
) -> Vec<SerializedTaskTiming> {
|
||||
timings
|
||||
.filter(|t| t.duration / NANOS_PER_MS >= 1)
|
||||
.filter(|t| include_self || !t.location.file.ends_with("miniprofiler_ui.rs"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn location_color_index(location: &SerializedLocation) -> u32 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
location.file.hash(&mut hasher);
|
||||
location.line.hash(&mut hasher);
|
||||
location.column.hash(&mut hasher);
|
||||
hasher.finish() as u32
|
||||
}
|
||||
|
||||
/// Merge K sorted `Vec<SerializedTaskTiming>` into a single sorted vec.
|
||||
/// Each input vec must already be sorted by `start`.
|
||||
fn kway_merge(lists: Vec<Vec<SerializedTaskTiming>>) -> Vec<SerializedTaskTiming> {
|
||||
let total_len: usize = lists.iter().map(|l| l.len()).sum();
|
||||
let mut result = Vec::with_capacity(total_len);
|
||||
let mut cursors = vec![0usize; lists.len()];
|
||||
|
||||
loop {
|
||||
let mut min_start = u128::MAX;
|
||||
let mut min_list = None;
|
||||
|
||||
for (list_idx, list) in lists.iter().enumerate() {
|
||||
let cursor = cursors[list_idx];
|
||||
if let Some(timing) = list.get(cursor) {
|
||||
if timing.start < min_start {
|
||||
min_start = timing.start;
|
||||
min_list = Some(list_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match min_list {
|
||||
Some(idx) => {
|
||||
result.push(lists[idx][cursors[idx]].clone());
|
||||
cursors[idx] += 1;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn append_to_thread(
|
||||
threads: &mut Vec<SerializedThreadTaskTimings>,
|
||||
thread_id: u64,
|
||||
thread_name: Option<String>,
|
||||
new_timings: Vec<SerializedTaskTiming>,
|
||||
) {
|
||||
if let Some(existing) = threads.iter_mut().find(|t| t.thread_id == thread_id) {
|
||||
existing.timings.extend(new_timings);
|
||||
if existing.thread_name.is_none() {
|
||||
existing.thread_name = thread_name;
|
||||
}
|
||||
} else {
|
||||
threads.push(SerializedThreadTaskTimings {
|
||||
thread_name,
|
||||
thread_id,
|
||||
timings: new_timings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,3 +63,31 @@ message AskPassRequest {
|
|||
message AskPassResponse {
|
||||
string response = 1;
|
||||
}
|
||||
|
||||
message GetRemoteProfilingData {
|
||||
uint64 project_id = 1;
|
||||
bool foreground_only = 2;
|
||||
}
|
||||
|
||||
message GetRemoteProfilingDataResponse {
|
||||
repeated RemoteProfilingThread threads = 1;
|
||||
uint64 now_nanos = 2;
|
||||
}
|
||||
|
||||
message RemoteProfilingThread {
|
||||
optional string thread_name = 1;
|
||||
uint64 thread_id = 2;
|
||||
repeated RemoteProfilingTiming timings = 3;
|
||||
}
|
||||
|
||||
message RemoteProfilingTiming {
|
||||
RemoteProfilingLocation location = 1;
|
||||
uint64 start_nanos = 2;
|
||||
uint64 duration_nanos = 3;
|
||||
}
|
||||
|
||||
message RemoteProfilingLocation {
|
||||
string file = 1;
|
||||
uint32 line = 2;
|
||||
uint32 column = 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ message Envelope {
|
|||
FindSearchCandidatesCancelled find_search_candidates_cancelled = 410;
|
||||
GetContextServerCommand get_context_server_command = 411;
|
||||
ContextServerCommand context_server_command = 412;
|
||||
|
||||
|
||||
AllocateWorktreeId allocate_worktree_id = 413;
|
||||
AllocateWorktreeIdResponse allocate_worktree_id_response = 414;
|
||||
|
||||
|
|
@ -469,7 +469,10 @@ message Envelope {
|
|||
SemanticTokensResponse semantic_tokens_response = 419;
|
||||
RefreshSemanticTokens refresh_semantic_tokens = 420;
|
||||
GetFoldingRanges get_folding_ranges = 421;
|
||||
GetFoldingRangesResponse get_folding_ranges_response = 422; // current max
|
||||
GetFoldingRangesResponse get_folding_ranges_response = 422;
|
||||
|
||||
GetRemoteProfilingData get_remote_profiling_data = 423;
|
||||
GetRemoteProfilingDataResponse get_remote_profiling_data_response = 424; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
|
|
|||
|
|
@ -359,6 +359,8 @@ messages!(
|
|||
(GetSharedAgentThreadResponse, Foreground),
|
||||
(FindSearchCandidatesChunk, Background),
|
||||
(FindSearchCandidatesCancelled, Background),
|
||||
(GetRemoteProfilingData, Background),
|
||||
(GetRemoteProfilingDataResponse, Background),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
|
@ -555,6 +557,7 @@ request_messages!(
|
|||
(TrustWorktrees, Ack),
|
||||
(RestrictWorktrees, Ack),
|
||||
(FindSearchCandidatesChunk, Ack),
|
||||
(GetRemoteProfilingData, GetRemoteProfilingDataResponse),
|
||||
);
|
||||
|
||||
lsp_messages!(
|
||||
|
|
@ -741,7 +744,8 @@ entity_messages!(
|
|||
RestrictWorktrees,
|
||||
FindSearchCandidatesChunk,
|
||||
FindSearchCandidatesCancelled,
|
||||
DownloadFileByPath
|
||||
DownloadFileByPath,
|
||||
GetRemoteProfilingData
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
|
|
|||
|
|
@ -562,6 +562,7 @@ mod tests {
|
|||
node_runtime,
|
||||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
@ -643,6 +644,7 @@ mod tests {
|
|||
node_runtime,
|
||||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ use std::{
|
|||
Arc,
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
||||
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
|
||||
|
|
@ -62,6 +63,7 @@ pub struct HeadlessProject {
|
|||
pub extensions: Entity<HeadlessExtensionStore>,
|
||||
pub git_store: Entity<GitStore>,
|
||||
pub environment: Entity<ProjectEnvironment>,
|
||||
pub profiling_collector: gpui::ProfilingCollector,
|
||||
// Used mostly to keep alive the toolchain store for RPC handlers.
|
||||
// Local variant is used within LSP store, but that's a separate entity.
|
||||
pub _toolchain_store: Entity<ToolchainStore>,
|
||||
|
|
@ -74,6 +76,7 @@ pub struct HeadlessAppState {
|
|||
pub node_runtime: NodeRuntime,
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub extension_host_proxy: Arc<ExtensionHostProxy>,
|
||||
pub startup_time: Instant,
|
||||
}
|
||||
|
||||
impl HeadlessProject {
|
||||
|
|
@ -90,6 +93,7 @@ impl HeadlessProject {
|
|||
node_runtime,
|
||||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
startup_time,
|
||||
}: HeadlessAppState,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -286,6 +290,7 @@ impl HeadlessProject {
|
|||
session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server);
|
||||
session.add_request_handler(cx.weak_entity(), Self::handle_ping);
|
||||
session.add_request_handler(cx.weak_entity(), Self::handle_get_processes);
|
||||
session.add_request_handler(cx.weak_entity(), Self::handle_get_remote_profiling_data);
|
||||
|
||||
session.add_entity_request_handler(Self::handle_add_worktree);
|
||||
session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree);
|
||||
|
|
@ -344,6 +349,7 @@ impl HeadlessProject {
|
|||
extensions,
|
||||
git_store,
|
||||
environment,
|
||||
profiling_collector: gpui::ProfilingCollector::new(startup_time),
|
||||
_toolchain_store: toolchain_store,
|
||||
}
|
||||
}
|
||||
|
|
@ -1101,6 +1107,53 @@ impl HeadlessProject {
|
|||
Ok(proto::GetProcessesResponse { processes })
|
||||
}
|
||||
|
||||
async fn handle_get_remote_profiling_data(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GetRemoteProfilingData>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::GetRemoteProfilingDataResponse> {
|
||||
let foreground_only = envelope.payload.foreground_only;
|
||||
|
||||
let (deltas, now_nanos) = cx.update(|cx| {
|
||||
let dispatcher = cx.foreground_executor().dispatcher();
|
||||
let timings = if foreground_only {
|
||||
vec![dispatcher.get_current_thread_timings()]
|
||||
} else {
|
||||
dispatcher.get_all_timings()
|
||||
};
|
||||
this.update(cx, |this, _cx| {
|
||||
let deltas = this.profiling_collector.collect_unseen(timings);
|
||||
let now_nanos = Instant::now()
|
||||
.duration_since(this.profiling_collector.startup_time())
|
||||
.as_nanos() as u64;
|
||||
(deltas, now_nanos)
|
||||
})
|
||||
});
|
||||
|
||||
let threads = deltas
|
||||
.into_iter()
|
||||
.map(|delta| proto::RemoteProfilingThread {
|
||||
thread_name: delta.thread_name,
|
||||
thread_id: delta.thread_id,
|
||||
timings: delta
|
||||
.new_timings
|
||||
.into_iter()
|
||||
.map(|t| proto::RemoteProfilingTiming {
|
||||
location: Some(proto::RemoteProfilingLocation {
|
||||
file: t.location.file.to_string(),
|
||||
line: t.location.line,
|
||||
column: t.location.column,
|
||||
}),
|
||||
start_nanos: t.start as u64,
|
||||
duration_nanos: t.duration as u64,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(proto::GetRemoteProfilingDataResponse { threads, now_nanos })
|
||||
}
|
||||
|
||||
async fn handle_get_directory_environment(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GetDirectoryEnvironment>,
|
||||
|
|
|
|||
|
|
@ -2091,6 +2091,7 @@ pub async fn init_test(
|
|||
node_runtime,
|
||||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
startup_time: std::time::Instant::now(),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Instant,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt, command::new_command};
|
||||
|
|
@ -447,6 +448,7 @@ pub fn execute_run(
|
|||
) -> Result<()> {
|
||||
init_paths()?;
|
||||
|
||||
let startup_time = Instant::now();
|
||||
let app = gpui::Application::headless();
|
||||
let pid = std::process::id();
|
||||
let id = pid.to_string();
|
||||
|
|
@ -567,6 +569,7 @@ pub fn execute_run(
|
|||
node_runtime,
|
||||
languages,
|
||||
extension_host_proxy,
|
||||
startup_time,
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ impl ForegroundExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Timer {
|
||||
self.scheduler.timer(duration)
|
||||
}
|
||||
|
|
@ -211,6 +212,7 @@ impl BackgroundExecutor {
|
|||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn timer(&self, duration: Duration) -> Timer {
|
||||
self.scheduler.timer(duration)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ pub trait Scheduler: Send + Sync {
|
|||
self.schedule_background_with_priority(runnable, Priority::default());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn timer(&self, timeout: Duration) -> Timer;
|
||||
fn clock(&self) -> Arc<dyn Clock>;
|
||||
|
||||
|
|
|
|||
|
|
@ -614,6 +614,7 @@ impl Scheduler for TestScheduler {
|
|||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn timer(&self, duration: Duration) -> Timer {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let state = &mut *self.state.lock();
|
||||
|
|
|
|||
|
|
@ -395,6 +395,10 @@ impl Scrollbars {
|
|||
Self::new_with_setting(show_along, |_| ShowScrollbar::default())
|
||||
}
|
||||
|
||||
pub fn always_visible(show_along: ScrollAxes) -> Self {
|
||||
Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
|
||||
}
|
||||
|
||||
pub fn for_settings<S: ScrollbarVisibility>() -> Scrollbars {
|
||||
Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::get_value(cx).visibility(cx))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue