/**
* Definitions for the CellMonitor object.
* @module CellMonitor
*/
import $ from 'jquery'; // For manipulating the DOM
import 'jquery-ui-bundle';
import 'jquery-ui-bundle/jquery-ui.css';
import './styles.css'; // CSS styles
import './jobtable.css'; // CSS specific to job table
import moment from 'moment'; // For handling durations
import 'moment-duration-format'; // Plugin for moment to format durations to strings
import { ServerConnection } from '@jupyterlab/services';
import { URLExt } from '@jupyterlab/coreutils';
import JobTimeline from './Timeline';
import TaskChart from './TaskChart';
import 'kuende-livestamp'; // Used for displaying auto-updating timestamps like '5 mins ago'
export default class CellMonitor {
/**
* Class this implements a monitoring display for a single cell.
* @constructor
* @param {SparkMonitor} monitor - The parent singleton SparkMonitor instance.
* @param {CodeCell} cell - The Jupyter CodeCell instance of the cell.
*/
constructor(monitor, cell) {
this.monitor = monitor; // Parent SparkMonitor instance
this.cell = cell;
this.view = 'jobs'; // The current display tab -- "jobs" || "timeline" || "tasks"
this.lastview = 'jobs'; // The previous display tab, used for restoring hidden display
this.baseurl = ServerConnection.makeSettings().baseUrl;
this.init();
// Listen to event for cell finished executing
this.monitor.kernel.statusChanged.connect((sender, status) => {
if (status === 'idle') {
this.onCellExecutionCompleted();
}
});
}
init() {
this.initialDisplayCreated = false; // Used by jobstart event to show display first time
this.displayVisible = false; // Used to toggle display
this.cellcompleted = false; // Cell has finished executing
this.allcompleted = false; // All job end messages have arrived for cell.
this.displayElement = null; // HTML DOM element of the monitor
this.cellStartTime = new Date(); // This is only from the frontend
this.cellEndTime = -1;
this.badgesmodified = false; // Used to refresh counts only if changed.
this.badgeInterval = null; // The return value of setInterval
// Values for badge counters in the title of display
this.numActiveJobs = 0;
this.numCompletedJobs = 0;
this.numFailedJobs = 0;
// Job Table Data----------------------------------
this.jobData = {};
this.stageData = {};
this.stageIdtoJobId = {};
// Only if the load successfully create these views.
this.timeline = new JobTimeline(this);
this.taskchart = new TaskChart(this);
// Our template HTML
this.template = document.createElement('div');
this.template.innerHTML = `
')
.text(data.name)
.html()
.split(' ')[0]; // Escaping HTML <, > from string
this.jobData[data.jobId] = {
id: data.jobId,
start: new Date(data.submissionTime),
name,
status: data.status,
stageIds: data.stageIds,
numTasks: data.numTasks,
numActiveTasks: 0,
numCompletedTasks: 0,
numFailedTasks: 0,
numStages: data.stageIds.length,
numActiveStages: 0,
numCompletedStages: 0,
numFailedStages: 0,
numSkippedStages: 0,
modified: true,
};
data.stageIds.forEach(stageid => {
if (!this.stageIdtoJobId[stageid]) this.stageIdtoJobId[stageid] = [];
this.stageIdtoJobId[stageid].push(data.jobId);
const stageName = $('
')
.text(data.stageInfos[stageid].name)
.html()
.split(' ')[0]; // Hack for escaping HTML <, > from string.
this.stageData[stageid] = {
id: stageid,
status: 'PENDING',
job: data.jobId,
name: stageName,
numTasks: data.stageInfos[stageid].numTasks,
numActiveTasks: 0,
numCompletedTasks: 0,
numFailedTasks: 0,
modified: true,
};
});
if (name === 'null') {
const laststageid = Math.max.apply(null, data.stageIds);
this.jobData[data.jobId].name = this.stageData[laststageid].name;
}
if (!this.initialDisplayCreated) {
this.createDisplay();
this.initialDisplayCreated = true;
}
if (this.timeline) this.timeline.onSparkJobStart(data);
if (this.taskchart) this.taskchart.onSparkJobStart(data);
}
/** Called when a Spark job ends. */
onSparkJobEnd(data) {
this.jobData[data.jobId].status = data.status;
this.jobData[data.jobId].stageIds.forEach(stageid => {
if (this.stageData[stageid].status === 'PENDING') {
this.stageData[stageid].status = 'SKIPPED';
this.jobData[data.jobId].numSkippedStages += 1;
this.jobData[data.jobId].numStages -= 1;
this.stageData[stageid].modified = true;
this.jobData[data.jobId].numTasks -= this.stageData[stageid].numTasks;
}
});
this.numActiveJobs -= 1;
if (data.status === 'SUCCEEDED') {
this.numCompletedJobs += 1;
this.jobData[data.jobId].status = 'COMPLETED';
} else {
this.numFailedJobs += 1;
this.jobData[data.jobId].status = 'FAILED';
}
this.badgesmodified = true;
this.jobData[data.jobId].end = new Date(data.completionTime);
this.jobData[data.jobId].modified = true;
if (this.timeline) this.timeline.onSparkJobEnd(data);
if (this.taskchart) this.taskchart.onSparkJobEnd(data);
if (this.numActiveJobs === 0 && this.cellcompleted && !this.allcompleted) {
this.onAllCompleted();
}
}
/** Called when a Spark stage is submitted. */
onSparkStageSubmitted(data) {
const name = $('
')
.text(data.name)
.html()
.split(' ')[0]; // Hack for escaping HTML <, > from string.
let submissionDate;
if (data.submissionTime === -1) submissionDate = new Date();
else submissionDate = new Date(data.submissionTime);
this.stageIdtoJobId[data.stageId].forEach(jobId => {
this.jobData[jobId].numActiveStages += 1;
this.jobData[jobId].modified = true;
});
this.stageData[data.stageId].status = 'RUNNING';
this.stageData[data.stageId].name = name;
this.stageData[data.stageId].start = submissionDate;
this.stageData[data.stageId].numTasks = data.numTasks;
this.stageData[data.stageId].modified = true;
if (this.timeline) this.timeline.onSparkStageSubmitted(data);
}
/** Called when a Spark stage is completed. */
onSparkStageCompleted(data) {
this.stageIdtoJobId[data.stageId].forEach(jobId => {
this.jobData[jobId].numActiveStages -= 1;
this.jobData[jobId].modified = true;
if (data.status === 'COMPLETED') {
this.jobData[jobId].numCompletedStages += 1;
} else {
this.jobData[jobId].numFailedStages += 1;
}
});
this.stageData[data.stageId].status = data.status;
this.stageData[data.stageId].start = new Date(data.submissionTime);
this.stageData[data.stageId].end = new Date(data.completionTime);
this.stageData[data.stageId].modified = true;
if (this.timeline) this.timeline.onSparkStageCompleted(data);
}
/** Called when a Spark task is started. */
onSparkTaskStart(data) {
this.stageData[data.stageId].numActiveTasks += 1;
this.stageData[data.stageId].firsttaskstart = new Date(data.launchTime);
this.stageData[data.stageId].modified = true;
this.stageIdtoJobId[data.stageId].forEach(jobId => {
this.jobData[jobId].numActiveTasks += 1;
this.jobData[jobId].modified = true;
});
if (this.timeline) this.timeline.onSparkTaskStart(data);
if (this.taskchart) this.taskchart.onSparkTaskStart(data);
}
/** Called when a Spark task is ended. */
onSparkTaskEnd(data) {
this.stageData[data.stageId].numActiveTasks -= 1;
this.stageData[data.stageId].modified = true;
if (data.status === 'SUCCESS') {
this.stageData[data.stageId].numCompletedTasks += 1;
} else {
this.stageData[data.stageId].numFailedTasks += 1;
}
this.stageIdtoJobId[data.stageId].forEach(jobId => {
this.jobData[jobId].numActiveTasks -= 1;
this.jobData[jobId].modified = true;
if (data.status === 'SUCCESS') {
this.jobData[jobId].numCompletedTasks += 1;
} else {
this.jobData[jobId].numFailedTasks += 1;
}
});
if (this.timeline) this.timeline.onSparkTaskEnd(data);
if (this.taskchart) this.taskchart.onSparkTaskEnd(data);
}
/** Called when an executor is added to spark */
onSparkExecutorAdded() {
this.badgesmodified = true;
}
/** Called when an executor is removed from spark */
onSparkExecutorRemoved() {
this.badgesmodified = true;
}
}