-
Notifications
You must be signed in to change notification settings - Fork 28.9k
[SPARK-4145] Web UI job pages #3009
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 all commits
2568a6c
4487dcb
bfce2b9
4b206fb
45343b8
a475ea1
56701fa
1cf4987
85e9c85
4d58e55
4846ce4
b7bf30e
8a2351b
3d0a007
1145c60
d62ea7b
79793cd
5884f91
8ab6c28
8955f4c
e2f2c43
171b53c
f2a15da
5eb39dc
d69c775
7d10b97
67080ba
eebdc2c
034aa8d
0b77e3e
1f45d44
61c265a
2bbf41a
6f17f3f
ff804cd
b89c258
f00c851
eb05e90
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 |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark.ui.jobs | ||
|
|
||
| import scala.xml.{Node, NodeSeq} | ||
|
|
||
| import javax.servlet.http.HttpServletRequest | ||
|
|
||
| import org.apache.spark.JobExecutionStatus | ||
| import org.apache.spark.ui.{WebUIPage, UIUtils} | ||
| import org.apache.spark.ui.jobs.UIData.JobUIData | ||
|
|
||
| /** Page showing list of all ongoing and recently finished jobs */ | ||
| private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { | ||
| private val startTime: Option[Long] = parent.sc.map(_.startTime) | ||
| private val listener = parent.listener | ||
|
|
||
| private def jobsTable(jobs: Seq[JobUIData]): Seq[Node] = { | ||
| val someJobHasJobGroup = jobs.exists(_.jobGroup.isDefined) | ||
|
|
||
| val columns: Seq[Node] = { | ||
| <th>{if (someJobHasJobGroup) "Job Id (Job Group)" else "Job Id"}</th> | ||
| <th>Description</th> | ||
| <th>Submitted</th> | ||
| <th>Duration</th> | ||
| <th class="sorttable_nosort">Stages: Succeeded/Total</th> | ||
| <th class="sorttable_nosort">Tasks (for all stages): Succeeded/Total</th> | ||
| } | ||
|
|
||
| def makeRow(job: JobUIData): Seq[Node] = { | ||
| val lastStageInfo = listener.stageIdToInfo.get(job.stageIds.max) | ||
| val lastStageData = lastStageInfo.flatMap { s => | ||
| listener.stageIdToData.get((s.stageId, s.attemptId)) | ||
| } | ||
|
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. Can you move the code to compute the last stage's name up here? It was hard for me to figure out why you were grabbing the last stage here. |
||
| val isComplete = job.status == JobExecutionStatus.SUCCEEDED | ||
| val lastStageName = lastStageInfo.map(_.name).getOrElse("(Unknown Stage Name)") | ||
| val lastStageDescription = lastStageData.flatMap(_.description).getOrElse("") | ||
| val duration: Option[Long] = { | ||
| job.startTime.map { start => | ||
| val end = job.endTime.getOrElse(System.currentTimeMillis()) | ||
| end - start | ||
| } | ||
| } | ||
| val formattedDuration = duration.map(d => UIUtils.formatDuration(d)).getOrElse("Unknown") | ||
| val formattedSubmissionTime = job.startTime.map(UIUtils.formatDate).getOrElse("Unknown") | ||
|
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 realize we use in
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. In other places, we have "Unknown stage name", etc; I'm not sure that this is a huge win (it would be beneficial if we decided to localize, though, but we're not doing that here). |
||
| val detailUrl = | ||
| "%s/jobs/job?id=%s".format(UIUtils.prependBaseUri(parent.basePath), job.jobId) | ||
| <tr> | ||
| <td sorttable_customkey={job.jobId.toString}> | ||
| {job.jobId} {job.jobGroup.map(id => s"($id)").getOrElse("")} | ||
| </td> | ||
| <td> | ||
| <div><em>{lastStageDescription}</em></div> | ||
| <a href={detailUrl}>{lastStageName}</a> | ||
| </td> | ||
| <td sorttable_customkey={job.startTime.getOrElse(-1).toString}> | ||
| {formattedSubmissionTime} | ||
| </td> | ||
| <td sorttable_customkey={duration.getOrElse(-1).toString}>{formattedDuration}</td> | ||
| <td class="stage-progress-cell"> | ||
| {job.completedStageIndices.size}/{job.stageIds.size - job.numSkippedStages} | ||
| {if (job.numFailedStages > 0) s"(${job.numFailedStages} failed)"} | ||
| {if (job.numSkippedStages > 0) s"(${job.numSkippedStages} skipped)"} | ||
| </td> | ||
| <td class="progress-cell"> | ||
| {UIUtils.makeProgressBar(started = job.numActiveTasks, completed = job.numCompletedTasks, | ||
| failed = job.numFailedTasks, skipped = job.numSkippedTasks, | ||
| total = job.numTasks - job.numSkippedTasks)} | ||
| </td> | ||
| </tr> | ||
| } | ||
|
|
||
| <table class="table table-bordered table-striped table-condensed sortable"> | ||
| <thead>{columns}</thead> | ||
| <tbody> | ||
| {jobs.map(makeRow)} | ||
| </tbody> | ||
| </table> | ||
| } | ||
|
|
||
| def render(request: HttpServletRequest): Seq[Node] = { | ||
| listener.synchronized { | ||
| val activeJobs = listener.activeJobs.values.toSeq | ||
| val completedJobs = listener.completedJobs.reverse.toSeq | ||
| val failedJobs = listener.failedJobs.reverse.toSeq | ||
| val now = System.currentTimeMillis | ||
|
|
||
| val activeJobsTable = | ||
| jobsTable(activeJobs.sortBy(_.startTime.getOrElse(-1L)).reverse) | ||
| val completedJobsTable = | ||
| jobsTable(completedJobs.sortBy(_.endTime.getOrElse(-1L)).reverse) | ||
| val failedJobsTable = | ||
| jobsTable(failedJobs.sortBy(_.endTime.getOrElse(-1L)).reverse) | ||
|
|
||
| val summary: NodeSeq = | ||
| <div> | ||
| <ul class="unstyled"> | ||
| {if (startTime.isDefined) { | ||
| // Total duration is not meaningful unless the UI is live | ||
| <li> | ||
| <strong>Total Duration: </strong> | ||
| {UIUtils.formatDuration(now - startTime.get)} | ||
| </li> | ||
| }} | ||
| <li> | ||
| <strong>Scheduling Mode: </strong> | ||
| {listener.schedulingMode.map(_.toString).getOrElse("Unknown")} | ||
| </li> | ||
| <li> | ||
| <a href="#active"><strong>Active Jobs:</strong></a> | ||
| {activeJobs.size} | ||
| </li> | ||
| <li> | ||
| <a href="#completed"><strong>Completed Jobs:</strong></a> | ||
| {completedJobs.size} | ||
| </li> | ||
| <li> | ||
| <a href="#failed"><strong>Failed Jobs:</strong></a> | ||
| {failedJobs.size} | ||
| </li> | ||
| </ul> | ||
| </div> | ||
|
|
||
| val content = summary ++ | ||
| <h4 id="active">Active Jobs ({activeJobs.size})</h4> ++ activeJobsTable ++ | ||
| <h4 id="completed">Completed Jobs ({completedJobs.size})</h4> ++ completedJobsTable ++ | ||
| <h4 id ="failed">Failed Jobs ({failedJobs.size})</h4> ++ failedJobsTable | ||
|
|
||
| val helpText = """A job is triggered by a action, like "count()" or "saveAsTextFile()".""" + | ||
|
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. minor: why not just escape the
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. I don't care either way; do you have a strong opinion on this or can I just leave it as is?
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. you can leave it |
||
| " Click on a job's title to see information about the stages of tasks associated with" + | ||
| " the job." | ||
|
|
||
| UIUtils.headerSparkPage("Spark Jobs", content, parent, helpText = Some(helpText)) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ import org.apache.spark.scheduler.Schedulable | |
| import org.apache.spark.ui.{WebUIPage, UIUtils} | ||
|
|
||
| /** Page showing list of all ongoing and recently finished stages and pools */ | ||
| private[ui] class JobProgressPage(parent: JobProgressTab) extends WebUIPage("") { | ||
| private[ui] class AllStagesPage(parent: StagesTab) extends WebUIPage("") { | ||
|
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 naming change is great |
||
| private val sc = parent.sc | ||
| private val listener = parent.listener | ||
| private def isFairScheduler = parent.isFairScheduler | ||
|
|
@@ -41,11 +41,14 @@ private[ui] class JobProgressPage(parent: JobProgressTab) extends WebUIPage("") | |
|
|
||
| val activeStagesTable = | ||
| new StageTableBase(activeStages.sortBy(_.submissionTime).reverse, | ||
| parent, parent.killEnabled) | ||
| parent.basePath, parent.listener, isFairScheduler = parent.isFairScheduler, | ||
| killEnabled = parent.killEnabled) | ||
| val completedStagesTable = | ||
| new StageTableBase(completedStages.sortBy(_.submissionTime).reverse, parent) | ||
| new StageTableBase(completedStages.sortBy(_.submissionTime).reverse, parent.basePath, | ||
| parent.listener, isFairScheduler = parent.isFairScheduler, killEnabled = false) | ||
| val failedStagesTable = | ||
| new FailedStageTable(failedStages.sortBy(_.submissionTime).reverse, parent) | ||
| new FailedStageTable(failedStages.sortBy(_.submissionTime).reverse, parent.basePath, | ||
| parent.listener, isFairScheduler = parent.isFairScheduler) | ||
|
|
||
| // For now, pool information is only accessible in live UIs | ||
| val pools = sc.map(_.getAllPools).getOrElse(Seq.empty[Schedulable]) | ||
|
|
@@ -93,7 +96,7 @@ private[ui] class JobProgressPage(parent: JobProgressTab) extends WebUIPage("") | |
| <h4 id ="failed">Failed Stages ({numFailedStages})</h4> ++ | ||
| failedStagesTable.toNodeSeq | ||
|
|
||
| UIUtils.headerSparkPage("Spark Stages", content, parent) | ||
| UIUtils.headerSparkPage("Spark Stages (for all jobs)", content, parent) | ||
| } | ||
| } | ||
| } | ||
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.
By the way, I moved this here to avoid a race-condition where a user could browse to the web UI before this field was initialized. There didn't seem to be any particular reason for it to be here as opposed to anywhere else.