diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..7d2533f383a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +packages/shared-data/compute-disk-limits.ts @supabase/infra +packages/shared-data/index.ts @supabase/infra + diff --git a/apps/docs/components/ComputeDiskLimitsTable.tsx b/apps/docs/components/ComputeDiskLimitsTable.tsx new file mode 100644 index 00000000000..908011429af --- /dev/null +++ b/apps/docs/components/ComputeDiskLimitsTable.tsx @@ -0,0 +1,34 @@ +import { + COMPUTE_BASELINE_IOPS, + COMPUTE_BASELINE_THROUGHPUT, + COMPUTE_DISK, + COMPUTE_MAX_IOPS, + COMPUTE_MAX_THROUGHPUT, +} from 'shared-data' + +export function ComputeDiskLimitsTable() { + return ( + + + + + + + + + + + + {Object.entries(COMPUTE_DISK).map(([key, value]) => ( + + + + + + + + ))} + +
Compute InstanceBaseline Throughput (MB/s)Max Throughput (MB/s)Baseline IOPSMax IOPS
{value.name}{COMPUTE_BASELINE_THROUGHPUT[key]?.toLocaleString()} MB/s{COMPUTE_MAX_THROUGHPUT[key]?.toLocaleString()} MB/s{COMPUTE_BASELINE_IOPS[key]?.toLocaleString()} IOPS{COMPUTE_MAX_IOPS[key]?.toLocaleString()} IOPS
+ ) +} diff --git a/apps/docs/content/guides/platform/compute-and-disk.mdx b/apps/docs/content/guides/platform/compute-and-disk.mdx index f9bb168413a..175bbeec3f8 100644 --- a/apps/docs/content/guides/platform/compute-and-disk.mdx +++ b/apps/docs/content/guides/platform/compute-and-disk.mdx @@ -83,23 +83,11 @@ The following sections explain how these attributes affect disk performance. ### Compute size -The compute size of your project sets the upper limit for disk throughput and IOPS. The table below shows the limits for each instance size. For instance, an 8XL compute instance has a maximum throughput of 9,500 Mbps and a maximum IOPS of 40,000. +The compute size of your project affects the effective disk throughput and IOPS. The table below shows both the baseline (sustained) limits and the burst (maximum) limits for each instance size. For instance, an 8XL compute instance has a throughput of 1,188 MB/s and IOPS of 40,000. -| Compute Instance | Disk Throughput | IOPS | -| ---------------- | --------------- | ----------- | -| Nano (free) | 43 Mbps | 250 IOPS | -| Micro | 87 Mbps | 500 IOPS | -| Small | 174 Mbps | 1,000 IOPS | -| Medium | 347 Mbps | 2,000 IOPS | -| Large | 630 Mbps | 3,600 IOPS | -| XL | 1,188 Mbps | 6,000 IOPS | -| 2XL | 2,375 Mbps | 12,000 IOPS | -| 4XL | 4,750 Mbps | 20,000 IOPS | -| 8XL | 9,500 Mbps | 40,000 IOPS | -| 12XL | 14,250 Mbps | 50,000 IOPS | -| 16XL | 19,000 Mbps | 80,000 IOPS | + -Smaller compute instances like Nano, Micro, Small, and Medium have baseline performance levels that can occasionally be exceeded for short periods of time. If it does exceed the baseline, you should consider upgrading your instance size for a more reliable performance. +Smaller compute instances like Nano, Micro, Small, and Medium can burst above baseline for short periods of time. Once burst capacity is exhausted, performance returns to baseline. If you need consistent disk performance, consider upgrading your compute size. Larger compute instances (4XL and above) are designed for sustained, high performance with specific IOPS and throughput limits which you can [configure](/docs/guides/platform/manage-your-usage/disk-throughput). If you hit your IOPS or throughput limit, throttling will occur. diff --git a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx index 9a9b1597f5d..5e191365a86 100644 --- a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx +++ b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx @@ -7,7 +7,7 @@ database_id = "4844905d-1456-44a1-858e-7a4995e5054c" ## Understanding disk IO and disk IO budget -Disk IO refers to two metrics: throughput in Megabits per Second and IOPS which are Input/Output Operations per Second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size). +Disk IO refers to two metrics: throughput in Megabytes per second (MB/s) and IOPS which are Input/Output Operations per Second. Throughput measures how much data you can move each second, while IOPS measures how many read/write operations you can perform each second. Depending on the compute add-on of your instance you will have [different baseline performances](/docs/guides/platform/compute-add-ons#compute-size). Smaller compute instances can burst and exceed their baseline performance for a short period of time every day. This is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. Learn more about [choosing the right compute instance for consistent disk performance](/docs/guides/platform/compute-add-ons#choosing-the-right-compute-instance-for-consistent-disk-performance). diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx index 6a77d87b7ad..9d5e0b77009 100644 --- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx @@ -14,22 +14,11 @@ There are two primary values that matter for IO: - **Disk Throughput**: how much data can be moved to and from disk per second - **IOPS(Input/Output per second)**: how many read/write requests can be performed against your disk per second -Each compute instance has unique IO settings. The settings at the time this was written are listed below. -| Plan | Baseline Disk Throughput | Baseline IOPS | -|---------------|--------------------------|---------------| -| Nano (free) | 43 Mbps | 250 IOPS | -| Micro | 87 Mbps | 500 IOPS | -| Small | 174 Mbps | 1,000 IOPS | -| Medium | 347 Mbps | 2,000 IOPS | -| Large | 630 Mbps | 3,000 IOPS | -| XL | 1,048 Mbps | 3,000 IOPS | -| 2XL | 1,048 Mbps | 3,000 IOPS | -| 4XL | 1,048 Mbps | 3,000 IOPS | -| 8XL | 1,048 Mbps | 3,000 IOPS | -| 12XL | 1,048 Mbps | 3,000 IOPS | -| 16XL | 1,048 Mbps | 3,000 IOPS | +Each compute instance has unique IO settings. The current baseline (sustained) and max (burst) limits are listed below. -Compute sizes below XL, to accommodate their limitations, have burst budgets. This is when instances are allowed to utilize 1048 Mbps of disk throughput and 3,000 IOPS for 30 minutes before returning back to their baseline behavior. + + +Compute sizes below XL can burst above baseline for short periods before returning back to their baseline behavior. There are other metrics that indicate IO strain. diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 06af05a873e..f29a70caec3 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -19,6 +19,7 @@ import { NavData } from '~/components/NavData' import { Price } from '~/components/Price' import { ProjectConfigVariables } from '~/components/ProjectConfigVariables' import { RealtimeLimitsEstimator } from '~/components/RealtimeLimitsEstimator' +import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable' import { RegionsList, SmartRegionsList } from '~/components/RegionsList' import { SharedData } from '~/components/SharedData' import StepHikeCompact from '~/components/StepHikeCompact' @@ -50,6 +51,7 @@ const components = { ButtonCard, CodeSampleDummy, CodeSampleWrapper, + ComputeDiskLimitsTable, CostWarning, ErrorCodes, Extensions, diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts index a93062840e8..2aee9a7ea1b 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.schema.ts @@ -1,24 +1,26 @@ -import { CloudProvider } from 'shared-data' +import { CloudProvider, COMPUTE_MAX_IOPS, computeInstanceAddonVariantIdSchema } from 'shared-data' import { z } from 'zod' -import { ComputeInstanceAddonVariantId } from './DiskManagement.types' import { calculateDiskSizeRequiredForIopsWithGp3, calculateDiskSizeRequiredForIopsWithIo2, calculateIopsRequiredForThroughput, + calculateMaxThroughput, calculateMaxIopsAllowedForDiskSizeWithGp3, calculateMaxIopsAllowedForDiskSizeWithio2, formatNumber, } from './DiskManagement.utils' import { DISK_LIMITS, DiskType } from './ui/DiskManagement.constants' +import { COMPUTE_MAX_THROUGHPUT } from 'shared-data' const baseSchema = z.object({ storageType: z.enum(['io2', 'gp3']).describe('Type of storage: io2 or gp3'), totalSize: z.number().int('Value must be an integer').describe('Allocated disk size in GB'), provisionedIOPS: z.number().describe('Provisioned IOPS for storage type'), throughput: z.number().optional().describe('Throughput in MB/s for gp3'), - computeSize: z - .custom((val): val is ComputeInstanceAddonVariantId => true) - .describe('Compute size'), + computeSize: computeInstanceAddonVariantIdSchema + .describe('Compute size') + .optional() + .default('ci_micro'), growthPercent: z .number() .int('Value must be an integer') @@ -55,7 +57,12 @@ export const CreateDiskStorageSchema = ({ const validateDiskConfiguration = !isFlyProject && !isAwsNimbusProject && !isAwsK8sProject const schema = baseSchema.superRefine((data, ctx) => { - const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb } = data + const { storageType, totalSize, provisionedIOPS, throughput, maxSizeGb, computeSize } = data + const computeMaxIops = (() => { + const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize) + if (!parsedCompute.success) return Number.POSITIVE_INFINITY + return COMPUTE_MAX_IOPS[parsedCompute.data] ?? Number.POSITIVE_INFINITY + })() if (validateDiskConfiguration && totalSize < 8) { ctx.addIssue({ @@ -85,10 +92,11 @@ export const CreateDiskStorageSchema = ({ if (validateDiskConfiguration && storageType === 'io2') { // Validation rules for io2 - if (provisionedIOPS > DISK_LIMITS[DiskType.IO2].maxIops) { + const maxIopsForIo2 = Math.min(DISK_LIMITS[DiskType.IO2].maxIops, computeMaxIops) + if (provisionedIOPS > maxIopsForIo2) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.IO2].maxIops)} for io2 Disk type. Please reach out to support if you need higher IOPS than this.`, + message: `IOPS cannot exceed ${formatNumber(maxIopsForIo2)} for io2 Disk type and the selected compute size.`, path: ['provisionedIOPS'], }) } @@ -141,11 +149,17 @@ export const CreateDiskStorageSchema = ({ if (validateDiskConfiguration && storageType === 'gp3') { const maxIopsAllowedForDiskSizeWithGp3 = calculateMaxIopsAllowedForDiskSizeWithGp3(totalSize) + const maxIopsForGp3 = Math.min(DISK_LIMITS[DiskType.GP3].maxIops, computeMaxIops) + const computeMaxThroughput = (() => { + const parsedCompute = computeInstanceAddonVariantIdSchema.safeParse(computeSize) + if (!parsedCompute.success) return DISK_LIMITS[DiskType.GP3].maxThroughput + return COMPUTE_MAX_THROUGHPUT[parsedCompute.data] ?? DISK_LIMITS[DiskType.GP3].maxThroughput + })() - if (provisionedIOPS > DISK_LIMITS[DiskType.GP3].maxIops) { + if (provisionedIOPS > maxIopsForGp3) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `IOPS can not exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxIops)} for GP3 Disk. Change the Disk type to io2 for higher IOPS support.`, + message: `IOPS cannot exceed ${formatNumber(maxIopsForGp3)} for GP3 Disk and the selected compute size.`, path: ['provisionedIOPS'], }) } @@ -176,16 +190,21 @@ export const CreateDiskStorageSchema = ({ } if (throughput !== undefined) { - const maxThroughput = Math.min(0.25 * provisionedIOPS, 1000) - - if (throughput > DISK_LIMITS['gp3'].maxThroughput) { + // gp3 throughput scales with provisioned IOPS (capped by gp3 max) + const iopsThroughputLimit = calculateMaxThroughput(provisionedIOPS) + if (throughput > DISK_LIMITS[DiskType.GP3].maxThroughput) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Throughput can not exceed ${formatNumber(maxThroughput)} MB/s`, + message: `Throughput cannot exceed ${formatNumber(DISK_LIMITS[DiskType.GP3].maxThroughput)} MB/s for GP3 disk type.`, path: ['throughput'], }) - } - if (throughput > maxThroughput) { + } else if (throughput > computeMaxThroughput) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Throughput cannot exceed ${formatNumber(computeMaxThroughput)} MB/s for the selected compute size.`, + path: ['throughput'], + }) + } else if (throughput > iopsThroughputLimit) { const iopsRequiredForThroughput = calculateIopsRequiredForThroughput(throughput) ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts index ee6317d6ac7..2ad56ab5d09 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts @@ -1,10 +1,70 @@ -import { describe, test, expect } from 'vitest' -import { DiskType } from './ui/DiskManagement.constants' +import { describe, expect, test } from 'vitest' + import { + calculateBaselineIopsForComputeSize, + calculateComputeSizeRequiredForIops, calculateDiskSizePrice, calculateIOPSPrice, + calculateMaxIopsForComputeSize, calculateThroughputPrice, + mapAddOnVariantIdToComputeSize, + mapComputeSizeNameToAddonVariantId, } from './DiskManagement.utils' +import { DiskType } from './ui/DiskManagement.constants' + +describe('DiskManagement utils', () => { + describe('mapComputeSizeNameToAddonVariantId', () => { + test('maps known infra sizes to addon variant ids', () => { + expect(mapComputeSizeNameToAddonVariantId('4xlarge')).toBe('ci_4xlarge') + }) + + test('falls back to nano for unknown infra sizes', () => { + // @ts-expect-error intentional invalid value for runtime guard + expect(mapComputeSizeNameToAddonVariantId('unknown-size')).toBe('ci_nano') + }) + }) + + describe('mapAddOnVariantIdToComputeSize', () => { + test('maps known addon ids to display names', () => { + expect(mapAddOnVariantIdToComputeSize('ci_4xlarge')).toBe('4XL') + }) + + test('falls back to Nano on invalid addon id', () => { + // @ts-expect-error intentional invalid value for runtime guard + expect(mapAddOnVariantIdToComputeSize('ci_invalid')).toBe('Nano') + }) + }) + + describe('calculateBaselineIopsForComputeSize / calculateMaxIopsForComputeSize', () => { + test('returns 0 for invalid compute ids', () => { + expect(calculateBaselineIopsForComputeSize('invalid')).toBe(0) + expect(calculateMaxIopsForComputeSize('invalid')).toBe(0) + }) + + test('returns baseline and max for valid compute ids', () => { + expect(calculateBaselineIopsForComputeSize('ci_2xlarge')).toBe(12000) + expect(calculateMaxIopsForComputeSize('ci_2xlarge')).toBe(20000) + }) + }) + + describe('calculateComputeSizeRequiredForIops', () => { + test('returns smallest size that satisfies requested IOPS', () => { + expect(calculateComputeSizeRequiredForIops(500)).toBe('ci_nano') + expect(calculateComputeSizeRequiredForIops(19000)).toBe('ci_large') + expect(calculateComputeSizeRequiredForIops(45000)).toBe('ci_12xlarge') + }) + + test('falls back to largest size when exceeding known max', () => { + const fallback = calculateComputeSizeRequiredForIops(500000) + expect([ + 'ci_48xlarge', + 'ci_48xlarge_optimized_cpu', + 'ci_48xlarge_optimized_memory', + 'ci_48xlarge_high_memory', + ]).toContain(fallback) + }) + }) +}) describe('DiskManagement.utils.ts:calculateDiskSizePrice', () => { test('GP3 with 8GB to GP3 with 10GB for pro plan', () => { diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts index a1040ca8b73..ecfac9c1bfa 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts @@ -1,18 +1,18 @@ import { ProjectDetail } from 'data/projects/project-detail-query' import { PlanId, ProjectAddonVariantMeta } from 'data/subscriptions/types' import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants' +import { + COMPUTE_BASELINE_IOPS, + COMPUTE_MAX_IOPS, + computeInstanceAddonVariantIdSchema, +} from 'shared-data' + import { ComputeInstanceAddonVariantId, ComputeInstanceSize, InfraInstanceSize, } from './DiskManagement.types' -import { - COMPUTE_BASELINE_IOPS, - DISK_LIMITS, - DISK_PRICING, - DiskType, - PLAN_DETAILS, -} from './ui/DiskManagement.constants' +import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './ui/DiskManagement.constants' // Included disk size only applies to primary, not replicas export const calculateDiskSizePrice = ({ @@ -159,31 +159,86 @@ export const calculateThroughputPrice = ({ return { oldPrice: '0.00', newPrice: '0.00' } } -export function getAvailableComputeOptions(availableAddons: any[], projectCloudProvider?: string) { - const computeOptions = - availableAddons - .find((addon) => addon.type === 'compute_instance') - ?.variants.filter((option: { [key: string]: any }) => { - if (!projectCloudProvider) { - return true - } +export type ComputeAddonVariant = { + identifier: ComputeInstanceAddonVariantId + name: string + price_description: string + price: number + price_interval: 'hourly' | 'monthly' + price_type: string + meta?: ProjectAddonVariantMeta +} - const meta = option.meta as ProjectAddonVariantMeta +type AvailableAddon = { + type: string + variants: Array<{ + identifier: string + name: string + price_description: string + price: number + price_interval: 'hourly' | 'monthly' + price_type: string + meta?: unknown + }> +} - return ( - !meta.supported_cloud_providers || - meta.supported_cloud_providers.includes(projectCloudProvider) - ) - }) ?? [] +const isProjectAddonVariantMeta = (meta: unknown): meta is ProjectAddonVariantMeta => { + if (typeof meta !== 'object' || meta === null) return false + + const obj = meta as Record + + // Validate supported_cloud_providers is an array if present (used at line 200) + if ('supported_cloud_providers' in obj && !Array.isArray(obj.supported_cloud_providers)) { + return false + } + + // Check for at least one expected property to ensure it's likely a real ProjectAddonVariantMeta + const hasExpectedProperty = + 'cpu_cores' in obj || + 'memory_gb' in obj || + 'cpu_dedicated' in obj || + 'baseline_disk_io_mbs' in obj || + 'max_disk_io_mbs' in obj || + 'connections_direct' in obj || + 'connections_pooler' in obj || + 'backup_duration_days' in obj || + 'supported_cloud_providers' in obj + + return hasExpectedProperty +} + +export function getAvailableComputeOptions( + availableAddons: AvailableAddon[], + projectCloudProvider?: string +) { + const computeAddon = availableAddons.find((addon) => addon.type === 'compute_instance') + const computeOptions: ComputeAddonVariant[] = + computeAddon?.variants.flatMap((option) => { + const parsedId = computeInstanceAddonVariantIdSchema.safeParse(option.identifier) + if (!parsedId.success) return [] + + if (projectCloudProvider && isProjectAddonVariantMeta(option.meta)) { + const isSupported = + !option.meta.supported_cloud_providers || + option.meta.supported_cloud_providers.includes(projectCloudProvider) + if (!isSupported) return [] + } + + return [ + { + ...option, + identifier: parsedId.data, + meta: isProjectAddonVariantMeta(option.meta) ? option.meta : undefined, + }, + ] + }) ?? [] function hasMicroOptionFromApi() { - return ( - availableAddons.find((addon) => addon.type === 'compute_instance')?.variants ?? [] - ).some((variant: any) => variant.identifier === 'ci_micro') + return (computeAddon?.variants ?? []).some((variant) => variant.identifier === 'ci_micro') } // Backwards comp until API is deployed - if (!hasMicroOptionFromApi) { + if (!hasMicroOptionFromApi()) { // Unshift to push to start of array computeOptions.unshift({ identifier: 'ci_micro', @@ -250,22 +305,42 @@ export const calculateIopsRequiredForThroughput = (throughput: number) => { return Math.max(125, Math.ceil(throughput / 0.256)) } -export const calculateMaxIopsAllowedForComputeSize = (computeSize: string): number => { - return COMPUTE_BASELINE_IOPS[computeSize as keyof typeof COMPUTE_BASELINE_IOPS] || 0 +export const calculateBaselineIopsForComputeSize = (computeSize: string): number => { + const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize) + if (!parsed.success) return 0 + return COMPUTE_BASELINE_IOPS[parsed.data] ?? 0 +} + +export const calculateMaxIopsForComputeSize = (computeSize: string): number => { + const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize) + if (!parsed.success) return 0 + return COMPUTE_MAX_IOPS[parsed.data] ?? 0 } export const calculateComputeSizeRequiredForIops = ( iops: number ): ComputeInstanceAddonVariantId | undefined => { - const computeSizes = Object.entries(COMPUTE_BASELINE_IOPS).sort((a, b) => a[1] - b[1]) - for (const [size, baselineIops] of computeSizes) { - if (iops <= baselineIops) { - return size as ComputeInstanceAddonVariantId + type ComputeSizeMax = { size: ComputeInstanceAddonVariantId; maxIops: number } + + const computeSizes: ComputeSizeMax[] = Object.entries(COMPUTE_MAX_IOPS) + .map((entry) => { + const [size, maxIops] = entry + const parsedSize = computeInstanceAddonVariantIdSchema.safeParse(size) + if (!parsedSize.success) return undefined + return { size: parsedSize.data, maxIops: Number(maxIops) } + }) + .filter((value): value is ComputeSizeMax => value !== undefined) + .sort((a, b) => a.maxIops - b.maxIops) + + for (const { size, maxIops } of computeSizes) { + if (iops <= maxIops) { + return size } } - // fallback to largest compute size - this should never happen though :-/ - return computeSizes[computeSizes.length - 1][0] as ComputeInstanceAddonVariantId + const fallbackSize = computeSizes[computeSizes.length - 1]?.size + if (!fallbackSize) return undefined + return fallbackSize } export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number | undefined => { @@ -291,57 +366,69 @@ export const formatComputeName = (compute: string) => { return compute.toUpperCase().replace('CI_', '') } +const infraToAddonVariant: Record = { + pico: 'ci_nano', + nano: 'ci_nano', + micro: 'ci_micro', + small: 'ci_small', + medium: 'ci_medium', + large: 'ci_large', + xlarge: 'ci_xlarge', + '2xlarge': 'ci_2xlarge', + '4xlarge': 'ci_4xlarge', + '8xlarge': 'ci_8xlarge', + '12xlarge': 'ci_12xlarge', + '16xlarge': 'ci_16xlarge', + '24xlarge': 'ci_24xlarge', + '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory', + '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu', + '24xlarge_high_memory': 'ci_24xlarge_high_memory', + '48xlarge': 'ci_48xlarge', + '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory', + '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu', + '48xlarge_high_memory': 'ci_48xlarge_high_memory', +} + +const isInfraInstanceSize = (value: string): value is InfraInstanceSize => + Object.prototype.hasOwnProperty.call(infraToAddonVariant, value) + export const mapComputeSizeNameToAddonVariantId = ( computeSize: ProjectDetail['infra_compute_size'] ): ComputeInstanceAddonVariantId => { - return { - pico: 'ci_pico', - nano: 'ci_nano', - micro: 'ci_micro', - small: 'ci_small', - medium: 'ci_medium', - large: 'ci_large', - xlarge: 'ci_xlarge', - '2xlarge': 'ci_2xlarge', - '4xlarge': 'ci_4xlarge', - '8xlarge': 'ci_8xlarge', - '12xlarge': 'ci_12xlarge', - '16xlarge': 'ci_16xlarge', - '24xlarge': 'ci_24xlarge', - '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory', - '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu', - '24xlarge_high_memory': 'ci_24xlarge_high_memory', - '48xlarge': 'ci_48xlarge', - '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory', - '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu', - '48xlarge_high_memory': 'ci_48xlarge_high_memory', - }[computeSize ?? 'nano'] as ComputeInstanceAddonVariantId + const fallback: InfraInstanceSize = 'nano' + const matchedSize = computeSize && isInfraInstanceSize(computeSize) ? computeSize : undefined + const sizeKey = matchedSize ?? fallback + return infraToAddonVariant[sizeKey] +} + +const addonVariantToComputeSize: Record = { + ci_nano: 'Nano', + ci_micro: 'Micro', + ci_small: 'Small', + ci_medium: 'Medium', + ci_large: 'Large', + ci_xlarge: 'XL', + ci_2xlarge: '2XL', + ci_4xlarge: '4XL', + ci_8xlarge: '8XL', + ci_12xlarge: '12XL', + ci_16xlarge: '16XL', + ci_24xlarge: '24XL', + ci_24xlarge_optimized_memory: '24XL - Optimized Memory', + ci_24xlarge_optimized_cpu: '24XL - Optimized CPU', + ci_24xlarge_high_memory: '24XL - High Memory', + ci_48xlarge: '48XL', + ci_48xlarge_optimized_memory: '48XL - Optimized Memory', + ci_48xlarge_optimized_cpu: '48XL - Optimized CPU', + ci_48xlarge_high_memory: '48XL - High Memory', } export const mapAddOnVariantIdToComputeSize = ( addonVariantId: ComputeInstanceAddonVariantId = 'ci_nano' ): ComputeInstanceSize => { - return { - ci_nano: 'Nano', - ci_micro: 'Micro', - ci_small: 'Small', - ci_medium: 'Medium', - ci_large: 'Large', - ci_xlarge: 'XL', - ci_2xlarge: '2XL', - ci_4xlarge: '4XL', - ci_8xlarge: '8XL', - ci_12xlarge: '12XL', - ci_16xlarge: '16XL', - ci_24xlarge: '24XL', - ci_24xlarge_optimized_memory: '24XL - Optimized Memory', - ci_24xlarge_optimized_cpu: '24XL - Optimized CPU', - ci_24xlarge_high_memory: '24XL - High Memory', - ci_48xlarge: '48XL', - ci_48xlarge_optimized_memory: '48XL - Optimized Memory', - ci_48xlarge_optimized_cpu: '48XL - Optimized CPU', - ci_48xlarge_high_memory: '48XL - High Memory', - }[addonVariantId] as ComputeInstanceSize + const parsed = computeInstanceAddonVariantIdSchema.safeParse(addonVariantId) + if (!parsed.success) return addonVariantToComputeSize.ci_nano + return addonVariantToComputeSize[parsed.data] } export const formatNumber = (num: number): string => { diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 4e5aa479707..29fdebcb6cd 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -140,7 +140,7 @@ export function DiskManagementForm() { provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, - computeSize, + computeSize: computeSize ?? 'ci_micro', growthPercent: growth_percent, minIncrementGb: min_increment_gb, maxSizeGb: max_size_gb, @@ -167,6 +167,7 @@ export function DiskManagementForm() { provisionedIOPS: iops, throughput: throughput_mbps, totalSize: size_gb, + computeSize: form.getValues('computeSize'), } if (!('requested_modification' in data)) { @@ -212,7 +213,9 @@ export function DiskManagementForm() { const usedPercentage = (usedSize / totalSize) * 100 const disableIopsThroughputConfig = - !isSpendCapEnabled && RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize) + modifiedComputeSize && + !isSpendCapEnabled && + RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize) const isBranch = project?.parent_project_ref !== undefined @@ -469,7 +472,7 @@ export function DiskManagementForm() {
disabled?: boolean @@ -170,7 +166,7 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { ) : ( <> - {availableOptions.map((compute: ComputeOption) => { + {availableOptions.map((compute) => { const cpuArchitecture = getCloudProviderArchitecture(project?.cloud_provider) const lockedMicroDueToPITR = @@ -187,10 +183,21 @@ export function ComputeSizeField({ form, disabled }: ComputeSizeFieldProps) { project?.infra_compute_size === 'nano' && compute.identifier === 'ci_nano' ? availableOptions.find( - (option: ComputeOption) => option.identifier === 'ci_micro' + (option: ComputeAddonVariant) => option.identifier === 'ci_micro' )?.price : compute.price + const cpuLabel = (() => { + const cpuCores = compute.meta?.cpu_cores + if (typeof cpuCores === 'number') { + return `${cpuCores}-core ${cpuArchitecture} CPU` + } + if (cpuCores) { + return `${cpuCores} CPU` + } + return 'CPU' + })() + return ( - - {compute.meta?.cpu_cores ?? 0} - {compute.meta?.cpu_cores !== 'Shared' && - `-core ${cpuArchitecture}`}{' '} - CPU - + {cpuLabel}
diff --git a/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeRecommendationSection.tsx b/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeRecommendationSection.tsx index b07bcc22ae8..40aa9d94bbf 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeRecommendationSection.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/ComputeSizeRecommendationSection.tsx @@ -6,13 +6,11 @@ import { Admonition } from 'ui-patterns/admonition' import { DiskStorageSchemaType } from '../DiskManagement.schema' import { calculateComputeSizeRequiredForIops, - calculateMaxIopsAllowedForComputeSize, + calculateMaxIopsForComputeSize, mapAddOnVariantIdToComputeSize, } from '../DiskManagement.utils' -import { - COMPUTE_BASELINE_IOPS, - RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3, -} from './DiskManagement.constants' +import { COMPUTE_BASELINE_IOPS, COMPUTE_MAX_IOPS } from 'shared-data' +import { RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from './DiskManagement.constants' interface ComputeSizeRecommendationSectionProps { actions?: ReactNode @@ -28,9 +26,10 @@ export function ComputeSizeRecommendationSection({ const iops = watch('provisionedIOPS') const computeSizeRecommendedForIops = calculateComputeSizeRequiredForIops(iops) - const maxIOPSforComputeSize = calculateMaxIopsAllowedForComputeSize(computeSize ?? 'ci_micro') + const maxIopsForComputeSize = calculateMaxIopsForComputeSize(computeSize ?? 'ci_micro') const isVisible = - iops > maxIOPSforComputeSize && !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize) + iops > maxIopsForComputeSize && + !RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3.includes(computeSize ?? 'ci_micro') return ( @@ -43,12 +42,12 @@ export function ComputeSizeRecommendationSection({ >

- To achieve sustained IOPS performance we recommend using the{' '} + To achieve higher IOPS performance we recommend using the{' '} {mapAddOnVariantIdToComputeSize(computeSizeRecommendedForIops)} {' '} diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx index b48b48c8bb0..4850a28837d 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx @@ -60,93 +60,6 @@ export const PLAN_DETAILS: Record = { platform: { includedDiskGB: { gp3: 8, io2: 0 } }, } -export const COMPUTE_BASELINE_IOPS = { - ci_nano: 250, - ci_micro: 500, - ci_small: 1000, - ci_medium: 2000, - ci_large: 3600, - ci_xlarge: 6000, - ci_2xlarge: 12000, - ci_4xlarge: 20000, - ci_8xlarge: 40000, - ci_12xlarge: 50000, - ci_16xlarge: 80000, - ci_24xlarge: 120000, - ci_24xlarge_optimized_cpu: 120000, - ci_24xlarge_optimized_memory: 120000, - ci_24xlarge_high_memory: 120000, - ci_48xlarge: 240000, - ci_48xlarge_optimized_cpu: 240000, - ci_48xlarge_optimized_memory: 240000, - ci_48xlarge_high_memory: 240000, -} -export const COMPUTE_MAX_IOPS = { - ci_nano: 11800, - ci_micro: 11800, - ci_small: 11800, - ci_medium: 11800, - ci_large: 20000, - ci_xlarge: 20000, - ci_2xlarge: 20000, - ci_4xlarge: 20000, - ci_8xlarge: 40000, - ci_12xlarge: 50000, - ci_16xlarge: 80000, - ci_24xlarge: 120000, - ci_24xlarge_optimized_cpu: 120000, - ci_24xlarge_optimized_memory: 120000, - ci_24xlarge_high_memory: 120000, - ci_48xlarge: 240000, - ci_48xlarge_optimized_cpu: 240000, - ci_48xlarge_optimized_memory: 240000, - ci_48xlarge_high_memory: 240000, -} - -export const COMPUTE_BASELINE_THROUGHPUT = { - ci_nano: 43, - ci_micro: 87, - ci_small: 174, - ci_medium: 347, - ci_large: 630, - ci_xlarge: 1188, - ci_2xlarge: 2375, - ci_4xlarge: 4750, - ci_8xlarge: 9500, - ci_12xlarge: 14250, - ci_16xlarge: 19000, - ci_24xlarge: 30000, - ci_24xlarge_optimized_cpu: 30000, - ci_24xlarge_optimized_memory: 30000, - ci_24xlarge_high_memory: 30000, - ci_48xlarge: 40000, - ci_48xlarge_optimized_cpu: 40000, - ci_48xlarge_optimized_memory: 40000, - ci_48xlarge_high_memory: 40000, -} - -export const COMPUTE_MAX_THROUGHPUT = { - ci_nano: 2085, - ci_micro: 2085, - ci_small: 2085, - ci_medium: 2085, - ci_large: 4750, - ci_xlarge: 4750, - ci_2xlarge: 4750, - ci_4xlarge: 4750, - ci_8xlarge: 9500, - ci_12xlarge: 14250, - ci_16xlarge: 19000, - ci_24xlarge: 30000, - ci_24xlarge_optimized_cpu: 30000, - ci_24xlarge_optimized_memory: 30000, - ci_24xlarge_high_memory: 30000, - ci_48xlarge: 40000, - ci_48xlarge_optimized_cpu: 40000, - ci_48xlarge_optimized_memory: 40000, - ci_48xlarge_high_memory: 40000, -} - export const RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 = ['ci_nano', 'ci_micro', 'ci_small', 'ci_medium'] export const RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3 = [ diff --git a/apps/studio/data/config/disk-attributes-update-mutation.ts b/apps/studio/data/config/disk-attributes-update-mutation.ts index 9a090d9e2a2..bb61a3ebfcc 100644 --- a/apps/studio/data/config/disk-attributes-update-mutation.ts +++ b/apps/studio/data/config/disk-attributes-update-mutation.ts @@ -11,7 +11,7 @@ export type UpdateDiskAttributesVariables = { ref?: string storageType: 'gp3' | 'io2' provisionedIOPS: number - throughput?: number + throughput?: number // MB/s totalSize: number } @@ -25,6 +25,8 @@ export async function updateDiskAttributes(updates: UpdateDiskAttributesVariable updates.storageType === 'gp3' ? { iops: updates.provisionedIOPS, + // API field name is throughput_mbps (legacy), but values are treated as MB/s in the platform API. + // If the platform switches to MiB/s, convert here before sending. throughput_mbps: updates.throughput!, size_gb: updates.totalSize, type: updates.storageType, diff --git a/packages/shared-data/compute-disk-limits.ts b/packages/shared-data/compute-disk-limits.ts new file mode 100644 index 00000000000..4d5597f4e84 --- /dev/null +++ b/packages/shared-data/compute-disk-limits.ts @@ -0,0 +1,180 @@ +import { z } from 'zod' + +/** + * [Jordi] Use instances.vantage.sh as source of truth for compute disk limits. + * Eg: https://instances.vantage.sh/aws/ec2/t4g.nano?currency=USD + * + * All compute from medium down are t4g and the bigger ones are m6g. + * + * Throughput values are stored in MB/s (AWS lists Mbps, so we convert with toMBps). + */ + +export type ComputeDiskLimit = { + name: string + baselineIops: number + maxIops: number + baselineThroughputMBps: number + maxThroughputMBps: number +} + +const toMBps = (throughputMbps: number) => Math.round(throughputMbps / 8) + +export const COMPUTE_DISK = { + ci_nano: { + name: 'Nano (free)', + baselineIops: 250, + maxIops: 11800, + baselineThroughputMBps: toMBps(43), + maxThroughputMBps: toMBps(2085), + }, + ci_micro: { + name: 'Micro', + baselineIops: 500, + maxIops: 11800, + baselineThroughputMBps: toMBps(87), + maxThroughputMBps: toMBps(2085), + }, + ci_small: { + name: 'Small', + baselineIops: 1000, + maxIops: 11800, + baselineThroughputMBps: toMBps(174), + maxThroughputMBps: toMBps(2085), + }, + ci_medium: { + name: 'Medium', + baselineIops: 2000, + maxIops: 11800, + baselineThroughputMBps: toMBps(347), + maxThroughputMBps: toMBps(2085), + }, + ci_large: { + name: 'Large', + baselineIops: 3600, + maxIops: 20000, + baselineThroughputMBps: toMBps(630), + maxThroughputMBps: toMBps(4750), + }, + ci_xlarge: { + name: 'XL', + baselineIops: 6000, + maxIops: 20000, + baselineThroughputMBps: toMBps(1188), + maxThroughputMBps: toMBps(4750), + }, + ci_2xlarge: { + name: '2XL', + baselineIops: 12000, + maxIops: 20000, + baselineThroughputMBps: toMBps(2375), + maxThroughputMBps: toMBps(4750), + }, + ci_4xlarge: { + name: '4XL', + baselineIops: 20000, + maxIops: 20000, + baselineThroughputMBps: toMBps(4750), + maxThroughputMBps: toMBps(4750), + }, + ci_8xlarge: { + name: '8XL', + baselineIops: 40000, + maxIops: 40000, + baselineThroughputMBps: toMBps(9500), + maxThroughputMBps: toMBps(9500), + }, + ci_12xlarge: { + name: '12XL', + baselineIops: 50000, + maxIops: 50000, + baselineThroughputMBps: toMBps(14250), + maxThroughputMBps: toMBps(14250), + }, + ci_16xlarge: { + name: '16XL', + baselineIops: 80000, + maxIops: 80000, + baselineThroughputMBps: toMBps(19000), + maxThroughputMBps: toMBps(19000), + }, + ci_24xlarge: { + name: '24XL', + baselineIops: 120000, + maxIops: 120000, + baselineThroughputMBps: toMBps(30000), + maxThroughputMBps: toMBps(30000), + }, + ci_24xlarge_optimized_cpu: { + name: '24XL - Optimized CPU', + baselineIops: 120000, + maxIops: 120000, + baselineThroughputMBps: toMBps(30000), + maxThroughputMBps: toMBps(30000), + }, + ci_24xlarge_optimized_memory: { + name: '24XL - Optimized Memory', + baselineIops: 120000, + maxIops: 120000, + baselineThroughputMBps: toMBps(30000), + maxThroughputMBps: toMBps(30000), + }, + ci_24xlarge_high_memory: { + name: '24XL - High Memory', + baselineIops: 120000, + maxIops: 120000, + baselineThroughputMBps: toMBps(30000), + maxThroughputMBps: toMBps(30000), + }, + ci_48xlarge: { + name: '48XL', + baselineIops: 240000, + maxIops: 240000, + baselineThroughputMBps: toMBps(40000), + maxThroughputMBps: toMBps(40000), + }, + ci_48xlarge_optimized_cpu: { + name: '48XL - Optimized CPU', + baselineIops: 240000, + maxIops: 240000, + baselineThroughputMBps: toMBps(40000), + maxThroughputMBps: toMBps(40000), + }, + ci_48xlarge_optimized_memory: { + name: '48XL - Optimized Memory', + baselineIops: 240000, + maxIops: 240000, + baselineThroughputMBps: toMBps(40000), + maxThroughputMBps: toMBps(40000), + }, + ci_48xlarge_high_memory: { + name: '48XL - High Memory', + baselineIops: 240000, + maxIops: 240000, + baselineThroughputMBps: toMBps(40000), + maxThroughputMBps: toMBps(40000), + }, +} as const satisfies Record + +export type ComputeDiskKey = keyof typeof COMPUTE_DISK + +export const COMPUTE_BASELINE_IOPS = Object.fromEntries( + Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.baselineIops]) +) + +export const COMPUTE_MAX_IOPS = Object.fromEntries( + Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.maxIops]) +) + +export const COMPUTE_BASELINE_THROUGHPUT = Object.fromEntries( + Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.baselineThroughputMBps]) +) + +export const COMPUTE_MAX_THROUGHPUT = Object.fromEntries( + Object.entries(COMPUTE_DISK).map(([key, value]) => [key, value.maxThroughputMBps]) +) + +const computeInstanceAddonVariantIds = Object.keys(COMPUTE_DISK) as ComputeDiskKey[] + +export const computeInstanceAddonVariantIdSchema = z.enum( + computeInstanceAddonVariantIds as [ComputeDiskKey, ...ComputeDiskKey[]] +) diff --git a/packages/shared-data/index.ts b/packages/shared-data/index.ts index 9f334c453e8..c1af4621d10 100644 --- a/packages/shared-data/index.ts +++ b/packages/shared-data/index.ts @@ -1,4 +1,12 @@ import config from './config' +import { + COMPUTE_BASELINE_IOPS, + COMPUTE_BASELINE_THROUGHPUT, + COMPUTE_DISK, + COMPUTE_MAX_IOPS, + COMPUTE_MAX_THROUGHPUT, + computeInstanceAddonVariantIdSchema, +} from './compute-disk-limits' import extensions from './extensions.json' import logConstants from './logConstants' import { plans, PricingInformation } from './plans' @@ -11,6 +19,12 @@ import tweets, { topTweets } from './tweets' export { AWS_REGIONS, + COMPUTE_BASELINE_IOPS, + COMPUTE_BASELINE_THROUGHPUT, + COMPUTE_DISK, + COMPUTE_MAX_IOPS, + COMPUTE_MAX_THROUGHPUT, + computeInstanceAddonVariantIdSchema, config, extensions, FLY_REGIONS, diff --git a/packages/shared-data/package.json b/packages/shared-data/package.json index 9aacccfae9e..2f94d2e9323 100644 --- a/packages/shared-data/package.json +++ b/packages/shared-data/package.json @@ -12,5 +12,8 @@ "tsconfig": "workspace:" }, "author": "", - "license": "MIT" + "license": "MIT", + "dependencies": { + "zod": "catalog:" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa8623e41db..64603779563 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,7 +61,7 @@ catalogs: specifier: ^3.2.0 version: 3.2.4 zod: - specifier: ^3.25.76 + specifier: 3.25.76 version: 3.25.76 overrides: @@ -2257,6 +2257,10 @@ importers: version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) packages/shared-data: + dependencies: + zod: + specifier: 'catalog:' + version: 3.25.76 devDependencies: tsconfig: specifier: 'workspace:' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 058cc24dc7f..408e1a88026 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,7 +25,7 @@ catalog: valtio: ^1.12.0 vite: ^7.1.11 vitest: ^3.2.0 - zod: ^3.25.76 + zod: 3.25.76 ignoredBuiltDependencies: - '@parcel/watcher'