Skip to content

Commit

Permalink
Add an experimental flag to capture a CPU profile in JFR format.
Browse files Browse the repository at this point in the history
Support for additional profile types or output formats may be added in the future. These are just the minimal changes to introduce the dependency on async-profiler and verify that it works.

Closes bazelbuild#18029.

PiperOrigin-RevId: 524798925
Change-Id: I46c384bee6240d36c732f10e50f0544b9132f321
  • Loading branch information
tjgq authored and copybara-github committed Apr 17, 2023
1 parent 0a47a1f commit 5cbb153
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 14 deletions.
57 changes: 49 additions & 8 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -688,19 +688,60 @@ maven_install(
"org.pcollections:pcollections:3.1.4",
"org.threeten:threeten-extra:1.5.0",
"org.tukaani:xz:1.9",
"tools.profiler:async-profiler:2.9",
# The following jars are for testing.
# junit is not test only due to //src/java_tools/junitrunner/java/com/google/testing/junit/junit4:runner,
# and hamcrest is a dependency of junit.
"junit:junit:4.13.2",
"org.hamcrest:hamcrest-core:1.3",
maven.artifact("com.google.guava", "guava-testlib", "31.1-jre", testonly = True),
maven.artifact("com.google.jimfs", "jimfs", "1.2", testonly = True),
maven.artifact("com.google.testing.compile", "compile-testing", "0.18", testonly = True),
maven.artifact("com.google.truth", "truth", "1.1.3", testonly = True),
maven.artifact("com.google.truth.extensions", "truth-java8-extension", "1.1.3", testonly = True),
maven.artifact("com.google.truth.extensions", "truth-liteproto-extension", "1.1.3", testonly = True),
maven.artifact("com.google.truth.extensions", "truth-proto-extension", "1.1.3", testonly = True),
maven.artifact("org.mockito", "mockito-core", "3.12.4", testonly = True),
maven.artifact(
"com.google.guava",
"guava-testlib",
"31.1-jre",
testonly = True,
),
maven.artifact(
"com.google.jimfs",
"jimfs",
"1.2",
testonly = True,
),
maven.artifact(
"com.google.testing.compile",
"compile-testing",
"0.18",
testonly = True,
),
maven.artifact(
"com.google.truth",
"truth",
"1.1.3",
testonly = True,
),
maven.artifact(
"com.google.truth.extensions",
"truth-java8-extension",
"1.1.3",
testonly = True,
),
maven.artifact(
"com.google.truth.extensions",
"truth-liteproto-extension",
"1.1.3",
testonly = True,
),
maven.artifact(
"com.google.truth.extensions",
"truth-proto-extension",
"1.1.3",
testonly = True,
),
maven.artifact(
"org.mockito",
"mockito-core",
"3.12.4",
testonly = True,
),
],
excluded_artifacts = [
# org.apache.httpcomponents and org.eclipse.jgit:org.eclipse.jgit
Expand Down
25 changes: 23 additions & 2 deletions maven_install.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"dependency_tree": {
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
"__INPUT_ARTIFACTS_HASH": 763877443,
"__RESOLVED_ARTIFACTS_HASH": 162079487,
"__INPUT_ARTIFACTS_HASH": 1060875532,
"__RESOLVED_ARTIFACTS_HASH": 261143074,
"conflict_resolution": {},
"dependencies": [
{
Expand Down Expand Up @@ -3896,6 +3896,27 @@
],
"sha256": "211b306cfc44f8f96df3a0a3ddaf75ba8c5289eed77d60d72f889bb855f535e5",
"url": "https://repo1.maven.org/maven2/org/tukaani/xz/1.9/xz-1.9.jar"
},
{
"coord": "tools.profiler:async-profiler:2.9",
"dependencies": [],
"directDependencies": [],
"exclusions": [
"com.google.protobuf:protobuf-javalite",
"org.apache.httpcomponents:httpcore",
"com.google.protobuf:protobuf-java",
"org.eclipse.jgit:org.eclipse.jgit",
"org.apache.httpcomponents:httpclient"
],
"file": "v1/https/repo1.maven.org/maven2/tools/profiler/async-profiler/2.9/async-profiler-2.9.jar",
"mirror_urls": [
"https://repo1.maven.org/maven2/tools/profiler/async-profiler/2.9/async-profiler-2.9.jar"
],
"packages": [
"one.profiler"
],
"sha256": "6c4e993c28cf2882964cac82a0f96e81a325840043884526565017b2f62c5ba4",
"url": "https://repo1.maven.org/maven2/tools/profiler/async-profiler/2.9/async-profiler-2.9.jar"
}
],
"version": "0.1.0"
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/packages/metrics",
"//src/main/java/com/google/devtools/build/lib/platform:sleep_prevention_module",
"//src/main/java/com/google/devtools/build/lib/platform:system_suspension_module",
"//src/main/java/com/google/devtools/build/lib/profiler:command_profiler_module",
"//src/main/java/com/google/devtools/build/lib/profiler/callcounts:callcounts_module",
"//src/main/java/com/google/devtools/build/lib/profiler/memory:allocationtracker_module",
"//src/main/java/com/google/devtools/build/lib/remote",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public final class Bazel {
com.google.devtools.build.lib.buildeventservice.BazelBuildEventServiceModule.class,
com.google.devtools.build.lib.profiler.callcounts.CallcountsModule.class,
com.google.devtools.build.lib.profiler.memory.AllocationTrackerModule.class,
com.google.devtools.build.lib.profiler.CommandProfilerModule.class,
com.google.devtools.build.lib.metrics.PostGCMemoryUseRecorder
.PostGCMemoryUseRecorderModule.class,
com.google.devtools.build.lib.metrics.PostGCMemoryUseRecorder.GcAfterBuildModule.class,
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/google/devtools/build/lib/profiler/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ java_library(
],
)

java_library(
name = "command_profiler_module",
srcs = ["CommandProfilerModule.java"],
deps = [
"//src/main/java/com/google/devtools/build/lib:runtime",
"//src/main/java/com/google/devtools/build/lib/events",
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/common/options",
"//third_party:flogger",
"//third_party:guava",
"//third_party:jsr305",
"@maven//:tools_profiler_async_profiler",
],
)

java_library(
name = "network_metrics_collector",
srcs = ["NetworkMetricsCollector.java"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2023 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.profiler;

import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import java.time.Duration;
import javax.annotation.Nullable;
import one.profiler.AsyncProfiler;

/** Bazel module to record pprof-compatible profiles for single invocations. */
public class CommandProfilerModule extends BlazeModule {

private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

private static final Duration PROFILING_INTERVAL = Duration.ofMillis(10);

@Nullable private static final AsyncProfiler profiler;

static {
AsyncProfiler profilerInstance = null;
try {
profilerInstance = AsyncProfiler.getInstance();
} catch (Throwable t) {
// Loading the JNI must be allowed to fail, as we might be running on an unsupported platform.
logger.atWarning().withCause(t).log("Failed to load async_profiler JNI");
}
profiler = profilerInstance;
}

/** CommandProfilerModule options. */
public static final class Options extends OptionsBase {
@Option(
name = "experimental_command_profile",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Records a Java Flight Recorder CPU profile into a profile.jfr file in the output base"
+ " directory. The syntax and semantics of this flag might change in the future to"
+ " support different profile types or output formats; use at your own risk.")
public boolean captureCommandProfile;
}

@Override
public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
return ImmutableList.of(Options.class);
}

private boolean captureCommandProfile;
@Nullable private Reporter reporter;
@Nullable private Path outputBase;
@Nullable private Path outputPath;

@Override
public void beforeCommand(CommandEnvironment env) {
Options options = env.getOptions().getOptions(Options.class);
captureCommandProfile = options.captureCommandProfile;
outputBase = env.getBlazeWorkspace().getOutputBase();
reporter = env.getReporter();

if (profiler == null || !captureCommandProfile) {
return;
}

outputPath = outputBase.getRelative("profile.jfr");

try {
profiler.execute(getProfilerCommand(outputPath));
} catch (Exception e) {
// This may occur if the user has insufficient privileges to capture performance events.
reporter.handle(Event.error("Starting JFR CPU profile failed: " + e));
captureCommandProfile = false;
}

if (captureCommandProfile) {
reporter.handle(Event.info("Writing JFR CPU profile to " + outputPath));
}
}

@Override
public void afterCommand() {
if (profiler == null || !captureCommandProfile) {
return;
}

profiler.stop();

captureCommandProfile = false;
outputBase = null;
reporter = null;
outputPath = null;
}

private static String getProfilerCommand(Path outputPath) {
// See https://github.com/async-profiler/async-profiler/blob/master/src/arguments.cpp.
return String.format(
"start,event=cpu,interval=%s,file=%s,jfr", PROFILING_INTERVAL.toNanos(), outputPath);
}
}
22 changes: 21 additions & 1 deletion src/test/java/com/google/devtools/build/lib/profiler/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ filegroup(

java_test(
name = "ProfilerTests",
srcs = glob(["*.java"]),
srcs = glob(
["*.java"],
exclude = ["CommandProfilerModuleTest.java"],
),
test_class = "com.google.devtools.build.lib.AllTests",
runtime_deps = [
"//src/test/java/com/google/devtools/build/lib:test_runner",
Expand All @@ -39,3 +42,20 @@ java_test(
"//third_party:truth",
],
)

java_test(
name = "CommandProfilerModuleTest",
srcs = ["CommandProfilerModuleTest.java"],
tags = [
# Bazel-specific tests
"manual",
],
deps = [
"//src/main/java/com/google/devtools/build/lib:runtime",
"//src/main/java/com/google/devtools/build/lib/profiler:command_profiler_module",
"//src/main/java/com/google/devtools/build/lib/util:os",
"//src/test/java/com/google/devtools/build/lib/buildtool/util",
"//third_party:junit4",
"//third_party:truth",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2023 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.profiler;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;

import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.util.OS;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link CommandProfilerModule}. */
@RunWith(JUnit4.class)
public final class CommandProfilerModuleTest extends BuildIntegrationTestCase {

@Override
protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
return super.getRuntimeBuilder().addBlazeModule(new CommandProfilerModule());
}

@Before
public void setUp() throws Exception {
write("BUILD", "");
}

@Test
public void testProfilingDisabled() throws Exception {
buildTarget("//:BUILD");
assertThat(outputBase.getChild("profile.jfr").exists()).isFalse();
}

@Test
public void testProfilingEnabled() throws Exception {
addOptions("--experimental_command_profile");

try {
buildTarget("//:BUILD");
} catch (Exception e) {
// Linux perf events are not supported on CI.
// See https://github.com/async-profiler/async-profiler/#troubleshooting.
if (e.getMessage().contains("No access to perf events")
|| e.getMessage().contains("Perf events unavailable")) {
return;
}
}

assumeTrue(OS.getCurrent() == OS.LINUX || OS.getCurrent() == OS.DARWIN);

assertThat(outputBase.getChild("profile.jfr").exists()).isTrue();
}
}
6 changes: 3 additions & 3 deletions src/test/shell/integration/minimal_jdk_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ export BAZEL_SUFFIX="_jdk_minimal"
source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
|| { echo "integration_test_setup.sh not found!" >&2; exit 1; }

# Bazel's install base is < 340MB with minimal JDK and > 340MB with an all
# Bazel's install base is < 342MB with minimal JDK and > 342MB with an all
# modules JDK.
function test_size_less_than_340MB() {
function test_size_less_than_342MB() {
bazel info
ib=$(bazel info install_base)
size=$(du -s "$ib" | cut -d\ -f1)
maxsize=$((1024*340))
maxsize=$((1024*342))
if [ $size -gt $maxsize ]; then
echo "$ib was too big:" 1>&2
du -a "$ib" 1>&2
Expand Down

0 comments on commit 5cbb153

Please sign in to comment.