blob: fedb4852d727b8284dc0393d23ec7e289b90f4dd [file] [log] [blame]
// Copyright 2025 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.runtime;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.eventbus.Subscribe;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.actions.ActionExecutionInactivityEvent;
import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildtool.BuildResult.BuildToolLogCollection;
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.runtime.BuildEventArtifactUploaderFactory.InvalidPackagePathSymlinkException;
import com.google.devtools.build.lib.runtime.InstrumentationOutputFactory.DestinationRelativeTo;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.ThreadDumpAnalyzer;
import com.google.devtools.build.lib.util.ThreadDumper;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
/** A {@link BlazeModule} that dumps the state of all threads periodically. */
public final class ThreadDumpModule extends BlazeModule {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final DateTimeFormatter TIME_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
@Nullable private ScheduledExecutorService scheduledExecutor;
@Nullable private ThreadDumpTask threadDumpTask;
@Override
public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
var commandOptions = env.getOptions().getOptions(CommonCommandOptions.class);
if (commandOptions == null || !commandOptions.enableThreadDump) {
return;
}
if (commandOptions.threadDumpInterval.isZero()
&& commandOptions.threadDumpActionExecutionInactivityDuration.isZero()) {
env.getReporter()
.handle(
Event.warn(
"--experimental_enable_thread_dump is set, but"
+ " --experimental_thread_dump_interval and"
+ " --experimental_thread_dump_action_execution_inactivity_duration are 0. No"
+ " thread dumps will be written."));
return;
}
checkState(threadDumpTask == null);
checkState(scheduledExecutor == null);
var bepOptions = env.getOptions().getOptions(BuildEventProtocolOptions.class);
BuildEventArtifactUploader uploader = null;
if (bepOptions != null && bepOptions.streamingLogFileUploads) {
try {
uploader = newUploader(env, bepOptions);
} catch (InvalidPackagePathSymlinkException e) {
throw createAbruptExitException("Failed to create uploader", e);
}
}
var outputBaseRelativeDumpDirectory = prepareDumpDirectory(env);
threadDumpTask =
new ThreadDumpTask(
env,
ProcessHandle.current().pid(),
env.getRuntime().getClock(),
outputBaseRelativeDumpDirectory,
commandOptions.threadDumpActionExecutionInactivityDuration,
uploader);
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
var threadDumpInterval = commandOptions.threadDumpInterval;
if (!threadDumpInterval.isZero()) {
var unused =
scheduledExecutor.scheduleAtFixedRate(
threadDumpTask,
threadDumpInterval.toMillis(),
threadDumpInterval.toMillis(),
MILLISECONDS);
}
env.getEventBus().register(this);
}
private static BuildEventArtifactUploader newUploader(
CommandEnvironment env, BuildEventProtocolOptions bepOptions)
throws InvalidPackagePathSymlinkException {
return env.getRuntime()
.getBuildEventArtifactUploaderFactoryMap()
.select(bepOptions.buildEventUploadStrategy)
.create(env);
}
private PathFragment prepareDumpDirectory(CommandEnvironment env) throws AbruptExitException {
var runtime = env.getRuntime();
var serverDirectory = runtime.getServerDirectory();
var dumpDirectory = serverDirectory.getChild("thread_dumps");
try {
dumpDirectory.deleteTree();
dumpDirectory.createDirectoryAndParents();
} catch (IOException e) {
throw createAbruptExitException("Failed to setup thread dump directory", e);
}
return dumpDirectory.relativeTo(env.getDirectories().getOutputBase());
}
private static AbruptExitException createAbruptExitException(String message, Throwable cause) {
return new AbruptExitException(
DetailedExitCode.of(
ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
FailureDetail.newBuilder().setMessage(message).build()),
cause);
}
@Subscribe
public void onActionExecutionInactivityEvent(ActionExecutionInactivityEvent event) {
checkState(scheduledExecutor != null);
checkState(threadDumpTask != null);
if (threadDumpTask.shouldDumpForActionExecutionInactivity(event)) {
var unused = scheduledExecutor.schedule(threadDumpTask, 0, MILLISECONDS);
}
}
@Subscribe
public void buildComplete(BuildCompleteEvent event) {
checkNotNull(scheduledExecutor);
checkNotNull(threadDumpTask);
scheduledExecutor.shutdownNow();
try (var sc = Profiler.instance().profile("Joining dump thread")) {
Uninterruptibles.awaitTerminationUninterruptibly(scheduledExecutor);
}
var buildToolLogCollection = event.getResult().getBuildToolLogCollection();
threadDumpTask.shutdown(buildToolLogCollection);
scheduledExecutor = null;
threadDumpTask = null;
}
private static final class ThreadDumpTask implements Runnable {
private final CommandEnvironment env;
private final long pid;
private final Clock clock;
private final PathFragment outputBaseRelativeDumpDirectory;
private final Duration dumpActionExecutionInactivityDuration;
@Nullable private final BuildEventArtifactUploader uploader;
private final List<InstrumentationOutput> instrumentationOutputs =
Collections.synchronizedList(new ArrayList<>());
private Instant lastDumpAt = Instant.EPOCH;
private ThreadDumpTask(
CommandEnvironment env,
long pid,
Clock clock,
PathFragment outputBaseRelativeDumpDirectory,
Duration dumpActionExecutionInactivityDuration,
@Nullable BuildEventArtifactUploader uploader) {
this.env = env;
this.pid = pid;
this.clock = clock;
this.outputBaseRelativeDumpDirectory = outputBaseRelativeDumpDirectory;
this.dumpActionExecutionInactivityDuration = dumpActionExecutionInactivityDuration;
this.uploader = uploader;
}
@Override
public void run() {
var bos = new ByteArrayOutputStream();
try (var sc = Profiler.instance().profile("Dumping threads")) {
ThreadDumper.dumpThreads(bos);
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to dump threads.");
}
String formattedTime =
Instant.ofEpochMilli(clock.currentTimeMillis())
.atZone(ZoneOffset.UTC)
.format(TIME_FORMAT);
var dumpOutput =
createThreadDumpOutput(String.format("thread_dump.%d.%s.txt", pid, formattedTime));
instrumentationOutputs.add(dumpOutput);
var analyzer = new ThreadDumpAnalyzer();
try (var sc = Profiler.instance().profile("Analyzing thread dump");
var out = dumpOutput.createOutputStream()) {
analyzer.analyze(new ByteArrayInputStream(bos.toByteArray()), out);
} catch (IOException e) {
logger.atWarning().withCause(e).log("Failed to analyze threads.");
}
lastDumpAt = clock.now();
}
boolean shouldDumpForActionExecutionInactivity(ActionExecutionInactivityEvent event) {
if (dumpActionExecutionInactivityDuration.isZero()) {
return false;
}
var now = clock.now();
if (now.isBefore(event.lastActionCompletedAt().plus(dumpActionExecutionInactivityDuration))) {
return false;
}
return now.isAfter(lastDumpAt.plus(dumpActionExecutionInactivityDuration));
}
void shutdown(BuildToolLogCollection buildToolLogCollection) {
for (var output : instrumentationOutputs) {
output.publish(buildToolLogCollection);
}
if (uploader != null) {
uploader.release();
}
}
private InstrumentationOutput createThreadDumpOutput(String name) {
var outputFactory = env.getRuntime().getInstrumentationOutputFactory();
if (uploader != null) {
return outputFactory.createBuildEventArtifactInstrumentationOutput(name, uploader);
}
return outputFactory.createInstrumentationOutput(
/* name= */ name,
/* destination= */ outputBaseRelativeDumpDirectory.getRelative(name),
DestinationRelativeTo.OUTPUT_BASE,
env,
env.getReporter(),
/* append= */ null,
/* internal= */ null,
/* createParent= */ true);
}
}
}