diff --git a/assets/sai.css b/assets/sai.css
index de2a5bb..793e832 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -717,3 +717,45 @@ img.branch {
.inst_idle { background-color: #d0d0d0; }
.inst_run { background-color: #78c678; }
.load_text { font-size: 9px; vertical-align: middle; }
+
+/* Container for all instance boxes for a given platform */
+.instload {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ min-height: 22px; /* Ensure container doesn't collapse if empty */
+}
+
+/* The outer box for a single instance */
+.inst_box {
+ width: 16px;
+ height: 16px;
+ border: 1px solid #555;
+ box-sizing: border-box;
+ background-color: #ddd; /* Dark background for the meter */
+ position: relative; /* Needed for positioning the child bar */
+ overflow: hidden; /* Hide any part of the bar that goes over 100% */
+}
+
+/* The actual colored bar inside the box */
+.inst_bar {
+ background-color: #00afff; /* A nice, high-contrast green */
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ transition: height 0.2s ease-out; /* Smoothly animate height changes */
+}
+
+.inst_idle {
+ /* An idle instance can have a subtle border or effect if desired */
+ border-color: #555;
+}
+
+.inst_busy {
+ /* A busy instance can have a brighter border */
+ border-color: #999;
+}
+
diff --git a/assets/sai.js b/assets/sai.js
index 9933114..1786fba 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -845,6 +845,8 @@ function getBuilderGroupKey(platName) {
return hostname;
}
+
+
function createBuilderDiv(plat) {
const platDiv = document.createElement("div");
platDiv.className = "ibuil bdr";
@@ -861,13 +863,17 @@ function createBuilderDiv(plat) {
innerHTML += `<img class="ip1 tread1" src="/sai/arch-${plat_arch}.svg" onerror="this.src='/sai/generic.svg';this.onerror=null;">`;
innerHTML += `<img class="ip1 tread2" src="/sai/tc-${plat_tc}.svg" onerror="this.src='/sai/generic.svg';this.onerror=null;">`;
innerHTML += `<br>${plat.peer_ip}`;
- innerHTML += `<div id="instload-` + plat.name.split('.')[0] + `">`;
+ innerHTML += `<div class="instload" id="instload-${plat.name}">`; // Changed class name for clarity
+ // Create initial idle squares
for (let i = 0; i < plat.instances; i++) {
- innerHTML += `<div class="inst_box inst_idle" title="instance ${i}: idle"></div>`;
+ innerHTML += `<div class="inst_box inst_idle" title="instance ${i}: idle">` +
+ `<div class="inst_bar"></div>` +
+ `</div>`;
}
innerHTML += `</div></td></tr></tbody></table>`;
+
platDiv.innerHTML = innerHTML;
return platDiv;
}
@@ -933,9 +939,9 @@ function render_builders(jso)
(e.peer_ip ? "<br>" + san(e.peer_ip) : "");
/* Add a container for the instance load boxes */
- s += "<div id=\"instload-" + san(e.name).split('.')[0] + "\">";
+ s += "<div id=\"instload-" + san(e.name)/*.split('.')[0]*/ + "\">";
for (var i = 0; i < e.instances; i++) {
- s += "<div class=\"inst_box inst_idle\" title=\"instance " + i + ": idle\"></div>";
+ s += "<div class=\"inst_box inst_idle\" title=\"instance " + i + ": idle\"><div class=\"inst_bar\"></div></div>";
}
s += "</div>";
@@ -1489,46 +1495,63 @@ function ws_open_sai()
buildersContainer.appendChild(table);
break;
- case "com.warmcat.sai.loadreport":
-
- // console.log("received com.warmcat.sai.loadreport" + jso);
+ case "com.warmcat.sai.loadreport":
+ if (!jso.platforms || !Array.isArray(jso.platforms)) {
+ break;
+ }
- if (!jso.builder_name)
- break;
+ for (const platformReport of jso.platforms) {
+ const platformName = platformReport.platform_name;
+ if (!platformName) {
+ continue;
+ }
+
+ const loadContainer = document.getElementById("instload-" + platformName);
+ if (!loadContainer) {
+ continue;
+ }
- const platformName = jso.builder_name;
+ if (platformReport.loads && Array.isArray(platformReport.loads)) {
+ const instanceDivs = loadContainer.getElementsByClassName("inst_box");
- console.log("loadreport: builder name " + platformName);
-
- // The container for the load squares has a predictable ID
- const loadContainerId = "instload-" + platformName;
- const loadContainer = document.getElementById(loadContainerId);
+ for (let i = 0; i < platformReport.loads.length; i++) {
+ const instanceLoad = platformReport.loads[i];
+ const instanceDiv = instanceDivs[i];
- if (loadContainer) {
+ if (!instanceDiv) {
+ break;
+ }
- // Clear any old load indicators
- loadContainer.innerHTML = "";
+ let cpu = instanceLoad.cpu_percent / 10.0;
+ let stateText = instanceLoad.state ? 'busy' : 'idle';
+ instanceDiv.title = `Instance ${i}: ${stateText}\nCPU: ${cpu.toFixed(1)}%`;
- // Loop through the new load data and create the squares
- if (jso.loads && Array.isArray(jso.loads)) {
+ if (instanceLoad.state) {
+ instanceDiv.classList.add("inst_busy");
+ instanceDiv.classList.remove("inst_idle");
+ } else {
+ instanceDiv.classList.add("inst_idle");
+ instanceDiv.classList.remove("inst_busy");
+ }
- for (const instanceLoad of jso.loads) {
- let instanceDiv = document.createElement("div");
-
- // Set class for styling (e.g., green for idle, red for busy)
- let stateClass = instanceLoad.state ? "inst_busy" : "inst_idle";
- instanceDiv.className = "inst_box " + stateClass;
+ // The bar is always the first (and only) child of the inst_box div.
+ const bar = instanceDiv.firstChild;
+ if (bar && bar.classList.contains("inst_bar")) {
+ let cpu_percentage = (instanceLoad.cpu_percent / 1000) * 100;
+ if (cpu_percentage > 100) cpu_percentage = 100;
+
+ // Let's add a minimum height so even a tiny load is visible
+ if (cpu_percentage > 0 && cpu_percentage < 1) cpu_percentage = 1;
- // Set tooltip to show the CPU percentage
- let cpu = instanceLoad.cpu_percent / 10.0;
- instanceDiv.title = `Instance: ${stateClass.split('_')[1]} \nCPU: ${cpu.toFixed(1)}%`;
-
- loadContainer.appendChild(instanceDiv);
- }
+ bar.style.height = `${cpu_percentage}%`;
+ } else {
+ // This console log will tell us if the bar element is missing
+ console.error("Could not find .inst_bar child in .inst_box for", instanceDiv);
}
}
-
- break;
+ }
+ }
+ break;
case "com-warmcat-sai-artifact":
console.log(jso);
diff --git a/src/builder/b-comms.c b/src/builder/b-comms.c
index 958323a..1740f38 100644
--- a/src/builder/b-comms.c
+++ b/src/builder/b-comms.c
@@ -289,8 +289,8 @@ saib_m_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, size_t *len,
n = (int)w;
lws_dll2_remove(&lr->list);
- lws_start_foreach_dll_safe(struct lws_dll2 *, il, il1, lr->loads.head) {
- sai_instance_load_t *i = lws_container_of(il, sai_instance_load_t, list);
+ lws_start_foreach_dll_safe(struct lws_dll2 *, il, il1, lr->platforms.head) {
+ sai_platform_load_t *i = lws_container_of(il, sai_platform_load_t, list);
lws_dll2_remove(&i->list);
free(i);
} lws_end_foreach_dll_safe(il, il1);
@@ -599,16 +599,22 @@ saib_sul_load_report_cb(struct lws_sorted_usec_list *sul)
* under one name.
*/
lws_strncpy(lr->builder_name, builder.host, sizeof(lr->builder_name));
- if (builder.sai_plat_owner.head) {
- sai_plat_t *any_plat = lws_container_of(builder.sai_plat_owner.head,
- sai_plat_t, sai_plat_list);
- lws_strncpy(lr->platform_name, any_plat->name, sizeof(lr->platform_name));
- }
int system_load = saib_get_system_cpu(&builder);
+ /*
+ * Iterate all platforms. For each platform, create a sai_platform_load_t
+ * and populate its list of instance loads.
+ */
+
lws_start_foreach_dll(struct lws_dll2 *, p, builder.sai_plat_owner.head) {
- sp = lws_container_of(p, sai_plat_t, sai_plat_list);
+ sp = lws_container_of(p, sai_plat_t, sai_plat_list);
+
+ sai_platform_load_t *pl = calloc(1, sizeof(*pl));
+ if (!pl)
+ continue;
+
+ lws_strncpy(pl->platform_name, sp->name, sizeof(pl->platform_name));
lws_start_foreach_dll(struct lws_dll2 *, d, sp->nspawn_owner.head) {
struct sai_nspawn *ns = lws_container_of(d, struct sai_nspawn, list);
@@ -616,20 +622,28 @@ saib_sul_load_report_cb(struct lws_sorted_usec_list *sul)
int load = -1;
if (il) {
- #if defined(__linux__)
- /* On Linux, try cgroup first, then fall back to system */
- load = saib_get_cgroup_cpu(ns);
- #endif
- if (load < 0)
- load = system_load;
- if (load < 0) /* If system load also failed */
- load = 10; /* Default to 1% */
-
il->state = (ns->state == NSSTATE_BUILD);
+
+ if (!il->state) {
+ /* If the instance is idle, its load is 0, regardless of system load. */
+ load = 0;
+ } else {
+ /* The instance is busy, try to get a specific load for it. */
+ load = saib_get_cgroup_cpu(ns);
+ if (load < 0)
+ /* Fall back to system load for busy instances on non-cgroup platforms. */
+ load = system_load;
+ }
+
+ if (load < 0) /* Final fallback for errors */
+ load = 10; /* Default to 1% if system load failed */
+
il->cpu_percent = (uint16_t)load;
- lws_dll2_add_tail(&il->list, &lr->loads);
+ lws_dll2_add_tail(&il->list, &pl->loads);
}
} lws_end_foreach_dll(d);
+
+ lws_dll2_add_tail(&pl->list, &lr->platforms);
} lws_end_foreach_dll(p);
lws_dll2_add_tail(&lr->list, &spm->load_report_owner);
diff --git a/src/common/include/private.h b/src/common/include/private.h
index 26918cb..387c314 100644
--- a/src/common/include/private.h
+++ b/src/common/include/private.h
@@ -67,11 +67,17 @@ typedef struct sai_instance_load {
/*
* load report struct, sent in its own schema.
*/
-typedef struct sai_load_report {
+typedef struct sai_platform_load {
lws_dll2_t list; /* Not used, for schema mapping */
- char builder_name[64];
char platform_name[128];
lws_dll2_owner_t loads;
+} sai_platform_load_t;
+
+/* The top-level load report message from a builder */
+typedef struct sai_load_report {
+ lws_dll2_t list; /* For queuing on sai_plat_server */
+ char builder_name[64];
+ lws_dll2_owner_t platforms; /* List of sai_platform_load_t */
} sai_load_report_t;
/*
diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c
index 560e6ac..d8568b7 100644
--- a/src/common/struct-metadata.c
+++ b/src/common/struct-metadata.c
@@ -26,14 +26,19 @@ const lws_struct_map_t lsm_instance_load[] = {
LSM_UNSIGNED (sai_instance_load_t, state, "state"),
};
+/* Map for a sai_platform_load_t object */
+const lws_struct_map_t lsm_platform_load[] = {
+ LSM_CARRAY(sai_platform_load_t, platform_name, "platform_name"),
+ LSM_LIST(sai_platform_load_t, loads, sai_instance_load_t, list,
+ NULL, lsm_instance_load, "loads"),
+};
+
const lws_struct_map_t lsm_load_report_members[] = {
LSM_CARRAY (sai_load_report_t, builder_name, "builder_name"),
- LSM_CARRAY (sai_load_report_t, platform_name, "platform_name"),
- LSM_LIST (sai_load_report_t, loads, sai_instance_load_t, list,
- NULL, lsm_instance_load, "loads"),
+ LSM_LIST (sai_load_report_t, platforms, sai_platform_load_t, list,
+ NULL, lsm_platform_load, "platforms"),
};
-
static const lws_struct_map_t lsm_plat[] = {
LSM_STRING_PTR (sai_plat_t, name, "name"),
LSM_UNSIGNED (sai_plat_t, ongoing, "ongoing"),
diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c
index 17bc2f7..a44f309 100644
--- a/src/server/s-ws-builder.c
+++ b/src/server/s-ws-builder.c
@@ -46,12 +46,17 @@ static const lws_struct_map_t lsm_instance_load[] = {
LSM_UNSIGNED(sai_instance_load_t, state, "state"),
};
+static const lws_struct_map_t lsm_platform_load[] = {
+ LSM_CARRAY(sai_platform_load_t, platform_name, "platform_name"),
+ LSM_LIST(sai_platform_load_t, loads, sai_instance_load_t, list,
+ NULL, lsm_instance_load, "loads"),
+};
+
/* map for the members of the load report object */
static const lws_struct_map_t lsm_load_report_members[] = {
LSM_CARRAY(sai_load_report_t, builder_name, "builder_name"),
- LSM_CARRAY(sai_load_report_t, platform_name, "platform_name"),
- LSM_LIST(sai_load_report_t, loads, sai_instance_load_t, list,
- NULL, lsm_instance_load, "loads"),
+ LSM_LIST (sai_load_report_t, platforms, sai_platform_load_t, list,
+ NULL, lsm_platform_load, "platforms"),
};
/*