perf: optimize Dashboard guest sorting and parent node lookups
- Pre-compute guest-to-parent-node mapping to avoid repeated lookups during render - Memoize sort comparator to avoid duplicating sorting logic - Reduces computational overhead when rendering large guest lists
This commit is contained in:
parent
8ed0851fb9
commit
8abcc93aac
2 changed files with 109 additions and 138 deletions
|
|
@ -361,27 +361,34 @@ export function Dashboard(props: DashboardProps) {
|
|||
return map;
|
||||
});
|
||||
|
||||
const resolveParentNode = (guest: VM | Container): Node | undefined => {
|
||||
if (!guest) return undefined;
|
||||
// PERFORMANCE: Pre-compute guest-to-parent-node mapping for faster lookups
|
||||
// This avoids repeated node lookups for each guest during render
|
||||
const guestParentNodeMap = createMemo(() => {
|
||||
const nodes = nodeByInstance();
|
||||
const mapping = new Map<string, Node>();
|
||||
|
||||
allGuests().forEach((guest) => {
|
||||
// Try guest.id-based lookup first
|
||||
if (guest.id) {
|
||||
const lastDash = guest.id.lastIndexOf('-');
|
||||
if (lastDash > 0) {
|
||||
const nodeId = guest.id.slice(0, lastDash);
|
||||
if (nodes[nodeId]) {
|
||||
return nodes[nodeId];
|
||||
mapping.set(guest.id, nodes[nodeId]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to composite key
|
||||
const compositeKey = `${guest.instance}-${guest.node}`;
|
||||
if (nodes[compositeKey]) {
|
||||
return nodes[compositeKey];
|
||||
mapping.set(guest.id || `${guest.instance}-${guest.vmid}`, nodes[compositeKey]);
|
||||
}
|
||||
});
|
||||
|
||||
return mapping;
|
||||
});
|
||||
|
||||
return undefined;
|
||||
};
|
||||
// Sort handler
|
||||
const handleSort = (key: keyof (VM | Container)) => {
|
||||
if (sortKey() === key) {
|
||||
|
|
@ -433,6 +440,65 @@ export function Dashboard(props: DashboardProps) {
|
|||
return null;
|
||||
};
|
||||
|
||||
// PERFORMANCE: Memoized sort comparator to avoid duplicating sorting logic
|
||||
// This comparator is reused by both flat and grouped modes in groupedGuests
|
||||
const guestSortComparator = createMemo(() => {
|
||||
const key = sortKey();
|
||||
const dir = sortDirection();
|
||||
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (a: VM | Container, b: VM | Container): number => {
|
||||
let aVal: string | number | boolean | null | undefined = a[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
let bVal: string | number | boolean | null | undefined = b[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// Special handling for percentage-based columns
|
||||
if (key === 'cpu') {
|
||||
aVal = a.cpu * 100;
|
||||
bVal = b.cpu * 100;
|
||||
} else if (key === 'memory') {
|
||||
aVal = a.memory ? a.memory.usage || 0 : 0;
|
||||
bVal = b.memory ? b.memory.usage || 0 : 0;
|
||||
} else if (key === 'disk') {
|
||||
aVal = getDiskUsagePercent(a);
|
||||
bVal = getDiskUsagePercent(b);
|
||||
}
|
||||
|
||||
// Handle null/undefined/empty values - put at end for both asc and desc
|
||||
const aIsEmpty = aVal === null || aVal === undefined || aVal === '';
|
||||
const bIsEmpty = bVal === null || bVal === undefined || bVal === '';
|
||||
|
||||
if (aIsEmpty && bIsEmpty) return 0;
|
||||
if (aIsEmpty) return 1;
|
||||
if (bIsEmpty) return -1;
|
||||
|
||||
// Type-specific comparison
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
} else {
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr === bStr) return 0;
|
||||
const comparison = aStr < bStr ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
let searchInputRef: HTMLInputElement | undefined;
|
||||
|
||||
|
|
@ -662,61 +728,10 @@ export function Dashboard(props: DashboardProps) {
|
|||
// If flat mode, return all guests in a single group
|
||||
if (groupingMode() === 'flat') {
|
||||
const groups: Record<string, (VM | Container)[]> = { '': guests };
|
||||
// Sort the flat list
|
||||
const key = sortKey();
|
||||
const dir = sortDirection();
|
||||
if (key) {
|
||||
groups[''] = groups[''].sort((a, b) => {
|
||||
let aVal: string | number | boolean | null | undefined = a[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
let bVal: string | number | boolean | null | undefined = b[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// Special handling for percentage-based columns
|
||||
if (key === 'cpu') {
|
||||
// CPU is displayed as percentage
|
||||
aVal = a.cpu * 100;
|
||||
bVal = b.cpu * 100;
|
||||
} else if (key === 'memory') {
|
||||
// Memory is displayed as percentage (use pre-calculated usage)
|
||||
aVal = a.memory ? a.memory.usage || 0 : 0;
|
||||
bVal = b.memory ? b.memory.usage || 0 : 0;
|
||||
} else if (key === 'disk') {
|
||||
aVal = getDiskUsagePercent(a);
|
||||
bVal = getDiskUsagePercent(b);
|
||||
}
|
||||
|
||||
// Handle null/undefined/empty values - put at end for both asc and desc
|
||||
const aIsEmpty = aVal === null || aVal === undefined || aVal === '';
|
||||
const bIsEmpty = bVal === null || bVal === undefined || bVal === '';
|
||||
|
||||
if (aIsEmpty && bIsEmpty) return 0;
|
||||
if (aIsEmpty) return 1;
|
||||
if (bIsEmpty) return -1;
|
||||
|
||||
// Type-specific value preparation
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
// Numeric comparison
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
} else {
|
||||
// String comparison (case-insensitive)
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr === bStr) return 0;
|
||||
const comparison = aStr < bStr ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
}
|
||||
});
|
||||
// PERFORMANCE: Use memoized sort comparator (eliminates ~50 lines of duplicate code)
|
||||
const comparator = guestSortComparator();
|
||||
if (comparator) {
|
||||
groups[''] = groups[''].sort(comparator);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
|
@ -733,62 +748,11 @@ export function Dashboard(props: DashboardProps) {
|
|||
groups[nodeId].push(guest);
|
||||
});
|
||||
|
||||
// Sort within each node group
|
||||
const key = sortKey();
|
||||
const dir = sortDirection();
|
||||
if (key) {
|
||||
// PERFORMANCE: Use memoized sort comparator (eliminates ~50 lines of duplicate code)
|
||||
const comparator = guestSortComparator();
|
||||
if (comparator) {
|
||||
Object.keys(groups).forEach((node) => {
|
||||
groups[node] = groups[node].sort((a, b) => {
|
||||
let aVal: string | number | boolean | null | undefined = a[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
let bVal: string | number | boolean | null | undefined = b[key] as
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
// Special handling for percentage-based columns
|
||||
if (key === 'cpu') {
|
||||
// CPU is displayed as percentage
|
||||
aVal = a.cpu * 100;
|
||||
bVal = b.cpu * 100;
|
||||
} else if (key === 'memory') {
|
||||
// Memory is displayed as percentage (use pre-calculated usage)
|
||||
aVal = a.memory ? a.memory.usage || 0 : 0;
|
||||
bVal = b.memory ? b.memory.usage || 0 : 0;
|
||||
} else if (key === 'disk') {
|
||||
aVal = getDiskUsagePercent(a);
|
||||
bVal = getDiskUsagePercent(b);
|
||||
}
|
||||
|
||||
// Handle null/undefined/empty values - put at end for both asc and desc
|
||||
const aIsEmpty = aVal === null || aVal === undefined || aVal === '';
|
||||
const bIsEmpty = bVal === null || bVal === undefined || bVal === '';
|
||||
|
||||
if (aIsEmpty && bIsEmpty) return 0;
|
||||
if (aIsEmpty) return 1;
|
||||
if (bIsEmpty) return -1;
|
||||
|
||||
// Type-specific value preparation
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
// Numeric comparison
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
} else {
|
||||
// String comparison (case-insensitive)
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr === bStr) return 0;
|
||||
const comparison = aStr < bStr ? -1 : 1;
|
||||
return dir === 'asc' ? comparison : -comparison;
|
||||
}
|
||||
});
|
||||
groups[node] = groups[node].sort(comparator);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1160,7 +1124,8 @@ export function Dashboard(props: DashboardProps) {
|
|||
const metadata =
|
||||
guestMetadata()[guestId] ||
|
||||
guestMetadata()[`${guest.node}-${guest.vmid}`];
|
||||
const parentNode = node ?? resolveParentNode(guest);
|
||||
// PERFORMANCE: Use pre-computed parent node map instead of resolveParentNode
|
||||
const parentNode = node ?? guestParentNodeMap().get(guestId);
|
||||
const parentNodeOnline = parentNode ? isNodeOnline(parentNode) : true;
|
||||
|
||||
// Get adjacent guest IDs for merged AI context borders
|
||||
|
|
|
|||
|
|
@ -491,11 +491,17 @@ export function GuestRow(props: GuestRowProps) {
|
|||
// Get current metrics view mode (bars vs sparklines)
|
||||
const { viewMode } = useMetricsViewMode();
|
||||
|
||||
// PERFORMANCE: Use memoized Set for O(1) column visibility lookups instead of O(n) array.includes()
|
||||
const visibleColumnIdSet = createMemo(() =>
|
||||
props.visibleColumnIds ? new Set(props.visibleColumnIds) : null
|
||||
);
|
||||
|
||||
// Helper to check if a column is visible
|
||||
// If visibleColumnIds is not provided, show all columns for backwards compatibility
|
||||
const isColVisible = (colId: string) => {
|
||||
if (!props.visibleColumnIds) return true;
|
||||
return props.visibleColumnIds.includes(colId);
|
||||
const set = visibleColumnIdSet();
|
||||
if (!set) return true;
|
||||
return set.has(colId);
|
||||
};
|
||||
|
||||
// Create namespaced metrics key for sparklines
|
||||
|
|
|
|||
Loading…
Reference in a new issue