u
This commit is contained in:
@@ -19,7 +19,6 @@ import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
|
||||
@@ -29,17 +28,21 @@ import HistoryIcon from '@mui/icons-material/History';
|
||||
import GridViewIcon from '@mui/icons-material/GridView';
|
||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import LayersIcon from '@mui/icons-material/Layers';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
|
||||
import orderAutomat, {
|
||||
CHUNK_SIZE,
|
||||
MAX_LRU_CHUNKS,
|
||||
isOrderPaid,
|
||||
isOrderDelivered,
|
||||
subscribeChunkStats,
|
||||
} from '../automats/orderAutomat';
|
||||
import { formatPrice } from '../utils/price';
|
||||
|
||||
const PANEL_WIDTH = 620;
|
||||
const PANEL_HEIGHT = 480;
|
||||
const PANEL_WIDTH = 680;
|
||||
const PANEL_HEIGHT = 520;
|
||||
|
||||
const pulseAnimationA = keyframes`
|
||||
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(25, 118, 210, 0); }
|
||||
@@ -69,29 +72,38 @@ function orderGrossTotal(lines) {
|
||||
class ChunkCacheMonitor extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialStats = orderAutomat.getChunkStats?.() || {
|
||||
chunkSize: CHUNK_SIZE,
|
||||
maxLruChunks: MAX_LRU_CHUNKS,
|
||||
lruChunksCount: 0,
|
||||
total: 0,
|
||||
chunkCount: 0,
|
||||
loadedChunks: [],
|
||||
inflightChunks: [],
|
||||
pendingChunks: [],
|
||||
loadedItemsCount: 0,
|
||||
status: 'idle',
|
||||
queryEpoch: 0,
|
||||
filter: '',
|
||||
onlyUnpaid: false,
|
||||
onlyUndelivered: false,
|
||||
negativeSeekMonths: 0,
|
||||
items: null,
|
||||
activeFilterKey: '',
|
||||
activeFilterLabel: 'Standard',
|
||||
filterCaches: [],
|
||||
lruEntries: [],
|
||||
events: [],
|
||||
};
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
position: null,
|
||||
tab: 'grid', // 'grid' | 'log'
|
||||
tab: 'grid', // 'grid' | 'lru' | 'log'
|
||||
inspectFilterKey: null, // null means follow activeFilterKey
|
||||
selectedChunk: null,
|
||||
pulseCounter: 0,
|
||||
stats: orderAutomat.getChunkStats?.() || {
|
||||
chunkSize: CHUNK_SIZE,
|
||||
total: 0,
|
||||
chunkCount: 0,
|
||||
loadedChunks: [],
|
||||
inflightChunks: [],
|
||||
pendingChunks: [],
|
||||
loadedItemsCount: 0,
|
||||
status: 'idle',
|
||||
queryEpoch: 0,
|
||||
filter: '',
|
||||
onlyUnpaid: false,
|
||||
onlyUndelivered: false,
|
||||
negativeSeekMonths: 0,
|
||||
items: null,
|
||||
events: [],
|
||||
},
|
||||
stats: initialStats,
|
||||
};
|
||||
|
||||
this.panelRef = createRef();
|
||||
@@ -105,6 +117,8 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
this.handleLoadChunk = this.handleLoadChunk.bind(this);
|
||||
this.handleEvictChunk = this.handleEvictChunk.bind(this);
|
||||
this.handleClearCache = this.handleClearCache.bind(this);
|
||||
this.handleClearFilter = this.handleClearFilter.bind(this);
|
||||
this.handleApplyFilter = this.handleApplyFilter.bind(this);
|
||||
this.handlePrefetchAll = this.handlePrefetchAll.bind(this);
|
||||
this.handleReload = this.handleReload.bind(this);
|
||||
}
|
||||
@@ -152,7 +166,7 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
}
|
||||
|
||||
handlePointerDown(event) {
|
||||
if (event.target.closest('button') || event.target.closest('.MuiTab-root')) return;
|
||||
if (event.target.closest('button') || event.target.closest('.MuiTab-root') || event.target.closest('.MuiChip-root')) return;
|
||||
const rect = this.panelRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
event.preventDefault();
|
||||
@@ -188,14 +202,16 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
}));
|
||||
}
|
||||
|
||||
handleLoadChunk(chunkIndex, e) {
|
||||
handleLoadChunk(chunkIndex, filterKey, e) {
|
||||
e?.stopPropagation();
|
||||
void orderAutomat.actions.requireChunk(chunkIndex, chunkIndex, { immediate: true });
|
||||
const targetFilter = filterKey || this.getInspectedFilterKey();
|
||||
void orderAutomat.actions.loadChunk(chunkIndex, orderAutomat.state.queryEpoch, targetFilter);
|
||||
}
|
||||
|
||||
handleEvictChunk(chunkIndex, e) {
|
||||
handleEvictChunk(chunkIndex, filterKey, e) {
|
||||
e?.stopPropagation();
|
||||
orderAutomat.evictChunk?.(chunkIndex);
|
||||
const targetFilter = filterKey || this.getInspectedFilterKey();
|
||||
orderAutomat.evictChunk?.(chunkIndex, targetFilter);
|
||||
if (this.state.selectedChunk === chunkIndex) {
|
||||
this.setState({ selectedChunk: null });
|
||||
}
|
||||
@@ -206,34 +222,145 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
this.setState({ selectedChunk: null });
|
||||
}
|
||||
|
||||
handlePrefetchAll() {
|
||||
orderAutomat.prefetchAllChunks?.();
|
||||
handleClearFilter(filterKey, e) {
|
||||
e?.stopPropagation();
|
||||
orderAutomat.clearFilterCache?.(filterKey);
|
||||
if (this.state.inspectFilterKey === filterKey) {
|
||||
this.setState({ inspectFilterKey: null, selectedChunk: null });
|
||||
}
|
||||
}
|
||||
|
||||
handleApplyFilter(filterKey) {
|
||||
const entry = this.state.stats.filterCaches?.find((f) => f.key === filterKey);
|
||||
if (!entry) return;
|
||||
const p = entry.params || {};
|
||||
orderAutomat.actions.resetFilters();
|
||||
if (p.filter) orderAutomat.actions.filter(p.filter);
|
||||
if (p.onlyUnpaid) orderAutomat.actions.setOnlyUnpaid(true);
|
||||
if (p.onlyUndelivered) orderAutomat.actions.setOnlyUndelivered(true);
|
||||
if (p.negativeSeekMonths) orderAutomat.actions.addNegativeSeek(p.negativeSeekMonths);
|
||||
this.setState({ inspectFilterKey: null, selectedChunk: null });
|
||||
}
|
||||
|
||||
handlePrefetchAll(filterKey) {
|
||||
const targetFilter = filterKey || this.getInspectedFilterKey();
|
||||
orderAutomat.prefetchAllChunks?.(targetFilter);
|
||||
}
|
||||
|
||||
handleReload() {
|
||||
void orderAutomat.actions.reload();
|
||||
void orderAutomat.actions.reload(false);
|
||||
this.setState({ selectedChunk: null });
|
||||
}
|
||||
|
||||
getInspectedFilterKey() {
|
||||
return this.state.inspectFilterKey || this.state.stats.activeFilterKey;
|
||||
}
|
||||
|
||||
getInspectedFilterEntry() {
|
||||
const key = this.getInspectedFilterKey();
|
||||
const entry = this.state.stats.filterCaches?.find((f) => f.key === key);
|
||||
if (entry) return entry;
|
||||
|
||||
return {
|
||||
key,
|
||||
label: this.state.stats.activeFilterLabel || 'Aktiver Filter',
|
||||
total: this.state.stats.total,
|
||||
loadedChunksCount: this.state.stats.loadedChunks.length,
|
||||
loadedChunks: this.state.stats.loadedChunks,
|
||||
chunkCount: this.state.stats.chunkCount,
|
||||
loadedItemsCount: this.state.stats.loadedItemsCount,
|
||||
isActive: true,
|
||||
items: this.state.stats.items,
|
||||
status: this.state.stats.status,
|
||||
};
|
||||
}
|
||||
|
||||
renderFilterSelector() {
|
||||
const { stats, inspectFilterKey } = this.state;
|
||||
const filterCaches = stats.filterCaches || [];
|
||||
const activeKey = stats.activeFilterKey;
|
||||
const currentInspected = this.getInspectedFilterKey();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': { height: 4 },
|
||||
'&::-webkit-scrollbar-thumb': { bgcolor: 'divider', borderRadius: 2 },
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', whiteSpace: 'nowrap' }}>
|
||||
Filter-Caches ({filterCaches.length}):
|
||||
</Typography>
|
||||
|
||||
{filterCaches.map((entry) => {
|
||||
const isInspected = entry.key === currentInspected;
|
||||
const isActive = entry.key === activeKey;
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={entry.key}
|
||||
size="small"
|
||||
variant={isInspected ? 'filled' : 'outlined'}
|
||||
color={isActive ? 'primary' : isInspected ? 'secondary' : 'default'}
|
||||
label={`${entry.label} (${entry.loadedChunksCount} Chunks)`}
|
||||
onClick={() => this.setState({ inspectFilterKey: entry.key, selectedChunk: null })}
|
||||
onDelete={!isActive ? (e) => this.handleClearFilter(entry.key, e) : undefined}
|
||||
sx={{
|
||||
fontWeight: isInspected ? 700 : 500,
|
||||
fontSize: '0.72rem',
|
||||
height: 26,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
borderWidth: isInspected ? 2 : 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderChunkGrid() {
|
||||
const { stats, selectedChunk } = this.state;
|
||||
const { total, chunkCount, loadedChunks, inflightChunks, pendingChunks, items } = stats;
|
||||
const loadedSet = new Set(loadedChunks);
|
||||
const inflightSet = new Set(inflightChunks);
|
||||
const pendingSet = new Set(pendingChunks);
|
||||
const currentFilter = this.getInspectedFilterEntry();
|
||||
const isActiveFilter = currentFilter.key === stats.activeFilterKey;
|
||||
|
||||
const { total, chunkCount, loadedChunks, items } = currentFilter;
|
||||
const loadedSet = new Set(loadedChunks || []);
|
||||
const inflightSet = new Set(isActiveFilter ? stats.inflightChunks : []);
|
||||
const pendingSet = new Set(isActiveFilter ? stats.pendingChunks : []);
|
||||
|
||||
// LRU rank lookup Map: lruKey -> rank (1..50)
|
||||
const lruRankMap = new Map();
|
||||
if (Array.isArray(stats.lruEntries)) {
|
||||
for (const entry of stats.lruEntries) {
|
||||
if (entry.filterKey === currentFilter.key) {
|
||||
lruRankMap.set(entry.chunkIndex, entry.lruRank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If total is 0 and no chunks loaded, show uninitialized state
|
||||
if (chunkCount === 0 && (!items || items.length === 0)) {
|
||||
return (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
||||
Automat ist noch nicht initialisiert (keine Daten geladen).
|
||||
Keine Daten für diesen Filter geladen ({currentFilter.label}).
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
startIcon={<CloudDownloadIcon />}
|
||||
onClick={() => this.handleLoadChunk(0)}
|
||||
onClick={() => this.handleLoadChunk(0, currentFilter.key)}
|
||||
>
|
||||
Ersten Chunk (#0) laden
|
||||
</Button>
|
||||
@@ -249,11 +376,11 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
const isInflight = inflightSet.has(c);
|
||||
const isPending = pendingSet.has(c);
|
||||
const isSelected = selectedChunk === c;
|
||||
const lruRank = lruRankMap.get(c);
|
||||
|
||||
const offsetStart = c * CHUNK_SIZE;
|
||||
const offsetEnd = Math.min((c + 1) * CHUNK_SIZE - 1, Math.max(0, total - 1));
|
||||
|
||||
// Calculate loaded items inside this chunk
|
||||
let itemsInChunk = 0;
|
||||
if (Array.isArray(items)) {
|
||||
for (let i = offsetStart; i <= offsetEnd; i++) {
|
||||
@@ -266,20 +393,16 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
let borderStyle = '1px solid';
|
||||
let borderColor = 'divider';
|
||||
let bgColor = 'background.paper';
|
||||
let statusLabel = 'Nicht geladen';
|
||||
|
||||
if (isLoaded) {
|
||||
statusLabel = `Geladen (${itemsInChunk}/${expectedInChunk})`;
|
||||
statusColor = 'success.main';
|
||||
borderColor = 'success.main';
|
||||
bgColor = 'rgba(46, 125, 50, 0.08)';
|
||||
} else if (isInflight) {
|
||||
statusLabel = 'Lädt über Netzwerk...';
|
||||
statusColor = 'info.main';
|
||||
borderColor = 'info.main';
|
||||
bgColor = 'rgba(2, 136, 209, 0.08)';
|
||||
} else if (isPending) {
|
||||
statusLabel = 'Vorgemerkt (Debounce)';
|
||||
statusColor = 'warning.main';
|
||||
borderStyle = '1px dashed';
|
||||
borderColor = 'warning.main';
|
||||
@@ -289,7 +412,7 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
blocks.push(
|
||||
<Box
|
||||
key={c}
|
||||
onClick={() => (isLoaded ? this.handleSelectChunk(c) : this.handleLoadChunk(c))}
|
||||
onClick={() => (isLoaded ? this.handleSelectChunk(c) : this.handleLoadChunk(c, currentFilter.key))}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
p: 1,
|
||||
@@ -309,7 +432,7 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: 74,
|
||||
minHeight: 82,
|
||||
}}
|
||||
>
|
||||
{/* Top Row: Chunk Number & Status Indicator */}
|
||||
@@ -318,27 +441,46 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
#{c}
|
||||
</Typography>
|
||||
|
||||
{isLoaded ? (
|
||||
<Tooltip title="Im Cache">
|
||||
<CheckCircleOutlinedIcon fontSize="small" color="success" sx={{ fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
) : isInflight ? (
|
||||
<Tooltip title="Netzwerk-Request läuft">
|
||||
<CircularProgress size={14} color="info" />
|
||||
</Tooltip>
|
||||
) : isPending ? (
|
||||
<Tooltip title="Wartet auf Debounce">
|
||||
<HourglassEmptyIcon fontSize="small" color="warning" sx={{ fontSize: 16 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Klicken zum Laden">
|
||||
<CloudDownloadIcon fontSize="small" color="action" sx={{ fontSize: 16, opacity: 0.6 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{isLoaded && lruRank != null && (
|
||||
<Tooltip title={lruRank === 1 ? 'Ältester Chunk im RAM: Wird bei Überschreiten von 50 Chunks als nächstes verworfen!' : `LRU-Position #${lruRank} von ${stats.lruChunksCount}`}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`LRU #${lruRank}`}
|
||||
color={lruRank === 1 ? 'warning' : 'default'}
|
||||
sx={{
|
||||
height: 18,
|
||||
fontSize: '0.62rem',
|
||||
fontWeight: 700,
|
||||
px: 0.25,
|
||||
'& .MuiChip-label': { px: 0.5 },
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isLoaded ? (
|
||||
<Tooltip title="Im RAM">
|
||||
<CheckCircleOutlinedIcon fontSize="small" color="success" sx={{ fontSize: 16 }} />
|
||||
</Tooltip>
|
||||
) : isInflight ? (
|
||||
<Tooltip title="Netzwerk-Request läuft">
|
||||
<CircularProgress size={14} color="info" />
|
||||
</Tooltip>
|
||||
) : isPending ? (
|
||||
<Tooltip title="Wartet auf Debounce">
|
||||
<HourglassEmptyIcon fontSize="small" color="warning" sx={{ fontSize: 16 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Klicken zum Laden">
|
||||
<CloudDownloadIcon fontSize="small" color="action" sx={{ fontSize: 16, opacity: 0.6 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Middle: Item range index */}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', fontSize: '0.72rem' }}>
|
||||
{offsetStart}–{offsetEnd}
|
||||
</Typography>
|
||||
|
||||
@@ -350,18 +492,18 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
fontWeight: 600,
|
||||
color: statusColor,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
fontSize: '0.7rem',
|
||||
fontSize: '0.68rem',
|
||||
}}
|
||||
>
|
||||
{isLoaded ? `${itemsInChunk} Aufträge` : isInflight ? 'Laden…' : isPending ? 'Wartet…' : 'Klick: Laden'}
|
||||
{isLoaded ? `${itemsInChunk}/${expectedInChunk}` : isInflight ? 'Laden…' : isPending ? 'Wartet…' : 'Klick: Laden'}
|
||||
</Typography>
|
||||
|
||||
{isLoaded && (
|
||||
<Tooltip title="Diesen Chunk aus Cache verwerfen (Evict)">
|
||||
<Tooltip title="Chunk aus RAM verwerfen (Evict)">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`Chunk ${c} verwerfen`}
|
||||
onClick={(e) => this.handleEvictChunk(c, e)}
|
||||
onClick={(e) => this.handleEvictChunk(c, currentFilter.key, e)}
|
||||
sx={{ p: 0.25, ml: 0.5 }}
|
||||
>
|
||||
<DeleteOutlinedIcon sx={{ fontSize: 14 }} color="action" />
|
||||
@@ -374,26 +516,70 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(105px, 1fr))',
|
||||
gap: 1.25,
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{blocks}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
|
||||
{/* Inactive filter inspector banner */}
|
||||
{!isActiveFilter && (
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.6,
|
||||
bgcolor: 'action.selected',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||
Inspektion: {currentFilter.label}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<PlayArrowIcon sx={{ fontSize: 14 }} />}
|
||||
onClick={() => this.handleApplyFilter(currentFilter.key)}
|
||||
sx={{ py: 0.1, px: 1, fontSize: '0.72rem', textTransform: 'none', height: 24 }}
|
||||
>
|
||||
In UI anwenden
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
color="inherit"
|
||||
onClick={() => this.setState({ inspectFilterKey: null, selectedChunk: null })}
|
||||
sx={{ py: 0.1, px: 1, fontSize: '0.72rem', textTransform: 'none', height: 24 }}
|
||||
>
|
||||
Zum aktiven Filter
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 1.5,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(115px, 1fr))',
|
||||
gap: 1.25,
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{blocks}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderChunkDetail() {
|
||||
const { stats, selectedChunk } = this.state;
|
||||
const { selectedChunk } = this.state;
|
||||
if (selectedChunk === null) return null;
|
||||
|
||||
const { items, total } = stats;
|
||||
const currentFilter = this.getInspectedFilterEntry();
|
||||
const { items, total } = currentFilter;
|
||||
const offsetStart = selectedChunk * CHUNK_SIZE;
|
||||
const offsetEnd = Math.min((selectedChunk + 1) * CHUNK_SIZE - 1, Math.max(0, total - 1));
|
||||
|
||||
@@ -432,11 +618,11 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
Chunk #{selectedChunk} Inspection
|
||||
Chunk #{selectedChunk} Detail
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${chunkOrders.length} Einträge (Index ${offsetStart}–${offsetEnd})`}
|
||||
label={`${chunkOrders.length} geladene Aufträge (Index ${offsetStart}–${offsetEnd})`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
@@ -447,16 +633,16 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
onClick={() => this.handleEvictChunk(selectedChunk)}
|
||||
onClick={() => this.handleEvictChunk(selectedChunk, currentFilter.key)}
|
||||
sx={{ textTransform: 'none', py: 0.25, fontSize: '0.75rem' }}
|
||||
>
|
||||
Evict (Aus Cache)
|
||||
Aus Cache verwerfen
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => this.handleLoadChunk(selectedChunk)}
|
||||
onClick={() => this.handleLoadChunk(selectedChunk, currentFilter.key)}
|
||||
sx={{ textTransform: 'none', py: 0.25, fontSize: '0.75rem' }}
|
||||
>
|
||||
Neu laden
|
||||
@@ -534,6 +720,97 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderLruQueue() {
|
||||
const { stats } = this.state;
|
||||
const lruEntries = stats.lruEntries || [];
|
||||
|
||||
if (lruEntries.length === 0) {
|
||||
return (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Der globale LRU-Cache ist aktuell leer (keine Chunks im RAM).
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<Box sx={{ px: 2, py: 1, bgcolor: 'action.hover', borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Geordnet nach Alter (LRU). Position #1 ist der älteste Chunk und wird als nächstes automatisch verworfen, sobald das Gesamtlimit von {stats.maxLruChunks || 50} Chunks überschritten wird.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', p: 1.5 }}>
|
||||
<Stack spacing={1}>
|
||||
{lruEntries.map((entry) => {
|
||||
const isOldest = entry.lruRank === 1;
|
||||
const isNewest = entry.lruRank === lruEntries.length;
|
||||
const offsetStart = entry.chunkIndex * CHUNK_SIZE;
|
||||
const offsetEnd = (entry.chunkIndex + 1) * CHUNK_SIZE - 1;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
key={entry.lruKey}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 1.25,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1.5,
|
||||
borderColor: isOldest ? 'warning.main' : isNewest ? 'primary.main' : 'divider',
|
||||
bgcolor: isOldest ? 'rgba(237, 108, 2, 0.04)' : 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`#${entry.lruRank}`}
|
||||
color={isOldest ? 'warning' : isNewest ? 'primary' : 'default'}
|
||||
sx={{ fontWeight: 700, width: 44 }}
|
||||
/>
|
||||
|
||||
{isOldest && (
|
||||
<Tooltip title="Wird bei neuem Chunk als nächstes verworfen">
|
||||
<WarningAmberIcon fontSize="small" color="warning" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{entry.filterLabel}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Chunk #{entry.chunkIndex} (Index {offsetStart}..{offsetEnd}) · {entry.orderCount} Aufträge geladen
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
{new Date(entry.accessedAt).toLocaleTimeString()}
|
||||
</Typography>
|
||||
<Tooltip title="Diesen Chunk manuell aus dem Cache entfernen">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
onClick={(e) => this.handleEvictChunk(entry.chunkIndex, entry.filterKey, e)}
|
||||
>
|
||||
<DeleteOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderEventLog() {
|
||||
const { stats } = this.state;
|
||||
const events = stats.events || [];
|
||||
@@ -555,9 +832,10 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
const timeStr = new Date(entry.time).toLocaleTimeString();
|
||||
let chipColor = 'default';
|
||||
if (entry.type === 'loaded' || entry.type === 'hit') chipColor = 'success';
|
||||
if (entry.type === 'start' || entry.type === 'prefetch') chipColor = 'info';
|
||||
if (entry.type === 'filter_hit') chipColor = 'secondary';
|
||||
if (entry.type === 'filter_miss' || entry.type === 'start') chipColor = 'info';
|
||||
if (entry.type === 'pending' || entry.type === 'debounce_fire') chipColor = 'warning';
|
||||
if (entry.type === 'error') chipColor = 'error';
|
||||
if (entry.type === 'lru_evict' || entry.type === 'evict' || entry.type === 'error') chipColor = 'error';
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -596,22 +874,23 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { open, position, tab, stats, pulseCounter } = this.state;
|
||||
const { total, chunkCount, loadedChunks, inflightChunks, pendingChunks, loadedItemsCount, status } = stats;
|
||||
const { lruChunksCount, maxLruChunks, inflightChunks } = stats;
|
||||
|
||||
const loadedCount = loadedChunks.length;
|
||||
const expectedChunks = Math.max(chunkCount, 1);
|
||||
const progressPercent = expectedChunks > 0 ? Math.min(100, Math.round((loadedCount / expectedChunks) * 100)) : 0;
|
||||
const maxLru = maxLruChunks || MAX_LRU_CHUNKS;
|
||||
const lruCount = lruChunksCount || 0;
|
||||
const lruPercent = Math.min(100, Math.round((lruCount / maxLru) * 100));
|
||||
const hasInflight = inflightChunks.length > 0;
|
||||
const filterCachesCount = stats.filterCaches?.length || 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open ? (
|
||||
<Tooltip title="Chunk-Cache-Monitor (Alt+Shift+C)" placement="right">
|
||||
<Tooltip title={`Chunk-Cache-Monitor (Alt+Shift+C) — ${lruCount}/${maxLru} Chunks im RAM`} placement="right">
|
||||
<Badge
|
||||
badgeContent={loadedCount}
|
||||
color="success"
|
||||
badgeContent={`${lruCount}/${maxLru}`}
|
||||
color={lruCount >= maxLru ? 'warning' : 'success'}
|
||||
overlap="circular"
|
||||
max={99}
|
||||
max={999}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
left: { xs: 27, sm: 171 },
|
||||
@@ -681,20 +960,26 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, flexGrow: 1 }}>
|
||||
Chunk-Cache-Monitor
|
||||
</Typography>
|
||||
|
||||
<Chip size="small" color="primary" variant="outlined" label="orderAutomat" />
|
||||
<Chip
|
||||
size="small"
|
||||
color={hasInflight ? 'info' : 'default'}
|
||||
label={`${loadedCount} / ${expectedChunks} Chunks`}
|
||||
color={lruCount >= maxLru ? 'warning' : 'success'}
|
||||
label={`LRU: ${lruCount}/${maxLru} Chunks`}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={`${filterCachesCount} Filter`}
|
||||
/>
|
||||
|
||||
<Tooltip title="Prefetch All">
|
||||
<IconButton size="small" aria-label="Alle Chunks vorladen" onClick={this.handlePrefetchAll}>
|
||||
<Tooltip title="Prefetch All Chunks">
|
||||
<IconButton size="small" aria-label="Alle Chunks vorladen" onClick={() => this.handlePrefetchAll()}>
|
||||
<PlaylistAddCheckIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Cache leeren">
|
||||
<Tooltip title="Gesamten Cache leeren">
|
||||
<IconButton size="small" aria-label="Cache leeren" onClick={this.handleClearCache}>
|
||||
<LayersClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -717,15 +1002,16 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
<Box sx={{ px: 2, py: 1, bgcolor: 'action.hover', borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.75 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||
Cache-Füllstand: {progressPercent}% ({loadedCount} von {expectedChunks} Chunks)
|
||||
LRU-Speicher: {lruPercent}% ({lruCount} von {maxLru} Chunks belegt)
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{loadedItemsCount} von {total} Aufträgen im RAM · Chunk-Größe {CHUNK_SIZE}
|
||||
Aktiver Filter: {stats.activeFilterLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progressPercent}
|
||||
value={lruPercent}
|
||||
color={lruCount >= maxLru ? 'warning' : 'primary'}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
@@ -733,63 +1019,11 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
'& .MuiLinearProgress-bar': { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 1, flexWrap: 'wrap', gap: 0.5 }}>
|
||||
<Chip size="small" label={`Status: ${status}`} sx={{ height: 20, fontSize: '0.7rem' }} />
|
||||
{hasInflight && (
|
||||
<Chip
|
||||
size="small"
|
||||
color="info"
|
||||
icon={<CircularProgress size={12} color="inherit" />}
|
||||
label={`${inflightChunks.length} lädt`}
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{pendingChunks.length > 0 && (
|
||||
<Chip
|
||||
size="small"
|
||||
color="warning"
|
||||
label={`${pendingChunks.length} vorgemerkt`}
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{stats.filter && (
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={`Filter: "${stats.filter}"`}
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{stats.onlyUnpaid && (
|
||||
<Chip
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
label="Nur unbezahlt"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{stats.onlyUndelivered && (
|
||||
<Chip
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
label="Nur ungeliefert"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{stats.negativeSeekMonths !== 0 && (
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
label={`Seek: ${stats.negativeSeekMonths}M`}
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Filter Caches Switcher Bar */}
|
||||
{this.renderFilterSelector()}
|
||||
|
||||
{/* View Tabs */}
|
||||
<Box sx={{ borderBottom: '1px solid', borderColor: 'divider', px: 1 }}>
|
||||
<Tabs
|
||||
@@ -798,6 +1032,12 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
sx={{ minHeight: 36, '& .MuiTab-root': { minHeight: 36, py: 0.5, fontSize: '0.75rem' } }}
|
||||
>
|
||||
<Tab icon={<GridViewIcon fontSize="small" />} iconPosition="start" label="Chunk-Map" value="grid" />
|
||||
<Tab
|
||||
icon={<LayersIcon fontSize="small" />}
|
||||
iconPosition="start"
|
||||
label={`LRU-Queue (${lruCount}/${maxLru})`}
|
||||
value="lru"
|
||||
/>
|
||||
<Tab
|
||||
icon={<HistoryIcon fontSize="small" />}
|
||||
iconPosition="start"
|
||||
@@ -809,8 +1049,10 @@ class ChunkCacheMonitor extends PureComponent {
|
||||
|
||||
{/* Body */}
|
||||
<Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{tab === 'grid' ? this.renderChunkGrid() : this.renderEventLog()}
|
||||
{tab === 'grid' && this.renderChunkGrid()}
|
||||
{tab === 'grid' && this.renderChunkDetail()}
|
||||
{tab === 'lru' && this.renderLruQueue()}
|
||||
{tab === 'log' && this.renderEventLog()}
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user