diff --git a/src/renderer/src/components/views/MemoryAnalysis.vue b/src/renderer/src/components/views/MemoryAnalysis.vue index 4c27db0..8008958 100644 --- a/src/renderer/src/components/views/MemoryAnalysis.vue +++ b/src/renderer/src/components/views/MemoryAnalysis.vue @@ -21,8 +21,13 @@ const sortAsc = ref(false) const scanAbort = ref(null) const SCAN_COUNT = 200 -/** How often (ms) to flush buffered scan results into the reactive ref */ -const FLUSH_INTERVAL = 300 +const FLUSH_INTERVAL = 100 + +// --- Virtual scroll --- +const listRef = ref(null) +const scrollTop = ref(0) +const ROW_HEIGHT = 36 +const BUFFER = 10 const sortedEntries = computed(() => { const filtered = minSizeKB.value > 0 @@ -33,6 +38,34 @@ const sortedEntries = computed(() => { const totalFiltered = computed(() => sortedEntries.value.reduce((s, e) => s + e.size, 0)) +const totalHeight = computed(() => sortedEntries.value.length * ROW_HEIGHT) + +const visibleRange = computed(() => { + const container = listRef.value + if (!container) return { start: 0, end: 50 } + const viewHeight = container.clientHeight - ROW_HEIGHT // minus header + const start = Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - BUFFER) + const end = Math.min( + sortedEntries.value.length, + Math.ceil((scrollTop.value + viewHeight) / ROW_HEIGHT) + BUFFER, + ) + return { start, end } +}) + +const visibleItems = computed(() => { + const { start, end } = visibleRange.value + return sortedEntries.value.slice(start, end).map((item, i) => ({ + ...item, + _idx: start + i, + })) +}) + +const offsetY = computed(() => visibleRange.value.start * ROW_HEIGHT) + +function onScroll(e: Event) { + scrollTop.value = (e.target as HTMLElement).scrollTop +} + function formatSize(bytes: number): string { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] @@ -58,12 +91,12 @@ async function startScan() { if (!connStore.activeId) return entries.value = [] scannedCount.value = 0 + scrollTop.value = 0 scanning.value = true paused.value = false const controller = new AbortController() scanAbort.value = controller - // Non-reactive buffer: scan loop pushes here, timer flushes to entries.value const buffer: KeyEntry[] = [] let flushTimer: ReturnType | null = null @@ -84,13 +117,13 @@ async function startScan() { if (controller.signal.aborted) break } const result = await window.electronAPI.redis.scanKeys( - connStore.activeId, cursor, '*', SCAN_COUNT + connStore.activeId, cursor, '*', SCAN_COUNT, ) cursor = result.cursor if (controller.signal.aborted || result.keys.length === 0) continue const batch = await window.electronAPI.redis.batchMemoryUsage( - connStore.activeId, result.keys + connStore.activeId, result.keys, ) if (controller.signal.aborted) break buffer.push(...batch) @@ -100,7 +133,7 @@ async function startScan() { console.error('Scan error:', err) } finally { if (flushTimer) clearInterval(flushTimer) - flush() // final flush: push any remaining buffered entries + flush() scanning.value = false paused.value = false scanAbort.value = null @@ -170,23 +203,31 @@ onUnmounted(() => { -
- - - - - - - - - -
+
+ +
+ {{ t('memory.key') }} + {{ t('memory.type') }} + {{ t('memory.size') }} +
+ +
+
+
+
+ {{ item.key }} + {{ formatType(item.type) }} + {{ formatSize(item.size) }} +
+
+
+
+
{{ t('memory.noData') }}
@@ -258,9 +299,83 @@ onUnmounted(() => { margin-left: auto; } -.ma-table-wrap { +/* --- Virtual list --- */ +.ma-list-wrap { flex: 1; + display: flex; + flex-direction: column; overflow: hidden; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); +} + +.ma-list-header { + display: flex; + height: 36px; + align-items: center; + padding: 0 12px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.ma-list-header .ma-col { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + user-select: none; +} + +.ma-list-body { + flex: 1; + overflow-y: auto; +} + +.ma-row { + display: flex; + height: 36px; + align-items: center; + padding: 0 12px; + border-bottom: 1px solid var(--border-color); + cursor: default; +} + +.ma-row:hover { + background: var(--bg-hover); +} + +.ma-row-alt { + background: rgba(255, 255, 255, 0.02); +} + +.ma-row-alt:hover { + background: var(--bg-hover); +} + +.ma-col { + font-size: 12px; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ma-col-key { + flex: 1; + min-width: 0; +} + +.ma-col-type { + width: 80px; + flex-shrink: 0; + color: var(--text-secondary); +} + +.ma-col-size { + width: 100px; + flex-shrink: 0; + text-align: right; + font-variant-numeric: tabular-nums; } .ma-empty {