Skip to content

Commit dab5725

Browse files
Support memory profiling with dhat
Unfortunately, this requires a custom build of r-a, and it's quite slow.
1 parent 7c810e9 commit dab5725

File tree

11 files changed

+138
-39
lines changed

11 files changed

+138
-39
lines changed

Cargo.lock

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

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>,

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(

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);

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

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": {

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

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
}

xtask/src/dist.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,22 @@ impl flags::Dist {
4545
allocator,
4646
self.zig,
4747
self.pgo,
48+
// Profiling requires debug information.
49+
self.enable_profiling,
4850
)?;
4951
let release_tag = if stable { date_iso(sh)? } else { "nightly".to_owned() };
5052
dist_client(sh, &version, &release_tag, &target)?;
5153
} else {
52-
dist_server(sh, "0.0.0-standalone", &target, allocator, self.zig, self.pgo)?;
54+
dist_server(
55+
sh,
56+
"0.0.0-standalone",
57+
&target,
58+
allocator,
59+
self.zig,
60+
self.pgo,
61+
// Profiling requires debug information.
62+
self.enable_profiling,
63+
)?;
5364
}
5465
Ok(())
5566
}
@@ -92,9 +103,11 @@ fn dist_server(
92103
allocator: Malloc,
93104
zig: bool,
94105
pgo: Option<PgoTrainingCrate>,
106+
dev_rel: bool,
95107
) -> anyhow::Result<()> {
96108
let _e = sh.push_env("CFG_RELEASE", release);
97109
let _e = sh.push_env("CARGO_PROFILE_RELEASE_LTO", "thin");
110+
let _e = sh.push_env("CARGO_PROFILE_DEV_REL_LTO", "thin");
98111

99112
// Uncomment to enable debug info for releases. Note that:
100113
// * debug info is split on windows and macs, so it does nothing for those platforms,
@@ -120,7 +133,7 @@ fn dist_server(
120133
None
121134
};
122135

123-
let mut cmd = build_command(sh, command, &target_name, features);
136+
let mut cmd = build_command(sh, command, &target_name, features, dev_rel);
124137
if let Some(profile) = pgo_profile {
125138
cmd = cmd.env("RUSTFLAGS", format!("-Cprofile-use={}", profile.to_str().unwrap()));
126139
}
@@ -141,10 +154,12 @@ fn build_command<'a>(
141154
command: &str,
142155
target_name: &str,
143156
features: &[&str],
157+
dev_rel: bool,
144158
) -> Cmd<'a> {
159+
let profile = if dev_rel { "dev-rel" } else { "release" };
145160
cmd!(
146161
sh,
147-
"cargo {command} --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target_name} {features...} --release"
162+
"cargo {command} --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target_name} {features...} --profile {profile}"
148163
)
149164
}
150165

0 commit comments

Comments
 (0)