Files
supabase/apps/studio/data/reports/database-charts.ts
Jordi EnricandClaude Sonnet 4.6 5d9cf3971f fix(studio): improve memory and swap units in DB reports (#45889)
Fixes [DEBUG-94](https://linear.app/supabase/issue/DEBUG-94)

## Summary

- **Min MB units**: Added \`formatBytesMinMB\` helper that always
formats byte values in at least MB. Applied to RAM and swap y-axis tick
formatters, tooltips, and chart headers.
- **Swap chart scale**: Every Supabase compute instance is provisioned
with **1 GB of swap regardless of size** (per
[docs](https://supabase.com/docs/guides/troubleshooting/memory-and-swap-usage-explained-aPNgm0)),
so the swap y-axis always shows at least 0 to 1 GB. Low swap usage no
longer fills the full chart height. Removed the show/hide limit toggle
since the limit is implicit in the y-axis scale.
- **Swap units in header/tooltip**: Fixed all three formatters (initial
header, hover-sync header, tooltip) to use \`formatBytesMinMB\` for swap
so the value always shows a unit like "2.00 MB".
- **Reference-line defensive fix**: Added a \`customValue\` fallback in
\`useAttributeQueries\` so future reference-line attributes that use
\`customValue\` instead of \`value\` are not silently overwritten to 0.

## Test plan

- [ ] Open DB Reports and verify RAM/Swap y-axis labels show MB (or GB
for large values), never KB or bytes
- [ ] Hover a bar in the swap chart and verify tooltip and header show
the same value with a unit (e.g. "2.00 MB")
- [ ] With low or zero swap usage, the chart bars are flat and the
y-axis goes 0 to 1 GB
- [ ] CPU chart limit toggle still renders at 100% (regression check on
the \`useAttributeQueries\` fallback change)

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:12:25 +00:00

603 lines
20 KiB
TypeScript

import { COMPUTE_DISK, COMPUTE_MAX_IOPS } from 'shared-data'
import { mapComputeSizeNameToAddonVariantId } from '@/components/interfaces/DiskManagement/DiskManagement.utils'
import { compactNumberFormatter } from '@/components/ui/Charts/Charts.utils'
import { ReportAttributes } from '@/components/ui/Charts/ComposedChart.utils'
import { DiskAttributesData } from '@/data/config/disk-attributes-query'
import { MaxConnectionsData } from '@/data/database/max-connections-query'
import { Project } from '@/data/projects/project-detail-query'
import { DOCS_URL } from '@/lib/constants'
import { formatBytes, formatBytesMinMB } from '@/lib/helpers'
// Compute variants below 4XL run on EBS instances that draw on a burst
// credit pool for disk IO, so the burst balance chart only makes sense for
// these. Larger instances have dedicated IOPS and don't expose this metric.
const BURSTABLE_IO_VARIANTS = new Set([
'ci_nano',
'ci_micro',
'ci_small',
'ci_medium',
'ci_large',
'ci_xlarge',
'ci_2xlarge',
])
export const getReportAttributesV2: (
entitledFeatures: string[],
project: Project,
diskConfig?: DiskAttributesData,
maxConnections?: MaxConnectionsData,
pgBouncerMaxConnections?: number,
isSpendCapEnabled?: boolean,
showDiskIOBurstBalanceChart?: boolean
) => ReportAttributes[] = (
entitledFeatures,
project,
diskConfig,
maxConnections,
pgBouncerMaxConnections,
isSpendCapEnabled,
showDiskIOBurstBalanceChart
) => {
const computeVariantId = mapComputeSizeNameToAddonVariantId(project?.infra_compute_size)
const provisionedDiskIops = diskConfig?.attributes?.iops
const computeIopsLimit = COMPUTE_MAX_IOPS[computeVariantId]
const effectiveMaxIops =
typeof provisionedDiskIops === 'number' && typeof computeIopsLimit === 'number'
? Math.min(provisionedDiskIops, computeIopsLimit)
: provisionedDiskIops
const hasBurstableIO = BURSTABLE_IO_VARIANTS.has(computeVariantId)
const showBurstBalanceChart = !!showDiskIOBurstBalanceChart && hasBurstableIO
const baselineThroughputMBps = COMPUTE_DISK[computeVariantId]?.baselineThroughputMBps
const baselineThroughputLabel =
typeof baselineThroughputMBps === 'number' ? `${baselineThroughputMBps} MB/s` : 'its baseline'
return [
{
id: 'ram-usage',
label: 'Memory usage',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#memory-usage`,
hide: false,
showTooltip: true,
showLegend: true,
hideChartType: false,
defaultChartStyle: 'bar',
showMaxValue: false,
showGrid: true,
syncId: 'database-reports',
valuePrecision: 2,
YAxisProps: {
width: 75,
tickFormatter: (value: number) => formatBytesMinMB(value, 2),
},
attributes: [
{
attribute: 'ram_usage_used',
provider: 'infra-monitoring',
label: 'Used',
tooltip:
'RAM in use by Postgres and the operating system. Sustained high usage may indicate memory pressure',
},
{
attribute: 'ram_usage_cache_and_buffers',
provider: 'infra-monitoring',
label: 'Cache + Buffers',
tooltip:
'RAM used by the operating system page cache and PostgreSQL buffers to accelerate disk reads/writes',
},
{
attribute: 'ram_usage_free',
provider: 'infra-monitoring',
label: 'Free',
tooltip:
'Unallocated memory available for use. A small portion is always reserved by the operating system',
},
{
attribute: 'ram_usage_total',
provider: 'infra-monitoring',
label: 'Total RAM',
isMaxValue: true,
omitFromTotal: true,
tooltip: 'Total RAM available on this instance',
},
],
},
{
id: 'swap-usage',
label: 'Swap usage',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#memory-usage`,
hide: false,
showTooltip: true,
showLegend: false,
hideChartType: false,
defaultChartStyle: 'bar',
showMaxValue: false,
showGrid: true,
syncId: 'database-reports',
valuePrecision: 2,
YAxisProps: {
width: 75,
tickFormatter: (value: number) => formatBytesMinMB(value, 2),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
domain: [0, (dataMax: number) => Math.max(dataMax, 1024 * 1024 * 1024)] as any,
},
attributes: [
{
attribute: 'swap_usage',
provider: 'infra-monitoring',
label: 'Swap',
tooltip:
'Swap space in use by the operating system. Sustained swap usage indicates memory pressure and may degrade database performance',
},
],
},
{
id: 'cpu-usage',
label: 'CPU usage',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#cpu-usage`,
syncId: 'database-reports',
format: '%',
valuePrecision: 2,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: false,
showGrid: true,
normalizeVisibleStackToPercent: true,
YAxisProps: {
width: 55,
domain: [0, 100] as [number, number],
allowDataOverflow: true,
tickFormatter: (v: number) => `${Math.round(v)}%`,
},
hideChartType: false,
defaultChartStyle: 'bar',
attributes: [
{
attribute: 'cpu_usage_busy_system',
provider: 'infra-monitoring',
label: 'System',
format: '%',
color: { light: '#EDC35E', dark: '#EDD35E' },
fill: { light: '#F6D99F', dark: '#5C5230' },
tooltip:
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead',
},
{
attribute: 'cpu_usage_busy_user',
provider: 'infra-monitoring',
label: 'User',
format: '%',
color: { light: '#0063E8', dark: '#65BCD9' },
fill: { light: '#80B1F4', dark: '#2A3D45' },
tooltip:
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries',
},
{
attribute: 'cpu_usage_busy_iowait',
provider: 'infra-monitoring',
label: 'IOwait',
format: '%',
color: { light: '#DB3A34', dark: '#FF6B6B' },
fill: { light: '#F2A7A3', dark: '#5C2A2A' },
tooltip:
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks',
},
{
attribute: 'cpu_usage_busy_irqs',
provider: 'infra-monitoring',
label: 'IRQs',
format: '%',
color: { light: '#DA760B', dark: '#DA760B' },
fill: { light: '#FFB885', dark: '#5C3D0A' },
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
},
{
attribute: 'cpu_usage_busy_other',
provider: 'infra-monitoring',
label: 'Other',
format: '%',
color: { light: '#B616A6', dark: '#DB8DF9' },
fill: { light: '#DB8BD3', dark: '#4A3D5C' },
tooltip:
'CPU time spent on other tasks (e.g., background processes, software interrupts)',
},
{
attribute: 'cpu_usage_busy_idle',
provider: 'infra-monitoring',
label: 'Idle',
format: '%',
omitFromTotal: true,
color: { light: '#6EA85F', dark: '#A3FFC2' },
fill: { light: '#A6D8AE', dark: '#2A5C3F' },
tooltip: 'CPU time spent idle and available for new work',
},
{
attribute: 'cpu_usage_max',
provider: 'reference-line',
label: 'Max',
value: 100,
tooltip: 'Max CPU usage',
isMaxValue: true,
},
],
},
{
id: 'network-throughput',
label: 'Network throughput',
syncId: 'database-reports',
hide: false,
showTooltip: true,
format: 'bytes-per-second',
valuePrecision: 1,
showLegend: true,
showMaxValue: false,
hideChartType: false,
showGrid: true,
YAxisProps: {
width: 70,
tickFormatter: (value: number) => `${formatBytes(value, 1)}/s`,
},
defaultChartStyle: 'stackedAreaLine',
attributes: [
{
attribute: 'network_receive_bytes',
provider: 'infra-monitoring',
label: 'Network in',
tooltip: 'Inbound network throughput (bytes per second)',
},
{
attribute: 'network_transmit_bytes',
provider: 'infra-monitoring',
label: 'Network out',
tooltip: 'Outbound network throughput (bytes per second)',
},
],
},
{
id: 'disk-iops',
label: 'Disk Input/Output operations per second (IOPS)',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-inputoutput-operations-per-second-iops`,
syncId: 'database-reports',
hide: false,
showTooltip: true,
valuePrecision: 0,
showLegend: true,
hideChartType: false,
showGrid: true,
showMaxValue: true,
YAxisProps: {
width: 55,
tickFormatter: (value: number) => compactNumberFormatter(value),
},
defaultChartStyle: 'bar',
attributes: [
{
attribute: 'disk_iops_write',
provider: 'infra-monitoring',
label: 'Write IOPS',
tooltip:
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity',
},
{
attribute: 'disk_iops_read',
provider: 'infra-monitoring',
label: 'Read IOPS',
tooltip:
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching',
},
{
attribute: 'disk_iops_max',
provider: 'reference-line',
label: 'Max IOPS',
value: effectiveMaxIops,
tooltip:
'Effective maximum IOPS for your current compute and disk configuration. Equal to the lower of the compute IOPS limit and the provisioned disk IOPS',
isMaxValue: true,
},
],
},
{
id: 'disk-throughput',
label: 'Disk throughput',
docsUrl: `${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput`,
syncId: 'database-reports',
hide: false,
showTooltip: true,
format: 'bytes-per-second',
valuePrecision: 1,
showLegend: true,
showMaxValue: true,
hideChartType: false,
showGrid: true,
YAxisProps: {
width: 70,
tickFormatter: (value: number) => `${formatBytes(value, 1)}/s`,
},
defaultChartStyle: 'stackedAreaLine',
attributes: [
{
attribute: 'disk_bytes_read',
provider: 'infra-monitoring',
label: 'Read throughput',
tooltip: 'Disk read throughput (bytes per second)',
},
{
attribute: 'disk_bytes_written',
provider: 'infra-monitoring',
label: 'Write throughput',
tooltip: 'Disk write throughput (bytes per second)',
},
{
attribute: 'disk_throughput_max',
provider: 'reference-line',
label: 'Max throughput',
value:
diskConfig?.attributes?.type === 'gp3' &&
typeof diskConfig.attributes.throughput_mbps === 'number'
? diskConfig.attributes.throughput_mbps * 1024 * 1024
: undefined,
tooltip: 'Maximum disk throughput for your current compute size',
isMaxValue: true,
},
],
},
{
id: 'disk-io-burst-balance',
label: 'Disk IO Burst Balance',
titleTooltip: `The burst credit pool that smaller compute instances draw on to sustain IO above their baseline. When the balance hits 0%, sustained throughput returns to its baseline of ${baselineThroughputLabel} until it refills.`,
docsUrl: `${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`,
syncId: 'database-reports',
hide: !showBurstBalanceChart,
format: '%',
valuePrecision: 0,
showTooltip: true,
showLegend: false,
showMaxValue: false,
showGrid: true,
YAxisProps: {
width: 55,
domain: [0, 100] as [number, number],
allowDataOverflow: true,
tickFormatter: (v: number) => `${Math.round(v)}%`,
},
hideChartType: false,
defaultChartStyle: 'bar',
attributes: [
{
attribute: 'disk_io_budget',
provider: 'infra-monitoring',
label: 'Burst credits remaining',
format: '%',
tooltip: `Percentage of EBS burst credits remaining. Drops only matter while the instance is bursting above its baseline IO. At 0%, sustained throughput returns to its baseline of ${baselineThroughputLabel} until it refills.`,
},
],
},
{
// Client Connections metric for free tier
id: 'client-connections-basic',
label: 'Database Connections',
syncId: 'database-reports',
valuePrecision: 0,
hide: entitledFeatures.includes('database'),
showTooltip: false,
showLegend: false,
showMaxValue: true,
hideChartType: false,
showGrid: true,
YAxisProps: { width: 30 },
defaultChartStyle: 'bar',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#database-connections`,
attributes: [
{
attribute: 'pg_stat_database_num_backends',
provider: 'infra-monitoring',
label: 'Total connections',
tooltip: 'Total number of active database connections',
},
{
attribute: 'max_db_connections',
provider: 'reference-line',
label: 'Max connections',
value: maxConnections?.maxConnections,
tooltip: 'Max available connections for your current compute size',
isMaxValue: true,
},
],
},
{
// advanced client connections metric for paid and above
id: 'client-connections',
label: 'Database Connections',
syncId: 'database-reports',
valuePrecision: 0,
entitlement: 'database',
requiredPlan: 'Pro',
hide: !entitledFeatures.includes('database'),
showTooltip: true,
showLegend: true,
showMaxValue: true,
hideChartType: false,
showGrid: true,
YAxisProps: { width: 30 },
defaultChartStyle: 'bar',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#database-connections`,
attributes: [
{
attribute: 'client_connections_postgres',
provider: 'infra-monitoring',
label: 'Postgres',
tooltip:
'Direct connections to the Postgres database from your application and external clients',
},
{
attribute: 'client_connections_authenticator',
provider: 'infra-monitoring',
label: 'PostgREST',
tooltip: 'Connection pool managed by PostgREST',
},
{
attribute: 'client_connections_supabase_admin',
provider: 'infra-monitoring',
label: 'Reserved',
tooltip:
'Administrative connections used by various Supabase services for internal operations and maintenance tasks',
},
{
attribute: 'client_connections_supabase_auth_admin',
provider: 'infra-monitoring',
label: 'Auth',
tooltip: 'Connection pool managed by Supabase Auth',
},
{
attribute: 'client_connections_supabase_storage_admin',
provider: 'infra-monitoring',
label: 'Storage',
tooltip: 'Connection pool managed by Supabase Storage',
},
{
attribute: 'client_connections_other',
provider: 'infra-monitoring',
label: 'Other roles',
tooltip: "Miscellaneous database connections that don't fall into other categories.",
},
{
attribute: 'max_db_connections',
provider: 'reference-line',
label: 'Max connections',
value: maxConnections?.maxConnections,
tooltip: 'Max available connections for your current compute size',
isMaxValue: true,
},
],
},
{
id: 'pgbouncer-connections',
label: 'Dedicated Pooler Client Connections',
syncId: 'database-reports',
valuePrecision: 0,
entitlement: 'database',
requiredPlan: 'Pro',
hide: !entitledFeatures.includes('database'),
showTooltip: true,
showLegend: true,
showMaxValue: true,
showGrid: true,
YAxisProps: { width: 30 },
hideChartType: false,
defaultChartStyle: 'bar',
docsUrl: `${DOCS_URL}/guides/platform/compute-and-disk#limits-and-constraints`,
attributes: [
{
attribute: 'client_connections_pgbouncer',
provider: 'infra-monitoring',
label: 'pgbouncer',
tooltip: 'PgBouncer connections',
},
{
attribute: 'pg_pooler_max_connections',
provider: 'reference-line',
label: 'Max pooler connections',
value: pgBouncerMaxConnections,
tooltip: 'Maximum allowed pooler connections for your current compute size',
isMaxValue: true,
},
],
},
{
id: 'supavisor-connections-active',
label: 'Shared Pooler (Supavisor) client connections',
syncId: 'database-reports',
valuePrecision: 0,
entitlement: 'database',
requiredPlan: 'Pro',
hide: !entitledFeatures.includes('database'),
showTooltip: false,
showLegend: false,
showMaxValue: false,
showGrid: true,
YAxisProps: { width: 30 },
hideChartType: false,
defaultChartStyle: 'bar',
attributes: [
{
attribute: 'supavisor_connections_active',
provider: 'infra-monitoring',
label: 'supavisor',
tooltip: 'Supavisor connections',
},
],
},
{
id: 'disk-size',
label: 'Disk Usage',
syncId: 'database-reports',
valuePrecision: 2,
hide: false,
showTooltip: true,
showLegend: true,
showMaxValue: true,
showGrid: true,
YAxisProps: {
width: 65,
tickFormatter: (value: number) => formatBytes(value, 1),
},
hideChartType: false,
defaultChartStyle: 'bar',
docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-size`,
attributes: [
{
attribute: 'disk_fs_used_system',
provider: 'infra-monitoring',
format: 'bytes',
label: 'System',
tooltip: 'Reserved space for the system to ensure your database runs smoothly',
},
{
attribute: 'disk_fs_used_wal',
provider: 'infra-monitoring',
format: 'bytes',
label: 'WAL',
tooltip:
'Disk usage by the write-ahead log. The usage depends on your WAL settings and the amount of data being written to the database',
},
{
attribute: 'pg_database_size',
provider: 'infra-monitoring',
format: 'bytes',
label: 'Database',
tooltip: 'Disk usage by your database (tables, indexes, data, ...)',
},
{
attribute: 'disk_fs_size',
provider: 'infra-monitoring',
isMaxValue: true,
format: 'bytes',
label: 'Disk Size',
tooltip: 'Disk Size refers to the total space your project occupies on disk',
},
entitledFeatures.includes('database') &&
(isSpendCapEnabled
? {
attribute: 'pg_database_size_percent_paid_spendCap',
provider: 'reference-line',
isReferenceLine: true,
strokeDasharray: '4 2',
label: 'Spend cap enabled',
value: diskConfig?.attributes?.size_gb! * 1024 * 1024 * 1024,
className: '[&_line]:stroke-yellow-800! [&_line]:opacity-100!',
opacity: 1,
}
: {
attribute: 'pg_database_size_percent_paid',
provider: 'reference-line',
isReferenceLine: true,
label: '90% - Disk resize threshold',
className: '[&_line]:stroke-yellow-800!',
value: diskConfig?.attributes?.size_gb! * 1024 * 1024 * 1024 * 0.9,
}),
],
},
]
}