This is an automated email from the ASF dual-hosted git repository.
yao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new f7a0b3ab50d [SPARK-44863][UI] Add a button to download thread dump as
a txt in Spark UI
f7a0b3ab50d is described below
commit f7a0b3ab50d832a738e073d89f403b78be33c974
Author: Kent Yao <[email protected]>
AuthorDate: Thu Aug 24 17:39:03 2023 +0800
[SPARK-44863][UI] Add a button to download thread dump as a txt in Spark UI
### What changes were proposed in this pull request?
This PR added a button to download thread dump as a txt w.r.t. jstack
formatting
### Why are the changes needed?
The formatting of raw jstack can be relatively easy to read and analyze
using various thread tools.
### Does this PR introduce _any_ user-facing change?

### How was this patch tested?
#### Raw Dump File
[driver.txt](https://github.com/apache/spark/files/12388302/driver.txt)
#### Reporting by external tools
https://fastthread.io/my-thread-report.jsp?p=c2hhcmVkLzIwMjMvMDgvMjAvZHJpdmVyLnR4dC0tMTMtNTMtMjI=&
### Was this patch authored or co-authored using generative AI tooling?
no
Closes #42575 from yaooqinn/SPARK-44863.
Authored-by: Kent Yao <[email protected]>
Signed-off-by: Kent Yao <[email protected]>
---
.../resources/org/apache/spark/ui/static/webui.css | 6 +++
.../scala/org/apache/spark/status/api/v1/api.scala | 52 +++++++++++++++++++---
.../spark/ui/exec/ExecutorThreadDumpPage.scala | 27 +++++------
.../main/scala/org/apache/spark/util/Utils.scala | 49 +++++++++++---------
project/MimaExcludes.scala | 5 ++-
5 files changed, 98 insertions(+), 41 deletions(-)
diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.css
b/core/src/main/resources/org/apache/spark/ui/static/webui.css
index f952f86503e..ca7c1f8ba65 100755
--- a/core/src/main/resources/org/apache/spark/ui/static/webui.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/webui.css
@@ -286,6 +286,12 @@ span.expand-dag-viz, .collapse-table {
a.expandbutton {
cursor: pointer;
+ margin-right: 10px;
+}
+
+a.downloadbutton {
+ cursor: pointer;
+ margin-right: 10px;
}
.threaddump-td-mouseover {
diff --git a/core/src/main/scala/org/apache/spark/status/api/v1/api.scala
b/core/src/main/scala/org/apache/spark/status/api/v1/api.scala
index 8d648b9df38..3e4e2f17a77 100644
--- a/core/src/main/scala/org/apache/spark/status/api/v1/api.scala
+++ b/core/src/main/scala/org/apache/spark/status/api/v1/api.scala
@@ -527,13 +527,51 @@ case class StackTrace(elems: Seq[String]) {
}
case class ThreadStackTrace(
- val threadId: Long,
- val threadName: String,
- val threadState: Thread.State,
- val stackTrace: StackTrace,
- val blockedByThreadId: Option[Long],
- val blockedByLock: String,
- val holdingLocks: Seq[String])
+ threadId: Long,
+ threadName: String,
+ threadState: Thread.State,
+ stackTrace: StackTrace,
+ blockedByThreadId: Option[Long],
+ blockedByLock: String,
+ @deprecated("using synchronizers and monitors instead", "4.0.0")
+ holdingLocks: Seq[String],
+ synchronizers: Seq[String],
+ monitors: Seq[String],
+ lockName: Option[String],
+ lockOwnerName: Option[String],
+ suspended: Boolean,
+ inNative: Boolean) {
+
+ /**
+ * Returns a string representation of this thread stack trace
+ * w.r.t java.lang.management.ThreadInfo(JDK 8)'s toString.
+ *
+ * TODO(SPARK-44895): Considering 'daemon', 'priority' from higher JDKs
+ *
+ * TODO(SPARK-44896): Also considering adding information os_prio, cpu,
elapsed, tid, nid, etc.,
+ * from the jstack tool
+ */
+ override def toString: String = {
+ val sb = new StringBuilder(s""""$threadName" Id=$threadId $threadState""")
+ lockName.foreach(lock => sb.append(s" on $lock"))
+ lockOwnerName.foreach {
+ owner => sb.append(s"""owned by "$owner"""")
+ }
+ blockedByThreadId.foreach(id => s" Id=$id")
+ if (suspended) sb.append(" (suspended)")
+ if (inNative) sb.append(" (in native)")
+ sb.append('\n')
+
+ sb.append(stackTrace.elems.map(e => s"\tat $e").mkString)
+
+ if (synchronizers.nonEmpty) {
+ sb.append(s"\n\tNumber of locked synchronizers =
${synchronizers.length}\n")
+ synchronizers.foreach(sync => sb.append(s"\t- $sync\n"))
+ }
+ sb.append('\n')
+ sb.toString
+ }
+}
class ProcessSummary private[spark](
val id: String,
diff --git
a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
index c3246dc9097..0be9df921d1 100644
--- a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
@@ -48,7 +48,9 @@ private[ui] class ExecutorThreadDumpPage(
</div>
case None => Text("")
}
- val heldLocks = thread.holdingLocks.mkString(", ")
+ val synchronizers = thread.synchronizers.map(l => s"Lock($l)")
+ val monitors = thread.monitors.map(m => s"Monitor($m)")
+ val heldLocks = (synchronizers ++ monitors).mkString(", ")
<tr id={s"thread_${threadId}_tr"} class="accordion-heading"
onclick={s"toggleThreadStackTrace($threadId, false)"}
@@ -67,18 +69,17 @@ private[ui] class ExecutorThreadDumpPage(
<p>Updated at {UIUtils.formatDate(time)}</p>
{
// scalastyle:off
- <p><a class="expandbutton" onClick="expandAllThreadStackTrace(true)">
- Expand All
- </a></p>
- <p><a class="expandbutton d-none"
onClick="collapseAllThreadStackTrace(true)">
- Collapse All
- </a></p>
- <div class="form-inline">
- <div class="bs-example" data-example-id="simple-form-inline">
- <div class="form-group">
- <div class="input-group">
- <label class="mr-2" for="search">Search:</label>
- <input type="text" class="form-control" id="search"
oninput="onSearchStringChange()"></input>
+ <div style="display: flex; align-items: center;">
+ <a class="expandbutton"
onClick="expandAllThreadStackTrace(true)">Expand All</a>
+ <a class="expandbutton d-none"
onClick="collapseAllThreadStackTrace(true)">Collapse All</a>
+ <a class="downloadbutton" href={"data:text/plain;charset=utf-8," +
threadDump.map(_.toString).mkString} download={"threaddump_" + executorId +
".txt"}>Download</a>
+ <div class="form-inline">
+ <div class="bs-example" data-example-id="simple-form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <label class="mr-2" for="search">Search:</label>
+ <input type="text" class="form-control" id="search"
oninput="onSearchStringChange()"></input>
+ </div>
</div>
</div>
</div>
diff --git a/core/src/main/scala/org/apache/spark/util/Utils.scala
b/core/src/main/scala/org/apache/spark/util/Utils.scala
index c211d6bba5d..7e4d9b78af2 100644
--- a/core/src/main/scala/org/apache/spark/util/Utils.scala
+++ b/core/src/main/scala/org/apache/spark/util/Utils.scala
@@ -2169,29 +2169,38 @@ private[spark] object Utils
}
private def threadInfoToThreadStackTrace(threadInfo: ThreadInfo):
ThreadStackTrace = {
- val monitors = threadInfo.getLockedMonitors.map(m => m.getLockedStackFrame
-> m).toMap
- val stackTrace = StackTrace(threadInfo.getStackTrace.map { frame =>
- monitors.get(frame) match {
- case Some(monitor) =>
- monitor.getLockedStackFrame.toString + s" => holding
${monitor.lockString}"
- case None =>
- frame.toString
- }
+ val threadState = threadInfo.getThreadState
+ val monitors = threadInfo.getLockedMonitors.map(m => m.getLockedStackDepth
-> m.toString).toMap
+ val stackTrace = StackTrace(threadInfo.getStackTrace.zipWithIndex.map {
case (frame, idx) =>
+ val locked = if (idx == 0 && threadInfo.getLockInfo != null) {
+ threadState match {
+ case Thread.State.BLOCKED =>
+ s"\t- blocked on ${threadInfo.getLockInfo}\n"
+ case Thread.State.WAITING | Thread.State.TIMED_WAITING =>
+ s"\t- waiting on ${threadInfo.getLockInfo}\n"
+ case _ => ""
+ }
+ } else ""
+ val locking = monitors.get(idx).map(mi => s"\t- locked
$mi\n").getOrElse("")
+ s"${frame.toString}\n$locked$locking"
})
- // use a set to dedup re-entrant locks that are held at multiple places
- val heldLocks =
- (threadInfo.getLockedSynchronizers ++
threadInfo.getLockedMonitors).map(_.lockString).toSet
-
+ val synchronizers = threadInfo.getLockedSynchronizers.map(_.toString)
+ val monitorStrs = monitors.values.toSeq
ThreadStackTrace(
- threadId = threadInfo.getThreadId,
- threadName = threadInfo.getThreadName,
- threadState = threadInfo.getThreadState,
- stackTrace = stackTrace,
- blockedByThreadId =
- if (threadInfo.getLockOwnerId < 0) None else
Some(threadInfo.getLockOwnerId),
- blockedByLock =
Option(threadInfo.getLockInfo).map(_.lockString).getOrElse(""),
- holdingLocks = heldLocks.toSeq)
+ threadInfo.getThreadId,
+ threadInfo.getThreadName,
+ threadState,
+ stackTrace,
+ if (threadInfo.getLockOwnerId < 0) None else
Some(threadInfo.getLockOwnerId),
+ Option(threadInfo.getLockInfo).map(_.lockString).getOrElse(""),
+ synchronizers ++ monitorStrs,
+ synchronizers,
+ monitorStrs,
+ Option(threadInfo.getLockName),
+ Option(threadInfo.getLockOwnerName),
+ threadInfo.isSuspended,
+ threadInfo.isInNative)
}
/**
diff --git a/project/MimaExcludes.scala b/project/MimaExcludes.scala
index f818d9aa2c8..6200f24eed1 100644
--- a/project/MimaExcludes.scala
+++ b/project/MimaExcludes.scala
@@ -80,7 +80,10 @@ object MimaExcludes {
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.sql.SaveMode"),
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.sql.streaming.GroupState"),
// [SPARK-44705][PYTHON] Make PythonRunner single-threaded
-
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.spark.api.python.BasePythonRunner#ReaderIterator.this")
+
ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.apache.spark.api.python.BasePythonRunner#ReaderIterator.this"),
+ // [SPARK-44863][UI] Add a button to download thread dump as a txt in
Spark UI
+
ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.spark.status.api.v1.ThreadStackTrace.*"),
+
ProblemFilters.exclude[MissingTypesProblem]("org.apache.spark.status.api.v1.ThreadStackTrace$")
)
// Default exclude rules
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]