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 f952f86503e3..ca7c1f8ba65e 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 8d648b9df38f..3e4e2f17a77e 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 c3246dc90976..0be9df921d1b 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( 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(", ") Updated at {UIUtils.formatDate(time)}

{ // scalastyle:off -

- Expand All -

-

- Collapse All -

-
-
-
-
- - +
+ Expand All + Collapse All + Download +
+
+
+
+ + +
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 c211d6bba5df..7e4d9b78af2d 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 f818d9aa2c8a..6200f24eed1c 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