diff --git a/Cargo.lock b/Cargo.lock index 535833d9e2a9..b4754df16d09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,22 @@ dependencies = [ "syn", ] +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1383,6 +1399,12 @@ dependencies = [ "adler2", ] +[[package]] +name = "mintex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" + [[package]] name = "mio" version = "1.1.0" @@ -1452,7 +1474,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2011,6 +2033,7 @@ dependencies = [ "cargo_metadata 0.21.0", "cfg", "crossbeam-channel", + "dhat", "dirs", "dissimilar", "expect-test", @@ -2528,6 +2551,12 @@ dependencies = [ "syn", ] +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "thread_local" version = "1.1.9" diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml index c746f848b6a0..5fdab458bb8e 100644 --- a/crates/rust-analyzer/Cargo.toml +++ b/crates/rust-analyzer/Cargo.toml @@ -53,6 +53,7 @@ semver.workspace = true memchr = "2.7.5" cargo_metadata.workspace = true process-wrap.workspace = true +dhat = { version = "0.3.3", optional = true } cfg.workspace = true hir-def.workspace = true @@ -105,6 +106,7 @@ in-rust-tree = [ "hir-ty/in-rust-tree", "load-cargo/in-rust-tree", ] +dhat = ["dep:dhat"] [lints] workspace = true diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 6d2907ee56aa..75eacdbab355 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -378,6 +378,12 @@ config_data! { /// Internal config, path to proc-macro server executable. procMacro_server: Option = None, + /// The path where to save memory profiling output. + /// + /// **Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build + /// from source for it. + profiling_memoryProfile: Option = None, + /// Exclude imports from find-all-references. references_excludeImports: bool = false, @@ -2165,6 +2171,11 @@ impl Config { Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path))) } + pub fn dhat_output_file(&self) -> Option { + let path = self.profiling_memoryProfile().clone()?; + Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path))) + } + pub fn ignored_proc_macros( &self, source_root: Option, diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 66a7a0b825f1..528ec70cb6e9 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -126,17 +126,35 @@ pub(crate) fn handle_analyzer_status( Ok(buf) } -pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> anyhow::Result { +pub(crate) fn handle_memory_usage(_state: &mut GlobalState, _: ()) -> anyhow::Result { let _p = tracing::info_span!("handle_memory_usage").entered(); - let mem = state.analysis_host.per_query_memory_usage(); - let mut out = String::new(); - for (name, bytes, entries) in mem { - format_to!(out, "{:>8} {:>6} {}\n", bytes, entries, name); + #[cfg(not(feature = "dhat"))] + { + Err(anyhow::anyhow!( + "Memory profiling is not enabled for this build of rust-analyzer.\n\n\ + To build rust-analyzer with profiling support, pass `--features dhat --profile dev-rel` to `cargo build` + when building from source, or pass `--enable-profiling` to `cargo xtask`." + )) + } + #[cfg(feature = "dhat")] + { + if let Some(dhat_output_file) = _state.config.dhat_output_file() { + let mutprofiler = crate::DHAT_PROFILER.lock().unwrap(); + let old_profiler = profiler.take(); + // Need to drop the old profiler before creating a new one. + drop(old_profiler); + *profiler = Some(dhat::Profiler::builder().file_name(&dhat_output_file).build()); + Ok(format!( + "Memory profile was saved successfully to {dhat_output_file}.\n\n\ + See https://docs.rs/dhat/latest/dhat/#viewing for how to inspect the profile." + )) + } else { + Err(anyhow::anyhow!( + "Please set `rust-analyzer.profiling.memoryProfile` to the path where you want to save the profile." + )) + } } - format_to!(out, "{:>8} Remaining\n", profile::memory_usage().allocated); - - Ok(out) } pub(crate) fn handle_view_syntax_tree( diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 44af8fbddf30..6ae527abb1ff 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -82,3 +82,10 @@ macro_rules! try_default_ { }; } pub(crate) use try_default_ as try_default; + +#[cfg(feature = "dhat")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[cfg(feature = "dhat")] +static DHAT_PROFILER: std::sync::Mutex> = std::sync::Mutex::new(None); diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index c0947b2a291e..f57e0fe15313 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -60,6 +60,14 @@ pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> { SetThreadPriority(thread, thread_priority_above_normal); } + #[cfg(feature = "dhat")] + { + if let Some(dhat_output_file) = config.dhat_output_file() { + *crate::DHAT_PROFILER.lock().unwrap() = + Some(dhat::Profiler::builder().file_name(&dhat_output_file).build()); + } + } + GlobalState::new(connection.sender, config).run(connection.receiver) } diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md index d768993f501f..4f456555a2e6 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -1289,6 +1289,16 @@ Default: `null` Internal config, path to proc-macro server executable. +## rust-analyzer.profiling.memoryProfile {#profiling.memoryProfile} + +Default: `null` + +The path where to save memory profiling output. + +**Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build +from source for it. + + ## rust-analyzer.references.excludeImports {#references.excludeImports} Default: `false` diff --git a/editors/code/package.json b/editors/code/package.json index d659421a0299..099397eafa97 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -2749,6 +2749,19 @@ } } }, + { + "title": "Profiling", + "properties": { + "rust-analyzer.profiling.memoryProfile": { + "markdownDescription": "The path where to save memory profiling output.\n\n**Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build\nfrom source for it.", + "default": null, + "type": [ + "null", + "string" + ] + } + } + }, { "title": "References", "properties": { diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 25b30013fa1c..16fc586d5df0 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -71,32 +71,9 @@ export function analyzerStatus(ctx: CtxInit): Cmd { } export function memoryUsage(ctx: CtxInit): Cmd { - const tdcp = new (class implements vscode.TextDocumentContentProvider { - readonly uri = vscode.Uri.parse("rust-analyzer-memory://memory"); - readonly eventEmitter = new vscode.EventEmitter(); - - provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult { - if (!vscode.window.activeTextEditor) return ""; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => { - return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)"; - }); - } - - get onDidChange(): vscode.Event { - return this.eventEmitter.event; - } - })(); - - ctx.pushExtCleanup( - vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp), - ); - return async () => { - tdcp.eventEmitter.fire(tdcp.uri); - const document = await vscode.workspace.openTextDocument(tdcp.uri); - return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true); + const response = await ctx.client.sendRequest(ra.memoryUsage); + vscode.window.showInformationMessage(response); }; } diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts index e3f43a80670a..a469a9cd1f45 100644 --- a/editors/code/src/snippets.ts +++ b/editors/code/src/snippets.ts @@ -24,7 +24,9 @@ export async function applySnippetWorkspaceEdit( for (const indel of edits) { assert( !(indel instanceof vscode.SnippetTextEdit), - `bad ws edit: snippet received with multiple edits: ${JSON.stringify(edit)}`, + `bad ws edit: snippet received with multiple edits: ${JSON.stringify( + edit, + )}`, ); builder.replace(indel.range, indel.newText); } diff --git a/xtask/src/dist.rs b/xtask/src/dist.rs index dbfecdbe1121..1b1fb532cae9 100644 --- a/xtask/src/dist.rs +++ b/xtask/src/dist.rs @@ -45,11 +45,22 @@ impl flags::Dist { allocator, self.zig, self.pgo, + // Profiling requires debug information. + self.enable_profiling, )?; let release_tag = if stable { date_iso(sh)? } else { "nightly".to_owned() }; dist_client(sh, &version, &release_tag, &target)?; } else { - dist_server(sh, "0.0.0-standalone", &target, allocator, self.zig, self.pgo)?; + dist_server( + sh, + "0.0.0-standalone", + &target, + allocator, + self.zig, + self.pgo, + // Profiling requires debug information. + self.enable_profiling, + )?; } Ok(()) } @@ -92,9 +103,11 @@ fn dist_server( allocator: Malloc, zig: bool, pgo: Option, + dev_rel: bool, ) -> anyhow::Result<()> { let _e = sh.push_env("CFG_RELEASE", release); let _e = sh.push_env("CARGO_PROFILE_RELEASE_LTO", "thin"); + let _e = sh.push_env("CARGO_PROFILE_DEV_REL_LTO", "thin"); // Uncomment to enable debug info for releases. Note that: // * debug info is split on windows and macs, so it does nothing for those platforms, @@ -120,7 +133,7 @@ fn dist_server( None }; - let mut cmd = build_command(sh, command, &target_name, features); + let mut cmd = build_command(sh, command, &target_name, features, dev_rel); if let Some(profile) = pgo_profile { cmd = cmd.env("RUSTFLAGS", format!("-Cprofile-use={}", profile.to_str().unwrap())); } @@ -141,10 +154,12 @@ fn build_command<'a>( command: &str, target_name: &str, features: &[&str], + dev_rel: bool, ) -> Cmd<'a> { + let profile = if dev_rel { "dev-rel" } else { "release" }; cmd!( sh, - "cargo {command} --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target_name} {features...} --release" + "cargo {command} --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target_name} {features...} --profile {profile}" ) } diff --git a/xtask/src/flags.rs b/xtask/src/flags.rs index 8f70a1861893..e72d8f22e4f0 100644 --- a/xtask/src/flags.rs +++ b/xtask/src/flags.rs @@ -42,6 +42,10 @@ xflags::xflags! { optional --mimalloc /// Use jemalloc allocator for server. optional --jemalloc + // Enable memory profiling support. + // + // **Warning:** This will produce a slower build of rust-analyzer, use only for profiling. + optional --enable-profiling /// Install the proc-macro server. optional --proc-macro-server @@ -67,6 +71,10 @@ xflags::xflags! { optional --mimalloc /// Use jemalloc allocator for server optional --jemalloc + // Enable memory profiling support. + // + // **Warning:** This will produce a slower build of rust-analyzer, use only for profiling. + optional --enable-profiling optional --client-patch-version version: String /// Use cargo-zigbuild optional --zig @@ -125,6 +133,7 @@ pub struct Install { pub server: bool, pub mimalloc: bool, pub jemalloc: bool, + pub enable_profiling: bool, pub proc_macro_server: bool, pub dev_rel: bool, pub force_always_assert: bool, @@ -143,6 +152,7 @@ pub struct Release { pub struct Dist { pub mimalloc: bool, pub jemalloc: bool, + pub enable_profiling: bool, pub client_patch_version: Option, pub zig: bool, pub pgo: Option, @@ -280,6 +290,7 @@ pub(crate) enum Malloc { System, Mimalloc, Jemalloc, + Dhat, } impl Malloc { @@ -288,6 +299,7 @@ impl Malloc { Malloc::System => &[][..], Malloc::Mimalloc => &["--features", "mimalloc"], Malloc::Jemalloc => &["--features", "jemalloc"], + Malloc::Dhat => &["--features", "dhat"], } } } @@ -301,12 +313,15 @@ impl Install { Malloc::Mimalloc } else if self.jemalloc { Malloc::Jemalloc + } else if self.enable_profiling { + Malloc::Dhat } else { Malloc::System }; Some(ServerOpt { malloc, - dev_rel: self.dev_rel, + // Profiling requires debug information. + dev_rel: self.dev_rel || self.enable_profiling, pgo: self.pgo.clone(), force_always_assert: self.force_always_assert, }) @@ -331,6 +346,8 @@ impl Dist { Malloc::Mimalloc } else if self.jemalloc { Malloc::Jemalloc + } else if self.enable_profiling { + Malloc::Dhat } else { Malloc::System }