fix(mysql): preserve fractional dashboard rates

This commit is contained in:
LRcoding 2026-07-15 19:34:26 +08:00 committed by GitHub
parent b9ac69f79a
commit 1a8c1b19a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 39 additions and 6 deletions

View File

@ -9,7 +9,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import MetricCard from "@/components/common/MetricCard.vue";
import MetricLineChart from "@/components/chart/MetricLineChart.vue";
import * as api from "@/lib/backend/api";
import { computeQps, computeRate, formatBytes, formatBytesPerSec, formatNumber, formatUptime, GLOBAL_STATUS_SQL, GLOBAL_VARIABLES_SQL, innodbBufferHitRatio, MAX_SAMPLES, parseStatusResult, statusEntries, statusNumber, type StatusSample } from "@/lib/database/mysqlServerStatus";
import { computeQps, computeRate, formatBytes, formatBytesPerSec, formatNumber, formatRate, formatUptime, GLOBAL_STATUS_SQL, GLOBAL_VARIABLES_SQL, innodbBufferHitRatio, MAX_SAMPLES, parseStatusResult, statusEntries, statusNumber, type StatusSample } from "@/lib/database/mysqlServerStatus";
const props = defineProps<{
connectionId: string;
@ -195,7 +195,7 @@ onUnmounted(stopAutoRefresh);
<div class="flex min-h-0 flex-1 flex-col gap-3 p-3">
<div class="grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-4">
<MetricCard :label="t('serverDashboard.qps')" :value="formatNumber(qps)" :icon="Gauge" />
<MetricCard :label="t('serverDashboard.qps')" :value="formatRate(qps)" :icon="Gauge" />
<MetricCard :label="t('serverDashboard.connections')" :value="`${formatNumber(threadsConnected)}${maxConnections ? ' / ' + formatNumber(maxConnections) : ''}`" :icon="Users" />
<MetricCard :label="t('serverDashboard.running')" :value="formatNumber(threadsRunning)" :icon="Activity" />
<MetricCard :label="t('serverDashboard.trafficIn')" :value="formatBytesPerSec(rate('Bytes_received'))" :icon="ArrowDownUp" />
@ -206,10 +206,10 @@ onUnmounted(stopAutoRefresh);
</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart :title="t('serverDashboard.qpsChart')" :labels="chartLabels" :series="qpsSeries" :value-formatter="formatNumber" />
<MetricLineChart :title="t('serverDashboard.sessionsChart')" :labels="chartLabels" :series="sessionsSeries" :value-formatter="formatNumber" />
<MetricLineChart :title="t('serverDashboard.qpsChart')" :labels="chartLabels" :series="qpsSeries" :value-formatter="formatRate" />
<MetricLineChart :title="t('serverDashboard.sessionsChart')" :labels="chartLabels" :series="sessionsSeries" :value-formatter="formatRate" />
<MetricLineChart :title="t('serverDashboard.trafficChart')" :labels="chartLabels" :series="trafficSeries" :value-formatter="formatBytes" />
<MetricLineChart :title="t('serverDashboard.commandChart')" :labels="chartLabels" :series="commandSeries" :value-formatter="formatNumber" />
<MetricLineChart :title="t('serverDashboard.commandChart')" :labels="chartLabels" :series="commandSeries" :value-formatter="formatRate" />
</div>
<div class="flex flex-col rounded-lg border bg-card" :class="showStatusTable ? 'min-h-0 flex-1' : 'shrink-0'">

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { QueryResult } from "@/types/database";
import { computeQps, computeRate, connectionSupportsServerDashboard, formatBytes, formatBytesPerSec, formatUptime, innodbBufferHitRatio, parseStatusResult, statusNumber, supportsServerDashboard, type StatusSample } from "@/lib/database/mysqlServerStatus";
import { computeQps, computeRate, connectionSupportsServerDashboard, formatBytes, formatBytesPerSec, formatNumber, formatRate, formatUptime, innodbBufferHitRatio, parseStatusResult, statusNumber, supportsServerDashboard, type StatusSample } from "@/lib/database/mysqlServerStatus";
function statusResult(rows: [string, string][]): QueryResult {
return { columns: ["Variable_name", "Value"], rows, affected_rows: 0, execution_time_ms: 0 };
@ -42,6 +42,14 @@ describe("computeRate", () => {
expect(computeRate(prev, curr, "Bytes_sent")).toBe(2000); // 4000 bytes / 2s
});
it("preserves a fractional connection rate over a longer sample interval", () => {
const prev = sample(0, { Connections: "100" });
const curr = sample(5000, { Connections: "101" });
const rate = computeRate(prev, curr, "Connections");
expect(rate).toBe(0.2);
expect(formatRate(rate)).toBe("0.2");
});
it("returns 0 on counter reset (decrease)", () => {
const prev = sample(1000, { Queries: "9000" });
const curr = sample(2000, { Queries: "10" });
@ -75,6 +83,24 @@ describe("innodbBufferHitRatio", () => {
});
describe("formatters", () => {
it("formats rates with up to three fractional digits", () => {
expect(formatRate(0)).toBe("0");
expect(formatRate(0.2)).toBe("0.2");
expect(formatRate(0.125)).toBe("0.125");
expect(formatRate(1 / 3)).toBe("0.333");
expect(formatRate(1)).toBe("1");
expect(formatRate(1000)).toBe("1,000");
expect(formatRate(1234.5678)).toBe("1,234.568");
expect(formatRate(Number.NaN)).toBe("0");
expect(formatRate(Number.POSITIVE_INFINITY)).toBe("0");
expect(formatRate(Number.NEGATIVE_INFINITY)).toBe("0");
});
it("keeps integer metric formatting unchanged", () => {
expect(formatNumber(0.2)).toBe("0");
expect(formatNumber(1234.6)).toBe("1,235");
});
it("formats bytes and bytes/sec", () => {
expect(formatBytes(0)).toBe("0 B");
expect(formatBytes(1024)).toBe("1.0 KB");

View File

@ -110,6 +110,13 @@ export function formatNumber(value: number): string {
return Math.round(value).toLocaleString("en-US");
}
const RATE_NUMBER_FORMATTER = new Intl.NumberFormat("en-US", { maximumFractionDigits: 3 });
/** Cumulative-counter rates can be below 1/s, so preserve their fractional value. */
export function formatRate(value: number): string {
return Number.isFinite(value) ? RATE_NUMBER_FORMATTER.format(value) : "0";
}
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"];
export function formatBytes(value: number): string {