Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
<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 @@ -2168,29 +2168,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 @@ -72,7 +72,10 @@ object MimaExcludes {
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.api.java.function.FlatMapGroupsWithStateFunction"),
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.api.java.function.MapGroupsWithStateFunction"),
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.sql.SaveMode"),
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.sql.streaming.GroupState")
ProblemFilters.exclude[MissingClassProblem]("org.apache.spark.sql.streaming.GroupState"),
// [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