Skip to content

Commit 7a33724

Browse files
authored
Merge pull request #20927 from ChayimFriedman2/dhat
feat: Support memory profiling with dhat
2 parents be6574b + 4d338cb commit 7a33724

File tree

12 files changed

+148
-39
lines changed

12 files changed

+148
-39
lines changed

src/tools/rust-analyzer/Cargo.lock

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,22 @@ dependencies = [
418418
"syn",
419419
]
420420

421+
[[package]]
422+
name = "dhat"
423+
version = "0.3.3"
424+
source = "registry+https://github.com/rust-lang/crates.io-index"
425+
checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827"
426+
dependencies = [
427+
"backtrace",
428+
"lazy_static",
429+
"mintex",
430+
"parking_lot",
431+
"rustc-hash 1.1.0",
432+
"serde",
433+
"serde_json",
434+
"thousands",
435+
]
436+
421437
[[package]]
422438
name = "dirs"
423439
version = "6.0.0"
@@ -1383,6 +1399,12 @@ dependencies = [
13831399
"adler2",
13841400
]
13851401

1402+
[[package]]
1403+
name = "mintex"
1404+
version = "0.1.4"
1405+
source = "registry+https://github.com/rust-lang/crates.io-index"
1406+
checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536"
1407+
13861408
[[package]]
13871409
name = "mio"
13881410
version = "1.1.0"
@@ -1452,7 +1474,7 @@ version = "0.50.3"
14521474
source = "registry+https://github.com/rust-lang/crates.io-index"
14531475
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
14541476
dependencies = [
1455-
"windows-sys 0.60.2",
1477+
"windows-sys 0.61.0",
14561478
]
14571479

14581480
[[package]]
@@ -2011,6 +2033,7 @@ dependencies = [
20112033
"cargo_metadata 0.21.0",
20122034
"cfg",
20132035
"crossbeam-channel",
2036+
"dhat",
20142037
"dirs",
20152038
"dissimilar",
20162039
"expect-test",
@@ -2529,6 +2552,12 @@ dependencies = [
25292552
"syn",
25302553
]
25312554

2555+
[[package]]
2556+
name = "thousands"
2557+
version = "0.2.0"
2558+
source = "registry+https://github.com/rust-lang/crates.io-index"
2559+
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
2560+
25322561
[[package]]
25332562
name = "thread_local"
25342563
version = "1.1.9"

src/tools/rust-analyzer/crates/rust-analyzer/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ semver.workspace = true
5454
memchr = "2.7.5"
5555
cargo_metadata.workspace = true
5656
process-wrap.workspace = true
57+
dhat = { version = "0.3.3", optional = true }
5758

5859
cfg.workspace = true
5960
hir-def.workspace = true
@@ -106,6 +107,7 @@ in-rust-tree = [
106107
"hir-ty/in-rust-tree",
107108
"load-cargo/in-rust-tree",
108109
]
110+
dhat = ["dep:dhat"]
109111

110112
[lints]
111113
workspace = true

src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,12 @@ config_data! {
381381
/// Internal config, path to proc-macro server executable.
382382
procMacro_server: Option<Utf8PathBuf> = None,
383383

384+
/// The path where to save memory profiling output.
385+
///
386+
/// **Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build
387+
/// from source for it.
388+
profiling_memoryProfile: Option<Utf8PathBuf> = None,
389+
384390
/// Exclude imports from find-all-references.
385391
references_excludeImports: bool = false,
386392

@@ -2170,6 +2176,11 @@ impl Config {
21702176
Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path)))
21712177
}
21722178

2179+
pub fn dhat_output_file(&self) -> Option<AbsPathBuf> {
2180+
let path = self.profiling_memoryProfile().clone()?;
2181+
Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path)))
2182+
}
2183+
21732184
pub fn ignored_proc_macros(
21742185
&self,
21752186
source_root: Option<SourceRootId>,

src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,35 @@ pub(crate) fn handle_analyzer_status(
126126
Ok(buf)
127127
}
128128

129-
pub(crate) fn handle_memory_usage(state: &mut GlobalState, _: ()) -> anyhow::Result<String> {
129+
pub(crate) fn handle_memory_usage(_state: &mut GlobalState, _: ()) -> anyhow::Result<String> {
130130
let _p = tracing::info_span!("handle_memory_usage").entered();
131-
let mem = state.analysis_host.per_query_memory_usage();
132131

133-
let mut out = String::new();
134-
for (name, bytes, entries) in mem {
135-
format_to!(out, "{:>8} {:>6} {}\n", bytes, entries, name);
132+
#[cfg(not(feature = "dhat"))]
133+
{
134+
Err(anyhow::anyhow!(
135+
"Memory profiling is not enabled for this build of rust-analyzer.\n\n\
136+
To build rust-analyzer with profiling support, pass `--features dhat --profile dev-rel` to `cargo build`
137+
when building from source, or pass `--enable-profiling` to `cargo xtask`."
138+
))
139+
}
140+
#[cfg(feature = "dhat")]
141+
{
142+
if let Some(dhat_output_file) = _state.config.dhat_output_file() {
143+
let mutprofiler = crate::DHAT_PROFILER.lock().unwrap();
144+
let old_profiler = profiler.take();
145+
// Need to drop the old profiler before creating a new one.
146+
drop(old_profiler);
147+
*profiler = Some(dhat::Profiler::builder().file_name(&dhat_output_file).build());
148+
Ok(format!(
149+
"Memory profile was saved successfully to {dhat_output_file}.\n\n\
150+
See https://docs.rs/dhat/latest/dhat/#viewing for how to inspect the profile."
151+
))
152+
} else {
153+
Err(anyhow::anyhow!(
154+
"Please set `rust-analyzer.profiling.memoryProfile` to the path where you want to save the profile."
155+
))
156+
}
136157
}
137-
format_to!(out, "{:>8} Remaining\n", profile::memory_usage().allocated);
138-
139-
Ok(out)
140158
}
141159

142160
pub(crate) fn handle_view_syntax_tree(

src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,10 @@ macro_rules! try_default_ {
8282
};
8383
}
8484
pub(crate) use try_default_ as try_default;
85+
86+
#[cfg(feature = "dhat")]
87+
#[global_allocator]
88+
static ALLOC: dhat::Alloc = dhat::Alloc;
89+
90+
#[cfg(feature = "dhat")]
91+
static DHAT_PROFILER: std::sync::Mutex<Option<dhat::Profiler>> = std::sync::Mutex::new(None);

src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> {
6060
SetThreadPriority(thread, thread_priority_above_normal);
6161
}
6262

63+
#[cfg(feature = "dhat")]
64+
{
65+
if let Some(dhat_output_file) = config.dhat_output_file() {
66+
*crate::DHAT_PROFILER.lock().unwrap() =
67+
Some(dhat::Profiler::builder().file_name(&dhat_output_file).build());
68+
}
69+
}
70+
6371
GlobalState::new(connection.sender, config).run(connection.receiver)
6472
}
6573

src/tools/rust-analyzer/docs/book/src/configuration_generated.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,16 @@ Default: `null`
12961296
Internal config, path to proc-macro server executable.
12971297

12981298

1299+
## rust-analyzer.profiling.memoryProfile {#profiling.memoryProfile}
1300+
1301+
Default: `null`
1302+
1303+
The path where to save memory profiling output.
1304+
1305+
**Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build
1306+
from source for it.
1307+
1308+
12991309
## rust-analyzer.references.excludeImports {#references.excludeImports}
13001310

13011311
Default: `false`

src/tools/rust-analyzer/editors/code/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2759,6 +2759,19 @@
27592759
}
27602760
}
27612761
},
2762+
{
2763+
"title": "Profiling",
2764+
"properties": {
2765+
"rust-analyzer.profiling.memoryProfile": {
2766+
"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.",
2767+
"default": null,
2768+
"type": [
2769+
"null",
2770+
"string"
2771+
]
2772+
}
2773+
}
2774+
},
27622775
{
27632776
"title": "References",
27642777
"properties": {

src/tools/rust-analyzer/editors/code/src/commands.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,9 @@ export function analyzerStatus(ctx: CtxInit): Cmd {
7171
}
7272

7373
export function memoryUsage(ctx: CtxInit): Cmd {
74-
const tdcp = new (class implements vscode.TextDocumentContentProvider {
75-
readonly uri = vscode.Uri.parse("rust-analyzer-memory://memory");
76-
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
77-
78-
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
79-
if (!vscode.window.activeTextEditor) return "";
80-
81-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
82-
return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => {
83-
return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)";
84-
});
85-
}
86-
87-
get onDidChange(): vscode.Event<vscode.Uri> {
88-
return this.eventEmitter.event;
89-
}
90-
})();
91-
92-
ctx.pushExtCleanup(
93-
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp),
94-
);
95-
9674
return async () => {
97-
tdcp.eventEmitter.fire(tdcp.uri);
98-
const document = await vscode.workspace.openTextDocument(tdcp.uri);
99-
return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
75+
const response = await ctx.client.sendRequest(ra.memoryUsage);
76+
vscode.window.showInformationMessage(response);
10077
};
10178
}
10279

src/tools/rust-analyzer/editors/code/src/snippets.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export async function applySnippetWorkspaceEdit(
2424
for (const indel of edits) {
2525
assert(
2626
!(indel instanceof vscode.SnippetTextEdit),
27-
`bad ws edit: snippet received with multiple edits: ${JSON.stringify(edit)}`,
27+
`bad ws edit: snippet received with multiple edits: ${JSON.stringify(
28+
edit,
29+
)}`,
2830
);
2931
builder.replace(indel.range, indel.newText);
3032
}

0 commit comments

Comments
 (0)