-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-29599][WEBUI] Support pagination for session table in JDBC/ODBC Tab #26253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
dfa225b
f209cb4
43a1621
c7c9479
ce588e5
59697bc
691eed1
5315c58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,33 +139,60 @@ private[ui] class ThriftServerPage(parent: ThriftServerTab) extends WebUIPage("" | |
|
|
||
| /** Generate stats of batch sessions of the thrift server program */ | ||
| private def generateSessionStatsTable(request: HttpServletRequest): Seq[Node] = { | ||
| val sessionList = listener.getSessionList | ||
| val numBatches = sessionList.size | ||
| val table = if (numBatches > 0) { | ||
| val dataRows = sessionList.sortBy(_.startTimestamp).reverse | ||
| val headerRow = Seq("User", "IP", "Session ID", "Start Time", "Finish Time", "Duration", | ||
| "Total Execute") | ||
| def generateDataRow(session: SessionInfo): Seq[Node] = { | ||
| val sessionLink = "%s/%s/session/?id=%s".format( | ||
| UIUtils.prependBaseUri(request, parent.basePath), parent.prefix, session.sessionId) | ||
| <tr> | ||
| <td> {session.userName} </td> | ||
| <td> {session.ip} </td> | ||
| <td> <a href={sessionLink}> {session.sessionId} </a> </td> | ||
| <td> {formatDate(session.startTimestamp)} </td> | ||
| <td> {if (session.finishTimestamp > 0) formatDate(session.finishTimestamp)} </td> | ||
| <td sorttable_customkey={session.totalTime.toString}> | ||
| {formatDurationOption(Some(session.totalTime))} </td> | ||
| <td> {session.totalExecution.toString} </td> | ||
| </tr> | ||
| val numSessions = listener.getSessionList.size | ||
| val table = if (numSessions > 0) { | ||
|
|
||
| val sessionTableTag = "sessionstat" | ||
|
|
||
| val parameterOtherTable = request.getParameterMap().asScala | ||
| .filterNot(_._1.startsWith(sessionTableTag)) | ||
| .map { case (name, vals) => | ||
| name + "=" + vals(0) | ||
| } | ||
|
|
||
| val parameterSessionTablePage = request.getParameter(s"$sessionTableTag.page") | ||
| val parameterSessionTableSortColumn = request.getParameter(s"$sessionTableTag.sort") | ||
| val parameterSessionTableSortDesc = request.getParameter(s"$sessionTableTag.desc") | ||
| val parameterSessionPageSize = request.getParameter(s"$sessionTableTag.pageSize") | ||
|
|
||
| val sessionTablePage = Option(parameterSessionTablePage).map(_.toInt).getOrElse(1) | ||
| val sessionTableSortColumn = Option(parameterSessionTableSortColumn).map { sortColumn => | ||
| UIUtils.decodeURLParameter(sortColumn) | ||
| }.getOrElse("Start Time") | ||
| val sessionTableSortDesc = Option(parameterSessionTableSortDesc).map(_.toBoolean).getOrElse( | ||
| // Old session should be shown above new session by default. | ||
| !(sessionTableSortColumn == "Start Time") | ||
| ) | ||
| val sessionTablePageSize = Option(parameterSessionPageSize).map(_.toInt).getOrElse(100) | ||
|
|
||
| try { | ||
| Some(new SessionStatsPagedTable( | ||
| request, | ||
| parent, | ||
| listener.getSessionList, | ||
| "sqlserver", | ||
| UIUtils.prependBaseUri(request, parent.basePath), | ||
| parameterOtherTable, | ||
| sessionTableTag, | ||
| pageSize = sessionTablePageSize, | ||
| sortColumn = sessionTableSortColumn, | ||
| desc = sessionTableSortDesc | ||
| ).table(sessionTablePage)) | ||
| } catch { | ||
| case e@(_: IllegalArgumentException | _: IndexOutOfBoundsException) => | ||
| Some(<div class="alert alert-error"> | ||
| <p>Error while rendering job table:</p> | ||
| <pre> | ||
| {Utils.exceptionString(e)} | ||
| </pre> | ||
| </div>) | ||
| } | ||
| Some(UIUtils.listingTable(headerRow, generateDataRow, dataRows, true, None, Seq(null), false)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Here for the sessions table
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Changed. Not so familiar with UI HTML part, thanks a lot for your suggestion |
||
| } else { | ||
| None | ||
| } | ||
|
|
||
| val content = | ||
| <h5 id="sessionstat">Session Statistics ({numBatches})</h5> ++ | ||
| <h5 id="sessionstat">Session Statistics ({numSessions})</h5> ++ | ||
| <div> | ||
| <ul class="unstyled"> | ||
| {table.getOrElse("No statistics have been generated yet.")} | ||
|
|
@@ -377,6 +404,164 @@ private[ui] class SqlStatsPagedTable( | |
| "%s/jobs/job/?id=%s".format(UIUtils.prependBaseUri(request, parent.basePath), jobId) | ||
| } | ||
|
|
||
| private[ui] class SessionStatsPagedTable( | ||
| request: HttpServletRequest, | ||
| parent: ThriftServerTab, | ||
| data: Seq[SessionInfo], | ||
| subPath: String, | ||
| basePath: String, | ||
| parameterOtherTable: Iterable[String], | ||
| sessionStatsTableTag: String, | ||
| pageSize: Int, | ||
| sortColumn: String, | ||
| desc: Boolean) extends PagedTable[SessionInfo] { | ||
|
|
||
| override val dataSource = new SessionStatsTableDataSource(data, pageSize, sortColumn, desc) | ||
|
|
||
| private val parameterPath = s"$basePath/$subPath/?${parameterOtherTable.mkString("&")}" | ||
|
|
||
| override def tableId: String = sessionStatsTableTag | ||
|
|
||
| override def tableCssClass: String = | ||
| "table table-bordered table-condensed table-striped " + | ||
| "table-head-clickable table-cell-width-limited" | ||
|
|
||
| override def pageLink(page: Int): String = { | ||
| val encodedSortColumn = URLEncoder.encode(sortColumn, UTF_8.name()) | ||
| parameterPath + | ||
| s"&$pageNumberFormField=$page" + | ||
| s"&$sessionStatsTableTag.sort=$encodedSortColumn" + | ||
| s"&$sessionStatsTableTag.desc=$desc" + | ||
| s"&$pageSizeFormField=$pageSize" | ||
| } | ||
|
|
||
| override def pageSizeFormField: String = s"$sessionStatsTableTag.pageSize" | ||
|
|
||
| override def pageNumberFormField: String = s"$sessionStatsTableTag.page" | ||
|
|
||
| override def goButtonFormPath: String = { | ||
| val encodedSortColumn = URLEncoder.encode(sortColumn, UTF_8.name()) | ||
| s"$parameterPath&$sessionStatsTableTag.sort=$encodedSortColumn&$sessionStatsTableTag.desc=$desc" | ||
| } | ||
|
|
||
| override def headers: Seq[Node] = { | ||
| val sessionTableHeaders = | ||
| Seq("User", "IP", "Session ID", "Start Time", "Finish Time", "Duration", "Total Execute") | ||
|
|
||
| val tooltips = Seq[Option[String]](None, None, None, None, None, None, None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ok for me to remove these. If we all think don't need and won't add some thing, just remove it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is already a PR working on it, related to tool tips here, #26138
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Very embarrassed..., for me , this table's information is so clear. Don't add tooltip is ok. |
||
|
|
||
| assert(sessionTableHeaders.length == tooltips.length) | ||
|
|
||
| val headerRow: Seq[Node] = { | ||
| sessionTableHeaders.zip(tooltips).map { case (header, tooltip) => | ||
| if (header == sortColumn) { | ||
| val headerLink = Unparsed( | ||
| parameterPath + | ||
| s"&$sessionStatsTableTag.sort=${URLEncoder.encode(header, UTF_8.name())}" + | ||
| s"&$sessionStatsTableTag.desc=${!desc}" + | ||
| s"&$sessionStatsTableTag.pageSize=$pageSize" + | ||
| s"#$sessionStatsTableTag") | ||
| val arrow = if (desc) "▾" else "▴" // UP or DOWN | ||
|
|
||
| if (tooltip.nonEmpty) { | ||
| <th> | ||
| <a href={headerLink}> | ||
| <span data-toggle="tooltip" title={tooltip.get}> | ||
| {header} {Unparsed(arrow)} | ||
| </span> | ||
| </a> | ||
| </th> | ||
| } else { | ||
| <th> | ||
| <a href={headerLink}> | ||
| {header} {Unparsed(arrow)} | ||
| </a> | ||
| </th> | ||
| } | ||
| } else { | ||
| val headerLink = Unparsed( | ||
| parameterPath + | ||
| s"&$sessionStatsTableTag.sort=${URLEncoder.encode(header, UTF_8.name())}" + | ||
| s"&$sessionStatsTableTag.pageSize=$pageSize" + | ||
| s"#$sessionStatsTableTag") | ||
|
|
||
| if(tooltip.nonEmpty) { | ||
| <th> | ||
| <a href={headerLink}> | ||
| <span data-toggle="tooltip" title={tooltip.get}> | ||
| {header} | ||
| </span> | ||
| </a> | ||
| </th> | ||
| } else { | ||
| <th> | ||
| <a href={headerLink}> | ||
| {header} | ||
| </a> | ||
| </th> | ||
| } | ||
| } | ||
| } | ||
| } | ||
| <thead> | ||
| {headerRow} | ||
| </thead> | ||
| } | ||
|
|
||
| override def row(session: SessionInfo): Seq[Node] = { | ||
| val sessionLink = "%s/%s/session/?id=%s".format( | ||
| UIUtils.prependBaseUri(request, parent.basePath), parent.prefix, session.sessionId) | ||
| <tr> | ||
| <td> {session.userName} </td> | ||
| <td> {session.ip} </td> | ||
| <td> <a href={sessionLink}> {session.sessionId} </a> </td> | ||
| <td> {formatDate(session.startTimestamp)} </td> | ||
| <td> {if (session.finishTimestamp > 0) formatDate(session.finishTimestamp)} </td> | ||
| <td sorttable_customkey={session.totalTime.toString}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Is it necessary to add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This seems to be the previous pr, change to use raw data for sort column, old version didn't have this and trouble me a lot. Don't need it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, please confirm if the sorting is working properly after changing this. Thanks
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Checked ok. Thanks |
||
| {formatDurationOption(Some(session.totalTime))} </td> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, we can directly call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good suggestion. Removed other old unused code, feel comfortable now. |
||
| <td> {session.totalExecution.toString} </td> | ||
| </tr> | ||
| } | ||
|
|
||
| /** | ||
| * Returns a human-readable string representing a duration such as "5 second 35 ms" | ||
| */ | ||
| private def formatDurationOption(msOption: Option[Long]): String = { | ||
| msOption.map(formatDurationVerbose).getOrElse("-") | ||
| } | ||
|
|
||
| private def errorMessageCell(errorMessage: String): Seq[Node] = { | ||
| val isMultiline = errorMessage.indexOf('\n') >= 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this method used anywhere?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
All remove |
||
| val errorSummary = StringEscapeUtils.escapeHtml4( | ||
| if (isMultiline) { | ||
| errorMessage.substring(0, errorMessage.indexOf('\n')) | ||
| } else { | ||
| errorMessage | ||
| }) | ||
| val details = if (isMultiline) { | ||
| // scalastyle:off | ||
| <span onclick="this.parentNode.querySelector('.stacktrace-details').classList.toggle('collapsed')" | ||
| class="expand-details"> | ||
| + details | ||
| </span> ++ | ||
| <div class="stacktrace-details collapsed"> | ||
| <pre> | ||
| {errorMessage} | ||
| </pre> | ||
| </div> | ||
| // scalastyle:on | ||
| } else { | ||
| "" | ||
| } | ||
| <td> | ||
| {errorSummary}{details} | ||
| </td> | ||
| } | ||
|
|
||
| private def jobURL(request: HttpServletRequest, jobId: String): String = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method also seems not used? |
||
| "%s/jobs/job/?id=%s".format(UIUtils.prependBaseUri(request, parent.basePath), jobId) | ||
| } | ||
|
|
||
| private[ui] class SqlStatsTableRow( | ||
| val jobId: Seq[String], | ||
| val duration: Long, | ||
|
|
@@ -442,3 +627,45 @@ private[ui] class SqlStatsPagedTable( | |
| } | ||
|
|
||
| } | ||
|
|
||
| private[ui] class SessionStatsTableDataSource( | ||
| info: Seq[SessionInfo], | ||
| pageSize: Int, | ||
| sortColumn: String, | ||
| desc: Boolean) extends PagedDataSource[SessionInfo](pageSize) { | ||
|
|
||
| // Sorting SessionInfo data | ||
| private val data = info.sorted(ordering(sortColumn, desc)) | ||
|
|
||
| private var _slicedStartTime: Set[Long] = null | ||
|
|
||
| override def dataSize: Int = data.size | ||
|
|
||
| override def sliceData(from: Int, to: Int): Seq[SessionInfo] = { | ||
| val r = data.slice(from, to) | ||
| r.map(x => x) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: what does this do?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For paging, get selected pages data.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I mean,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Same implement with #26215 Since same function... so don't check this method.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we can remove the line. |
||
| _slicedStartTime = r.map(_.startTimestamp).toSet | ||
| r | ||
| } | ||
|
|
||
| /** | ||
| * Return Ordering according to sortColumn and desc. | ||
| */ | ||
| private def ordering(sortColumn: String, desc: Boolean): Ordering[SessionInfo] = { | ||
| val ordering: Ordering[SessionInfo] = sortColumn match { | ||
| case "User" => Ordering.by(_.userName) | ||
| case "IP" => Ordering.by(_.ip) | ||
| case "Session ID" => Ordering.by(_.sessionId) | ||
| case "Start Time" => Ordering by (_.startTimestamp) | ||
| case "Finish Time" => Ordering.by(_.finishTimestamp) | ||
| case "Duration" => Ordering.by(_.totalTime) | ||
| case "Total Execute" => Ordering.by(_.totalExecution) | ||
| case unknownColumn => throw new IllegalArgumentException(s"Unknown column: $unknownColumn") | ||
| } | ||
| if (desc) { | ||
| ordering.reverse | ||
| } else { | ||
| ordering | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AngersZhuuuu By default new session should come above right? (Screenshot from master branch)

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, I could have been misled by the modified thriftserver I was using