Skip to content

Commit 407e045

Browse files
coeuvrecopybara-github
authored andcommitted
Dump thread periodically
configured by flag `--experimental_thread_dump_interval`. The thread dumps can be found under `<output_base>/server/thread_dumps/`. Dumps from last invocation are deleted at the start of the current invocation to avoid accumulating too much dumps in the output base. RELNOTES: Added flag `--experimental_thread_dump_interval` to allow Bazel dump threads periodically. PiperOrigin-RevId: 792131384 Change-Id: Ib9fe7a659f51bdd404a28229055441a6d3a87f3d
1 parent 2f94fa9 commit 407e045

File tree

5 files changed

+146
-1
lines changed

5 files changed

+146
-1
lines changed

src/main/java/com/google/devtools/build/lib/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ java_library(
526526
"//src/main/java/com/google/devtools/build/lib/util:shallow_object_size_computer",
527527
"//src/main/java/com/google/devtools/build/lib/util:string",
528528
"//src/main/java/com/google/devtools/build/lib/util:string_encoding",
529+
"//src/main/java/com/google/devtools/build/lib/util:thread_dumper",
529530
"//src/main/java/com/google/devtools/build/lib/util/io",
530531
"//src/main/java/com/google/devtools/build/lib/util/io:io-proto",
531532
"//src/main/java/com/google/devtools/build/lib/util/io:out-err",

src/main/java/com/google/devtools/build/lib/bazel/Bazel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public final class Bazel {
4949
CredentialModule.class,
5050
com.google.devtools.build.lib.runtime.CommandLogModule.class,
5151
com.google.devtools.build.lib.runtime.MemoryPressureModule.class,
52+
com.google.devtools.build.lib.runtime.ThreadDumpModule.class,
5253
com.google.devtools.build.lib.platform.SleepPreventionModule.class,
5354
com.google.devtools.build.lib.platform.SystemSuspensionModule.class,
5455
BazelFileSystemModule.class,

src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ public ActionKeyContext getActionKeyContext() {
533533
}
534534

535535
/** The directory in which blaze stores the server state - that is, the socket file and a log. */
536-
private Path getServerDirectory() {
536+
public Path getServerDirectory() {
537537
return workspace.getDirectories().getOutputBase().getChild("server");
538538
}
539539

src/main/java/com/google/devtools/build/lib/runtime/CommonCommandOptions.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ public class CommonCommandOptions extends OptionsBase {
155155
+ " --experimental_action_cache_gc_threshold flags.")
156156
public Duration actionCacheGcMaxAge;
157157

158+
@Option(
159+
name = "experimental_thread_dump_interval",
160+
defaultValue = "0",
161+
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
162+
effectTags = {OptionEffectTag.BAZEL_MONITORING},
163+
converter = DurationConverter.class,
164+
help =
165+
"How often to dump the state of all threads (including virtual threads) to a file. The"
166+
+ " dumps will be written to the <output_base>/server/thread_dumps/ directory. If"
167+
+ " zero, no thread dumps are written.")
168+
public Duration threadDumpInterval;
169+
158170
/** Converter for UUID. Accepts values as specified by {@link UUID#fromString(String)}. */
159171
public static class UUIDConverter extends Converter.Contextless<UUID> {
160172

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2025 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.devtools.build.lib.runtime;
15+
16+
import static com.google.common.base.Preconditions.checkState;
17+
18+
import com.google.common.flogger.GoogleLogger;
19+
import com.google.common.util.concurrent.Uninterruptibles;
20+
import com.google.devtools.build.lib.clock.Clock;
21+
import com.google.devtools.build.lib.profiler.Profiler;
22+
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
23+
import com.google.devtools.build.lib.util.AbruptExitException;
24+
import com.google.devtools.build.lib.util.DetailedExitCode;
25+
import com.google.devtools.build.lib.util.ExitCode;
26+
import com.google.devtools.build.lib.util.JavaSleeper;
27+
import com.google.devtools.build.lib.util.Sleeper;
28+
import com.google.devtools.build.lib.util.ThreadDumper;
29+
import com.google.devtools.build.lib.vfs.Path;
30+
import java.io.IOException;
31+
import java.time.Duration;
32+
import java.time.Instant;
33+
import java.time.ZoneOffset;
34+
import java.time.format.DateTimeFormatter;
35+
import javax.annotation.Nullable;
36+
37+
/** A {@link BlazeModule} that dumps the state of all threads periodically. */
38+
public final class ThreadDumpModule extends BlazeModule {
39+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
40+
private static final DateTimeFormatter TIME_FORMAT =
41+
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
42+
43+
@Nullable private Thread dumpThread;
44+
45+
@Override
46+
public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
47+
var commandOptions = env.getOptions().getOptions(CommonCommandOptions.class);
48+
if (commandOptions == null || commandOptions.threadDumpInterval.isZero()) {
49+
return;
50+
}
51+
52+
var runtime = env.getRuntime();
53+
var clock = runtime.getClock();
54+
var threadDumpInterval = commandOptions.threadDumpInterval;
55+
56+
var serverDirectory = runtime.getServerDirectory();
57+
var dumpDirectory = serverDirectory.getChild("thread_dumps");
58+
try {
59+
dumpDirectory.deleteTree();
60+
dumpDirectory.createDirectoryAndParents();
61+
} catch (IOException e) {
62+
throw new AbruptExitException(
63+
DetailedExitCode.of(
64+
ExitCode.LOCAL_ENVIRONMENTAL_ERROR,
65+
FailureDetail.newBuilder()
66+
.setMessage("Failed to setup thread dump directory")
67+
.build()),
68+
e);
69+
}
70+
71+
var pid = ProcessHandle.current().pid();
72+
checkState(dumpThread == null);
73+
dumpThread =
74+
new Thread(
75+
new ThreadDumpTask(pid, clock, new JavaSleeper(), threadDumpInterval, dumpDirectory),
76+
"thread-dumper");
77+
dumpThread.start();
78+
}
79+
80+
@Override
81+
public void afterCommand() {
82+
if (dumpThread != null) {
83+
dumpThread.interrupt();
84+
try (var sc = Profiler.instance().profile("Joining dump thread")) {
85+
Uninterruptibles.joinUninterruptibly(dumpThread);
86+
}
87+
dumpThread = null;
88+
}
89+
}
90+
91+
private static final class ThreadDumpTask implements Runnable {
92+
private final long pid;
93+
private final Clock clock;
94+
private final Sleeper sleeper;
95+
private final Duration threadDumpInterval;
96+
private final Path dumpDirectory;
97+
98+
private ThreadDumpTask(
99+
long pid, Clock clock, Sleeper sleeper, Duration threadDumpInterval, Path dumpDirectory) {
100+
this.pid = pid;
101+
this.clock = clock;
102+
this.sleeper = sleeper;
103+
this.threadDumpInterval = threadDumpInterval;
104+
this.dumpDirectory = dumpDirectory;
105+
}
106+
107+
@Override
108+
public void run() {
109+
while (true) {
110+
try {
111+
sleeper.sleepMillis(threadDumpInterval.toMillis());
112+
} catch (InterruptedException e) {
113+
Thread.currentThread().interrupt();
114+
return;
115+
}
116+
117+
String formattedTime =
118+
Instant.ofEpochMilli(clock.currentTimeMillis())
119+
.atZone(ZoneOffset.UTC)
120+
.format(TIME_FORMAT);
121+
Path dumpFile = dumpDirectory.getChild(String.format("dump.%d.%s.txt", pid, formattedTime));
122+
try (var sc = Profiler.instance().profile("Dumping threads");
123+
var out = dumpFile.getOutputStream()) {
124+
ThreadDumper.dumpThreads(out);
125+
} catch (IOException e) {
126+
logger.atWarning().withCause(e).log("Failed to dump threads.");
127+
}
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)