diff --git a/assets/index.html b/assets/index.html
index 0053d50..ad020fb 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -12,7 +12,8 @@
<table><tr><td class="topregion">
<div class="logo">
<img class="logo" src="sai.svg"><br>
- <a href="https://warmcat.com/git/sai">Sai git</a>
+ <span class="logo"><a href="https://warmcat.com/git/sai">Sai git</a></span>
+ <span class="logo" id="apirev"></span>
<div>
<div id="login" class="login">
<div id="creds" class="creds hide">
diff --git a/assets/sai.css b/assets/sai.css
index 7aeef9c..de2a5bb 100644
--- a/assets/sai.css
+++ b/assets/sai.css
@@ -563,6 +563,18 @@ img.logo {
height: auto;
}
+span.logo {
+ margin: 3px;
+ margin-right: 4px;
+ padding: 2px;
+ vertical-align: middle;
+ z-index: 1002;
+ width: 164px;
+ height: auto;
+ font-size: 7pt;
+ text-align: left;
+}
+
td.summary {
background: #f8f8f8;
width: 100%;
diff --git a/assets/sai.js b/assets/sai.js
index 9248896..9933114 100644
--- a/assets/sai.js
+++ b/assets/sai.js
@@ -641,6 +641,10 @@ function sai_event_summary_render(o, now_ut, reset_all_icon)
var s, q, ctn = "", wai, s1 = "", n, e = o.e;
s = "<table class=\"comp";
+
+ if (!o.e)
+ return;
+
if (e.state == 3)
s += " comp_pass";
if (e.state == 4 || e.state == 6)
@@ -857,7 +861,7 @@ 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}">`;
+ innerHTML += `<div id="instload-` + plat.name.split('.')[0] + `">`;
for (let i = 0; i < plat.instances; i++) {
innerHTML += `<div class="inst_box inst_idle" title="instance ${i}: idle"></div>`;
@@ -929,7 +933,7 @@ 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) + "\">";
+ 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>";
}
@@ -977,7 +981,10 @@ function after_delete() {
function ws_open_sai()
{
var s = "", q, qa, qi, q5, q5s;
-
+
+ if (document.getElementById("apirev"))
+ document.getElementById("apirev").innerHTML = "API rev " + SAI_JS_API_VERSION;
+
q = window.location.href;
console.log(q);
qi = q.indexOf("/git/");
@@ -1073,7 +1080,7 @@ function ws_open_sai()
var u, ci, n;
var now_ut = Math.round((new Date().getTime() / 1000));
- // console.log(msg.data);
+ console.log(msg.data);
// if (msg.data.length < 10)
// return;
jso = JSON.parse(msg.data);
@@ -1483,11 +1490,16 @@ function ws_open_sai()
break;
case "com.warmcat.sai.loadreport":
- if (!jso.platform_name)
- break;
- const platformName = jso.platform_name;
+ // console.log("received com.warmcat.sai.loadreport" + jso);
+
+ if (!jso.builder_name)
+ break;
+
+ const platformName = jso.builder_name;
+ 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);
diff --git a/src/builder/CMakeLists.txt b/src/builder/CMakeLists.txt
index f1aba7b..d5866cd 100644
--- a/src/builder/CMakeLists.txt
+++ b/src/builder/CMakeLists.txt
@@ -11,6 +11,7 @@ set(SRCS
b-artifacts.c
b-logproxy.c
b-refproxy.c
+ b-load.c
)
set(requirements 1)
@@ -54,7 +55,11 @@ if (requirements)
endif()
target_link_libraries(${SUB} ${SAI_GIT2_LIB_PATH})
- endif()
+
+ if (APPLE)
+ target_link_libraries(sai-builder "-framework IOKit")
+ endif()
+ endif()
set(CMAKE_REQUIRED_LIBRARIES ${SAI_GIT2_LIB_PATH})
diff --git a/src/builder/b-comms.c b/src/builder/b-comms.c
index 9e7cf97..958323a 100644
--- a/src/builder/b-comms.c
+++ b/src/builder/b-comms.c
@@ -605,22 +605,28 @@ saib_sul_load_report_cb(struct lws_sorted_usec_list *sul)
lws_strncpy(lr->platform_name, any_plat->name, sizeof(lr->platform_name));
}
- /*
- * Iterate all platforms and their nspawn instances to collect load.
- * In a real implementation, you would query the system for CPU usage
- * of each nspawn process. For now, we will simulate it.
- */
+ int system_load = saib_get_system_cpu(&builder);
+
lws_start_foreach_dll(struct lws_dll2 *, p, builder.sai_plat_owner.head) {
sp = lws_container_of(p, sai_plat_t, sai_plat_list);
lws_start_foreach_dll(struct lws_dll2 *, d, sp->nspawn_owner.head) {
struct sai_nspawn *ns = lws_container_of(d, struct sai_nspawn, list);
sai_instance_load_t *il = calloc(1, sizeof(*il));
+ 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);
- /* Simulate load: 50% if building, 1% if idle */
- il->cpu_percent = il->state ? 500 : 10;
+ il->cpu_percent = (uint16_t)load;
lws_dll2_add_tail(&il->list, &lr->loads);
}
} lws_end_foreach_dll(d);
diff --git a/src/builder/b-load.c b/src/builder/b-load.c
new file mode 100644
index 0000000..9d37f16
--- /dev/null
+++ b/src/builder/b-load.c
@@ -0,0 +1,221 @@
+/*
+ * Sai builder - platform-specific load reporting
+ *
+ * Copyright (C) 2021 Andy Green <andy@warmcat.com>
+ *
+ * This file is part of Sai.
+ *
+ * Sai is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Sai is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <libwebsockets.h>
+#include "b-private.h"
+
+#if defined(__APPLE__)
+#include <mach/mach_host.h>
+#endif
+
+/*
+ * Returns CPU usage as an integer from 0-1000 (for 0.0% to 100.0%)
+ * or -1 on error.
+ */
+#if defined(__linux__)
+int
+saib_get_cgroup_cpu(struct sai_nspawn *ns)
+{
+ uint64_t usage_usec = 0;
+ char path[256], buf[128];
+ lws_usec_t now;
+ FILE *f;
+ int n, ret = -1;
+
+ /*
+ * On systemd systems, nspawn creates a scope unit for the container,
+ * e.g., /sys/fs/cgroup/sai.slice/sai-builder-instance-1.scope
+ * We can get per-instance CPU usage from the cpu.stat file inside.
+ */
+ lws_snprintf(path, sizeof(path),
+ "/sys/fs/cgroup/sai.slice/sai-%s.scope/cpu.stat",
+ ns->fsm.ovname);
+
+ f = fopen(path, "r");
+ if (!f)
+ return -1; /* cgroup file not found, fall back to system load */
+
+ while (fgets(buf, sizeof(buf) - 1, f)) {
+ if (sscanf(buf, "usage_usec %llu",
+ (unsigned long long *)&usage_usec) == 1) {
+ break;
+ }
+ }
+ fclose(f);
+
+ if (!usage_usec)
+ return -1;
+
+ now = lws_now_usecs();
+
+ if (ns->last_cpu_usec_time) {
+ uint64_t delta_usec = usage_usec - ns->last_cpu_usec;
+ uint64_t delta_time = (uint64_t)now - (uint64_t)ns->last_cpu_usec_time;
+
+ if (delta_time) {
+ /*
+ * Percentage is (cpu_time / wall_time) * 100.
+ * We multiply by 10 for fixed-point, so * 1000.
+ */
+ n = (int)((delta_usec * 1000) / delta_time);
+ if (n > 1000)
+ n = 1000;
+ ret = n;
+ }
+ }
+
+ ns->last_cpu_usec = usage_usec;
+ ns->last_cpu_usec_time = now;
+
+ return ret;
+}
+
+int
+saib_get_system_cpu(struct sai_builder *b)
+{
+ unsigned long long user, nice, system, idle, iowait, irq, softirq, steal;
+ uint64_t total, idle_all, total_delta, idle_delta;
+ int n, ret = -1;
+ char buf[256];
+ FILE *f;
+
+ f = fopen("/proc/stat", "r");
+ if (!f)
+ return -1;
+
+ if (!fgets(buf, sizeof(buf) -1, f)) {
+ fclose(f);
+ return -1;
+ }
+ fclose(f);
+
+ n = sscanf(buf, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
+ &user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal);
+ if (n < 4)
+ return -1;
+
+ idle_all = idle + iowait;
+ total = user + nice + system + idle_all + irq + softirq + steal;
+
+ if (b->last_sys_total) {
+ total_delta = total - b->last_sys_total;
+ idle_delta = idle_all - b->last_sys_idle;
+
+ if (total_delta) {
+ n = (int)(((total_delta - idle_delta) * 1000) / total_delta);
+ if (n > 1000)
+ n = 1000;
+ ret = n;
+ }
+ }
+
+ b->last_sys_total = total;
+ b->last_sys_idle = idle_all;
+
+ return ret;
+}
+#elif defined(__APPLE__)
+int saib_get_cgroup_cpu(struct sai_nspawn *ns)
+{
+ return -1; /* No cgroups on macOS */
+}
+
+int saib_get_system_cpu(struct sai_builder *b)
+{
+ host_cpu_load_info_data_t cpuinfo;
+ mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT;
+ uint64_t total_ticks = 0, idle_ticks = 0;
+ uint64_t total_delta, idle_delta;
+ int n, ret = -1;
+
+ if (host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO,
+ (host_info_t)&cpuinfo, &count) != KERN_SUCCESS)
+ return -1;
+
+ total_ticks = cpuinfo.cpu_ticks[CPU_STATE_USER] +
+ cpuinfo.cpu_ticks[CPU_STATE_SYSTEM] +
+ cpuinfo.cpu_ticks[CPU_STATE_NICE] +
+ cpuinfo.cpu_ticks[CPU_STATE_IDLE];
+ idle_ticks = cpuinfo.cpu_ticks[CPU_STATE_IDLE];
+
+ if (b->last_sys_total) {
+ total_delta = total_ticks - b->last_sys_total;
+ idle_delta = idle_ticks - b->last_sys_idle;
+
+ if (total_delta) {
+ n = (int)(((total_delta - idle_delta) * 1000) / total_delta);
+ if (n > 1000)
+ n = 1000;
+ ret = n;
+ }
+ }
+
+ b->last_sys_total = total_ticks;
+ b->last_sys_idle = idle_ticks;
+
+ return ret;
+}
+#elif defined(WIN32)
+int saib_get_cgroup_cpu(struct sai_nspawn *ns)
+{
+ return -1; /* No cgroups on Windows */
+}
+
+int saib_get_system_cpu(struct sai_builder *b)
+{
+ ULARGE_INTEGER idle, kernel, user;
+ int n, ret = -1;
+
+ if (!GetSystemTimes((FILETIME *)&idle, (FILETIME *)&kernel, (FILETIME *)&user))
+ return -1;
+
+ if (b->last_sys_total.QuadPart) {
+ ULONGLONG total_delta, idle_delta;
+
+ total_delta = (kernel.QuadPart - b->last_sys_kernel.QuadPart) +
+ (user.QuadPart - b->last_sys_user.QuadPart);
+ idle_delta = idle.QuadPart - b->last_sys_idle.QuadPart;
+
+ if (total_delta) {
+ n = (int)(((total_delta - idle_delta) * 1000) / total_delta);
+ if (n > 1000)
+ n = 1000;
+ ret = n;
+ }
+ }
+
+ b->last_sys_idle = idle;
+ b->last_sys_kernel = kernel;
+ b->last_sys_user = user;
+
+ return ret;
+}
+#else
+int saib_get_cgroup_cpu(struct sai_nspawn *ns)
+{
+ return -1; /* Not implemented on this platform */
+}
+int saib_get_system_cpu(struct sai_builder *b)
+{
+ return 10; /* Return a dummy 1% for unsupported platforms */
+}
+#endif
diff --git a/src/builder/b-private.h b/src/builder/b-private.h
index e37159f..61255f6 100644
--- a/src/builder/b-private.h
+++ b/src/builder/b-private.h
@@ -131,6 +131,16 @@ struct sai_builder {
sai_mirror_instance_t mi;
char path[256];
+
+#if defined(__linux__) || defined(__APPLE__)
+ /* For system-wide load calculation */
+ uint64_t last_sys_total;
+ uint64_t last_sys_idle;
+#elif defined(WIN32)
+ ULARGE_INTEGER last_sys_idle;
+ ULARGE_INTEGER last_sys_kernel;
+ ULARGE_INTEGER last_sys_user;
+#endif
char stay;
};
@@ -225,3 +235,9 @@ saib_handle_resource_result(struct sai_plat_server *spm, const char *in, size_t
void
saib_sul_load_report_cb(struct lws_sorted_usec_list *sul);
+
+int
+saib_get_cgroup_cpu(struct sai_nspawn *ns);
+
+int
+saib_get_system_cpu(struct sai_builder *b);
diff --git a/src/common/include/private.h b/src/common/include/private.h
index d6a75f2..26918cb 100644
--- a/src/common/include/private.h
+++ b/src/common/include/private.h
@@ -161,6 +161,9 @@ struct sai_nspawn {
sai_plat_t *sp; /* the sai_plat */
struct sai_plat_server *spm; /* the sai plat / server with the ss / wsi */
+ uint64_t last_cpu_usec;
+ lws_usec_t last_cpu_usec_time;
+
size_t chunk_cache_size;
const char *server_name; /* sai-server name who triggered this, eg, 'warmcat' */
diff --git a/src/web/w-comms.c b/src/web/w-comms.c
index ffe07c5..6c8a505 100644
--- a/src/web/w-comms.c
+++ b/src/web/w-comms.c
@@ -1219,7 +1219,8 @@ clean_spa:
break;
}
- return saiw_ws_json_tx_browser(vhd, pss, buf, sizeof(buf));
+ saiw_ws_json_tx_browser(vhd, pss, buf, sizeof(buf));
+ break;
default:
passthru:
diff --git a/src/web/w-private.h b/src/web/w-private.h
index 4c88487..884a6c2 100644
--- a/src/web/w-private.h
+++ b/src/web/w-private.h
@@ -321,7 +321,7 @@ saiw_sched_destroy(struct lws_dll2 *d, void *user);
void
-saiw_ws_broadcast_raw(struct vhd *vhd, const void *buf, size_t len, unsigned int min_api_version);
+saiw_ws_broadcast_raw(struct vhd *vhd, const void *buf, size_t len, unsigned int min_api_version, int flags);
void
saiw_browser_state_changed(struct pss *pss, int established);
diff --git a/src/web/w-websrv.c b/src/web/w-websrv.c
index 9c75049..803e557 100644
--- a/src/web/w-websrv.c
+++ b/src/web/w-websrv.c
@@ -89,10 +89,10 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
return LWSSSSRET_DISCONNECT_ME;
}
- if (!(flags & LWSSS_FLAG_EOM))
- return 0;
+// if (!(flags & LWSSS_FLAG_EOM))
+// return 0;
- lwsl_notice("%s: schema idx %d parsed correctly from sai-server\n", __func__, m->a.top_schema_index);
+ // lwsl_notice("%s: schema idx %d parsed correctly from sai-server\n", __func__, m->a.top_schema_index);
switch (m->a.top_schema_index) {
@@ -117,7 +117,8 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
m->a.ac = NULL;
vhd->builders_owner =
&((sai_plat_owner_t *)m->a.dest)->plat_owner;
- saiw_ws_broadcast_raw(vhd, buf, len, 0);
+ saiw_ws_broadcast_raw(vhd, buf, len, 0,
+ lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM));
break;
case SAIS_WS_WEBSRV_RX_OVERVIEW:
@@ -148,9 +149,10 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags)
case SAIS_WS_WEBSRV_RX_LOADREPORT:
/* A builder sent a load report, forward to all browsers */
- lwsl_notice("%s: ===== Received load report, broadcasting to %d browsers\n",
- __func__, (int)vhd->browsers.count);
- saiw_ws_broadcast_raw(vhd, buf, len, 2);
+ // lwsl_notice("%s: ===== Received load report, broadcasting to %d browsers\n",
+ // __func__, (int)vhd->browsers.count);
+ saiw_ws_broadcast_raw(vhd, buf, len, 2,
+ lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM));
break;
}
diff --git a/src/web/w-ws-browser.c b/src/web/w-ws-browser.c
index 9a44614..46f05d6 100644
--- a/src/web/w-ws-browser.c
+++ b/src/web/w-ws-browser.c
@@ -1,7 +1,7 @@
/*
* Sai server - ./src/server/m-ws-browser.c
*
- * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com>
+ * Copyright (C) 2019 - 2025 Andy Green <andy@warmcat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -35,7 +35,7 @@
* all connected browsers, eg, for load reports.
*/
void
-saiw_ws_broadcast_raw(struct vhd *vhd, const void *buf, size_t len, unsigned int api_ver_min)
+saiw_ws_broadcast_raw(struct vhd *vhd, const void *buf, size_t len, unsigned int api_ver_min, int flags)
{
int eff = 0;
// lwsl_err("%s: sai-web broadcasting to browsers\n", __func__);
@@ -43,13 +43,15 @@ saiw_ws_broadcast_raw(struct vhd *vhd, const void *buf, size_t len, unsigned int
lws_start_foreach_dll(struct lws_dll2 *, p, vhd->browsers.head) {
struct pss *pss = lws_container_of(p, struct pss, same);
+ int *pi = (int *)((const char *)buf -sizeof(int));
if (pss->js_api_version >= api_ver_min) {
eff++;
- if (lws_buflist_append_segment(&pss->raw_tx, buf, len) >= 0)
+ *pi = flags;
+
+ if (lws_buflist_append_segment(&pss->raw_tx, buf - sizeof(int), len + sizeof(int)) > 0)
lws_callback_on_writable(pss->wsi);
}
-
} lws_end_foreach_dll(p);
lwsl_notice("%s: broadcast to %d / %d browsers\n", __func__,
@@ -625,29 +627,34 @@ again:
// lwsl_notice("%s: send_state %d, pss %p, wsi %p\n", __func__,
// pss->send_state, pss, pss->wsi);
+ /*
+ * Send anything waiting on broadcast_raw buflist first
+ */
+
if (pss->raw_tx) {
- char som, eom;
- int used;
+ char som, eom, rb[4096];
+ int used, *pi = (int *)rb;
- p = start; /* buf + LWS_PRE */
- used = lws_buflist_fragment_use(&pss->raw_tx, p,
- lws_ptr_diff_size_t(end, p), &som, &eom);
+ used = lws_buflist_fragment_use(&pss->raw_tx, (uint8_t *)rb,
+ sizeof(rb), &som, &eom);
if (!used)
return 0;
- flags = lws_write_ws_flags(LWS_WRITE_TEXT, som, eom);
- if (lws_write(pss->wsi, p, (size_t)used, (enum lws_write_protocol)flags) < 0)
- return -1;
+ // lwsl_wsi_notice(pss->wsi, "writing %d bytes flags 0x%x: '%.*s'",
+ // (int)(used - (int)sizeof(int)), (int)*pi,
+ // (int)(used - (int)sizeof(int)), rb + sizeof(int));
- /*
- * if there are more fragments, we must exit now and wait for
- * the next writable callback to send the rest. Otherwise, we
- * can fall through and check for other work to do.
- */
- if (pss->raw_tx) {
- lws_callback_on_writable(pss->wsi);
- return 0;
+ if (lws_write(pss->wsi, (uint8_t *)rb + sizeof(int),
+ (size_t)used - sizeof(int),
+ (enum lws_write_protocol)*pi) < 0) {
+ lwsl_wsi_err(pss->wsi, "attempt to write %d failed", (int)used - (int)sizeof(int));
+
+ return -1;
}
+
+ lws_callback_on_writable(pss->wsi);
+
+ return 0;
}
if (pss->sched.count)