Add Shardok profiling admin controls (#8755)

This commit is contained in:
2026-07-23 07:38:51 -07:00
committed by GitHub
parent 46400dd6e1
commit 0766b63b9e
3 changed files with 112 additions and 5 deletions
@@ -2,8 +2,10 @@ package main
import (
"encoding/json"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
shardokpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface"
@@ -75,3 +77,23 @@ func TestShardokProfileEndpointsRequireExpectedMethod(t *testing.T) {
}
}
}
func TestLayoutTemplateIncludesShardokProfileControls(t *testing.T) {
parsed, err := template.ParseFS(templatesFS, "templates/layout.html", "templates/games.html")
if err != nil {
t.Fatalf("parse layout: %v", err)
}
if parsed.Lookup("layout.html") == nil {
t.Fatal("layout template was not parsed")
}
layout, err := templatesFS.ReadFile("templates/layout.html")
if err != nil {
t.Fatalf("read layout: %v", err)
}
for _, expected := range []string{"shardok-profile-controls", "shardok-profile-dialog"} {
if !strings.Contains(string(layout), expected) {
t.Fatalf("layout does not include %q", expected)
}
}
}
@@ -496,7 +496,8 @@ dialog .modal-actions button {
}
/* JFR controls in nav */
#jfr-controls {
#jfr-controls,
#shardok-profile-controls {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -559,7 +560,8 @@ dialog .modal-actions button {
font-size: 0.875rem;
}
#jfr-controls .btn-small {
#jfr-controls .btn-small,
#shardok-profile-controls .btn-small {
padding: 0.25rem 0.5rem !important;
font-size: 0.75rem;
height: auto !important;
@@ -568,19 +570,22 @@ dialog .modal-actions button {
display: inline-block;
}
#jfr-controls button.btn-small {
#jfr-controls button.btn-small,
#shardok-profile-controls button.btn-small {
background: var(--pico-primary-background);
color: var(--pico-primary-inverse);
border: none;
cursor: pointer;
}
#jfr-controls button.btn-small.secondary {
#jfr-controls button.btn-small.secondary,
#shardok-profile-controls button.btn-small.secondary {
background: var(--pico-secondary-background);
color: var(--pico-secondary-inverse);
}
#jfr-controls a.btn-small {
#jfr-controls a.btn-small,
#shardok-profile-controls a.btn-small {
background: var(--pico-primary-background);
color: var(--pico-primary-inverse);
border-radius: var(--pico-border-radius);
@@ -24,6 +24,9 @@
<li id="jfr-controls" hx-get="/jfr/status" hx-trigger="load" hx-swap="innerHTML">
<span class="jfr-loading">JFR: ...</span>
</li>
<li id="shardok-profile-controls" hx-get="/shardok-profile/status" hx-trigger="load, every 5s" hx-swap="none">
<span class="jfr-loading">Shardok CPU: ...</span>
</li>
<li>
<button onclick="confirmEagleRestart()" class="btn-small secondary outline">Restart Eagle</button>
<span id="eagle-restart-result"></span>
@@ -47,6 +50,20 @@
</article>
</dialog>
<!-- Shardok CPU Profile Dialog -->
<dialog id="shardok-profile-dialog">
<article style="max-width: 440px;">
<h3>Record Shardok CPU Profile</h3>
<p>Choose a bounded recording duration. The recording stops automatically.</p>
<footer style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<button class="secondary outline" onclick="document.getElementById('shardok-profile-dialog').close()">Cancel</button>
<button class="secondary" onclick="startShardokProfile(30)">30 sec</button>
<button class="secondary" onclick="startShardokProfile(60)">60 sec</button>
<button onclick="startShardokProfile(120)">120 sec</button>
</footer>
</article>
</dialog>
<!-- JFR Stop Dialog -->
<dialog id="jfr-stop-dialog">
<article style="max-width: 400px;">
@@ -87,6 +104,16 @@
// Refresh status on error
htmx.ajax('GET', '/jfr/status', '#jfr-controls');
}
} else if (evt.detail.pathInfo.requestPath.startsWith('/shardok-profile/')) {
if (evt.detail.pathInfo.requestPath === '/shardok-profile/download') {
return;
}
try {
const data = JSON.parse(evt.detail.xhr.responseText);
updateShardokProfileControls(data);
} catch(e) {
updateShardokProfileControls({error: evt.detail.xhr.responseText || e.message});
}
}
});
@@ -139,6 +166,59 @@
document.getElementById('jfr-stop-dialog').close();
}
function updateShardokProfileControls(data) {
const container = document.getElementById('shardok-profile-controls');
if (data.error) {
container.innerHTML = `<span class="jfr-error" title="${escapeHtml(data.error)}">Shardok CPU: Error</span>`;
return;
}
if (data.status === 'disabled') {
container.innerHTML = '<span class="jfr-status jfr-off">Shardok CPU: OFF</span>';
return;
}
if (data.recording) {
container.innerHTML = `
<span class="jfr-status jfr-on">Shardok CPU: ON (${data.duration_seconds}s)</span>
<button onclick="stopShardokProfile()" class="btn-small secondary">Stop</button>
`;
} else {
const download = data.profile_available
? '<a href="/shardok-profile/download" class="btn-small">Download</a>'
: '';
container.innerHTML = `
<span class="jfr-status jfr-off">Shardok CPU: ${data.profile_available ? 'READY' : 'OFF'}</span>
<button onclick="showShardokProfileDialog()" class="btn-small">Start</button>
${download}
`;
}
}
function escapeHtml(value) {
const element = document.createElement('span');
element.textContent = value || '';
return element.innerHTML;
}
function showShardokProfileDialog() {
document.getElementById('shardok-profile-dialog').showModal();
}
function startShardokProfile(durationSeconds) {
document.getElementById('shardok-profile-dialog').close();
htmx.ajax('POST', `/shardok-profile/start?duration=${durationSeconds}`, {
target: '#shardok-profile-controls',
swap: 'none'
});
}
function stopShardokProfile() {
htmx.ajax('POST', '/shardok-profile/stop', {
target: '#shardok-profile-controls',
swap: 'none'
});
}
function confirmEagleRestart() {
document.getElementById('eagle-restart-dialog').showModal();
}