Skip to content

Commit 5932417

Browse files
1vipulguptaVipul Gupta
andauthored
Added support for rustc self-profiling (bazelbuild#4073)
Introduced a new build setting, `--//rust/settings:zself_profile_events`, to enable rustc's `-Zself-profile` and `-Zself-profile-events` flag. Rust `nightly` toolchain is required for using this feature. The setting accepts patterns to match against crate labels or execution paths, allowing fine-grained control over which crates are profiled. Profiling data is output to a `<crate_name>_self-profile` directory. Co-authored-by: Vipul Gupta <ivip@google.com>
1 parent 97e0d25 commit 5932417

10 files changed

Lines changed: 206 additions & 16 deletions

File tree

rust/private/rust.bzl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ load(
3232
)
3333
load(
3434
":rustc.bzl",
35+
"UnstableSelfProfileInfo",
3536
"collect_extra_rustc_flags",
3637
"is_no_std",
3738
"rustc_compile_action",
@@ -853,6 +854,11 @@ _COMMON_ATTRS = {
853854
doc = "A version to inject in the cargo environment variable.",
854855
default = "0.0.0",
855856
),
857+
"zself_profile_events": attr.label(
858+
doc = "Passes -Zself-profile and -Zself-profile-events flag to rustc, requires a nightly toolchain.",
859+
providers = [UnstableSelfProfileInfo],
860+
default = None,
861+
),
856862
"_collect_cfgs": attr.label(
857863
doc = "Enable collection of cfg flags with results stored in CrateInfo.cfgs.",
858864
default = Label("//rust/settings:collect_cfgs"),

rust/private/rustc.bzl

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ load(
5353
"is_exec_configuration",
5454
"is_std_dylib",
5555
"make_static_lib_symlink",
56+
"matches_prefix_filter",
5657
"parse_env_strings",
5758
"relativize",
5859
)
@@ -105,6 +106,19 @@ PerCrateRustcFlagsInfo = provider(
105106
fields = {"per_crate_rustc_flags": "List[string] Extra flags to pass to rustc in non-exec configuration"},
106107
)
107108

109+
UnstableSelfProfileInfo = provider(
110+
doc = "Passes -Zself-profile and -Zself-profile-events flags to matching rust crates.",
111+
fields = {
112+
"events": (
113+
"List[tuple[str, str]]: A list of `(pattern, event_types)` pairs. The `pattern` " +
114+
"matches against a target's label (with leading `@//` stripped) or " +
115+
"its execution path (an empty `pattern` matches all targets). The `event_types` " +
116+
"specifies comma-separated categories of self-profile events to pass to " +
117+
"`-Zself-profile-events` (e.g., `all`)."
118+
),
119+
},
120+
)
121+
108122
def _get_rustc_env(attr, toolchain, crate_name):
109123
"""Gathers rustc environment variables
110124
@@ -1455,6 +1469,45 @@ def collect_extra_rustc_flags(ctx, toolchain, crate_root, crate_type):
14551469

14561470
return flags
14571471

1472+
def setup_zself_profile(ctx, crate_info):
1473+
"""Sets up rustc self-profiling if enabled by zself_profile_events.
1474+
1475+
Args:
1476+
ctx (ctx): The current rule's context object.
1477+
crate_info (CrateInfo): The CrateInfo provider of the target crate.
1478+
1479+
Returns:
1480+
tuple: A tuple containing:
1481+
- File: The declared self-profile directory, or None if disabled.
1482+
- list[str]: The self-profile flags to pass to rustc.
1483+
"""
1484+
if not getattr(ctx.attr, "zself_profile_events", None) or UnstableSelfProfileInfo not in ctx.attr.zself_profile_events:
1485+
return None, []
1486+
1487+
events_info = ctx.attr.zself_profile_events[UnstableSelfProfileInfo].events
1488+
1489+
is_self_profile_enabled = False
1490+
event_types_to_use = None
1491+
1492+
# Check if the current crate matches any of the specified prefix filters.
1493+
# Matching works by comparing against the target's label or its execution path.
1494+
for pattern, event_types in events_info:
1495+
if matches_prefix_filter(ctx.label, crate_info.root.path, pattern):
1496+
is_self_profile_enabled = True
1497+
event_types_to_use = event_types
1498+
break
1499+
1500+
if not is_self_profile_enabled:
1501+
return None, []
1502+
1503+
profiling_dir = ctx.actions.declare_directory(crate_info.output.basename + "_self-profile", sibling = crate_info.output)
1504+
1505+
profiling_flags = ["-Zself-profile=%s" % profiling_dir.path]
1506+
if event_types_to_use:
1507+
profiling_flags.append("-Zself-profile-events=%s" % event_types_to_use)
1508+
1509+
return profiling_dir, profiling_flags
1510+
14581511
def rustc_compile_action(
14591512
*,
14601513
ctx,
@@ -1542,6 +1595,9 @@ def rustc_compile_action(
15421595
rust_flags = rust_flags + ctx.attr.lint_config[LintsInfo].rustc_lint_flags
15431596
lint_files = lint_files + ctx.attr.lint_config[LintsInfo].rustc_lint_files
15441597

1598+
profiling_dir, profiling_flags = setup_zself_profile(ctx, crate_info)
1599+
rust_flags = rust_flags + profiling_flags
1600+
15451601
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, ambiguous_libs = collect_inputs(
15461602
ctx = ctx,
15471603
file = ctx.file,
@@ -1676,6 +1732,8 @@ def rustc_compile_action(
16761732
action_outputs = list(outputs)
16771733
if rustc_output:
16781734
action_outputs.append(rustc_output)
1735+
if profiling_dir:
1736+
action_outputs.append(profiling_dir)
16791737

16801738
# Get the compilation mode for the current target.
16811739
compilation_mode = get_compilation_mode_opts(ctx, toolchain)
@@ -1950,7 +2008,8 @@ def rustc_compile_action(
19502008
output_group_info["rustc_rmeta_output"] = depset([rustc_rmeta_output])
19512009
if rustc_output:
19522010
output_group_info["rustc_output"] = depset([rustc_output])
1953-
2011+
if profiling_dir:
2012+
output_group_info["self_profile"] = depset([profiling_dir])
19542013
if output_group_info:
19552014
providers.append(OutputGroupInfo(**output_group_info))
19562015

@@ -2737,19 +2796,7 @@ def _collect_per_crate_rustc_flags(ctx, crate_root, per_crate_rustc_flags):
27372796
if not flag:
27382797
fail("per_crate_rustc_flag '{}' does not follow the expected format: prefix_filter@flag".format(per_crate_rustc_flag))
27392798

2740-
label_string = str(ctx.label)
2741-
if label_string.startswith("@//"):
2742-
label = label_string[1:]
2743-
elif label_string.startswith(
2744-
# buildifier: disable=canonical-repository
2745-
"@@//",
2746-
):
2747-
label = label_string[2:]
2748-
else:
2749-
label = label_string
2750-
execution_path = crate_root.path
2751-
2752-
if label.startswith(prefix_filter) or execution_path.startswith(prefix_filter):
2799+
if matches_prefix_filter(ctx.label, crate_root.path, prefix_filter):
27532800
flags.append(flag)
27542801

27552802
return flags
@@ -2946,3 +2993,38 @@ no_std = rule(
29462993
},
29472994
implementation = _no_std_impl,
29482995
)
2996+
2997+
def _zself_profile_events_impl(ctx):
2998+
events = []
2999+
for val in ctx.build_setting_value:
3000+
if not val:
3001+
continue
3002+
if "@" in val:
3003+
pattern, event_types = val.split("@", 1)
3004+
events.append((pattern, event_types))
3005+
else:
3006+
fail("zself_profile_events '{}' does not follow the expected format: prefix_filter@comma_separated_flag".format(val))
3007+
return [UnstableSelfProfileInfo(events = events)]
3008+
3009+
zself_profile_events = rule(
3010+
doc = (
3011+
"Passes -Zself-profile and -Zself-profile-events flags to matching Rust crates." +
3012+
"This feature allows end-users to profile rustc compiler performance on specific crates " +
3013+
"using rustc's self-profiler. Because these flags are unstable, using them requires a " +
3014+
"nightly compiler toolchain. The setting is configured from the command line via " +
3015+
"`--@rules_rust//rust/settings:zself_profile_events`." +
3016+
"The expected value format is `<prefix_filter>@<events_specification>`. Multiple uses of " +
3017+
"this flag are accumulated, however only first <events_specification> will be applied for same" +
3018+
"<prefix_filter>." +
3019+
"If the target prefix matches with <prefix_filter>, `-Zself-profile` and `-Zself-profile-events` " +
3020+
"with values as `<crate_name>.self-profile/` and <events_specification> respectively " +
3021+
"is passed to rustc compiler. The generated profile files (e.g., `.mm_profdata`) are placed" +
3022+
"under `bazel-out/bin/path/to/package/crate_name_self-profile/` which can be seen by passing" +
3023+
" `--output_groups=self_profile` flag." +
3024+
"blaze build //my/project:my_lib \\" +
3025+
"--@rules_rust//rust/settings:zself_profile_events=//my/project@all \\" +
3026+
"--output_groups=self_profile"
3027+
),
3028+
implementation = _zself_profile_events_impl,
3029+
build_setting = config.string_list(flag = True, repeatable = True),
3030+
)

rust/private/utils.bzl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,3 +978,32 @@ def is_std_dylib(file):
978978
# for windows
979979
basename.startswith("std-") and basename.endswith(".dll")
980980
)
981+
982+
def matches_prefix_filter(label, crate_root_path, pattern):
983+
"""Determines if a target matches a prefix filter pattern.
984+
985+
Matching works by comparing against the target's label or its execution path.
986+
987+
Args:
988+
label (Label): The target's label.
989+
crate_root_path (str): The target's execution path (e.g. crate root path).
990+
pattern (str): The prefix pattern to match against.
991+
992+
Returns:
993+
bool: True if the target matches the pattern, False otherwise.
994+
"""
995+
if not pattern:
996+
return True
997+
998+
label_string = str(label)
999+
if label_string.startswith("@//"):
1000+
target_label = label_string[1:]
1001+
elif label_string.startswith(
1002+
# buildifier: disable=canonical-repository
1003+
"@@//",
1004+
):
1005+
target_label = label_string[2:]
1006+
else:
1007+
target_label = label_string
1008+
1009+
return target_label.startswith(pattern) or crate_root_path.startswith(pattern)

rust/rust_common.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ load(
3030
_TestCrateInfo = "TestCrateInfo",
3131
_UnstableRustFeaturesInfo = "UnstableRustFeaturesInfo",
3232
)
33+
load("//rust/private:rustc.bzl", _UnstableSelfProfileInfo = "UnstableSelfProfileInfo")
3334

3435
BuildInfo = _BuildInfo
3536
ClippyInfo = _ClippyInfo
@@ -39,7 +40,7 @@ DepInfo = _DepInfo
3940
DepVariantInfo = _DepVariantInfo
4041
TestCrateInfo = _TestCrateInfo
4142
UnstableRustFeaturesInfo = _UnstableRustFeaturesInfo
42-
43+
UnstableSelfProfileInfo = _UnstableSelfProfileInfo
4344
COMMON_PROVIDERS = _COMMON_PROVIDERS
4445

4546
rust_common = _rust_common

rust/settings/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ load(
4040
"toolchain_generated_sysroot",
4141
"toolchain_linker_preference",
4242
"unpretty",
43+
"zself_profile_events",
4344
)
4445

4546
package(default_visibility = ["//visibility:public"])
@@ -137,3 +138,5 @@ toolchain_generated_sysroot()
137138
toolchain_linker_preference()
138139

139140
unpretty()
141+
142+
zself_profile_events()

rust/settings/settings.bzl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ load(
3030
_no_std = "no_std",
3131
_per_crate_rustc_flag = "per_crate_rustc_flag",
3232
_rustc_output_diagnostics = "rustc_output_diagnostics",
33+
_zself_profile_events = "zself_profile_events",
3334
)
3435
load("//rust/private:unpretty.bzl", "UNPRETTY_MODES", "rust_unpretty_flag")
3536
load(":incompatible.bzl", "incompatible_flag")
@@ -575,3 +576,11 @@ def collect_cfgs():
575576
scope = "universal",
576577
build_setting_default = False,
577578
)
579+
580+
def zself_profile_events(name = "zself_profile_events"):
581+
"""Passes -Zself-profile and -Zself-profile-events flags to rustc, requires a nightly toolchain.
582+
"""
583+
_zself_profile_events(
584+
name = name,
585+
build_setting_default = [],
586+
)

test/integration/unstable_rust_features/BUILD.bazel

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
load("@rules_rust//rust:defs.bzl", "rust_binary")
1+
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
2+
load(":self_profile_provider.bzl", "unstable_self_profiling_flags")
23
load(":unstable_features_for_test.bzl", "unstable_rust_features_for_test_rule")
34

45
unstable_rust_features_for_test_rule(name = "unstable_rust_features_for_test")
@@ -8,3 +9,26 @@ rust_binary(
89
srcs = ["main.rs"],
910
unstable_rust_features_config = ":unstable_rust_features_for_test",
1011
)
12+
13+
unstable_self_profiling_flags(name = "unstable_self_profiling_flags")
14+
15+
rust_binary(
16+
name = "sample_binary",
17+
srcs = ["hello.rs"],
18+
zself_profile_events = ":unstable_self_profiling_flags",
19+
)
20+
21+
filegroup(
22+
name = "self_profile_data",
23+
srcs = [":sample_binary"],
24+
output_group = "self_profile",
25+
)
26+
27+
rust_test(
28+
name = "test",
29+
srcs = ["validate.rs"],
30+
data = [":self_profile_data"],
31+
edition = "2021",
32+
env = {"PROFILE_PATH": "$(rootpath :sample_binary)"},
33+
deps = [":sample_binary"],
34+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fn say_hello(){
2+
println!("Hello, World!");
3+
}
4+
5+
pub fn main(){
6+
say_hello()
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Defines a test rule providing UnstableSelfProfileInfo"""
2+
3+
load("@rules_rust//rust:rust_common.bzl", "UnstableSelfProfileInfo")
4+
5+
def _get_self_profiling_flag_impl(_ctx):
6+
return UnstableSelfProfileInfo(
7+
events = [("//:sample_binary", "all")],
8+
)
9+
10+
unstable_self_profiling_flags = rule(
11+
attrs = {},
12+
provides = [UnstableSelfProfileInfo],
13+
implementation = _get_self_profiling_flag_impl,
14+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#[test]
2+
fn test_genquery_file_empty() {
3+
let profile_base_path = std::path::PathBuf::from(std::env::var_os("PROFILE_PATH").unwrap());
4+
let profile_path = profile_base_path.parent().unwrap().join("sample_binary_self-profile");
5+
let profile_path = std::path::Path::new(&profile_path);
6+
assert!(profile_path.is_dir());
7+
8+
let files_count = match std::fs::read_dir(profile_path) {
9+
Ok(entries) => {
10+
entries.filter_map(Result::ok).count()
11+
}
12+
Err(_) => 0,
13+
};
14+
assert!(files_count > 0);
15+
}

0 commit comments

Comments
 (0)