blob: 9aa2a9ea3aacfa9014b4b36696c50253046f20d5 [file] [log] [blame]
// 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.EnumConverter;
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 java.util.Locale;
import javax.annotation.Nullable;
import one.profiler.AsyncProfiler;
/** Bazel module to record a Java Flight Recorder profile for a single command. */
public class CommandProfilerModule extends BlazeModule {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final Duration PROFILING_INTERVAL = Duration.ofMillis(10);
/** The type of profile to capture. */
enum ProfileType {
CPU,
WALL,
ALLOC,
LOCK;
@Override
public String toString() {
return name().toLowerCase(Locale.US);
}
}
/** Options converter for --experimental_command_profile. */
public static final class ProfileTypeConverter extends EnumConverter<ProfileType> {
public ProfileTypeConverter() {
super(ProfileType.class, "--experimental_command_profile setting");
}
}
/** CommandProfilerModule options. */
public static final class Options extends OptionsBase {
@Option(
name = "experimental_command_profile",
defaultValue = "null",
converter = ProfileTypeConverter.class,
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"Records a Java Flight Recorder profile for the duration of the command. One of the"
+ " supported profiling event types (cpu, wall, alloc or lock) must be given as an"
+ " argument. The profile is written to a file named after the event type under the"
+ " output base directory."
+ " The syntax and semantics of this flag might change in the future to support"
+ " additional profile types or output formats; use at your own risk.")
public ProfileType profileType;
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
return ImmutableList.of(Options.class);
}
@Nullable private ProfileType profileType;
@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);
profileType = options.profileType;
outputBase = env.getBlazeWorkspace().getOutputBase();
reporter = env.getReporter();
if (profileType == null) {
// Early exit so we don't attempt to load the JNI unless necessary.
return;
}
AsyncProfiler profiler = getProfiler();
if (profiler == null) {
return;
}
outputPath = getProfilerOutputPath(profileType);
try {
profiler.execute(getProfilerCommand(profileType, outputPath));
} catch (Exception e) {
// This may occur if the user has insufficient privileges to capture performance events.
reporter.handle(
Event.error(String.format("Starting JFR %s profile failed: %s", profileType, e)));
profileType = null;
}
if (profileType != null) {
reporter.handle(
Event.info(String.format("Writing JFR %s profile to %s", profileType, outputPath)));
}
}
@Override
public void afterCommand() {
if (profileType == null) {
// Early exit so we don't attempt to load the JNI unless necessary.
return;
}
AsyncProfiler profiler = getProfiler();
if (profiler == null) {
return;
}
profiler.stop();
profileType = null;
outputBase = null;
reporter = null;
outputPath = null;
}
@Nullable
private static AsyncProfiler getProfiler() {
try {
return 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");
}
return null;
}
private Path getProfilerOutputPath(ProfileType profileType) {
return outputBase.getChild(profileType + ".jfr");
}
private static String getProfilerCommand(ProfileType profileType, Path outputPath) {
// See https://github.com/async-profiler/async-profiler/blob/master/src/arguments.cpp.
return String.format(
"start,event=%s,interval=%s,file=%s,jfr",
profileType, PROFILING_INTERVAL.toNanos(), outputPath);
}
}