Skip to content
21 changes: 19 additions & 2 deletions webview-ui/src/diagnostics_panel/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ main {

.minecraft-grouped-statistic-table-grid-select {
text-align: center !important;
width: 72px;
width: 50px;
padding: 6px 4px !important;
}

Expand All @@ -438,6 +438,12 @@ main {
padding: 6px 4px !important;
}

.minecraft-grouped-statistic-table-grid-numeric {
text-align: right !important;
white-space: nowrap;
width: 10%;
}

.minecraft-grouped-statistic-table-grid-sparkline {
width: 240px;
min-width: 240px;
Expand Down Expand Up @@ -469,13 +475,19 @@ main {
display: inline-block;
margin-right: 0;
font-weight: 600;
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
max-width: 100%;
vertical-align: middle;
}

.minecraft-grouped-statistic-group-meta {
display: block;
margin-left: 0;
color: var(--vscode-descriptionForeground);
white-space: normal;
overflow-wrap: anywhere;
max-width: 100%;
}

.minecraft-grouped-statistic-toggle {
Expand All @@ -492,6 +504,11 @@ main {
.minecraft-grouped-statistic-child-key {
display: inline-block;
padding-left: 18px;
white-space: normal;
overflow: hidden;
overflow-wrap: anywhere;
max-width: 100%;
vertical-align: middle;
}

.minecraft-grouped-statistic-row-pinned {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type MinecraftGroupedStatisticTableProps = {
rowAction?: GroupedStatisticTableRowAction;
nonConsolidatedColumnResolver?: NonConsolidatedColumnResolver;
valueFormatter?: (value: string | number, columnIndex: number) => string;
keyFormatter?: (key: string) => string;
prettifyNames?: boolean;
selectionEnabled?: boolean;
selectionHeaderLabel?: string;
Expand All @@ -116,6 +117,27 @@ const SPARKLINE_BOUNDS_EXPAND_LERP = 0.35;
const SPARKLINE_BOUNDS_CONTRACT_LERP = 0.12;
const SPARKLINE_HISTORY_RETENTION_TICKS = 8;

const TICKS_PER_SECOND = 20;
const TICKS_PER_SPARKLINE_UPDATE = 12; // TODO I don't know if this is accurate or not, but it gives us the right values for now

function sparklineTickRangeToSeconds(updatePoints: number): string {
const totalTicks = updatePoints * TICKS_PER_SPARKLINE_UPDATE;
const seconds = totalTicks / TICKS_PER_SECOND;

if (seconds <= 0) {
return '0s';
}

if (seconds >= 60) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);

return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
}

// For short ranges, show one decimal place for precision
return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
}
function getRowSparklineSeriesKey(category: string): string {
return `row:${category}`;
}
Expand Down Expand Up @@ -400,6 +422,7 @@ const MinecraftGroupedStatisticTable = forwardRef<
rowAction,
nonConsolidatedColumnResolver,
valueFormatter,
keyFormatter,
prettifyNames = false,
selectionEnabled = false,
selectionHeaderLabel = 'Selected',
Expand Down Expand Up @@ -1264,7 +1287,9 @@ const MinecraftGroupedStatisticTable = forwardRef<
</td>
)}
<td>
<span className="minecraft-grouped-statistic-child-key">{row.category}</span>
<span className="minecraft-grouped-statistic-child-key">
{keyFormatter ? keyFormatter(row.category) : row.category}
</span>
</td>
{row.values.map((value, valueIndex) => (
<td key={valueIndex} className="minecraft-grouped-statistic-table-grid-numeric">
Expand Down Expand Up @@ -1363,7 +1388,9 @@ const MinecraftGroupedStatisticTable = forwardRef<
</th>
))}
{sparklineColumnIndex !== undefined && (
<th className="minecraft-grouped-statistic-table-grid-sparkline">Trend</th>
<th className="minecraft-grouped-statistic-table-grid-sparkline">
Trend ({sparklineTickRangeToSeconds(sparklineTickRange)})
</th>
)}
{rowAction && (
<th className="minecraft-grouped-statistic-table-grid-action">
Expand Down Expand Up @@ -1434,7 +1461,7 @@ const MinecraftGroupedStatisticTable = forwardRef<
{isExpanded ? '▾' : '▸'}
</button>
<span className="minecraft-grouped-statistic-group-key">
{group.key}
{keyFormatter ? keyFormatter(group.key) : group.key}
</span>
</div>
<span className="minecraft-grouped-statistic-group-meta">
Expand Down
37 changes: 37 additions & 0 deletions webview-ui/src/diagnostics_panel/controls/SparklineCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ function formatSparklineLabelValue(value: number, formatValue?: (value: number)
return formatValue ? formatValue(value) : value.toFixed(1);
}

function calculateMedian(values: number[]): number {
if (values.length === 0) {
return 0;
}

// For median, we need to sort the values and find the middle one (or average of two middle ones)
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

function calculateStandardDeviation(values: number[]): number {
if (values.length === 0) {
return 0;
}

// Calculate the mean by summing all values and dividing by the count
const mean = values.reduce((acc, v) => acc + v, 0) / values.length;
// Calculate the variance by averaging the squared differences from the mean
const variance = values.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / values.length;
// Finally, take the square root of the variance to get the standard deviation
return Math.sqrt(variance);
}

// A simple sparkline component that renders
// a basic line chart of the provided values along with min and max labels.
// Implemented using a dynamic SVG polyline.
Expand All @@ -30,6 +54,9 @@ export function SparklineCell({

const rawMax = Math.max(...values);
const rawMin = Math.min(...values);
const median = calculateMedian(values);
const stddev = calculateStandardDeviation(values);

const max = displayedMax ?? rawMax;
const min = displayedMin ?? rawMin;
const range = max - min || 1;
Expand All @@ -44,6 +71,8 @@ export function SparklineCell({

const maxLabel = formatSparklineLabelValue(rawMax, formatValue);
const minLabel = formatSparklineLabelValue(rawMin, formatValue);
const medianLabel = formatSparklineLabelValue(median, formatValue);
const stddevLabel = formatSparklineLabelValue(stddev, formatValue);

return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
Expand All @@ -67,6 +96,14 @@ export function SparklineCell({
Min {minLabel}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', lineHeight: 1.2 }}>
<span style={{ fontSize: '10px', color: 'var(--vscode-descriptionForeground)', whiteSpace: 'nowrap' }}>
Median {medianLabel}
</span>
<span style={{ fontSize: '10px', color: 'var(--vscode-descriptionForeground)', whiteSpace: 'nowrap' }}>
Std Dev {stddevLabel}
</span>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ function ceilToDecimalPlace(value: number, decimalPlaces: number): string {

function getTimingColumnLabel(unit: TimingUnit): string {
if (unit === 'ms') {
return 'Time In Milliseconds';
return 'Time (ms)';
}

if (unit === 'us') {
return 'Time In Microseconds';
return 'Time (µs)';
}

return 'Time In Nanoseconds';
return 'Time (ns)';
}

function formatTimingValue(value: number, unit: TimingUnit): string {
Expand All @@ -97,7 +97,7 @@ function formatTimingValue(value: number, unit: TimingUnit): string {
}

if (unit === 'us') {
return `${ceilToDecimalPlace(value / 1_000, 1)} us`;
return `${ceilToDecimalPlace(value / 1_000, 1)} µs`;
}

return `${value} ns`;
Expand Down Expand Up @@ -136,6 +136,10 @@ function extractEntityId(entityCategory: string): string | undefined {
return match?.[1];
}

function formatStatKey(key: string): string {
return key.split('(')[0].trim();
}

function resolveSystemId(fullName: string): string | undefined {
// Example fullname: "System Name (4)"
const indexAndBracket = fullName ? fullName.split('(')[1] : undefined;
Expand Down Expand Up @@ -299,7 +303,7 @@ const StatsTab: TabPrefab = {
[filterSelectionMode],
);

const entityValueLabels = [getTimingColumnLabel(entityTimingUnit), 'Percent Of Total'];
const entityValueLabels = [getTimingColumnLabel(entityTimingUnit), '% Of Total'];

const filteredEntityLabel = (() => {
if (filterSelectionMode === 'filter-by-single-entity') {
Expand Down Expand Up @@ -604,7 +608,7 @@ const StatsTab: TabPrefab = {
: undefined
}
keyLabel="System"
valueLabels={[getTimingColumnLabel(systemTimingUnit), 'Percent Of Total']}
valueLabels={[getTimingColumnLabel(systemTimingUnit), '% Of Total']}
displayMode={
systemViewMode === 'grouped'
? MinecraftGroupedStatisticTableDisplayMode.Grouped
Expand Down Expand Up @@ -641,6 +645,7 @@ const StatsTab: TabPrefab = {
sparklineColumnIndex={0}
sparklineTickRange={100}
sparklineValueFormatter={value => formatTimingValue(value, systemTimingUnit)}
keyFormatter={formatStatKey}
valueFormatter={(value, columnIndex) => {
// Timing column
if (columnIndex === 0) {
Expand Down