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
42 changes: 41 additions & 1 deletion core/src/main/scala/org/apache/spark/status/api/v1/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,47 @@ case class ThreadStackTrace(
val stackTrace: StackTrace,
val blockedByThreadId: Option[Long],
val blockedByLock: String,
val holdingLocks: Seq[String])
@deprecated("using synchronizers and monitors instead", "4.0.0")
val holdingLocks: Seq[String],
val synchronizers: Seq[String],
val monitors: Seq[String],
val lockName: Option[String],
val lockOwnerName: Option[String],
val suspended: Boolean,
val inNative: Boolean) {

/**
* Returns a string representation of this thread stack trace
* w.r.t java.lang.management.ThreadInfo(JDK 8)'s toString.
*
* TODO(Kent Yao): Considering 'daemon', 'priority' from higher JDKs
*
* TODO(Kent Yao): Also considering adding information os_prio, cpu, elapsed, tid, nid, etc.,
* from the jstack tool
*/
override def toString: String = {
val sb = new StringBuilder()
val basic = "\"" + threadName + "\" Id=" + threadId + " " + threadState
Copy link
Contributor

Choose a reason for hiding this comment

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

val basic = s""""$threadName" Id=$threadId $threadState"""
sb.append(basic)

or

sb.append("\"").append(threadName).append("\" Id=").append(threadId).append(" ").append(threadState)
// sb.append("\"", threadName, "\" Id=", threadId, " ", threadState) multiple parameters api needs to wait until only Scala 2.13+ is supported.

The former has better readability, while the latter can avoid creating the intermediate string basic.

Copy link
Member Author

Choose a reason for hiding this comment

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

I choose another simple way val sb = new StringBuilder(s""""$threadName" Id=$threadId $threadState""")

sb.append(basic)
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