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?
    
    
![image](https://github.com/apache/spark/assets/8326978/86c8e87d-970d-4ddb-967e-20c1d3534d42)
    
    ### 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]

Reply via email to