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;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolveParentNode = (guest: VM | Container): Node | undefined => {
|
// PERFORMANCE: Pre-compute guest-to-parent-node mapping for faster lookups
|
||||||
if (!guest) return undefined;
|
// This avoids repeated node lookups for each guest during render
|
||||||
|
const guestParentNodeMap = createMemo(() => {
|
||||||
const nodes = nodeByInstance();
|
const nodes = nodeByInstance();
|
||||||
|
const mapping = new Map<string, Node>();
|
||||||
|
|
||||||
|
allGuests().forEach((guest) => {
|
||||||
|
// Try guest.id-based lookup first
|
||||||
if (guest.id) {
|
if (guest.id) {
|
||||||
const lastDash = guest.id.lastIndexOf('-');
|
const lastDash = guest.id.lastIndexOf('-');
|
||||||
if (lastDash > 0) {
|
if (lastDash > 0) {
|
||||||
const nodeId = guest.id.slice(0, lastDash);
|
const nodeId = guest.id.slice(0, lastDash);
|
||||||
if (nodes[nodeId]) {
|
if (nodes[nodeId]) {
|
||||||
return nodes[nodeId];
|
mapping.set(guest.id, nodes[nodeId]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fallback to composite key
|
||||||
const compositeKey = `${guest.instance}-${guest.node}`;
|
const compositeKey = `${guest.instance}-${guest.node}`;
|
||||||
if (nodes[compositeKey]) {
|
if (nodes[compositeKey]) {
|
||||||
return nodes[compositeKey];
|
mapping.set(guest.id || `${guest.instance}-${guest.vmid}`, nodes[compositeKey]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
});
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
// Sort handler
|
// Sort handler
|
||||||
const handleSort = (key: keyof (VM | Container)) => {
|
const handleSort = (key: keyof (VM | Container)) => {
|
||||||
if (sortKey() === key) {
|
if (sortKey() === key) {
|
||||||
|
|
@ -433,6 +440,65 @@ export function Dashboard(props: DashboardProps) {
|
||||||
return null;
|
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
|
// Handle keyboard shortcuts
|
||||||
let searchInputRef: HTMLInputElement | undefined;
|
let searchInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
|
@ -662,61 +728,10 @@ export function Dashboard(props: DashboardProps) {
|
||||||
// If flat mode, return all guests in a single group
|
// If flat mode, return all guests in a single group
|
||||||
if (groupingMode() === 'flat') {
|
if (groupingMode() === 'flat') {
|
||||||
const groups: Record<string, (VM | Container)[]> = { '': guests };
|
const groups: Record<string, (VM | Container)[]> = { '': guests };
|
||||||
// Sort the flat list
|
// PERFORMANCE: Use memoized sort comparator (eliminates ~50 lines of duplicate code)
|
||||||
const key = sortKey();
|
const comparator = guestSortComparator();
|
||||||
const dir = sortDirection();
|
if (comparator) {
|
||||||
if (key) {
|
groups[''] = groups[''].sort(comparator);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
@ -733,62 +748,11 @@ export function Dashboard(props: DashboardProps) {
|
||||||
groups[nodeId].push(guest);
|
groups[nodeId].push(guest);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort within each node group
|
// PERFORMANCE: Use memoized sort comparator (eliminates ~50 lines of duplicate code)
|
||||||
const key = sortKey();
|
const comparator = guestSortComparator();
|
||||||
const dir = sortDirection();
|
if (comparator) {
|
||||||
if (key) {
|
|
||||||
Object.keys(groups).forEach((node) => {
|
Object.keys(groups).forEach((node) => {
|
||||||
groups[node] = groups[node].sort((a, b) => {
|
groups[node] = groups[node].sort(comparator);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1160,7 +1124,8 @@ export function Dashboard(props: DashboardProps) {
|
||||||
const metadata =
|
const metadata =
|
||||||
guestMetadata()[guestId] ||
|
guestMetadata()[guestId] ||
|
||||||
guestMetadata()[`${guest.node}-${guest.vmid}`];
|
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;
|
const parentNodeOnline = parentNode ? isNodeOnline(parentNode) : true;
|
||||||
|
|
||||||
// Get adjacent guest IDs for merged AI context borders
|
// 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)
|
// Get current metrics view mode (bars vs sparklines)
|
||||||
const { viewMode } = useMetricsViewMode();
|
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
|
// Helper to check if a column is visible
|
||||||
// If visibleColumnIds is not provided, show all columns for backwards compatibility
|
// If visibleColumnIds is not provided, show all columns for backwards compatibility
|
||||||
const isColVisible = (colId: string) => {
|
const isColVisible = (colId: string) => {
|
||||||
if (!props.visibleColumnIds) return true;
|
const set = visibleColumnIdSet();
|
||||||
return props.visibleColumnIds.includes(colId);
|
if (!set) return true;
|
||||||
|
return set.has(colId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create namespaced metrics key for sparklines
|
// Create namespaced metrics key for sparklines
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue