From 046aa1f8fde8386e61f38e76fd12483d4e919eeb Mon Sep 17 00:00:00 2001 From: Gaetan Lepage <gaetan@glepage.com> Date: Fri, 16 Dec 2022 10:50:26 +0100 Subject: [PATCH] waiting list --- backend/cluster.py | 153 +++++++++++++++------ public/cluster.js | 267 ------------------------------------ public/css/style.css | 1 + public/css/waiting_list.css | 57 ++++++++ public/index.html | 2 +- public/js/cluster.js | 56 ++++++++ public/js/gpu.js | 187 +++++++++++++++++++++++++ public/js/main.js | 38 +++++ public/js/node.js | 39 ++++++ public/{ => js}/utils.js | 29 +++- public/js/waiting_list.js | 108 +++++++++++++++ public/main.js | 30 ---- 12 files changed, 621 insertions(+), 346 deletions(-) delete mode 100644 public/cluster.js create mode 100644 public/css/waiting_list.css create mode 100644 public/js/cluster.js create mode 100644 public/js/gpu.js create mode 100644 public/js/main.js create mode 100644 public/js/node.js rename public/{ => js}/utils.js (63%) create mode 100644 public/js/waiting_list.js delete mode 100644 public/main.js diff --git a/backend/cluster.py b/backend/cluster.py index c3338eb..cee075e 100644 --- a/backend/cluster.py +++ b/backend/cluster.py @@ -3,7 +3,7 @@ import subprocess from datetime import datetime, timedelta import logging import re -from typing import Optional +from typing import Optional, Any from matplotlib import cm # type: ignore import numpy as np @@ -17,6 +17,7 @@ PRETTY_GPU_MODELS: dict[str, str] = { 'rtx8000': 'Quadro RTX 8000' } +TIME_FORMAT: str = '%m/%d %H:%M:%S' USERNAMES: list[str] = [ 'aballou', @@ -31,7 +32,7 @@ USERNAMES: list[str] = [ 'dmeng', 'wguo', 'xbie', - 'adupless', + 'UNKNOWN', 'bbasavas', ] @@ -52,6 +53,10 @@ USER_COLORS: dict[str, str] = { ) } +# keys: job_id +# value: dict containing 1 job information +JobDict = dict[str, dict[str, Any]] + def _run_remote_command_and_fetch_dict(update_cmd: list[str]) -> dict: @@ -80,8 +85,10 @@ def _run_remote_command_and_fetch_dict(update_cmd: list[str]) -> dict: return json.loads(stdout) - raise subprocess.CalledProcessError(returncode=return_code, - cmd=update_cmd) + raise subprocess.CalledProcessError( + returncode=return_code, + cmd=update_cmd + ) except subprocess.TimeoutExpired as timeout_exception: LOGGER.warning(f"`{update_cmd[2]}` command has timed out.") @@ -147,32 +154,39 @@ def _fetch_and_parse_oarnodes_dict() -> tuple[dict, list[str]]: return nodes_dict, jobs_list -def _fetch_and_parse_oarstat_dict(jobs_list: list[str]) -> dict: +############################################ +# OARSTAT (fetch running and waiting jobs) # +############################################ - oarstat_raw_dict: dict = _run_remote_command_and_fetch_dict( - update_cmd=['oarstat', '-J', '--sql', '"state = \'Running\'"']) +def _extract_walltime(message: str) -> str: + match: Optional[re.Match] = re.compile(r"(?<=W=).+?(?=,)").search(message) + + walltime: str = '' + if match: + walltime = ':'.join( + [ + sub.zfill(2) + for sub in match.group().split(':') + ] + ) - oarstat_filtered_dict: dict = {} + return walltime - time_format: str = '%m/%d %H:%M:%S' - def _extract_walltime(message: str) -> str: - match: Optional[re.Match] = re.compile(r"(?<=W=).+?(?=,)").search(message) +def _date_time_from_timestamp(timestamp: str) -> str: + date_time: datetime = datetime.fromtimestamp(int(timestamp)) - assert match is not None - walltime = ':'.join([sub.zfill(2) - for sub in match.group().split(':')]) - return walltime + return date_time.strftime(TIME_FORMAT) - def _date_time_from_timestamp(timestamp: str) -> str: - date_time: datetime = datetime.fromtimestamp(int(timestamp)) - return date_time.strftime(time_format) +def _get_max_time(job_dict: dict) -> str: + start_time: datetime = datetime.fromtimestamp(job_dict['startTime']) - def _get_max_time(job_dict: dict) -> str: - start_time: datetime = datetime.fromtimestamp(job_dict['startTime']) + walltime_str: str = _extract_walltime(job_dict['message']) - hours, mins, secs = _extract_walltime(job_dict['message']).split(':') + max_time_str: str = '' + if walltime_str: + hours, mins, secs = walltime_str.split(':') walltime: timedelta = timedelta( hours=int(hours), minutes=int(mins), @@ -180,34 +194,72 @@ def _fetch_and_parse_oarstat_dict(jobs_list: list[str]) -> dict: ) max_time: datetime = start_time + walltime + max_time_str = max_time.strftime(TIME_FORMAT) + + return max_time_str + + +def _get_requested_gpu(properties: str) -> str: + + requested_gpu: str = '' + match: Optional[re.Match] = re.compile(r"gpu[1-8]").search(properties) + + if match: + requested_gpu = match.group() - return max_time.strftime(time_format) + return requested_gpu - for job_id in jobs_list: - job_dict: dict = oarstat_raw_dict[job_id] - owner: str = job_dict['owner'] +def _fetch_and_parse_oarstat_dict( + jobs_running_on_perception_cluster: list[str] +) -> tuple[JobDict, JobDict]: - oarstat_filtered_dict[job_id] = { - 'id': int(job_dict['Job_Id']), - 'name': job_dict['name'], + oarstat_raw_dict: dict = _run_remote_command_and_fetch_dict( + update_cmd=['oarstat', '-J'] + ) + + running_jobs_dict: JobDict = {} + waiting_jobs_dict: JobDict = {} + + for job_id, job_raw_dict in oarstat_raw_dict.items(): + # job_raw_dict: dict = oarstat_raw_dict[job_id] + if 'perception' not in job_raw_dict['properties']: + continue + + owner: str = job_raw_dict['owner'] + + job_curated_dict: dict[str, Any] = { + 'id': int(job_raw_dict['Job_Id']), + 'name': job_raw_dict['name'], 'owner': owner, - 'type': job_dict['jobType'].lower(), - 'besteffort': 'besteffort' in job_dict['types'], - 'idempotent': 'idempotent' in job_dict['types'], - 'submission_time': _date_time_from_timestamp(job_dict['submissionTime']), - 'start_time': _date_time_from_timestamp(job_dict['startTime']), - 'walltime': _extract_walltime(job_dict['message']), - 'max_time': _get_max_time(job_dict) + 'type': job_raw_dict['jobType'].lower(), + 'besteffort': 'besteffort' in job_raw_dict['types'], + 'idempotent': 'idempotent' in job_raw_dict['types'], + 'submission_time': _date_time_from_timestamp(job_raw_dict['submissionTime']), + 'start_time': _date_time_from_timestamp(job_raw_dict['startTime']), + 'walltime': _extract_walltime(job_raw_dict['message']), + 'max_time': _get_max_time(job_raw_dict), + 'requested_gpu': _get_requested_gpu(job_raw_dict['properties']) } + state: str = job_raw_dict['state'] if owner in USER_COLORS: - oarstat_filtered_dict[job_id]['owner_color'] = USER_COLORS[owner] + job_curated_dict['owner_color'] = USER_COLORS[owner] + else: + job_curated_dict['owner_color'] = USER_COLORS['UNKNOWN'] + + if state == 'Running' and job_id in jobs_running_on_perception_cluster: + running_jobs_dict[job_id] = job_curated_dict + elif state == 'Waiting': + waiting_jobs_dict[job_id] = job_curated_dict - return oarstat_filtered_dict + return running_jobs_dict, waiting_jobs_dict -def _merge(nodes_dict: dict, jobs_dict: dict) -> dict: +def _merge_running_jobs( + nodes_dict: dict, + jobs_dict: JobDict +) -> dict[int, dict]: output_dict: dict = {} @@ -226,13 +278,26 @@ def _merge(nodes_dict: dict, jobs_dict: dict) -> dict: def update() -> dict: - nodes_dict, jobs_list = _fetch_and_parse_oarnodes_dict() - - jobs_dict: dict = _fetch_and_parse_oarstat_dict(jobs_list=jobs_list) + raw_nodes_dict: dict + jobs_running_on_perception_cluster: list[str] + raw_nodes_dict, jobs_running_on_perception_cluster = _fetch_and_parse_oarnodes_dict() + + running_jobs_dict: JobDict + waiting_jobs_dict: JobDict + ( + running_jobs_dict, + waiting_jobs_dict + ) = _fetch_and_parse_oarstat_dict( + jobs_running_on_perception_cluster=jobs_running_on_perception_cluster + ) - cluster_dict: dict = _merge( - nodes_dict=nodes_dict, - jobs_dict=jobs_dict + nodes_dict: dict = _merge_running_jobs( + nodes_dict=raw_nodes_dict, + jobs_dict=running_jobs_dict ) + cluster_dict: dict[str, dict] = { + 'nodes': nodes_dict, + 'waiting_jobs': waiting_jobs_dict + } return cluster_dict diff --git a/public/cluster.js b/public/cluster.js deleted file mode 100644 index 906eb03..0000000 --- a/public/cluster.js +++ /dev/null @@ -1,267 +0,0 @@ -import * as utils from "./utils.js"; - -class BoolField extends utils.Field { - constructor(name, class_name) { - super(name, 'x', true, class_name); - this.enabled_char = '✅'; - this.disabled_char = 'âŒ'; - } - - update(enabled) { - if ( enabled ) { - this.set_text(this.enabled_char); - } - else { - this.set_text(this.disabled_char); - } - } -} - -class Gpu { - constructor(id, cluster_container) { - - this.cluster_container = cluster_container; - - this.div = document.createElement('div'); - this.div.className = 'gpu'; - - // Name - this.name_div = document.createElement('div'); - this.name_div.className = 'info-gpu container centered'; - this.name_field = new utils.Field(`GPU ${id}`, 'model'); - this.name_div.appendChild(this.name_field.node); - - this.div.appendChild(this.name_div); - - // Job - this.job_div = document.createElement('div'); - this.job_div.className = 'job container'; - - // If the GPU is free. - this.free_sub_div = document.createElement('div'); - this.free_sub_div.className = 'free container'; - let free_par = document.createElement('p'); - free_par.innerHTML = '<strong>FREE</strong>'; - this.free_sub_div.appendChild(free_par); - this.free_div = document.createElement('div'); - this.free_div.className = 'job container'; - this.free_div.appendChild(this.free_sub_div); - - this.div.appendChild(this.free_div); - - // Job ID and owner - this.job_info_div = document.createElement('div'); - this.job_info_div.className = 'job-info container'; - - // Job ID - this.job_id_field = new utils.Field('ID', - 'job_id', - true, - 'job-id'); - this.job_info_div.appendChild(this.job_id_field.node); - - // Job owner - this.job_owner_field = new utils.Field('owner', - 'job_owner', - true, - 'job-owner'); - this.job_info_div.appendChild(this.job_owner_field.node); - - // Job type - this.job_type_field = new utils.Field('type', - 'job_type', - true, - 'job-type'); - this.job_info_div.appendChild(this.job_type_field.node); - - this.job_div.appendChild(this.job_info_div); - - // Job name - this.job_name_div = document.createElement('div'); - this.job_name_field = new utils.Field('name', - 'job_name', - true, - 'job-name'); - this.job_name_div.appendChild(this.job_name_field.node); - this.job_div.appendChild(this.job_name_div); - - // Job properties - this.properties_div = document.createElement('div'); - this.properties_div.className = 'booleans container'; - - // besteffort - this.besteffort_field = new BoolField('besteffort', - 'besteffort'); - this.properties_div.appendChild(this.besteffort_field.node); - - // idempotent - this.idempotent_field = new BoolField('idempotent', - 'idempotent'); - this.properties_div.appendChild(this.idempotent_field.node); - - this.job_div.appendChild(this.properties_div); - - // Job times (submission, start, walltime...) - this.times_div = document.createElement('div'); - this.times_div.className = 'times container'; - - // Start and submit times - this.submit_start_time_div = document.createElement('div'); - this.submit_start_time_div.className = 'two-times container'; - - // Submit time - this.submit_time_field = new utils.Field('submitted', - '00/00 00:00:00', - true, - 'submit-time'); - this.submit_start_time_div.appendChild(this.submit_time_field.node); - // Start time - this.start_time_field = new utils.Field('started', - '00/00 00:00:00', - true, - 'start-time'); - this.submit_start_time_div.appendChild(this.start_time_field.node); - this.times_div.appendChild(this.submit_start_time_div); - - // Walltime and max time - this.wall_max_finished_time_div = document.createElement('div'); - this.wall_max_finished_time_div.className = 'two-times container'; - - // Walltime - this.wall_time_field = new utils.Field('wall-time', - '00:00:00', - true, - 'wall-time'); - this.wall_max_finished_time_div.appendChild(this.wall_time_field.node); - - // Max tilme - this.max_finish_time_field = new utils.Field('ends before', - '00/00 00:00:00', - true, - 'max-finished-time'); - this.wall_max_finished_time_div.appendChild(this.max_finish_time_field.node); - this.times_div.appendChild(this.wall_max_finished_time_div); - this.job_div.appendChild(this.times_div); - - this.cluster_container.appendChild(this.div); - } - - update_free() { - if (this.div.childNodes[1] === this.job_div) - this.div.replaceChild(this.free_div, this.job_div); - this.div.style.backgroundColor = getComputedStyle( - document.documentElement).getPropertyValue('--primary'); - } - - update_busy(dict) { - if (this.div.childNodes[1] === this.free_div) - this.div.replaceChild(this.job_div, this.free_div); - - this.job_id_field.set_text(dict['id']); - - this.job_owner_field.set_text(dict['owner']); - if ('owner_color' in dict) { - this.div.style.backgroundColor = dict['owner_color']; - } - - let job_name = dict['name'] || ''; - this.job_name_field.set_text(job_name.slice(0, 38)); - this.job_type_field.set_text(dict['type']); - this.idempotent_field.update(dict['idempotent']); - this.besteffort_field.update(dict['besteffort']); - this.submit_time_field.set_text(dict['submission_time']); - this.start_time_field.set_text(dict['start_time']); - this.wall_time_field.set_text(dict['walltime']); - this.max_finish_time_field.set_text(dict['max_time']); - } - - update_from_dict(gpu_model, dict) { - this.name_field.set_text(gpu_model); - - if (dict == 'free') - this.update_free(); - else { - this.update_busy(dict); - } - } -} - -class Node { - constructor(id, num_gpus, root_node) { - this.root_container = root_node; - - this.id = id; - this.container = document.createElement('div'); - this.container.id = `gpu${id}`; - this.container.className = 'cluster container'; - this.root_container.appendChild(this.container); - - this.div_name = document.createElement('div'); - this.div_name.className = 'specs container centered'; - this.container.appendChild(this.div_name); - this.par_name = document.createElement('p'); - this.par_name.className = 'cluster-name'; - this.par_name.innerHTML = `gpu${id}-perception`; - this.div_name.append(this.par_name); - - this.num_gpus = num_gpus; - this.gpus = []; - for (let gpu_id = 0; gpu_id < this.num_gpus; gpu_id++ ) { - this.gpus.push(new Gpu(gpu_id, this.container)); - } - } - - update(dict) { - - this.gpu_model = dict['gpu_model']; - - for (let gpu_id = 0; gpu_id < this.num_gpus; gpu_id++ ) { - this.gpus[gpu_id].update_from_dict( - this.gpu_model, - dict['gpus'][gpu_id] - ); - } - } -} - -export class Cluster { - constructor() { - this.nodes = {}; - - this.main_container = document.createElement('div'); - this.main_container.className = 'main container'; - - // Add cluster nodes 1 to 7 (they each have 2 GPUs) - for (let node_id = 1; node_id < 8; node_id++ ) { - this.nodes[node_id] = new Node(node_id, 2, this.main_container); - } - - // Invisible node fills the gap at the right of 'gpu7', as longer 'gpu8' is put below - let invisible_node = document.createElement('div'); - invisible_node.className = 'cluster container'; - invisible_node.id = 'invisible-cluster'; - this.main_container.appendChild(invisible_node); - - // Add cluster node 8 (which has 4 GPUs) - this.nodes[8] = new Node(8, 4, this.main_container); - - let last_container = document.createElement('div'); - last_container.className = 'cluster container'; - last_container.id = 'last-cluster'; - this.main_container.appendChild(last_container); - } - - update(dict) { - for (let node_id in dict) { - this.nodes[node_id].update(dict[node_id]); - } - } - - show() { - var root = document.getElementById('home'); - root.appendChild(this.main_container); - } - - hide() { - } -} diff --git a/public/css/style.css b/public/css/style.css index 877932f..8585bda 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,5 +1,6 @@ @import url(header.css); @import url(clusters.css); +@import url(waiting_list.css); @media (min-width: 1200px) { body { diff --git a/public/css/waiting_list.css b/public/css/waiting_list.css new file mode 100644 index 0000000..6ab276a --- /dev/null +++ b/public/css/waiting_list.css @@ -0,0 +1,57 @@ +#waiting-list { + background-color: var(--bleu-moche); + margin: 2vw 2vw 0.5vw 2vw; + padding: 0.5vw 0.5vw 0.5vw 0.5vw; + border-radius: 10px; + justify-content: space-between; + flex-wrap: wrap; +} + +#waiting-list-title { + font-family: 'Montserrat', sans-serif; + font-weight: bold; + font-size: 1.5vw; + margin-left: 0.5vw; +} + +#waiting-list-table { + padding: 0.5vw 0.5vw 0.5vw 0.5vw; + border-radius: 10px; + /* background-color: red; */ +} + +.waiting-list-header { + float: left; + clear: both; + font-weight: bold; + /* color: var(--orange-moche); */ + background-color: var(--primary); +} + +.waiting-list-row { + padding: 0.2vw 1vw 0.2vw 1vw; + float: left; + clear: both; + border-radius: 10px; + margin-top: 0.2vw; +} + +.waiting-row-element { + float: left; + width: 6vw; +} + +.waiting-job-id { +} + +.waiting-job-owner { +} + +.waiting-job-walltime { +} + +.waiting-job-besteffort { +} + +.waiting-job-gpu { +} diff --git a/public/index.html b/public/index.html index 39459b2..bcf3319 100644 --- a/public/index.html +++ b/public/index.html @@ -38,7 +38,7 @@ <!-- </div> --> <!-- </div> --> </header> - <script type="module" src="./main.js"></script> + <script type="module" src="./js/main.js"></script> </div> <div id="source-code"> <a href="https://gitlab.inria.fr/robotlearn/cluster-monitor"><p>View source code</p></a> diff --git a/public/js/cluster.js b/public/js/cluster.js new file mode 100644 index 0000000..369a7e0 --- /dev/null +++ b/public/js/cluster.js @@ -0,0 +1,56 @@ +import * as utils from "./utils.js"; +import { WaitingList } from "./waiting_list.js"; +import { Node } from "./node.js"; + + +export class Cluster { + constructor() { + + // Nodes ---------------------------------------------------------- + this.nodes = {}; + + this.main_container = document.createElement('div'); + this.main_container.className = 'main container'; + + // Add cluster nodes 1 to 7 (they each have 2 GPUs) + for (let node_id = 1; node_id < 8; node_id++ ) { + this.nodes[node_id] = new Node(node_id, 2, this.main_container); + } + + // Invisible node fills the gap at the right of 'gpu7', as longer 'gpu8' is put below + let invisible_node = document.createElement('div'); + invisible_node.className = 'cluster container'; + invisible_node.id = 'invisible-cluster'; + this.main_container.appendChild(invisible_node); + + // Add cluster node 8 (which has 4 GPUs) + this.nodes[8] = new Node(8, 4, this.main_container); + + let last_container = document.createElement('div'); + last_container.className = 'cluster container'; + last_container.id = 'last-cluster'; + this.main_container.appendChild(last_container); + + // Waiting list --------------------------------------------------- + this.waiting_list = new WaitingList(); + this.main_container.appendChild(this.waiting_list.main_div); + } + + update(dict) { + let nodes_dict = dict['nodes']; + for (let node_id in nodes_dict) { + this.nodes[node_id].update(nodes_dict[node_id]); + } + + let waiting_list_dict = dict['waiting_jobs']; + this.waiting_list.update(waiting_list_dict); + } + + show() { + var root = document.getElementById('home'); + root.appendChild(this.main_container); + } + + hide() { + } +} diff --git a/public/js/gpu.js b/public/js/gpu.js new file mode 100644 index 0000000..4f6a67d --- /dev/null +++ b/public/js/gpu.js @@ -0,0 +1,187 @@ +import { Field, BoolField } from "./utils.js"; + +export class Gpu { + constructor(id, cluster_container) { + + this.cluster_container = cluster_container; + + this.div = document.createElement('div'); + this.div.className = 'gpu'; + + // Name + this.name_div = document.createElement('div'); + this.name_div.className = 'info-gpu container centered'; + this.name_field = new Field(`GPU ${id}`, 'model'); + this.name_div.appendChild(this.name_field.node); + + this.div.appendChild(this.name_div); + + // Job + this.job_div = document.createElement('div'); + this.job_div.className = 'job container'; + + this.state = 'FREE'; + + // If the GPU is free or suspected. + this.state_sub_div = document.createElement('div'); + this.state_sub_div.className = 'free container'; + let state_par = document.createElement('p'); + state_par.innerHTML = '<strong>FREE</strong>'; + this.state_sub_div.appendChild(state_par); + this.state_div = document.createElement('div'); + this.state_div.className = 'job container'; + this.state_div.appendChild(this.state_sub_div); + + this.div.appendChild(this.state_div); + + // Job ID and owner + this.job_info_div = document.createElement('div'); + this.job_info_div.className = 'job-info container'; + + // Job ID + this.job_id_field = new Field( + 'ID', + 'job_id', + true, + 'job-id' + ); + this.job_info_div.appendChild(this.job_id_field.node); + + // Job owner + this.job_owner_field = new Field( + 'owner', + 'job_owner', + true, + 'job-owner' + ); + this.job_info_div.appendChild(this.job_owner_field.node); + + // Job type + this.job_type_field = new Field( + 'type', + 'job_type', + true, + 'job-type' + ); + this.job_info_div.appendChild(this.job_type_field.node); + + this.job_div.appendChild(this.job_info_div); + + // Job name + this.job_name_div = document.createElement('div'); + this.job_name_field = new Field( + 'name', + 'job_name', + true, + 'job-name' + ); + this.job_name_div.appendChild(this.job_name_field.node); + this.job_div.appendChild(this.job_name_div); + + // Job properties + this.properties_div = document.createElement('div'); + this.properties_div.className = 'booleans container'; + + // besteffort + this.besteffort_field = new BoolField('besteffort', 'besteffort'); + this.properties_div.appendChild(this.besteffort_field.node); + + // idempotent + this.idempotent_field = new BoolField('idempotent', 'idempotent'); + this.properties_div.appendChild(this.idempotent_field.node); + + this.job_div.appendChild(this.properties_div); + + // Job times (submission, start, walltime...) + this.times_div = document.createElement('div'); + this.times_div.className = 'times container'; + + // Start and submit times + this.submit_start_time_div = document.createElement('div'); + this.submit_start_time_div.className = 'two-times container'; + + // Submit time + this.submit_time_field = new Field( + 'submitted', + '00/00 00:00:00', + true, + 'submit-time' + ); + this.submit_start_time_div.appendChild(this.submit_time_field.node); + // Start time + this.start_time_field = new Field( + 'started', + '00/00 00:00:00', + true, + 'start-time' + ); + this.submit_start_time_div.appendChild(this.start_time_field.node); + this.times_div.appendChild(this.submit_start_time_div); + + // Walltime and max time + this.wall_max_finished_time_div = document.createElement('div'); + this.wall_max_finished_time_div.className = 'two-times container'; + + // Walltime + this.wall_time_field = new Field( + 'wall-time', + '00:00:00', + true, + 'wall-time' + ); + this.wall_max_finished_time_div.appendChild(this.wall_time_field.node); + + // Max time + this.max_finish_time_field = new Field( + 'ends before', + '00/00 00:00:00', + true, + 'max-finished-time' + ); + this.wall_max_finished_time_div.appendChild(this.max_finish_time_field.node); + this.times_div.appendChild(this.wall_max_finished_time_div); + this.job_div.appendChild(this.times_div); + + this.cluster_container.appendChild(this.div); + } + + update_free() { + if (this.div.childNodes[1] === this.job_div) + this.div.replaceChild(this.state_div, this.job_div); + this.div.style.backgroundColor = getComputedStyle( + document.documentElement + ).getPropertyValue('--primary'); + } + + update_busy(dict) { + if (this.div.childNodes[1] === this.state_div) + this.div.replaceChild(this.job_div, this.state_div); + + this.job_id_field.set_text(dict['id']); + + this.job_owner_field.set_text(dict['owner']); + if ('owner_color' in dict) { + this.div.style.backgroundColor = dict['owner_color']; + } + + let job_name = dict['name'] || ''; + this.job_name_field.set_text(job_name.slice(0, 38)); + this.job_type_field.set_text(dict['type']); + this.idempotent_field.update(dict['idempotent']); + this.besteffort_field.update(dict['besteffort']); + this.submit_time_field.set_text(dict['submission_time']); + this.start_time_field.set_text(dict['start_time']); + this.wall_time_field.set_text(dict['walltime']); + this.max_finish_time_field.set_text(dict['max_time']); + } + + update_from_dict(gpu_model, dict) { + this.name_field.set_text(gpu_model); + + if (dict == 'free') + this.update_free(); + else { + this.update_busy(dict); + } + } +} diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..a7397cb --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,38 @@ +import { Field } from "./utils.js"; +import { Cluster } from "./cluster.js"; + +var last_updated = new Field('Last update', '', false); +last_updated.get_node_from_id('last-updated'); + +var cluster = new Cluster(); + +// Show the default page +cluster.show(); + +// console.log("Connecting to socket") +// var socket = io('https://ws.robotlearn.inrialpes.fr'); +var socket = io('0.0.0.0:8888'); +// console.log("Succesfully connected to socket server.") + +function update_timestamp() { + let currentDate = new Date(); + let current_time = currentDate.getHours() + ':'; + current_time += String( + currentDate.getMinutes() + ).padStart(2, '0') + ":"; + current_time += String( + currentDate.getSeconds() + ).padStart(2, '0'); + last_updated.set_text(current_time); +} + +socket.on( + 'update', + function(new_dict) { + // Timestamp + update_timestamp(); + + // Update data + cluster.update(new_dict); + } +); diff --git a/public/js/node.js b/public/js/node.js new file mode 100644 index 0000000..be1f7a9 --- /dev/null +++ b/public/js/node.js @@ -0,0 +1,39 @@ +import { Gpu } from "./gpu.js"; + +export class Node { + constructor(id, num_gpus, root_node) { + this.root_container = root_node; + + this.id = id; + this.container = document.createElement('div'); + this.container.id = `gpu${id}`; + this.container.className = 'cluster container'; + this.root_container.appendChild(this.container); + + this.div_name = document.createElement('div'); + this.div_name.className = 'specs container centered'; + this.container.appendChild(this.div_name); + this.par_name = document.createElement('p'); + this.par_name.className = 'cluster-name'; + this.par_name.innerHTML = `gpu${id}-perception`; + this.div_name.append(this.par_name); + + this.num_gpus = num_gpus; + this.gpus = []; + for (let gpu_id = 0; gpu_id < this.num_gpus; gpu_id++ ) { + this.gpus.push(new Gpu(gpu_id, this.container)); + } + } + + update(dict) { + + this.gpu_model = dict['gpu_model']; + + for (let gpu_id = 0; gpu_id < this.num_gpus; gpu_id++ ) { + this.gpus[gpu_id].update_from_dict( + this.gpu_model, + dict['gpus'][gpu_id] + ); + } + } +} diff --git a/public/utils.js b/public/js/utils.js similarity index 63% rename from public/utils.js rename to public/js/utils.js index 0eedff6..fb1e8dc 100644 --- a/public/utils.js +++ b/public/js/utils.js @@ -8,11 +8,14 @@ export class DivField { } } + export class Field { - constructor(name = 'default', - text = 'default', - create_node = true, - css_class = undefined) { + constructor( + name = 'default', + text = 'default', + create_node = true, + css_class = undefined + ) { this.name = name; this.text = text; @@ -39,3 +42,21 @@ export class Field { this.node = document.getElementById(id); } } + + +export class BoolField extends Field { + constructor(name, class_name) { + super(name, 'x', true, class_name); + this.enabled_char = '✅'; + this.disabled_char = 'âŒ'; + } + + update(enabled) { + if ( enabled ) { + this.set_text(this.enabled_char); + } + else { + this.set_text(this.disabled_char); + } + } +} diff --git a/public/js/waiting_list.js b/public/js/waiting_list.js new file mode 100644 index 0000000..9ee4a20 --- /dev/null +++ b/public/js/waiting_list.js @@ -0,0 +1,108 @@ +export class WaitingList { + constructor() { + this.main_div = document.createElement('div'); + this.main_div.id = 'waiting-list'; + + // Title + let title = document.createElement('h'); + title.id = 'waiting-list-title'; + title.innerHTML = 'Waiting list'; + this.main_div.appendChild(title); + + let header = document.createElement('div'); + header.className = 'waiting-list-header waiting-list-row'; + let header_job_id = document.createElement('div'); + header_job_id.className = 'waiting-row-element waiting-job-id'; + header_job_id.innerHTML = 'ID'; + header.appendChild(header_job_id); + + let header_job_owner = document.createElement('div'); + header_job_owner.className = 'waiting-row-element waiting-job-owner'; + header_job_owner.innerHTML = 'owner'; + header.appendChild(header_job_owner); + + let header_walltime = document.createElement('div'); + header_walltime.className = 'waiting-row-element waiting-job-walltime'; + header_walltime.innerHTML = 'walltime'; + header.appendChild(header_walltime); + + let header_besteffort = document.createElement('div'); + header_besteffort.className = 'waiting-row-element waiting-job-besteffort'; + header_besteffort.innerHTML = 'besteffort'; + header.appendChild(header_besteffort); + + let header_gpu = document.createElement('div'); + header_gpu.className = 'waiting-row-element waiting-job-gpu'; + header_gpu.innerHTML = 'requested gpu'; + header.appendChild(header_gpu); + + let table = document.createElement('div'); + table.id = 'waiting-list-table'; + table.appendChild(header); + this.table_rows = document.createElement('div'); + this.table_rows.id = 'waiting-list-table-rows'; + table.appendChild(this.table_rows); + + this.main_div.appendChild(table); + } + + update(waiting_jobs_dict) { + let jobs = []; + + for (var job_id in waiting_jobs_dict) { + // Check if job already exists in the + jobs.push(waiting_jobs_dict[job_id]); + } + + // Sort by job ID + jobs.sort( + function(a, b) { + return a['id'] - b['id']; + } + ); + + this.table_rows.innerHTML = ''; + for (let job_dict of jobs) { + + // Create and fill row + let row = document.createElement('div'); + row.className = 'waiting-list-row'; + + let job_id = document.createElement('div'); + job_id.className = 'waiting-row-element waiting-job-id'; + job_id.innerHTML = job_dict['id']; + row.appendChild(job_id); + + let owner = document.createElement('div'); + owner.className = 'waiting-row-element waiting-job-owner'; + owner.innerHTML = job_dict['owner']; + row.appendChild(owner); + + if ('owner_color' in job_dict) { + row.style.backgroundColor = job_dict['owner_color']; + } + + let walltime = document.createElement('div'); + walltime.className = 'waiting-row-element waiting-job-walltime'; + walltime.innerHTML = job_dict['walltime']; + row.appendChild(walltime); + + let best_effort = document.createElement('div'); + best_effort.className = 'waiting-row-element waiting-job-besteffort'; + if (job_dict['besteffort']) { + best_effort.innerHTML = '✅'; + } else { + best_effort.innerHTML = 'âŒ'; + } + row.appendChild(best_effort); + + let gpu = document.createElement('div'); + gpu.className = 'waiting-row-element waiting-job-gpu'; + gpu.innerHTML = job_dict['requested_gpu']; + row.appendChild(gpu); + + this.table_rows.appendChild(row); + } + + } +} diff --git a/public/main.js b/public/main.js deleted file mode 100644 index 6139128..0000000 --- a/public/main.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as utils from "./utils.js"; -import { Cluster } from "./cluster.js"; - -var last_updated = new utils.Field('Last update', '', false); -last_updated.get_node_from_id('last-updated'); - -var cluster = new Cluster(); - -// Show the default page -cluster.show(); - -// console.log("Connecting to socket") -var socket = io('https://ws.robotlearn.inrialpes.fr'); -// console.log("Succesfully connected to socket server.") - -function update_timestamp() { - let currentDate = new Date(); - let current_time = currentDate.getHours() + ':'; - current_time += String(currentDate.getMinutes()).padStart(2, '0') + ":"; - current_time += String(currentDate.getSeconds()).padStart(2, '0'); - last_updated.set_text(current_time); -} - -socket.on('update', function(new_dict) { - // Timestamp - update_timestamp(); - - // Update data - cluster.update(new_dict); -}); -- GitLab