Skip to content

Commit 4d338cb

Browse files
Support memory profiling with dhat
Unfortunately, this requires a custom build of r-a, and it's quite slow.
1 parent 0e60684 commit 4d338cb

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",
@@ -2528,6 +2551,12 @@ dependencies = [
25282551
"syn",
25292552
]
25302553

2554+
[[package]]
2555+
name = "thousands"
2556+
version = "0.2.0"
2557+
source = "registry+https://github.com/rust-lang/crates.io-index"
2558+
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
2559+
25312560
[[package]]
25322561
name = "thread_local"
25332562
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
@@ -53,6 +53,7 @@ semver.workspace = true
5353
memchr = "2.7.5"
5454
cargo_metadata.workspace = true
5555
process-wrap.workspace = true
56+
dhat = { version = "0.3.3", optional = true }
5657

5758
cfg.workspace = true
5859
hir-def.workspace = true
@@ -105,6 +106,7 @@ in-rust-tree = [
105106
"hir-ty/in-rust-tree",
106107
"load-cargo/in-rust-tree",
107108
]
109+
dhat = ["dep:dhat"]
108110

109111
[lints]
110112
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
@@ -378,6 +378,12 @@ config_data! {
378378
/// Internal config, path to proc-macro server executable.
379379
procMacro_server: Option<Utf8PathBuf> = None,
380380

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

@@ -2165,6 +2171,11 @@ impl Config {
21652171
Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path)))
21662172
}
21672173

2174+
pub fn dhat_output_file(&self) -> Option<AbsPathBuf> {
2175+
let path = self.profiling_memoryProfile().clone()?;
2176+
Some(AbsPathBuf::try_from(path).unwrap_or_else(|path| self.root_path.join(path)))
2177+
}
2178+
21682179
pub fn ignored_proc_macros(
21692180
&self,
21702181
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
@@ -1289,6 +1289,16 @@ Default: `null`
12891289
Internal config, path to proc-macro server executable.
12901290

12911291

1292+
## rust-analyzer.profiling.memoryProfile {#profiling.memoryProfile}
1293+
1294+
Default: `null`
1295+
1296+
The path where to save memory profiling output.
1297+
1298+
**Note:** Memory profiling is not enabled by default in rust-analyzer builds, you need to build
1299+
from source for it.
1300+
1301+
12921302
## rust-analyzer.references.excludeImports {#references.excludeImports}
12931303

12941304
Default: `false`

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,19 @@
27492749
}
27502750
}
27512751
},
2752+
{
2753+
"title": "Profiling",
2754+
"properties": {
2755+
"rust-analyzer.profiling.memoryProfile": {
2756+
"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.",
2757+
"default": null,
2758+
"type": [
2759+
"null",
2760+
"string"
2761+
]
2762+
}
2763+
}
2764+
},
27522765
{
27532766
"title": "References",
27542767
"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)