Skip to content
6 changes: 6 additions & 0 deletions core/src/main/resources/org/apache/spark/ui/static/webui.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
52 changes: 45 additions & 7 deletions core/src/main/scala/org/apache/spark/status/api/v1/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"}
Expand All @@ -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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about adding "application id" as part of the file name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When it comes to distinguishing files, there are several options such as using app id, attempt id, timestamps, executor details, and more to name the files. However, I prefer to keep it simple to allow for easy renaming when users click the button.

<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>
Expand Down
49 changes: 29 additions & 20 deletions core/src/main/scala/org/apache/spark/util/Utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
5 changes: 4 additions & 1 deletion project/MimaExcludes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down