v0.36.8
This commit is contained in:
1 parent
0cbe0566f5
commit
992a1795af
34 files changed
+1409
-604
No files matched your search
@@ -6,6 +6,8 @@ struct DashboardPresetLayout {
|
||||
dashboard_widgets: Vec<String>,
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
dashboard_widget_sizes: Option<BTreeMap<String, u8>>,
|
||||
#[serde(default)]
|
||||
dashboard_widget_heights: Option<BTreeMap<String, u8>>,
|
||||
dashboard_density: String,
|
||||
overview_samples: u16,
|
||||
}
|
||||
@@ -43,6 +45,8 @@ fn validate_dashboard_preset_layout(layout: &DashboardPresetLayout) -> Result<()
|
||||
.clone_from(&layout.dashboard_hidden_widgets);
|
||||
pref.dashboard_widget_sizes
|
||||
.clone_from(&layout.dashboard_widget_sizes);
|
||||
pref.dashboard_widget_heights
|
||||
.clone_from(&layout.dashboard_widget_heights);
|
||||
pref.dashboard_density.clone_from(&layout.dashboard_density);
|
||||
validate_admin_preferences(&pref)
|
||||
}
|
||||
@@ -66,12 +70,14 @@ async fn dashboard_presets(
|
||||
dashboard_widgets: layout.dashboard_widgets,
|
||||
dashboard_hidden_widgets: layout.dashboard_hidden_widgets,
|
||||
dashboard_widget_sizes: layout.dashboard_widget_sizes,
|
||||
dashboard_widget_heights: layout.dashboard_widget_heights,
|
||||
..Default::default()
|
||||
};
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
layout.dashboard_widgets = prefs.dashboard_widgets;
|
||||
layout.dashboard_hidden_widgets = prefs.dashboard_hidden_widgets;
|
||||
layout.dashboard_widget_sizes = prefs.dashboard_widget_sizes;
|
||||
layout.dashboard_widget_heights = prefs.dashboard_widget_heights;
|
||||
if validate_dashboard_preset_layout(&layout).is_ok() {
|
||||
presets.push(NamedDashboardPreset { name, layout });
|
||||
}
|
||||
@@ -170,6 +176,7 @@ mod dashboard_preset_validation_tests {
|
||||
dashboard_widgets: default_admin_dashboard_widgets_for_tests(),
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_widget_heights: None,
|
||||
dashboard_density: default_admin_dashboard_density(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
|
||||
+60
-1
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.8",
|
||||
"version": "0.36.9",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -1560,6 +1560,41 @@
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"memory_allocator": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"description": "glibc heap statistics (mallinfo2), sampled at most every 2 s; null with other allocators. free_bytes is an upper bound of resident allocator-held free memory.",
|
||||
"properties": {
|
||||
"allocator": {
|
||||
"type": "string"
|
||||
},
|
||||
"heap_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"in_use_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"free_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"releasable_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"mmap_bytes": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"mmap_blocks": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"system": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
@@ -3954,6 +3989,7 @@
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"memory_breakdown",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
@@ -4063,6 +4099,17 @@
|
||||
},
|
||||
"description": "Nullable account-scoped per-card widths (12-column grid). Null preserves the legacy grouped visual arrangement; an object, including an empty one, enables freely ordered cards. Key must be a registered Overview widget ID. Panels require width >= 4, charts >= 3 and backend table >= 6."
|
||||
},
|
||||
"dashboard_widget_heights": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"maxProperties": 96,
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"description": "Optional per-card heights in Overview row units (1-32; a compact stat card spans 2 rows). Cards without an entry size to their content; a height smaller than the content needs is raised automatically. Keys must be registered Overview widget IDs."
|
||||
},
|
||||
"dashboard_hidden_widgets": {
|
||||
"type": "array",
|
||||
"maxItems": 78,
|
||||
@@ -4135,6 +4182,7 @@
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"memory_breakdown",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
@@ -7180,6 +7228,17 @@
|
||||
},
|
||||
"description": "Width overrides in a 12-column grid; null selects legacy groups"
|
||||
},
|
||||
"dashboard_widget_heights": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"maxProperties": 96,
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"description": "Optional per-card heights in Overview row units (1-32; a compact stat card spans 2 rows). Cards without an entry size to their content; a height smaller than the content needs is raised automatically. Keys must be registered Overview widget IDs."
|
||||
},
|
||||
"dashboard_density": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -43,6 +43,11 @@ fn reconcile_dashboard_widgets(prefs: &mut AdminPreferences) {
|
||||
.any(|widget| widget.id == *id && valid_dashboard_span(&widget.display_type, *span))
|
||||
});
|
||||
}
|
||||
if let Some(heights) = prefs.dashboard_widget_heights.as_mut() {
|
||||
heights.retain(|id, rows| {
|
||||
valid_dashboard_rows(*rows) && DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id)
|
||||
});
|
||||
}
|
||||
for widget in DASHBOARD_WIDGETS.iter() {
|
||||
if !seen.contains(&widget.id) {
|
||||
prefs.dashboard_widgets.push(widget.id.clone());
|
||||
@@ -136,6 +141,9 @@ struct AdminPreferences {
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
#[serde(default)]
|
||||
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
|
||||
/// Optional per-card heights in Overview row units (1..=32, a compact stat card is 2); missing cards size to content.
|
||||
#[serde(default)]
|
||||
dashboard_widget_heights: Option<std::collections::BTreeMap<String, u8>>,
|
||||
#[serde(default = "default_file_log_columns")]
|
||||
file_log_columns: Vec<String>,
|
||||
#[serde(default = "default_file_log_sort")]
|
||||
@@ -193,6 +201,7 @@ impl Default for AdminPreferences {
|
||||
dashboard_widgets: default_admin_dashboard_widgets(),
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_widget_heights: None,
|
||||
file_log_columns: default_file_log_columns(),
|
||||
file_log_sort: default_file_log_sort(),
|
||||
file_log_descending: default_file_log_descending(),
|
||||
@@ -392,6 +401,12 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
|
||||
"invalid dashboard card width preference".into(),
|
||||
));
|
||||
}
|
||||
if !valid_dashboard_heights(&preferences.dashboard_widget_heights) {
|
||||
return Err(ApiError(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
"invalid dashboard card height preference".into(),
|
||||
));
|
||||
}
|
||||
if preferences.backend_table_columns.is_empty()
|
||||
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
|
||||
{
|
||||
|
||||
@@ -162,6 +162,19 @@ fn valid_dashboard_sizes(sizes: &Option<std::collections::BTreeMap<String, u8>>)
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_dashboard_rows(rows: u8) -> bool {
|
||||
(1..=32).contains(&rows)
|
||||
}
|
||||
|
||||
fn valid_dashboard_heights(heights: &Option<std::collections::BTreeMap<String, u8>>) -> bool {
|
||||
heights.as_ref().is_none_or(|heights| {
|
||||
heights.len() <= DASHBOARD_WIDGETS.len()
|
||||
&& heights.iter().all(|(id, rows)| {
|
||||
valid_dashboard_rows(*rows) && DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn default_hidden_dashboard_widgets() -> Vec<String> {
|
||||
let mut hidden = DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets.clone();
|
||||
for widget in DASHBOARD_WIDGETS.iter() {
|
||||
|
||||
@@ -31,6 +31,7 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
"backend_health",
|
||||
"backend_tiny",
|
||||
"process_simple",
|
||||
"memory_breakdown",
|
||||
"extra_process_cpu",
|
||||
"stream_traffic",
|
||||
"wide_cache_activity",
|
||||
@@ -328,3 +329,46 @@ fn customized_dashboard_preferences_fit_persistent_storage_limit() {
|
||||
assert!(document.len() > 4096);
|
||||
assert!(document.len() <= crate::db::ADMIN_PREFERENCES_MAX_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_height_preferences_are_optional_bounded_and_reconciled() {
|
||||
let old: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
|
||||
assert!(old.dashboard_widget_heights.is_none());
|
||||
let mut prefs = AdminPreferences {
|
||||
dashboard_widget_sizes: Some(std::collections::BTreeMap::new()),
|
||||
dashboard_widget_heights: Some(std::collections::BTreeMap::from([
|
||||
("process_diagnostics".to_owned(), 6),
|
||||
("requests".to_owned(), 1),
|
||||
("memory_breakdown".to_owned(), 32),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
let stored = serde_json::to_string(&prefs).unwrap();
|
||||
let recovered: AdminPreferences = serde_json::from_str(&stored).unwrap();
|
||||
assert_eq!(recovered.dashboard_widget_heights, prefs.dashboard_widget_heights);
|
||||
for rows in [0, 33] {
|
||||
prefs
|
||||
.dashboard_widget_heights
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert("requests".to_owned(), rows);
|
||||
assert!(validate_admin_preferences(&prefs).is_err(), "rows {rows}");
|
||||
}
|
||||
prefs
|
||||
.dashboard_widget_heights
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert("requests".to_owned(), 2);
|
||||
prefs
|
||||
.dashboard_widget_heights
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.insert("undefined-widget".to_owned(), 3);
|
||||
assert!(validate_admin_preferences(&prefs).is_err());
|
||||
reconcile_dashboard_widgets(&mut prefs);
|
||||
let heights = prefs.dashboard_widget_heights.as_ref().unwrap();
|
||||
assert!(!heights.contains_key("undefined-widget"));
|
||||
assert_eq!(heights.get("requests"), Some(&2));
|
||||
assert!(validate_admin_preferences(&prefs).is_ok());
|
||||
}
|
||||
@@ -76,8 +76,10 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/overview-wide-metrics.js"),
|
||||
include_str!("web/app/web-overview.js"),
|
||||
include_str!("web/app/overview-masonry.js"),
|
||||
include_str!("web/app/overview-rows.js"),
|
||||
include_str!("web/app/config-history.js"),
|
||||
include_str!("web/app/certificate-inspection.js"),
|
||||
include_str!("web/app/memory-breakdown.js"),
|
||||
include_str!("web/app/runtime.js"),
|
||||
include_str!("web/app/settings.js"),
|
||||
include_str!("web/management.js"),
|
||||
|
||||
@@ -161,6 +161,7 @@ fn status_payload(state: &App) -> Value {
|
||||
let cache_view = state.cache.view();
|
||||
let transform_resources = crate::transform::resource_usage(&snapshot.envelope.config.transform);
|
||||
payload["cache"] = json!(&cache_view);
|
||||
payload["memory_allocator"] = json!(crate::diagnostics::allocator_stats());
|
||||
payload["transform_resources"] = json!(&transform_resources);
|
||||
payload["geoip"] = json!(state.geoip.load().view());
|
||||
let system_info = system_info_payload(state);
|
||||
|
||||
@@ -55,6 +55,7 @@ fn live_status_payload(state: &App) -> Value {
|
||||
"web_server_metrics": web_server_metrics_payload(&snapshot),
|
||||
"live_system": live_chart_system_payload(state),
|
||||
"cache": state.cache.view(),
|
||||
"memory_allocator": crate::diagnostics::allocator_stats(),
|
||||
"transform_metrics": state.transform_metrics.total(),
|
||||
"logs_dropped": state.logs.dropped.load(Ordering::Relaxed),
|
||||
"backends": snapshot.envelope.config.vhosts.iter().map(|v| v.backends.len()).sum::<usize>(),
|
||||
|
||||
@@ -1978,6 +1978,7 @@ mod tests {
|
||||
dashboard_widgets: AdminPreferences::default().dashboard_widgets,
|
||||
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
|
||||
dashboard_widget_sizes: None,
|
||||
dashboard_widget_heights: None,
|
||||
dashboard_density: "compact".to_owned(),
|
||||
overview_samples: 600,
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ function translate() {
|
||||
document.querySelectorAll("[data-i18n]").forEach(e => e.textContent = t(e.dataset.i18n));
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach(e => e.placeholder = t(e.dataset.i18nPlaceholder));
|
||||
document.querySelectorAll("[data-i18n-aria]").forEach(e => e.setAttribute("aria-label", t(e.dataset.i18nAria)));
|
||||
document.querySelectorAll("[data-i18n-title]").forEach(e => { e.title = t(e.dataset.i18nTitle); });
|
||||
window.rustProxyAclProfiles?.refreshTranslations?.();
|
||||
$("language").value = languagePreference === "auto" ? "auto" : language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
|
||||
window.rustProxyDrafts?.refresh();
|
||||
|
||||
@@ -28,7 +28,8 @@ window.rustProxyInlineLayout = (() => {
|
||||
const label = id => dashboardWidgetLabel(id);
|
||||
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|
||||
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id))
|
||||
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes));
|
||||
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes)
|
||||
|| JSON.stringify(normalizeDashboardWidgetHeights(draft.heights)) !== JSON.stringify(normalizeDashboardWidgetHeights(original.heights)));
|
||||
|
||||
function renderHidden() {
|
||||
if (!hiddenList || !draft) return;
|
||||
@@ -52,7 +53,8 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
|
||||
}
|
||||
function paint() {
|
||||
applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes);
|
||||
applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes, draft.heights);
|
||||
window.rustProxyOverviewRows?.flush?.();
|
||||
renderHidden();
|
||||
const visible = visibleIds();
|
||||
for (const node of nodes()) {
|
||||
@@ -70,6 +72,21 @@ window.rustProxyInlineLayout = (() => {
|
||||
const widen = node.querySelector(".overview-inline-widen");
|
||||
if (narrow) narrow.disabled = at <= 0;
|
||||
if (widen) widen.disabled = at === widths.length - 1;
|
||||
const rows = window.rustProxyOverviewRows?.rowsFor?.(node.dataset.dashboardWidget);
|
||||
const heightControls = node.querySelector(".overview-inline-height");
|
||||
if (heightControls) heightControls.hidden = !rows;
|
||||
if (rows) {
|
||||
const explicit = draft.heights?.[node.dataset.dashboardWidget] !== undefined;
|
||||
const heightValue = node.querySelector(".overview-inline-height-value");
|
||||
if (heightValue) {
|
||||
heightValue.textContent = `↕${rows.span}${explicit ? "" : " A"}`;
|
||||
heightValue.title = explicit ? t("overviewLayoutHeightFixed") : t("overviewLayoutHeightAuto");
|
||||
}
|
||||
const lower = node.querySelector(".overview-inline-lower");
|
||||
const taller = node.querySelector(".overview-inline-taller");
|
||||
if (lower) lower.disabled = !explicit;
|
||||
if (taller) taller.disabled = rows.span >= DASHBOARD_MAX_ROWS;
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearDrag() { if (pointerDrag) settle(false); }
|
||||
@@ -95,6 +112,19 @@ window.rustProxyInlineLayout = (() => {
|
||||
paint();
|
||||
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-narrow" : ".overview-inline-widen")?.focus();
|
||||
}
|
||||
function resizeHeight(id, delta) {
|
||||
if (!draft) return;
|
||||
const rows = window.rustProxyOverviewRows?.rowsFor?.(id);
|
||||
if (!rows) return;
|
||||
enterFreeLayout();
|
||||
draft.heights = { ...(draft.heights || {}) };
|
||||
const next = rows.span + delta;
|
||||
// At or below the content height the card returns to automatic sizing.
|
||||
if (next <= rows.min) delete draft.heights[id];
|
||||
else draft.heights[id] = Math.min(DASHBOARD_MAX_ROWS, next);
|
||||
paint();
|
||||
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-lower" : ".overview-inline-taller")?.focus();
|
||||
}
|
||||
function nudge(id, delta) {
|
||||
const visible = visibleIds();
|
||||
const index = visible.indexOf(id), target = visible[index + delta];
|
||||
@@ -124,7 +154,14 @@ window.rustProxyInlineLayout = (() => {
|
||||
control("−", `${t("overviewLayoutNarrow")}: ${title}`, "overview-inline-narrow", () => resize(id, -1)),
|
||||
widthValue,
|
||||
control("+", `${t("overviewLayoutWiden")}: ${title}`, "overview-inline-widen", () => resize(id, 1)));
|
||||
bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls,
|
||||
const heightControls = el("span", undefined, "overview-inline-width overview-inline-height");
|
||||
const heightValue = el("span", "", "overview-inline-width-value overview-inline-height-value");
|
||||
heightValue.setAttribute("aria-label", t("overviewLayoutHeight"));
|
||||
heightControls.append(
|
||||
control("−", `${t("overviewLayoutLower")}: ${title}`, "overview-inline-lower", () => resizeHeight(id, -1)),
|
||||
heightValue,
|
||||
control("+", `${t("overviewLayoutTaller")}: ${title}`, "overview-inline-taller", () => resizeHeight(id, 1)));
|
||||
bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls, heightControls,
|
||||
control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)),
|
||||
control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
|
||||
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
|
||||
@@ -158,7 +195,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
||||
if (hiddenList) hiddenList.replaceChildren();
|
||||
if (modalButton) modalButton.disabled = false;
|
||||
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes);
|
||||
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes, original.heights);
|
||||
draft = original = null;
|
||||
}
|
||||
async function begin() {
|
||||
@@ -171,8 +208,9 @@ window.rustProxyInlineLayout = (() => {
|
||||
const order = normalizeDashboardWidgets(saved.dashboard_widgets);
|
||||
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
|
||||
const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
|
||||
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
|
||||
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
|
||||
const heights = normalizeDashboardWidgetHeights(saved.dashboard_widget_heights);
|
||||
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null };
|
||||
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes }, heights: heights ? { ...heights } : null };
|
||||
showEditing();
|
||||
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
|
||||
finally { opening = false; unlock.disabled = false; }
|
||||
@@ -186,6 +224,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
dashboard_widget_heights: normalizeDashboardWidgetHeights(draft.heights),
|
||||
});
|
||||
finish(true);
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
@@ -269,6 +308,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
const rect = drag.node.getBoundingClientRect();
|
||||
const placeholder = el("div", undefined, "overview-drop-placeholder");
|
||||
placeholder.dataset.dashboardSpan = drag.node.dataset.dashboardSpan || "2";
|
||||
if (drag.node.dataset.dashboardRows) placeholder.style.setProperty("--overview-rows", drag.node.dataset.dashboardRows);
|
||||
placeholder.style.minHeight = `${Math.round(rect.height)}px`;
|
||||
placeholder.setAttribute("aria-hidden", "true");
|
||||
drag.origin = drag.node.nextSibling;
|
||||
@@ -380,6 +420,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
currentDraft: () => draft ? {
|
||||
order: [...draft.order], hidden: [...draft.hidden],
|
||||
sizes: draft.sizes === null ? null : { ...draft.sizes },
|
||||
heights: draft.heights ? { ...draft.heights } : null,
|
||||
} : null,
|
||||
cancel,
|
||||
};
|
||||
|
||||
@@ -237,7 +237,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
|
||||
dashboard_widget_sizes: null,
|
||||
dashboard_widget_sizes: null, dashboard_widget_heights: null,
|
||||
});
|
||||
resetConfirm.close();
|
||||
dialog.close();
|
||||
|
||||
@@ -18,6 +18,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
dashboard_widgets: [...normalizeDashboardWidgets(current.dashboard_widgets)],
|
||||
dashboard_hidden_widgets: [...normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets)],
|
||||
dashboard_widget_sizes: normalizeDashboardWidgetSizes(current.dashboard_widget_sizes),
|
||||
dashboard_widget_heights: normalizeDashboardWidgetHeights(current.dashboard_widget_heights),
|
||||
dashboard_density: normalizeAdminDensity(current.dashboard_density, "compact"),
|
||||
overview_samples: window.rustProxyOverviewSamples.getLimit(),
|
||||
};
|
||||
@@ -97,6 +98,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
dashboard_widgets: [...layout.dashboard_widgets],
|
||||
dashboard_hidden_widgets: [...layout.dashboard_hidden_widgets],
|
||||
dashboard_widget_sizes: layout.dashboard_widget_sizes === null ? null : { ...layout.dashboard_widget_sizes },
|
||||
dashboard_widget_heights: normalizeDashboardWidgetHeights(layout.dashboard_widget_heights),
|
||||
dashboard_density: layout.dashboard_density,
|
||||
});
|
||||
window.rustProxyOverviewSamples.setLimit(layout.overview_samples, true);
|
||||
@@ -130,6 +132,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
|
||||
dashboard_widget_sizes: null,
|
||||
dashboard_widget_heights: null,
|
||||
dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density,
|
||||
overview_samples: ADMIN_DASHBOARD_DEFAULTS.overview_samples,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Memory breakdown card (memory_breakdown): splits process RSS into cache data,
|
||||
// cache bookkeeping, cache buffers, allocator-held free memory, file-backed pages
|
||||
// and the rest of the process, and tracks the "outside cache" share over time so
|
||||
// a steady climb (a leak) is distinguishable from flat overhead.
|
||||
window.rustProxyMemoryBreakdown = (() => {
|
||||
const $ = id => document.getElementById(id);
|
||||
const segments = ["cache-data", "cache-meta", "transient", "alloc-free", "files", "rest"];
|
||||
const MIN_TREND_MS = 10 * 60 * 1000;
|
||||
let samples = [];
|
||||
const num = value => value == null || value === "" ? null : (Number.isFinite(Number(value)) ? Number(value) : null);
|
||||
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 600;
|
||||
|
||||
function compute(status) {
|
||||
const live = status.live_system || {}, system = status.system || {}, cache = status.cache || {};
|
||||
const rss = num(live.process_rss_bytes) ?? num(system.process_rss_bytes);
|
||||
if (rss == null || rss <= 0) return null;
|
||||
const allocated = num(cache.hot_allocated_bytes) ?? num(cache.resident_bytes) ?? 0;
|
||||
const logical = Math.min(num(cache.hot_logical_bytes) ?? allocated, allocated);
|
||||
const transient = num(cache.transient_bytes) ?? 0;
|
||||
const fileBacked = (num(system.process_rss_file_bytes) ?? 0) + (num(system.process_rss_shmem_bytes) ?? 0);
|
||||
const allocator = status.memory_allocator || null;
|
||||
let free = allocator ? (num(allocator.free_bytes) ?? 0) : 0;
|
||||
const values = { "cache-data": logical, "cache-meta": allocated - logical, transient, "alloc-free": free, files: fileBacked };
|
||||
let rest = rss - Object.values(values).reduce((sum, value) => sum + value, 0);
|
||||
// Allocator free memory may already be released to the kernel (malloc_trim),
|
||||
// so it is the first share to shrink when the parts exceed the measured RSS.
|
||||
if (rest < 0) { const cut = Math.min(free, -rest); free -= cut; values["alloc-free"] = free; rest += cut; }
|
||||
values.rest = Math.max(0, rest);
|
||||
return {
|
||||
rss, values, allocator, allocated, logical,
|
||||
entries: num(cache.entries),
|
||||
outside: Math.max(0, rss - allocated - transient)
|
||||
};
|
||||
}
|
||||
|
||||
function slopePerHour(points) {
|
||||
if (points.length < 3) return null;
|
||||
const t0 = points[0].t, n = points.length;
|
||||
let sx = 0, sy = 0, sxx = 0, sxy = 0;
|
||||
for (const p of points) { const x = (p.t - t0) / 3600000; sx += x; sy += p.v; sxx += x * x; sxy += x * p.v; }
|
||||
const den = n * sxx - sx * sx;
|
||||
return den > 0 ? (n * sxy - sx * sy) / den : null;
|
||||
}
|
||||
|
||||
function setText(id, text) { const node = $(id); if (node) node.textContent = text; }
|
||||
|
||||
function render(status, appendSample = true) {
|
||||
const card = document.querySelector('[data-dashboard-widget="memory_breakdown"]');
|
||||
if (!card || !$("mb-bar")) return;
|
||||
const lang = typeof locale === "function" ? locale() : undefined;
|
||||
const fmtBytes = value => value == null || !Number.isFinite(value) ? "-" : bytes(Math.max(0, value));
|
||||
const percent = value => Number.isFinite(value) ? `${(value * 100).toLocaleString(lang, { maximumFractionDigits: 1 })}%` : "-";
|
||||
const tr = key => (typeof t === "function" ? t(key) : key);
|
||||
const data = compute(status);
|
||||
if (!data) {
|
||||
setText("mb-rss", "-");
|
||||
return;
|
||||
}
|
||||
setText("mb-rss", `RSS ${fmtBytes(data.rss)}`);
|
||||
const bar = $("mb-bar").children;
|
||||
segments.forEach((key, index) => {
|
||||
const value = data.values[key], share = value / data.rss;
|
||||
bar[index].style.width = `${Math.max(0, share * 100)}%`;
|
||||
bar[index].title = `${tr(rowKey(key))}: ${fmtBytes(value)} (${percent(share)})`;
|
||||
setText(`mb-${key}-value`, fmtBytes(value));
|
||||
setText(`mb-${key}-share`, percent(share));
|
||||
const row = card.querySelector(`[data-mb-row="${key}"]`);
|
||||
if (row) row.hidden = key === "alloc-free" && !data.allocator;
|
||||
});
|
||||
setText("mb-ratio", data.allocated > 0 ? `${(data.rss / data.allocated).toFixed(2)}×` : "-");
|
||||
setText("mb-per-object", data.entries > 0 ? fmtBytes((data.allocated - data.logical) / data.entries) : "-");
|
||||
setText("mb-objects", data.entries == null ? "-" : data.entries.toLocaleString(lang));
|
||||
|
||||
const allocator = data.allocator, heap = num(allocator?.heap_bytes);
|
||||
$("mb-allocator").hidden = !allocator;
|
||||
$("mb-allocator-unavailable").hidden = !!allocator;
|
||||
setText("mb-fragmentation", allocator && heap > 0 ? percent(num(allocator.free_bytes) / heap) : "-");
|
||||
if (allocator) {
|
||||
setText("mb-heap", fmtBytes(heap));
|
||||
setText("mb-in-use", fmtBytes(num(allocator.in_use_bytes)));
|
||||
setText("mb-free", `${fmtBytes(num(allocator.free_bytes))}${heap > 0 ? ` · ${percent(num(allocator.free_bytes) / heap)}` : ""}`);
|
||||
setText("mb-mmap", `${fmtBytes(num(allocator.mmap_bytes))} · ${num(allocator.mmap_blocks) ?? 0}`);
|
||||
}
|
||||
|
||||
if (appendSample) {
|
||||
samples.push({ t: Date.now(), v: data.outside });
|
||||
samples = samples.slice(-limit());
|
||||
}
|
||||
const values = samples.map(sample => sample.v);
|
||||
const peak = Math.max(1, ...values) * 1.1;
|
||||
const width = 300, height = 60, pad = 5;
|
||||
const points = values.length < 2 ? "0,55 300,55" : values.map((value, index) =>
|
||||
`${index * width / (values.length - 1)},${height - pad - value / peak * (height - pad * 2)}`).join(" ");
|
||||
$("mb-outside-sparkline").setAttribute("points", points);
|
||||
window.rustProxyChartScale?.axes?.("mb-outside-sparkline", peak, "bytes", values.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setText("mb-outside-value", fmtBytes(data.outside));
|
||||
|
||||
const badge = $("mb-trend-badge");
|
||||
const span = samples.length ? samples.at(-1).t - samples[0].t : 0;
|
||||
const slope = span >= MIN_TREND_MS ? slopePerHour(samples) : null;
|
||||
let state = "collecting";
|
||||
if (slope != null) {
|
||||
// Rising faster than 2% of the current outside-cache size per hour (and at least 4 MiB/h).
|
||||
const threshold = Math.max(4 * 1048576, data.outside * 0.02);
|
||||
state = slope > threshold ? "rising" : slope < -threshold ? "falling" : "stable";
|
||||
}
|
||||
badge.dataset.state = state;
|
||||
badge.textContent = tr({ collecting: "memoryTrendCollecting", rising: "memoryTrendRising", falling: "memoryTrendFalling", stable: "memoryTrendStable" }[state]);
|
||||
const minutes = Math.round(span / 60000);
|
||||
setText("mb-trend-rate", slope == null
|
||||
? `${minutes} / ${MIN_TREND_MS / 60000} min`
|
||||
: `${slope >= 0 ? "+" : "−"}${fmtBytes(Math.abs(slope))}/h · ${minutes} min`);
|
||||
}
|
||||
|
||||
function rowKey(key) {
|
||||
return { "cache-data": "memoryCacheData", "cache-meta": "memoryCacheMeta", transient: "memoryTransient",
|
||||
"alloc-free": "memoryAllocatorFree", files: "memoryFiles", rest: "memoryRest" }[key];
|
||||
}
|
||||
|
||||
window.rustProxyOverviewSamples?.onChange?.(() => { samples = samples.slice(-limit()); });
|
||||
return { render, compute };
|
||||
})();
|
||||
@@ -0,0 +1,179 @@
|
||||
// Row units for the custom Overview layout. Widths stay on the 12-column grid;
|
||||
// heights snap to a shared row unit (the tallest compact stat card), so a tall
|
||||
// panel can sit next to several rows of small cards and every edge lines up.
|
||||
// Each card gets at least the rows its content needs; an explicit height
|
||||
// (dashboard_widget_heights) can only make it taller, and charts stretch into
|
||||
// the extra space. Below the narrow breakpoint the grid falls back to content
|
||||
// height (CSS sets --overview-row-units: off).
|
||||
window.rustProxyOverviewRows = (() => {
|
||||
const grid = document.getElementById("dashboard-overview-grid");
|
||||
// A compact stat card spans ROW_DIVISIONS rows; finer rows keep taller cards
|
||||
// close to their natural height instead of rounding up by a whole stat card.
|
||||
const MAX_ROWS = 32, ROW_DIVISIONS = 2, FALLBACK_STAT = 84, MIN_INTERVAL_MS = 1500, SHRINK_CONFIRMATIONS = 3;
|
||||
const noop = { schedule() { }, flush() { }, setHeights() { }, rowsFor() { return null; } };
|
||||
if (!grid) return noop;
|
||||
let heights = null, frame = 0, lastRun = 0, lastWidth = 0;
|
||||
const state = new Map();
|
||||
const shrinkVotes = new Map();
|
||||
const cards = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
|
||||
|
||||
function enabled() {
|
||||
if (grid.dataset.overviewLayoutMode !== "custom") return false;
|
||||
return getComputedStyle(grid).getPropertyValue("--overview-row-units").trim() !== "off";
|
||||
}
|
||||
function clear() {
|
||||
grid.classList.remove("has-row-units");
|
||||
grid.style.removeProperty("--overview-row-unit");
|
||||
for (const node of cards()) { node.style.removeProperty("--overview-rows"); delete node.dataset.dashboardRows; }
|
||||
state.clear(); shrinkVotes.clear();
|
||||
}
|
||||
function apply() {
|
||||
frame = 0;
|
||||
if (!grid.getClientRects().length || grid.classList.contains("is-overview-sorting")) return;
|
||||
if (!enabled()) { if (grid.classList.contains("has-row-units")) clear(); return; }
|
||||
lastRun = performance.now();
|
||||
lastWidth = grid.clientWidth;
|
||||
const items = cards().filter(node => !node.hidden && node.getClientRects().length);
|
||||
if (!items.length) return;
|
||||
grid.classList.add("is-measuring-rows");
|
||||
// Bounding boxes include CSS zoom (Settings → Interface scale); grid tracks,
|
||||
// gaps and the row unit are in CSS pixels, so divide measurements by it.
|
||||
const zoom = zoomOf();
|
||||
const natural = items.map(node => node.getBoundingClientRect().height / zoom);
|
||||
grid.classList.remove("is-measuring-rows");
|
||||
const gap = parseFloat(getComputedStyle(grid).rowGap) || 0;
|
||||
const isStat = node => node.classList.contains("stat");
|
||||
// The unit is half of the smallest stat card, so every card ends within a
|
||||
// few pixels of its natural height instead of being rounded up by a whole tile.
|
||||
const stats = natural.filter((_, index) => isStat(items[index]) && natural[index] > 0);
|
||||
const statHeight = stats.length ? Math.min(...stats) : FALLBACK_STAT;
|
||||
const unit = Math.max(16, (statHeight - gap * (ROW_DIVISIONS - 1)) / ROW_DIVISIONS);
|
||||
grid.style.setProperty("--overview-row-unit", `${unit}px`);
|
||||
const seen = new Set();
|
||||
items.forEach((node, index) => {
|
||||
const id = node.dataset.dashboardWidget;
|
||||
seen.add(id);
|
||||
// Small tolerance so sub-pixel rounding never adds a whole row.
|
||||
let min = Math.max(1, Math.ceil((natural[index] + gap) / (unit + gap) - 0.04));
|
||||
const previous = state.get(id);
|
||||
// Grow at once; shrink only after the smaller size is seen repeatedly,
|
||||
// so live data (table rows, wrapping labels) cannot make the grid jump.
|
||||
if (previous && min < previous.min) {
|
||||
const votes = (shrinkVotes.get(id) || 0) + 1;
|
||||
if (votes < SHRINK_CONFIRMATIONS) { shrinkVotes.set(id, votes); min = previous.min; }
|
||||
else shrinkVotes.delete(id);
|
||||
} else shrinkVotes.delete(id);
|
||||
const span = Math.max(min, Math.min(MAX_ROWS, heights?.[id] ?? 0));
|
||||
setRows(node, span);
|
||||
state.set(id, { span, min });
|
||||
});
|
||||
for (const id of [...state.keys()]) if (!seen.has(id)) state.delete(id);
|
||||
grid.classList.add("has-row-units");
|
||||
const geometry = gridGeometry(unit + gap, zoom);
|
||||
if (!geometry) return;
|
||||
equalizeStatRows(items.filter(isStat), geometry);
|
||||
alignBottom(items, geometry);
|
||||
}
|
||||
function setRows(node, rows) {
|
||||
if (node.dataset.dashboardRows === String(rows)) return;
|
||||
node.style.setProperty("--overview-rows", String(rows));
|
||||
node.dataset.dashboardRows = String(rows);
|
||||
}
|
||||
const rowsOf = node => Number(node.dataset.dashboardRows) || 1;
|
||||
function zoomOf() {
|
||||
const css = parseFloat(getComputedStyle(grid).width);
|
||||
const visual = grid.getBoundingClientRect().width;
|
||||
return css > 0 && visual > 0 ? visual / css : 1;
|
||||
}
|
||||
// Geometry in the grid's own CSS pixels (content box), independent of zoom.
|
||||
function gridGeometry(step, zoom) {
|
||||
const style = getComputedStyle(grid);
|
||||
const tracks = style.gridTemplateColumns.split(" ").map(parseFloat).filter(Number.isFinite);
|
||||
if (tracks.length < 2) return null;
|
||||
const columnGap = parseFloat(style.columnGap) || 0;
|
||||
const starts = [];
|
||||
tracks.reduce((x, width) => { starts.push(x); return x + width + columnGap; }, 0);
|
||||
return {
|
||||
starts, step, zoom,
|
||||
left: (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.paddingLeft) || 0),
|
||||
top: (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.paddingTop) || 0),
|
||||
};
|
||||
}
|
||||
function place(node, geometry) {
|
||||
const box = grid.getBoundingClientRect(), rect = node.getBoundingClientRect();
|
||||
const { starts, zoom } = geometry;
|
||||
const left = (rect.left - box.left) / zoom - geometry.left;
|
||||
const right = (rect.right - box.left) / zoom - geometry.left;
|
||||
const top = (rect.top - box.top) / zoom - geometry.top;
|
||||
const first = starts.reduce((best, start, index) => Math.abs(start - left) < Math.abs(starts[best] - left) ? index : best, 0);
|
||||
let last = first;
|
||||
while (last + 1 < starts.length && starts[last + 1] < right - 1) last++;
|
||||
const row = Math.max(0, Math.round(top / geometry.step));
|
||||
return { node, first, last, row, end: row + rowsOf(node) };
|
||||
}
|
||||
// Stat cards that start on the same grid row get the height of the tallest of
|
||||
// them, like a classic row of tiles. Raising a row can move later cards, so the
|
||||
// pass repeats until nothing changes. Large cards never stretch the tiles.
|
||||
function equalizeStatRows(stats, geometry) {
|
||||
for (let pass = 0; pass < 6; pass++) {
|
||||
const groups = new Map();
|
||||
for (const node of stats) {
|
||||
const { row } = place(node, geometry);
|
||||
if (!groups.has(row)) groups.set(row, []);
|
||||
groups.get(row).push(node);
|
||||
}
|
||||
let changed = false;
|
||||
for (const group of groups.values()) {
|
||||
const target = Math.max(...group.map(rowsOf));
|
||||
for (const node of group) if (rowsOf(node) !== target) { setRows(node, target); changed = true; }
|
||||
}
|
||||
if (!changed) return;
|
||||
}
|
||||
}
|
||||
// A card is stretched down over the empty cells beneath it, up to the next card
|
||||
// below (or the grid's last row), so rows end on straight edges instead of
|
||||
// jagged ones. Only empty cells are taken, which never changes where any other
|
||||
// card is placed. The stored / edited height (state.span) is unaffected.
|
||||
// Only small steps are closed (up to two stat tiles or half the card's own
|
||||
// height); a large hole is left visible rather than hidden by a stretched card.
|
||||
function alignBottom(items, geometry) {
|
||||
const cards = items.map(node => place(node, geometry));
|
||||
const bottom = Math.max(...cards.map(card => card.end));
|
||||
for (const card of cards) {
|
||||
const next = cards.reduce((limit, other) => other !== card && other.first <= card.last
|
||||
&& other.last >= card.first && other.row >= card.end ? Math.min(limit, other.row) : limit, bottom);
|
||||
const rows = card.end - card.row, extra = next - card.end;
|
||||
if (extra > 0 && extra <= Math.max(ROW_DIVISIONS * 2, Math.ceil(rows / 2))) setRows(card.node, rows + extra);
|
||||
}
|
||||
}
|
||||
function schedule(force = false) {
|
||||
if (frame) return;
|
||||
const wait = force ? 0 : Math.max(0, MIN_INTERVAL_MS - (performance.now() - lastRun));
|
||||
if (wait > 0) { frame = setTimeout(() => { frame = 0; schedule(true); }, wait); return; }
|
||||
frame = requestAnimationFrame(apply);
|
||||
}
|
||||
function flush() {
|
||||
if (frame) { cancelAnimationFrame(frame); clearTimeout(frame); frame = 0; }
|
||||
shrinkVotes.clear();
|
||||
state.forEach(entry => { entry.min = 1; });
|
||||
apply();
|
||||
}
|
||||
function setHeights(value) {
|
||||
heights = value && typeof value === "object" ? { ...value } : null;
|
||||
schedule(true);
|
||||
}
|
||||
if (typeof ResizeObserver === "function") {
|
||||
new ResizeObserver(() => {
|
||||
if (grid.clientWidth && grid.clientWidth !== lastWidth) { shrinkVotes.clear(); schedule(true); }
|
||||
}).observe(grid);
|
||||
}
|
||||
document.fonts?.ready?.then(() => schedule(true));
|
||||
return {
|
||||
schedule,
|
||||
flush,
|
||||
setHeights,
|
||||
rowsFor(id) {
|
||||
return grid.classList.contains("has-row-units") ? state.get(id) || null : null;
|
||||
},
|
||||
};
|
||||
})();
|
||||
@@ -14,7 +14,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
chart_axes: false, interface_icons: false, logo_animation: false,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null, dashboard_widget_heights: null,
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool", sunset: "chartStyleSunset", aurora: "chartStyleAurora", ocean: "chartStyleOcean", forest: "chartStyleForest", pastel: "chartStylePastel", terminal: "chartStyleTerminal", blueprint: "chartStyleBlueprint", colorblind: "chartStyleColorblind" });
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
||||
@@ -69,6 +69,15 @@ function normalizeDashboardWidgetSizes(value) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const DASHBOARD_MAX_ROWS = 32;
|
||||
function normalizeDashboardWidgetHeights(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
const result = {};
|
||||
for (const [id, rows] of Object.entries(value)) {
|
||||
if (ADMIN_DASHBOARD_BY_ID.has(id) && Number.isInteger(rows) && rows >= 1 && rows <= DASHBOARD_MAX_ROWS) result[id] = rows;
|
||||
}
|
||||
return Object.keys(result).length ? result : null;
|
||||
}
|
||||
function dashboardWidgetWidth(id, sizes) {
|
||||
return sizes?.[id] ?? dashboardDefaultWidth(id);
|
||||
}
|
||||
@@ -144,7 +153,7 @@ function applyAdminDashboardDensity(value) {
|
||||
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
|
||||
const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
|
||||
}
|
||||
function applyDashboardLayout(order, hidden, sizes = null) {
|
||||
function applyDashboardLayout(order, hidden, sizes = null, heights = undefined) {
|
||||
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
|
||||
const widths = normalizeDashboardWidgetSizes(sizes);
|
||||
const grid = document.querySelector("#panel-overview .stats-grid");
|
||||
@@ -161,6 +170,8 @@ function applyDashboardLayout(order, hidden, sizes = null) {
|
||||
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyMasonry?.refresh?.();
|
||||
if (heights !== undefined) window.rustProxyOverviewRows?.setHeights?.(normalizeDashboardWidgetHeights(heights));
|
||||
window.rustProxyOverviewRows?.schedule?.();
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
|
||||
}
|
||||
@@ -283,14 +294,15 @@ function applyAdminPreferences(value, cache = true) {
|
||||
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
|
||||
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
|
||||
const savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
|
||||
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes);
|
||||
else applyDashboardLayout(savedOrder, savedHidden, savedSizes);
|
||||
const savedHeights = normalizeDashboardWidgetHeights(value.dashboard_widget_heights);
|
||||
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes, editing.heights);
|
||||
else applyDashboardLayout(savedOrder, savedHidden, savedSizes, savedHeights);
|
||||
const normalized = {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), logo_animation: applyAdminLogoAnimation(value.logo_animation), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes, dashboard_widget_heights: savedHeights,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyRefresh?.update(normalized.refresh);
|
||||
@@ -338,7 +350,7 @@ function updateAdminPreferences(patch) {
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.logo_animation = next.logo_animation === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes); next.dashboard_widget_heights = normalizeDashboardWidgetHeights(next.dashboard_widget_heights);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
return applyAdminPreferences(saved);
|
||||
@@ -461,7 +473,7 @@ $("ui-dashboard-density")?.addEventListener("change", () => {
|
||||
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
|
||||
if (control) control.disabled = true; let previous = null;
|
||||
try {
|
||||
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets], dashboard_widget_sizes: current.dashboard_widget_sizes === null ? null : { ...current.dashboard_widget_sizes } } : null;
|
||||
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets], dashboard_widget_sizes: current.dashboard_widget_sizes === null ? null : { ...current.dashboard_widget_sizes }, dashboard_widget_heights: current.dashboard_widget_heights ? { ...current.dashboard_widget_heights } : null } : null;
|
||||
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
|
||||
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { if (control) control.disabled = false; }
|
||||
@@ -469,9 +481,9 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, logo_animation: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
}));
|
||||
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null }, $("appearance-reset"), "appearanceResetDone"));
|
||||
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null, dashboard_widget_heights: null }, $("appearance-reset"), "appearanceResetDone"));
|
||||
refreshAppearanceUi();
|
||||
@@ -665,6 +665,8 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null;
|
||||
$("process-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null;
|
||||
renderProcessOverview(d, status, live, systemSample);
|
||||
window.rustProxyMemoryBreakdown?.render(status, appendSample);
|
||||
window.rustProxyOverviewRows?.schedule();
|
||||
updateCacheMemorySizing();
|
||||
const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null;
|
||||
if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`);
|
||||
|
||||
@@ -1977,6 +1977,72 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
grid-column-start: auto;
|
||||
}
|
||||
|
||||
/* Row units (custom layout): heights snap to the compact stat card height so tall
|
||||
panels sit beside several rows of small cards with aligned edges.
|
||||
overview-rows.js sets --overview-row-unit and each card's --overview-rows. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned.has-row-units {
|
||||
grid-auto-rows: var(--overview-row-unit, 84px);
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-aligned.has-row-units>:is([data-dashboard-widget], .overview-drop-placeholder) {
|
||||
grid-row: auto/span var(--overview-rows, 1);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-aligned.has-row-units>.overview-drop-placeholder {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-measuring-rows>[data-dashboard-widget] {
|
||||
align-self: start !important;
|
||||
}
|
||||
|
||||
/* A card taller than its content gives the extra height to its charts instead of
|
||||
leaving an empty band at the bottom. */
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget]:not([hidden]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget]>svg:not(.ui-icon),
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget]>.mini-live-chart-stack,
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget]>.table-scroll {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget] .mini-live-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget] .mini-live-chart>svg:not(.ui-icon) {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/* With chart scales enabled the SVG sits in an axis host: grow the host and its SVG. */
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget] :is(.rp-chart-axis-host, .mini-live-chart>.rp-chart-axis-host) {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget] .rp-chart-axis-host>svg:not(.ui-icon) {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units {
|
||||
background-image: linear-gradient(to bottom, color-mix(in srgb, var(--accent) 7%, transparent) 0 var(--overview-row-unit, 84px), transparent var(--overview-row-unit, 84px));
|
||||
background-size: 100% calc(var(--overview-row-unit, 84px) + var(--overview-gap, 8px));
|
||||
}
|
||||
|
||||
#panel-overview .overview-inline-height-value {
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
#panel-overview .overview-inline-height[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#panel-overview .overview-inline-width {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -2462,6 +2528,38 @@ html[data-table-density=ultra] .pr-chip { padding: 1px 7px; }
|
||||
.pr-cols, .pr-charts { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Memory breakdown card (memory_breakdown) */
|
||||
.mb-card { display: flex; flex-direction: column; }
|
||||
.mb-card .mb-chips { margin-bottom: 12px; }
|
||||
.mb-bar { display: flex; height: 22px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); gap: 1px; }
|
||||
.mb-bar i { display: block; width: 0; min-width: 0; transition: width .4s ease; }
|
||||
.mb-cache-data { background: var(--chart-accent); }
|
||||
.mb-cache-meta { background: color-mix(in srgb, var(--chart-accent) 50%, var(--panel)); }
|
||||
.mb-transient { background: var(--chart-tertiary); }
|
||||
.mb-alloc-free { background: repeating-linear-gradient(135deg, var(--chart-quinary) 0 4px, color-mix(in srgb, var(--chart-quinary) 35%, var(--panel)) 4px 8px); }
|
||||
.mb-files { background: var(--chart-secondary); }
|
||||
.mb-rest { background: color-mix(in srgb, var(--muted) 55%, var(--panel)); }
|
||||
.mb-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 0 22px; margin: 10px 0 0; padding: 0; list-style: none; }
|
||||
.mb-item { display: grid; grid-template-columns: 10px 1fr auto 46px; align-items: baseline; gap: 8px; padding: 6px 2px; border-top: 1px solid var(--row-line); }
|
||||
.mb-item[hidden] { display: none; }
|
||||
.mb-swatch { width: 10px; height: 10px; align-self: center; border-radius: var(--rp-progress-radius, 0); }
|
||||
.mb-name { min-width: 0; color: var(--muted); font-size: 11px; overflow-wrap: anywhere; }
|
||||
.mb-item b { font: 500 12px var(--mono); font-variant-numeric: tabular-nums; text-align: end; }
|
||||
.mb-item em { color: var(--text-subtle); font: normal 11px var(--mono); font-variant-numeric: tabular-nums; text-align: end; }
|
||||
.mb-cols { flex: 1 0 auto; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); margin-top: 4px; }
|
||||
.mb-trend { display: flex; flex-direction: column; margin-top: 10px; }
|
||||
.mb-trend svg:not(.ui-icon) { flex: 1 1 0; height: 0; min-height: 56px; }
|
||||
.diagnostic-line.mb-outside-line { stroke: var(--chart-quinary); }
|
||||
.mb-trend-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
|
||||
.mb-badge { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); color: var(--muted); font: 9px var(--mono); letter-spacing: 1px; text-transform: uppercase; }
|
||||
.mb-badge[data-state="stable"] { border-color: color-mix(in srgb, var(--green) 55%, transparent); color: var(--green); }
|
||||
.mb-badge[data-state="falling"] { border-color: color-mix(in srgb, var(--blue) 55%, transparent); color: var(--blue); }
|
||||
.mb-badge[data-state="rising"] { border-color: color-mix(in srgb, var(--red) 60%, transparent); color: var(--red); }
|
||||
.mb-unavailable { margin: 6px 0 0; }
|
||||
html[data-table-density=compact] .mb-item { padding-top: 5px; padding-bottom: 5px; }
|
||||
html[data-table-density=ultra] .mb-item { padding-top: 3px; padding-bottom: 3px; }
|
||||
html[data-table-density=ultra] .mb-bar { height: 16px; }
|
||||
|
||||
/* initial width of the cache outcome segments (set from JS via CSSOM; inline style attributes are blocked by the CSP) */
|
||||
.cache-mix-hit, .cache-mix-miss, .cache-mix-bypass, .cache-mix-other { width: 0; }
|
||||
|
||||
|
||||
@@ -1597,8 +1597,10 @@
|
||||
--overview-widget-placement: auto/span 3;
|
||||
}
|
||||
|
||||
/* A third of the wide grid becomes half of the 6-column grid, so two panels or
|
||||
charts sit side by side instead of leaving a two-column hole. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
|
||||
--overview-widget-placement: auto/span 4;
|
||||
--overview-widget-placement: auto/span 3;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"],
|
||||
@@ -1608,6 +1610,10 @@
|
||||
}
|
||||
|
||||
@container overview-canvas (max-width:700px) {
|
||||
#panel-overview .dashboard-overview-grid {
|
||||
--overview-row-units: off;
|
||||
}
|
||||
|
||||
#panel-overview .dashboard-overview-grid.is-aligned {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"extra_cold_queue_memory",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"memory_breakdown",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
@@ -144,7 +145,8 @@
|
||||
"extra_rejected_rate",
|
||||
"extra_cert_expiry",
|
||||
"backend_tiny",
|
||||
"process_simple"
|
||||
"process_simple",
|
||||
"memory_breakdown"
|
||||
],
|
||||
"dashboard_density": "compact",
|
||||
"overview_samples": 600
|
||||
|
||||
@@ -725,6 +725,17 @@
|
||||
"default_hidden": true,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "memory_breakdown",
|
||||
"label_key": "memoryBreakdownTitle",
|
||||
"description_key": "memoryBreakdownDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"memory_breakdown\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "network_diagnostics",
|
||||
"label_key": "networkDiagnostics",
|
||||
|
||||
@@ -339,6 +339,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card mb-card" data-dashboard-widget="memory_breakdown">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="memoryBreakdownTitle">Memory breakdown</h3>
|
||||
<div class="mini-heading-actions">
|
||||
<details class="metric-info">
|
||||
<summary data-i18n-aria="memoryBreakdownInfo" aria-label="Memory breakdown information">i</summary>
|
||||
<div class="metric-info-popover">
|
||||
<p class="fine" data-i18n="memoryBreakdownHint">Splits the process RSS into cached object data, the cache's own bookkeeping (index, keys, headers), short-lived cache buffers, memory the allocator keeps free for reuse (fragmentation; an upper bound, part of it may already be returned to the kernel), program code and mapped files, and everything else (connections, TLS, runtime, logs, GeoIP). For comparison, Varnish documents about 1 KiB of per-object overhead outside its storage size. A steadily rising "outside cache" trend while the cache is full suggests a leak; a flat one means overhead or fragmentation.</p>
|
||||
</div>
|
||||
</details>
|
||||
<span class="meta" id="mb-rss">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="pr-chips mb-chips">
|
||||
<div class="pr-chip" title="Process RSS divided by the accounted HOT cache size" data-i18n-title="memoryRatioHelp"><dt data-i18n="memoryRatio">RSS / cache</dt><dd id="mb-ratio">-</dd></div>
|
||||
<div class="pr-chip" title="Cache bookkeeping per stored object (accounted size minus object data)" data-i18n-title="memoryPerObjectHelp"><dt data-i18n="memoryPerObject">Overhead / object</dt><dd id="mb-per-object">-</dd></div>
|
||||
<div class="pr-chip"><dt data-i18n="memoryObjects">HOT objects</dt><dd id="mb-objects">-</dd></div>
|
||||
<div class="pr-chip" title="Free memory held by the allocator as a share of its heap" data-i18n-title="memoryFragmentationHelp"><dt data-i18n="memoryFragmentation">Fragmentation</dt><dd id="mb-fragmentation">-</dd></div>
|
||||
</dl>
|
||||
<div class="mb-bar" id="mb-bar" role="img" aria-label="Process memory breakdown" data-i18n-aria="memoryBreakdownTitle">
|
||||
<i class="mb-cache-data"></i><i class="mb-cache-meta"></i><i class="mb-transient"></i><i class="mb-alloc-free"></i><i class="mb-files"></i><i class="mb-rest"></i>
|
||||
</div>
|
||||
<ul class="mb-legend">
|
||||
<li class="mb-item" data-mb-row="cache-data"><i class="mb-swatch mb-cache-data" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryCacheData">Cache data (object bodies)</span><b id="mb-cache-data-value">-</b><em id="mb-cache-data-share">-</em></li>
|
||||
<li class="mb-item" data-mb-row="cache-meta"><i class="mb-swatch mb-cache-meta" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryCacheMeta">Cache bookkeeping</span><b id="mb-cache-meta-value">-</b><em id="mb-cache-meta-share">-</em></li>
|
||||
<li class="mb-item" data-mb-row="transient"><i class="mb-swatch mb-transient" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryTransient">Cache buffers (fills, queues)</span><b id="mb-transient-value">-</b><em id="mb-transient-share">-</em></li>
|
||||
<li class="mb-item" data-mb-row="alloc-free"><i class="mb-swatch mb-alloc-free" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryAllocatorFree">Free, held by allocator</span><b id="mb-alloc-free-value">-</b><em id="mb-alloc-free-share">-</em></li>
|
||||
<li class="mb-item" data-mb-row="files"><i class="mb-swatch mb-files" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryFiles">Code and mapped files</span><b id="mb-files-value">-</b><em id="mb-files-share">-</em></li>
|
||||
<li class="mb-item" data-mb-row="rest"><i class="mb-swatch mb-rest" aria-hidden="true"></i><span class="mb-name" data-i18n="memoryRest">Rest of process</span><b id="mb-rest-value">-</b><em id="mb-rest-share">-</em></li>
|
||||
</ul>
|
||||
<div class="pr-cols mb-cols">
|
||||
<section>
|
||||
<h4 class="pr-title" data-i18n="memoryAllocatorTitle">Allocator</h4>
|
||||
<dl class="pr-list pr-plain" id="mb-allocator">
|
||||
<div class="pr-row"><dt data-i18n="memoryAllocatorHeap">Heap (arenas)</dt><dd id="mb-heap">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="memoryAllocatorInUse">In use</dt><dd id="mb-in-use">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="memoryAllocatorFreeHeld">Free, held</dt><dd id="mb-free">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="memoryAllocatorMmap">Large blocks (mmap) · count</dt><dd id="mb-mmap">-</dd></div>
|
||||
</dl>
|
||||
<p class="fine mb-unavailable" id="mb-allocator-unavailable" hidden data-i18n="memoryAllocatorUnavailable">Allocator statistics are available only with the default glibc allocator.</p>
|
||||
</section>
|
||||
<section class="mini-live-chart mb-trend">
|
||||
<div class="mini-live-chart-head"><span data-i18n="memoryOutsideCache">Outside cache</span><b id="mb-outside-value">-</b></div>
|
||||
<svg preserveAspectRatio="none" aria-label="Memory outside cache history" data-i18n-aria="memoryOutsideCacheHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line mb-outside-line" id="mb-outside-sparkline" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
<div class="mb-trend-foot"><span class="mb-badge" id="mb-trend-badge" data-state="collecting">-</span><span class="meta" id="mb-trend-rate">-</span></div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card" data-dashboard-widget="process_simple">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="processClassicTitle">Process simple</h3>
|
||||
|
||||
Reference in new issue
Block a user