From d188b50c0746fd38e7070b40614c6a3b80050a0b Mon Sep 17 00:00:00 2001 From: David Cowan Date: Thu, 11 Dec 2025 07:14:57 -0800 Subject: [PATCH 1/9] Adding editor network stats --- .../src/diagnostics_panel/prefabs/index.tsx | 3 +++ .../prefabs/tabs/EditorNetworkStats.tsx | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx diff --git a/webview-ui/src/diagnostics_panel/prefabs/index.tsx b/webview-ui/src/diagnostics_panel/prefabs/index.tsx index 4c88ce0d..5ca19bd9 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/index.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/index.tsx @@ -12,6 +12,8 @@ import serverScriptSubscriberCountsTab from './tabs/ServerScriptSubscriberCounts import clientTimingTab from './tabs/ClientTiming'; import clientMemoryTab from './tabs/ClientMemory'; +import editorNetworkStatsTab from './tabs/EditorNetworkStats'; + export default [ worldTab, serverMemoryTab, @@ -23,4 +25,5 @@ export default [ clientTimingTab, clientMemoryTab, dynamicPropertyTab, + editorNetworkStatsTab, ]; diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx new file mode 100644 index 00000000..0199e7f1 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx @@ -0,0 +1,24 @@ +import { MultipleStatisticProvider, SimpleStatisticProvider } from '../../StatisticProvider'; +import { TabPrefab, TabPrefabDataSource } from '../TabPrefab'; +import { MinecraftDynamicPropertiesTable } from '../../controls/MinecraftDynamicPropertiesTable'; + +const statsTab: TabPrefab = { + name: 'Editor Network Stats', + dataSource: TabPrefabDataSource.Server, + content: () => { + return ( +
+ +
+ ); + }, +}; + +export default statsTab; From 8b14fa0317ed5073496893cb612d4251acf09a83 Mon Sep 17 00:00:00 2001 From: David Cowan Date: Thu, 11 Dec 2025 11:39:37 -0800 Subject: [PATCH 2/9] Network Stats are in (first pass) --- src/stats/stats-provider.ts | 18 +- webview-ui/src/diagnostics_panel/App.css | 41 ++ .../diagnostics_panel/StatisticProvider.tsx | 5 +- .../MinecraftMultiColumnStatisticTable.tsx | 360 ++++++++++++++++++ .../prefabs/tabs/EditorNetworkStats.tsx | 21 +- 5 files changed, 437 insertions(+), 8 deletions(-) create mode 100644 webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx diff --git a/src/stats/stats-provider.ts b/src/stats/stats-provider.ts index 7574f570..48c53f80 100644 --- a/src/stats/stats-provider.ts +++ b/src/stats/stats-provider.ts @@ -84,11 +84,25 @@ export class StatsProvider { const childStringValues: string[][] = []; for (const child of stat.children ?? []) { - if (!(child.values && typeof child.values[0] === 'string' && child.values[0].length > 0)) { + if (!child.values || child.values.length === 0) { continue; } - childStringValues.push([child.name, child.values[0]]); + // Handle different value types + if (typeof child.values[0] === 'string' && child.values[0].length > 0) { + // Original behavior: string values + childStringValues.push([child.name, child.values[0]]); + } else if (typeof child.values[0] === 'number') { + // New behavior: numeric values + if (child.values.length === 1) { + // Single numeric value + childStringValues.push([child.name, child.values[0].toString()]); + } else if (child.values.length >= 2) { + // Multiple numeric values - create a row with all values + const valueStrings = child.values.map(v => v.toString()); + childStringValues.push([child.name, ...valueStrings]); + } + } } const childStatData: StatData = { diff --git a/webview-ui/src/diagnostics_panel/App.css b/webview-ui/src/diagnostics_panel/App.css index 69fe245d..f3d7990e 100644 --- a/webview-ui/src/diagnostics_panel/App.css +++ b/webview-ui/src/diagnostics_panel/App.css @@ -156,3 +156,44 @@ main { background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); } + +/* Multi-column table resizable columns */ +#multi-column-grid { + width: 100%; + overflow-x: auto; + table-layout: auto; +} + +/* Use CSS Grid for better column control */ +#multi-column-grid vscode-data-grid-row { + display: grid; + grid-template-columns: minmax(200px, 1fr) repeat(auto-fit, minmax(120px, 1fr)); + gap: 4px; +} + +#multi-column-grid vscode-data-grid-cell { + padding: 8px 12px; + border-right: 1px solid var(--vscode-editorGroup-border); + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +#multi-column-grid vscode-data-grid-cell[cell-type="columnheader"] { + font-weight: bold; + background-color: var(--vscode-editor-background); + border-bottom: 2px solid var(--vscode-editorGroup-border); +} + +/* Make the packet name column wider */ +#multi-column-grid vscode-data-grid-cell[grid-column="1"] { + min-width: 200px; + flex: 2; +} + +/* Value columns */ +#multi-column-grid vscode-data-grid-cell:not([grid-column="1"]) { + min-width: 120px; + flex: 1; + text-align: right; +} diff --git a/webview-ui/src/diagnostics_panel/StatisticProvider.tsx b/webview-ui/src/diagnostics_panel/StatisticProvider.tsx index 07bca1ce..6443ba8b 100644 --- a/webview-ui/src/diagnostics_panel/StatisticProvider.tsx +++ b/webview-ui/src/diagnostics_panel/StatisticProvider.tsx @@ -118,7 +118,10 @@ export class MultipleStatisticProvider extends StatisticProvider { } if (event.values.length === 0) { - return; + // Allow events through if they have children_string_values even if values is empty + if (!event.children_string_values || event.children_string_values.length === 0) { + return; + } } if (this.options.valuesFilter && !this.options.valuesFilter(event)) { diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx new file mode 100644 index 00000000..e07af0b8 --- /dev/null +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -0,0 +1,360 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +import { useCallback, useEffect, useState } from 'react'; +import { MultipleStatisticProvider, StatisticUpdatedMessage } from '../StatisticProvider'; +import { StatisticResolver, TrackedStat, YAxisStyle } from '../StatisticResolver'; +import { + VSCodeDataGrid, + VSCodeDataGridRow, + VSCodeDataGridCell, + VSCodeDropdown, + VSCodeOption, +} from '@vscode/webview-ui-toolkit/react'; + +export enum MinecraftMultiColumnStatisticTableSortOrder { + Ascending, + Descending, +} + +export enum MinecraftMultiColumnStatisticTableSortType { + Alphabetical, + Numerical, +} + +export enum MinecraftMultiColumnStatisticTableSortColumn { + Key = 'key', + // Value columns will be added dynamically based on valueLabels +} + +type MultiColumnTrackedStat = { + category: string; + values: number[]; + time: number; +}; + +type MinecraftMultiColumnStatisticTableProps = { + title: string; + statisticDataProvider: MultipleStatisticProvider; + statisticResolver: StatisticResolver; + defaultSortOrder?: MinecraftMultiColumnStatisticTableSortOrder; + defaultSortType?: MinecraftMultiColumnStatisticTableSortType; + defaultSortColumn?: string; + + keyLabel: string; + valueLabels: string[]; // Array of labels for value columns + prettifyNames?: boolean; // Whether to format packet names (camelCase -> Camel Case) or keep original format +}; + +const sortOrderOptions = [ + { id: MinecraftMultiColumnStatisticTableSortOrder.Ascending, label: 'Ascending' }, + { id: MinecraftMultiColumnStatisticTableSortOrder.Descending, label: 'Descending' }, +]; + +const sortTypeOptions = [ + { id: MinecraftMultiColumnStatisticTableSortType.Alphabetical, label: 'Alphabetical' }, + { id: MinecraftMultiColumnStatisticTableSortType.Numerical, label: 'Numerical' }, +]; + +export default function MinecraftMultiColumnStatisticTable({ + title, + statisticDataProvider, + statisticResolver, + defaultSortOrder = MinecraftMultiColumnStatisticTableSortOrder.Descending, + defaultSortType = MinecraftMultiColumnStatisticTableSortType.Numerical, + defaultSortColumn, + keyLabel, + valueLabels, + prettifyNames = true, // Default to prettifying names for backward compatibility +}: MinecraftMultiColumnStatisticTableProps): JSX.Element { + // states + const [data, setData] = useState([]); + const [selectedSortOrder, setSelectedSortOrder] = + useState(defaultSortOrder); + const [selectedSortType, setSelectedSortType] = + useState(defaultSortType); + const [selectedSortColumn, setSelectedSortColumn] = useState( + defaultSortColumn || MinecraftMultiColumnStatisticTableSortColumn.Key + ); + + // Create sort column options + const sortColumnOptions = [ + { id: MinecraftMultiColumnStatisticTableSortColumn.Key, label: keyLabel }, + ...valueLabels.map((label, index) => ({ id: `value_${index}`, label })), + ]; + + const _onSelectedSortOrderChange = useCallback((e: Event | React.FormEvent): void => { + const target = e.target as HTMLSelectElement; + setSelectedSortOrder(sortOrderOptions[target.selectedIndex].id); + }, []); + + const _onSelectedSortTypeChange = useCallback((e: Event | React.FormEvent): void => { + const target = e.target as HTMLSelectElement; + setSelectedSortType(sortTypeOptions[target.selectedIndex].id); + }, []); + + const _onSelectedSortColumnChange = useCallback( + (e: Event | React.FormEvent): void => { + const target = e.target as HTMLSelectElement; + setSelectedSortColumn(sortColumnOptions[target.selectedIndex].id); + }, + [sortColumnOptions] + ); + + useEffect(() => { + const eventHandler = (event: StatisticUpdatedMessage): void => { + // Update data with new data point + setData((_prevState: MultiColumnTrackedStat[]): MultiColumnTrackedStat[] => { + // Group stats by category and collect values + const categoryMap = new Map(); + + // For consolidated_data events with children_string_values, skip the statisticResolver + // and process the data directly since it's already in the correct format + if ( + event.id === 'consolidated_data' && + event.children_string_values && + event.children_string_values.length > 0 + ) { + event.children_string_values.forEach(childRow => { + if (childRow.length >= 3) { + // Format: [packet_name, sent_count, received_count] + const packetName = childRow[0]; + const sentCount = parseFloat(childRow[1]) || 0; + const receivedCount = parseFloat(childRow[2]) || 0; + + // Process packet name based on prettifyNames setting + const cleanPacketName = prettifyNames + ? packetName + .split('::') + .pop() + ?.replace(/([a-z])([A-Z])/g, '$1 $2') // Add spaces before capital letters + ?.replace(/^./, (str: string) => str.toUpperCase()) || // Capitalize first letter + packetName + : packetName.split('::').pop() || packetName; + + categoryMap.set(cleanPacketName, { + category: cleanPacketName, + values: [sentCount, receivedCount], + time: event.time || Date.now(), + }); + } + }); + } else { + // Use the statisticResolver for other event types + const rawStats = statisticResolver(event, []); + + rawStats.forEach(stat => { + if (!stat.category) { + return; + } + + const existing = categoryMap.get(stat.category); + if (existing) { + // Add value to existing category (ensuring we have enough slots) + while (existing.values.length <= valueLabels.length) { + existing.values.push(0); + } + existing.values[existing.values.length - 1] = stat.absoluteValue; + existing.time = Math.max(existing.time, stat.time); + } else { + // Create new entry + const newStat: MultiColumnTrackedStat = { + category: stat.category, + values: [stat.absoluteValue], + time: stat.time, + }; + // Pad values array to match number of columns + while (newStat.values.length < valueLabels.length) { + newStat.values.push(0); + } + categoryMap.set(stat.category, newStat); + } + }); + + // Handle multi-value events (children_string_values) for non-consolidated events + if (event.children_string_values && event.children_string_values.length > 0) { + event.children_string_values.forEach(childRow => { + if (childRow.length >= 3) { + // Format: [packet_name, sent_count, received_count] + const packetName = childRow[0]; + const sentCount = parseFloat(childRow[1]) || 0; + const receivedCount = parseFloat(childRow[2]) || 0; + + // Process packet name based on prettifyNames setting + const cleanPacketName = prettifyNames + ? packetName + .split('::') + .pop() + ?.replace(/([a-z])([A-Z])/g, '$1 $2') // Add spaces before capital letters + ?.replace(/^./, (str: string) => str.toUpperCase()) || // Capitalize first letter + packetName + : packetName.split('::').pop() || packetName; + + categoryMap.set(cleanPacketName, { + category: cleanPacketName, + values: [sentCount, receivedCount], + time: event.time, + }); + } else if (childRow.length === 2) { + // Fallback: handle key-value pairs + const [childName, childValue] = childRow; + const value = parseFloat(childValue); + + // Process name based on prettifyNames setting + const cleanName = prettifyNames + ? childName + .split('::') + .pop() + ?.replace(/([a-z])([A-Z])/g, '$1 $2') + ?.replace(/^./, (str: string) => str.toUpperCase()) || childName + : childName.split('::').pop() || childName; + + categoryMap.set(cleanName, { + category: cleanName, + values: [value, 0], // Single value goes to first column + time: event.time, + }); + } + }); + } + } + + let newData = Array.from(categoryMap.values()); + + // Calculate the latest tick for each category + const latestTicks = new Map(); + newData.forEach(dataPoint => { + const currentTick = latestTicks.get(dataPoint.category) ?? 0; + if (dataPoint.time > currentTick) { + latestTicks.set(dataPoint.category, dataPoint.time); + } + }); + + // Filter out data points that are older than the latest tick + newData = newData.filter(dataPoint => { + const latestTick = latestTicks.get(dataPoint.category); + return latestTick !== undefined && dataPoint.time === latestTick; + }); + + // Sort based on sortOrder, sortType, and sortColumn + newData.sort((a, b) => { + let compareValue: number; + + if (selectedSortColumn === MinecraftMultiColumnStatisticTableSortColumn.Key) { + // Sort by category name + if (selectedSortType === MinecraftMultiColumnStatisticTableSortType.Alphabetical) { + compareValue = a.category.localeCompare(b.category); + } else { + // For numerical sort on key, still use alphabetical + compareValue = a.category.localeCompare(b.category); + } + } else { + // Sort by specific value column + const columnIndex = parseInt(selectedSortColumn.replace('value_', '')); + if (columnIndex >= 0 && columnIndex < valueLabels.length) { + const aValue = a.values[columnIndex] || 0; + const bValue = b.values[columnIndex] || 0; + + if (selectedSortType === MinecraftMultiColumnStatisticTableSortType.Alphabetical) { + compareValue = aValue.toString().localeCompare(bValue.toString()); + } else { + compareValue = aValue - bValue; + } + } else { + compareValue = a.category.localeCompare(b.category); + } + } + + return selectedSortOrder === MinecraftMultiColumnStatisticTableSortOrder.Ascending + ? compareValue + : -compareValue; + }); + + return newData; + }); + }; + + statisticDataProvider.registerWindowListener(window); + statisticDataProvider.addSubscriber(eventHandler); + + // Remove old listener + return () => { + statisticDataProvider.removeSubscriber(eventHandler); + statisticDataProvider.unregisterWindowListener(window); + }; + }, [ + statisticDataProvider, + statisticResolver, + selectedSortOrder, + selectedSortType, + selectedSortColumn, + valueLabels, + ]); + + return ( +
+

{title}

+
+
+ + elem.id === selectedSortOrder)} + > + {sortOrderOptions.map(sortOption => ( + {sortOption.label} + ))} + +
+
+
+ + elem.id === selectedSortType)} + > + {sortTypeOptions.map(sortOption => ( + {sortOption.label} + ))} + +
+
+
+ + elem.id === selectedSortColumn)} + > + {sortColumnOptions.map(sortOption => ( + {sortOption.label} + ))} + +
+
+ + + + {keyLabel} + + {valueLabels.map((label, index) => ( + + {label} + + ))} + + {data.map(dataPoint => ( + + {dataPoint.category} + {dataPoint.values.map((value, index) => ( + + {value.toFixed(1)} + + ))} + + ))} + +
+ ); +} diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx index 0199e7f1..cfab5163 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx @@ -1,6 +1,7 @@ -import { MultipleStatisticProvider, SimpleStatisticProvider } from '../../StatisticProvider'; +import { MultipleStatisticProvider } from '../../StatisticProvider'; import { TabPrefab, TabPrefabDataSource } from '../TabPrefab'; -import { MinecraftDynamicPropertiesTable } from '../../controls/MinecraftDynamicPropertiesTable'; +import MinecraftMultiColumnStatisticTable from '../../controls/MinecraftMultiColumnStatisticTable'; +import { createStatResolver, StatisticType, YAxisType } from '../../StatisticResolver'; const statsTab: TabPrefab = { name: 'Editor Network Stats', @@ -8,13 +9,23 @@ const statsTab: TabPrefab = { content: () => { return (
-
); From 08b4c7c09f40243e05e7409e137a167540de76a1 Mon Sep 17 00:00:00 2001 From: David Cowan Date: Thu, 11 Dec 2025 12:48:03 -0800 Subject: [PATCH 3/9] Added Column header styling, column variants and new editor stats fields - Trying to add resizable columns to the multi-column table but the web view implementation doesn't support it apparently - Added some support for column variants (previously, it assumed all columns were the same numeric format) - Added new columns for packet sizes into the Editor pane --- webview-ui/src/diagnostics_panel/App.css | 97 +++++++++++++++---- .../MinecraftMultiColumnStatisticTable.tsx | 87 ++++++++++------- .../prefabs/tabs/EditorNetworkStats.tsx | 2 +- 3 files changed, 131 insertions(+), 55 deletions(-) diff --git a/webview-ui/src/diagnostics_panel/App.css b/webview-ui/src/diagnostics_panel/App.css index f3d7990e..116ac0fa 100644 --- a/webview-ui/src/diagnostics_panel/App.css +++ b/webview-ui/src/diagnostics_panel/App.css @@ -157,21 +157,47 @@ main { color: var(--vscode-button-foreground); } -/* Multi-column table resizable columns */ +/* Multi-column table with enhanced column interaction */ #multi-column-grid { width: 100%; overflow-x: auto; - table-layout: auto; + table-layout: fixed; } -/* Use CSS Grid for better column control */ -#multi-column-grid vscode-data-grid-row { - display: grid; - grid-template-columns: minmax(200px, 1fr) repeat(auto-fit, minmax(120px, 1fr)); - gap: 4px; +/* Enhanced column styling for better visual feedback */ +#multi-column-grid vscode-data-grid-cell[cell-type="columnheader"] { + font-weight: bold; + background-color: var(--vscode-editor-background); + border-bottom: 2px solid var(--vscode-editorGroup-border); + border-right: 1px solid var(--vscode-editorGroup-border); + position: relative; + cursor: default; + user-select: none; + padding: 8px 12px; + overflow: hidden; + text-overflow: ellipsis; } -#multi-column-grid vscode-data-grid-cell { +/* Add visual resize handle to column headers */ +#multi-column-grid vscode-data-grid-cell[cell-type="columnheader"]::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 4px; + height: 100%; + background: transparent; + cursor: col-resize; + z-index: 1; +} + +#multi-column-grid vscode-data-grid-cell[cell-type="columnheader"]:hover::after { + background: var(--vscode-focusBorder); + opacity: 0.5; +} + +/* Regular cell styling */ +#multi-column-grid vscode-data-grid-cell:not([cell-type="columnheader"]) { padding: 8px 12px; border-right: 1px solid var(--vscode-editorGroup-border); overflow: hidden; @@ -179,21 +205,54 @@ main { min-width: 0; } -#multi-column-grid vscode-data-grid-cell[cell-type="columnheader"] { - font-weight: bold; - background-color: var(--vscode-editor-background); - border-bottom: 2px solid var(--vscode-editorGroup-border); +/* Set specific column widths using CSS custom properties */ +#multi-column-grid { + --col1-width: 250px; + --col2-width: 120px; + --col3-width: 120px; + --col4-width: 120px; + --col5-width: 120px; + --col6-width: 120px; } -/* Make the packet name column wider */ +/* Apply column widths */ #multi-column-grid vscode-data-grid-cell[grid-column="1"] { - min-width: 200px; - flex: 2; + width: var(--col1-width); + min-width: 180px; + flex: none; +} + +#multi-column-grid vscode-data-grid-cell[grid-column="2"] { + width: var(--col2-width); + min-width: 100px; + text-align: right; + flex: none; +} + +#multi-column-grid vscode-data-grid-cell[grid-column="3"] { + width: var(--col3-width); + min-width: 100px; + text-align: right; + flex: none; +} + +#multi-column-grid vscode-data-grid-cell[grid-column="4"] { + width: var(--col4-width); + min-width: 100px; + text-align: right; + flex: none; +} + +#multi-column-grid vscode-data-grid-cell[grid-column="5"] { + width: var(--col5-width); + min-width: 100px; + text-align: right; + flex: none; } -/* Value columns */ -#multi-column-grid vscode-data-grid-cell:not([grid-column="1"]) { - min-width: 120px; - flex: 1; +#multi-column-grid vscode-data-grid-cell[grid-column="6"] { + width: var(--col6-width); + min-width: 100px; text-align: right; + flex: none; } diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index e07af0b8..35a34dd4 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -28,7 +28,7 @@ export enum MinecraftMultiColumnStatisticTableSortColumn { type MultiColumnTrackedStat = { category: string; - values: number[]; + values: (string | number)[]; time: number; }; @@ -115,11 +115,23 @@ export default function MinecraftMultiColumnStatisticTable({ event.children_string_values.length > 0 ) { event.children_string_values.forEach(childRow => { - if (childRow.length >= 3) { - // Format: [packet_name, sent_count, received_count] + if (childRow.length >= 2) { + // Format: [packet_name, value1, value2, value3, value4, value5, ...] const packetName = childRow[0]; - const sentCount = parseFloat(childRow[1]) || 0; - const receivedCount = parseFloat(childRow[2]) || 0; + + // Parse values from the row, preserving original types + const values: (string | number)[] = []; + for (let i = 1; i < childRow.length; i++) { + const rawValue = childRow[i]; + // Try to parse as number, but keep as string if it's not numeric + const numValue = parseFloat(rawValue); + values.push(isNaN(numValue) ? rawValue : numValue); + } + + // Ensure we have at least as many values as valueLabels expects + while (values.length < valueLabels.length) { + values.push(''); + } // Process packet name based on prettifyNames setting const cleanPacketName = prettifyNames @@ -133,7 +145,7 @@ export default function MinecraftMultiColumnStatisticTable({ categoryMap.set(cleanPacketName, { category: cleanPacketName, - values: [sentCount, receivedCount], + values: values, time: event.time || Date.now(), }); } @@ -173,11 +185,23 @@ export default function MinecraftMultiColumnStatisticTable({ // Handle multi-value events (children_string_values) for non-consolidated events if (event.children_string_values && event.children_string_values.length > 0) { event.children_string_values.forEach(childRow => { - if (childRow.length >= 3) { - // Format: [packet_name, sent_count, received_count] + if (childRow.length >= 2) { + // Format: [packet_name, value1, value2, value3, ...] const packetName = childRow[0]; - const sentCount = parseFloat(childRow[1]) || 0; - const receivedCount = parseFloat(childRow[2]) || 0; + + // Parse values from the row, preserving original types + const values: (string | number)[] = []; + for (let i = 1; i < childRow.length; i++) { + const rawValue = childRow[i]; + // Try to parse as number, but keep as string if it's not numeric + const numValue = parseFloat(rawValue); + values.push(isNaN(numValue) ? rawValue : numValue); + } + + // Ensure we have at least as many values as valueLabels expects + while (values.length < valueLabels.length) { + values.push(''); + } // Process packet name based on prettifyNames setting const cleanPacketName = prettifyNames @@ -191,26 +215,7 @@ export default function MinecraftMultiColumnStatisticTable({ categoryMap.set(cleanPacketName, { category: cleanPacketName, - values: [sentCount, receivedCount], - time: event.time, - }); - } else if (childRow.length === 2) { - // Fallback: handle key-value pairs - const [childName, childValue] = childRow; - const value = parseFloat(childValue); - - // Process name based on prettifyNames setting - const cleanName = prettifyNames - ? childName - .split('::') - .pop() - ?.replace(/([a-z])([A-Z])/g, '$1 $2') - ?.replace(/^./, (str: string) => str.toUpperCase()) || childName - : childName.split('::').pop() || childName; - - categoryMap.set(cleanName, { - category: cleanName, - values: [value, 0], // Single value goes to first column + values: values, time: event.time, }); } @@ -251,13 +256,25 @@ export default function MinecraftMultiColumnStatisticTable({ // Sort by specific value column const columnIndex = parseInt(selectedSortColumn.replace('value_', '')); if (columnIndex >= 0 && columnIndex < valueLabels.length) { - const aValue = a.values[columnIndex] || 0; - const bValue = b.values[columnIndex] || 0; + const aValue = a.values[columnIndex] ?? ''; + const bValue = b.values[columnIndex] ?? ''; if (selectedSortType === MinecraftMultiColumnStatisticTableSortType.Alphabetical) { - compareValue = aValue.toString().localeCompare(bValue.toString()); + compareValue = String(aValue).localeCompare(String(bValue)); } else { - compareValue = aValue - bValue; + // For numerical sort, convert to numbers if possible + const aNum = typeof aValue === 'number' ? aValue : parseFloat(String(aValue)); + const bNum = typeof bValue === 'number' ? bValue : parseFloat(String(bValue)); + + if (isNaN(aNum) && isNaN(bNum)) { + compareValue = String(aValue).localeCompare(String(bValue)); + } else if (isNaN(aNum)) { + compareValue = 1; + } else if (isNaN(bNum)) { + compareValue = -1; + } else { + compareValue = aNum - bNum; + } } } else { compareValue = a.category.localeCompare(b.category); @@ -349,7 +366,7 @@ export default function MinecraftMultiColumnStatisticTable({ {dataPoint.category} {dataPoint.values.map((value, index) => ( - {value.toFixed(1)} + {typeof value === 'number' ? value.toFixed(1) : value} ))} diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx index cfab5163..5bb41cc5 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx @@ -23,7 +23,7 @@ const statsTab: TabPrefab = { tickRange: 20 * 15, // About 15 seconds })} keyLabel="Packet Type" - valueLabels={['Sent Count', 'Received Count']} + valueLabels={['Sent Count', 'Received Count', 'Min Size', 'Max Size']} prettifyNames={false} // Keep original packet name format defaultSortColumn="value_0" // Sort by "Sent Count" column by default /> From 92f04a5396e9e6e197169c3288025f5d807d74f3 Mon Sep 17 00:00:00 2001 From: David Cowan <37003198+dacowan@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:11:47 -0800 Subject: [PATCH 4/9] Update webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../controls/MinecraftMultiColumnStatisticTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index 35a34dd4..e1eec451 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -216,7 +216,7 @@ export default function MinecraftMultiColumnStatisticTable({ categoryMap.set(cleanPacketName, { category: cleanPacketName, values: values, - time: event.time, + time: event.time || Date.now(), }); } }); From e8d61864d391603014d3ceefd1ff5a7e082fa089 Mon Sep 17 00:00:00 2001 From: David Cowan <37003198+dacowan@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:12:10 -0800 Subject: [PATCH 5/9] Update webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../controls/MinecraftMultiColumnStatisticTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index e1eec451..a1dc0a49 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -162,7 +162,7 @@ export default function MinecraftMultiColumnStatisticTable({ const existing = categoryMap.get(stat.category); if (existing) { // Add value to existing category (ensuring we have enough slots) - while (existing.values.length <= valueLabels.length) { + while (existing.values.length < valueLabels.length) { existing.values.push(0); } existing.values[existing.values.length - 1] = stat.absoluteValue; From 70d7b2a7b3703e90af520ace3201497cdf55524a Mon Sep 17 00:00:00 2001 From: David Cowan <37003198+dacowan@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:13:02 -0800 Subject: [PATCH 6/9] Update webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../controls/MinecraftMultiColumnStatisticTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index a1dc0a49..26385b25 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { MultipleStatisticProvider, StatisticUpdatedMessage } from '../StatisticProvider'; -import { StatisticResolver, TrackedStat, YAxisStyle } from '../StatisticResolver'; +import { StatisticResolver } from '../StatisticResolver'; import { VSCodeDataGrid, VSCodeDataGridRow, From b6daddd91e764c3860a70c1c6af16771c402941c Mon Sep 17 00:00:00 2001 From: David Cowan <37003198+dacowan@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:13:25 -0800 Subject: [PATCH 7/9] Update webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../controls/MinecraftMultiColumnStatisticTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index 26385b25..8584717c 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -305,6 +305,7 @@ export default function MinecraftMultiColumnStatisticTable({ selectedSortType, selectedSortColumn, valueLabels, + prettifyNames, ]); return ( From 35185dd9c2ed57db9c259bf5cc656429184360a9 Mon Sep 17 00:00:00 2001 From: David Cowan Date: Thu, 11 Dec 2025 14:25:54 -0800 Subject: [PATCH 8/9] PR Feedback - Fixed up column widths to be a little more realistic - Removed some duplicate code --- webview-ui/src/diagnostics_panel/App.css | 12 +- .../MinecraftMultiColumnStatisticTable.tsx | 147 ++++++++---------- 2 files changed, 75 insertions(+), 84 deletions(-) diff --git a/webview-ui/src/diagnostics_panel/App.css b/webview-ui/src/diagnostics_panel/App.css index 116ac0fa..f9666be1 100644 --- a/webview-ui/src/diagnostics_panel/App.css +++ b/webview-ui/src/diagnostics_panel/App.css @@ -207,12 +207,12 @@ main { /* Set specific column widths using CSS custom properties */ #multi-column-grid { - --col1-width: 250px; - --col2-width: 120px; - --col3-width: 120px; - --col4-width: 120px; - --col5-width: 120px; - --col6-width: 120px; + --col1-width: 400px; + --col2-width: 80px; + --col3-width: 80px; + --col4-width: 80px; + --col5-width: 80px; + --col6-width: 80px; } /* Apply column widths */ diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index 8584717c..4d2d22c4 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -1,6 +1,6 @@ // Copyright (C) Microsoft Corporation. All rights reserved. -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useMemo } from 'react'; import { MultipleStatisticProvider, StatisticUpdatedMessage } from '../StatisticProvider'; import { StatisticResolver } from '../StatisticResolver'; import { @@ -55,6 +55,52 @@ const sortTypeOptions = [ { id: MinecraftMultiColumnStatisticTableSortType.Numerical, label: 'Numerical' }, ]; +// Helper function to process children_string_values and populate categoryMap +function processChildrenStringValues( + children_string_values: string[][], + categoryMap: Map, + valueLabels: string[], + prettifyNames: boolean, + eventTime: number +): void { + children_string_values.forEach(childRow => { + if (childRow.length >= 2) { + // Format: [packet_name, value1, value2, value3, value4, value5, ...] + const packetName = childRow[0]; + + // Parse values from the row, preserving original types + const values: (string | number)[] = []; + for (let i = 1; i < childRow.length; i++) { + const rawValue = childRow[i]; + // Try to parse as number, but keep as string if it's not numeric + const numValue = parseFloat(rawValue); + values.push(isNaN(numValue) ? rawValue : numValue); + } + + // Ensure we have at least as many values as valueLabels expects + while (values.length < valueLabels.length) { + values.push(''); + } + + // Process packet name based on prettifyNames setting + const cleanPacketName = prettifyNames + ? packetName + .split('::') + .pop() + ?.replace(/([a-z])([A-Z])/g, '$1 $2') // Add spaces before capital letters + ?.replace(/^./, (str: string) => str.toUpperCase()) || // Capitalize first letter + packetName + : packetName.split('::').pop() || packetName; + + categoryMap.set(cleanPacketName, { + category: cleanPacketName, + values: values, + time: eventTime, + }); + } + }); +} + export default function MinecraftMultiColumnStatisticTable({ title, statisticDataProvider, @@ -76,11 +122,14 @@ export default function MinecraftMultiColumnStatisticTable({ defaultSortColumn || MinecraftMultiColumnStatisticTableSortColumn.Key ); - // Create sort column options - const sortColumnOptions = [ - { id: MinecraftMultiColumnStatisticTableSortColumn.Key, label: keyLabel }, - ...valueLabels.map((label, index) => ({ id: `value_${index}`, label })), - ]; + // Memoize sort column options to prevent unnecessary recreations + const sortColumnOptions = useMemo( + () => [ + { id: MinecraftMultiColumnStatisticTableSortColumn.Key, label: keyLabel }, + ...valueLabels.map((label, index) => ({ id: `value_${index}`, label })), + ], + [keyLabel, valueLabels] + ); const _onSelectedSortOrderChange = useCallback((e: Event | React.FormEvent): void => { const target = e.target as HTMLSelectElement; @@ -114,42 +163,13 @@ export default function MinecraftMultiColumnStatisticTable({ event.children_string_values && event.children_string_values.length > 0 ) { - event.children_string_values.forEach(childRow => { - if (childRow.length >= 2) { - // Format: [packet_name, value1, value2, value3, value4, value5, ...] - const packetName = childRow[0]; - - // Parse values from the row, preserving original types - const values: (string | number)[] = []; - for (let i = 1; i < childRow.length; i++) { - const rawValue = childRow[i]; - // Try to parse as number, but keep as string if it's not numeric - const numValue = parseFloat(rawValue); - values.push(isNaN(numValue) ? rawValue : numValue); - } - - // Ensure we have at least as many values as valueLabels expects - while (values.length < valueLabels.length) { - values.push(''); - } - - // Process packet name based on prettifyNames setting - const cleanPacketName = prettifyNames - ? packetName - .split('::') - .pop() - ?.replace(/([a-z])([A-Z])/g, '$1 $2') // Add spaces before capital letters - ?.replace(/^./, (str: string) => str.toUpperCase()) || // Capitalize first letter - packetName - : packetName.split('::').pop() || packetName; - - categoryMap.set(cleanPacketName, { - category: cleanPacketName, - values: values, - time: event.time || Date.now(), - }); - } - }); + processChildrenStringValues( + event.children_string_values, + categoryMap, + valueLabels, + prettifyNames, + event.time || Date.now() + ); } else { // Use the statisticResolver for other event types const rawStats = statisticResolver(event, []); @@ -184,42 +204,13 @@ export default function MinecraftMultiColumnStatisticTable({ // Handle multi-value events (children_string_values) for non-consolidated events if (event.children_string_values && event.children_string_values.length > 0) { - event.children_string_values.forEach(childRow => { - if (childRow.length >= 2) { - // Format: [packet_name, value1, value2, value3, ...] - const packetName = childRow[0]; - - // Parse values from the row, preserving original types - const values: (string | number)[] = []; - for (let i = 1; i < childRow.length; i++) { - const rawValue = childRow[i]; - // Try to parse as number, but keep as string if it's not numeric - const numValue = parseFloat(rawValue); - values.push(isNaN(numValue) ? rawValue : numValue); - } - - // Ensure we have at least as many values as valueLabels expects - while (values.length < valueLabels.length) { - values.push(''); - } - - // Process packet name based on prettifyNames setting - const cleanPacketName = prettifyNames - ? packetName - .split('::') - .pop() - ?.replace(/([a-z])([A-Z])/g, '$1 $2') // Add spaces before capital letters - ?.replace(/^./, (str: string) => str.toUpperCase()) || // Capitalize first letter - packetName - : packetName.split('::').pop() || packetName; - - categoryMap.set(cleanPacketName, { - category: cleanPacketName, - values: values, - time: event.time || Date.now(), - }); - } - }); + processChildrenStringValues( + event.children_string_values, + categoryMap, + valueLabels, + prettifyNames, + event.time || Date.now() + ); } } From ada2dccf4f83ee90c778205b354dbaa46ddc2364 Mon Sep 17 00:00:00 2001 From: David Cowan Date: Thu, 11 Dec 2025 14:31:09 -0800 Subject: [PATCH 9/9] Made column widths a property of the table - Fixed column widths was nasty, made them properties of the table --- webview-ui/src/diagnostics_panel/App.css | 51 +------------------ .../MinecraftMultiColumnStatisticTable.tsx | 45 ++++++++++++++-- .../prefabs/tabs/EditorNetworkStats.tsx | 1 + 3 files changed, 43 insertions(+), 54 deletions(-) diff --git a/webview-ui/src/diagnostics_panel/App.css b/webview-ui/src/diagnostics_panel/App.css index f9666be1..c36e182b 100644 --- a/webview-ui/src/diagnostics_panel/App.css +++ b/webview-ui/src/diagnostics_panel/App.css @@ -205,54 +205,7 @@ main { min-width: 0; } -/* Set specific column widths using CSS custom properties */ -#multi-column-grid { - --col1-width: 400px; - --col2-width: 80px; - --col3-width: 80px; - --col4-width: 80px; - --col5-width: 80px; - --col6-width: 80px; -} - -/* Apply column widths */ -#multi-column-grid vscode-data-grid-cell[grid-column="1"] { - width: var(--col1-width); - min-width: 180px; - flex: none; -} - -#multi-column-grid vscode-data-grid-cell[grid-column="2"] { - width: var(--col2-width); - min-width: 100px; - text-align: right; - flex: none; -} - -#multi-column-grid vscode-data-grid-cell[grid-column="3"] { - width: var(--col3-width); - min-width: 100px; - text-align: right; - flex: none; -} - -#multi-column-grid vscode-data-grid-cell[grid-column="4"] { - width: var(--col4-width); - min-width: 100px; - text-align: right; - flex: none; -} - -#multi-column-grid vscode-data-grid-cell[grid-column="5"] { - width: var(--col5-width); - min-width: 100px; - text-align: right; - flex: none; -} - -#multi-column-grid vscode-data-grid-cell[grid-column="6"] { - width: var(--col6-width); - min-width: 100px; +/* Default styling for value columns when no columnWidths prop is provided */ +#multi-column-grid vscode-data-grid-cell:not([grid-column="1"]) { text-align: right; - flex: none; } diff --git a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx index 4d2d22c4..c4b6bf6e 100644 --- a/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx +++ b/webview-ui/src/diagnostics_panel/controls/MinecraftMultiColumnStatisticTable.tsx @@ -43,6 +43,7 @@ type MinecraftMultiColumnStatisticTableProps = { keyLabel: string; valueLabels: string[]; // Array of labels for value columns prettifyNames?: boolean; // Whether to format packet names (camelCase -> Camel Case) or keep original format + columnWidths?: string[]; // Optional array of column widths (first is key column, rest are value columns) }; const sortOrderOptions = [ @@ -111,6 +112,7 @@ export default function MinecraftMultiColumnStatisticTable({ keyLabel, valueLabels, prettifyNames = true, // Default to prettifying names for backward compatibility + columnWidths, }: MinecraftMultiColumnStatisticTableProps): JSX.Element { // states const [data, setData] = useState([]); @@ -342,22 +344,55 @@ export default function MinecraftMultiColumnStatisticTable({ - + - + {keyLabel} {valueLabels.map((label, index) => ( - + {label} ))} {data.map(dataPoint => ( - {dataPoint.category} + + {dataPoint.category} + {dataPoint.values.map((value, index) => ( - + {typeof value === 'number' ? value.toFixed(1) : value} ))} diff --git a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx index 5bb41cc5..e5ba3b7a 100644 --- a/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx +++ b/webview-ui/src/diagnostics_panel/prefabs/tabs/EditorNetworkStats.tsx @@ -26,6 +26,7 @@ const statsTab: TabPrefab = { valueLabels={['Sent Count', 'Received Count', 'Min Size', 'Max Size']} prettifyNames={false} // Keep original packet name format defaultSortColumn="value_0" // Sort by "Sent Count" column by default + columnWidths={['400px', '80px', '80px', '80px', '80px']} // Custom column widths /> );