diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..1472c69 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,37 @@ +name: development +type: docker +kind: pipeline + +volumes: +- name: bun_cache + host: + path: /var/lib/cache/docker/bun +- name: node_modules + host: + path: /var/lib/cache/docker/node_modules/pgtune +- name: dist + host: + path: /var/lib/docker/volumes/pgtune/_data + +steps: + - name: build and deploy static + image: dockerhub.timeweb.cloud/oven/bun:alpine + volumes: + - name: bun_cache + path: /root/.bun/install/cache + - name: node_modules + path: /drone/src/node_modules + - name: dist + path: /dist + commands: + - apk add --no-cache rsync + - bun install --frozen_lockfile + - bun --bun run build + - rsync -cavhP --delete-after ./dist/ /dist/ + +image_pull_secrets: + - GiteaRegistryConfig + +trigger: + branch: + - develop diff --git a/.gitignore b/.gitignore index 56c6038..bda0a99 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ dist-ssr # Auto-generated type declarations auto-imports.d.ts components.d.ts + +dev-dist + +pgtune.tar.zst \ No newline at end of file diff --git a/README.md b/README.md index 1511959..7ba6a76 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ -# Vue 3 + Vite +# [PgTune](https://pgtune.mudrov.tech/) -This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` diff --git a/src/components/configurationFormView.vue b/src/components/configurationFormView.vue new file mode 100644 index 0000000..0aabe3e --- /dev/null +++ b/src/components/configurationFormView.vue @@ -0,0 +1,149 @@ + + + diff --git a/src/components/defaultDescription.vue b/src/components/defaultDescription.vue index 5361bbe..28581b3 100644 --- a/src/components/defaultDescription.vue +++ b/src/components/defaultDescription.vue @@ -1,52 +1,60 @@ diff --git a/src/pages/index.vue b/src/pages/index.vue index ea03bca..b0bb6fd 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,14 +1,37 @@ diff --git a/router.js b/src/router.js similarity index 100% rename from router.js rename to src/router.js diff --git a/src/stores/useConfigurationStore.js b/src/stores/useConfigurationStore.js deleted file mode 100644 index 1a73645..0000000 --- a/src/stores/useConfigurationStore.js +++ /dev/null @@ -1,479 +0,0 @@ -import { defineStore } from 'pinia' -import { reactive, computed } from 'vue' -import { useSettingsStore } from './useSettingsStore' -import { - DEFAULT_DB_VERSION, - OS_LINUX, - OS_WINDOWS, - OS_MAC, - DB_TYPE_WEB, - DB_TYPE_OLTP, - DB_TYPE_DW, - DB_TYPE_DESKTOP, - DB_TYPE_MIXED, - SIZE_UNIT_GB, - HARD_DRIVE_SSD, - HARD_DRIVE_HDD, - HARD_DRIVE_SAN, - HARD_DRIVE_NVME, - DB_SIZE_LESS_RAM, - DB_SIZE_MID_RAM, - DB_SIZE_GREATER_RAM, -} from '/src/utils/constants/configuration.js' - -const SIZE_UNIT_MAP = { - KB: 1024, - MB: 1048576, - GB: 1073741824, - TB: 1099511627776, - PB: 1125899906842624, -} - -const DEFAULT_DB_SETTINGS = { - default: { - ['max_worker_processes']: 8, - ['max_parallel_workers_per_gather']: 2, - ['max_parallel_workers']: 8, - }, -} - -const initialState = { - dbVersion: DEFAULT_DB_VERSION, - osType: OS_LINUX, - dbType: DB_TYPE_WEB, - totalMemory: null, - totalMemoryUnit: SIZE_UNIT_GB, - cpuNum: null, - connectionNum: null, - hdType: HARD_DRIVE_SSD, - dbSize: DB_SIZE_MID_RAM, -} - -const selectCheckpointCompletionTarget = 0.9 - -export const useConfigurationStore = defineStore('configuration', () => { - const state = reactive({ ...initialState }) - - const getConfiguration = computed(() => ({ ...state })) - const getDBVersion = computed(() => getConfiguration.value.dbVersion) - const getOSType = computed(() => getConfiguration.value.osType) - const getDBType = computed(() => getConfiguration.value.dbType) - const getTotalMemory = computed(() => getConfiguration.value.totalMemory) - const getTotalMemoryUnit = computed( - () => getConfiguration.value.totalMemoryUnit, - ) - const getCPUNum = computed(() => getConfiguration.value.cpuNum || null) - const getConnectionNum = computed( - () => getConfiguration.value.connectionNum || null, - ) - const getHDType = computed(() => getConfiguration.value.hdType) - const getDBSize = computed(() => getConfiguration.value.dbSize) - const getTotalMemoryInBytes = computed( - () => getTotalMemory.value * SIZE_UNIT_MAP[getTotalMemoryUnit.value], - ) - const getTotalMemoryInKb = computed( - () => getTotalMemoryInBytes.value / SIZE_UNIT_MAP['KB'], - ) - const getDBDefaultValues = computed( - () => DEFAULT_DB_SETTINGS[getDBVersion] || DEFAULT_DB_VERSION.default, - ) - const getIsConfigured = computed(() => !!getTotalMemory.value) - const getMaxConnections = computed( - () => - getConnectionNum.value ?? - { - [DB_TYPE_WEB]: 200, - [DB_TYPE_OLTP]: 300, - [DB_TYPE_DW]: 40, - [DB_TYPE_DESKTOP]: 20, - [DB_TYPE_MIXED]: 100, - }[getDBType.value], - ) - const getSharedBuffers = computed(() => { - const totalMemoryKb = getTotalMemoryInKb.value - - const val = { - [DB_TYPE_WEB]: Math.floor(totalMemoryKb / 4), - [DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 4), - [DB_TYPE_DW]: Math.floor(totalMemoryKb / 4), - [DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16), - [DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 4), - }[getDBType.value] - - return getDBVersion.value < 10 && OS_WINDOWS === getOSType.value - ? Math.min(val, (512 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB']) - : val - }) - const getHugePages = computed(() => - getOSType.value !== OS_MAC && - getSharedBuffers.value >= (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - ? 'try' - : 'off', - ) - const getEffectiveCacheSize = computed(() => { - const totalMemoryKb = getTotalMemoryInKb.value - - return { - [DB_TYPE_WEB]: Math.floor((totalMemoryKb * 3) / 4), - [DB_TYPE_OLTP]: Math.floor((totalMemoryKb * 3) / 4), - [DB_TYPE_DW]: Math.floor((totalMemoryKb * 3) / 4), - [DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 4), - [DB_TYPE_MIXED]: Math.floor((totalMemoryKb * 3) / 4), - }[getDBType.value] - }) - const getMaintenanceWorkMem = computed(() => { - const totalMemoryKb = getTotalMemoryInKb.value - // 1. Вычисляем базовое значение по типу БД - const baseMemValue = { - [DB_TYPE_WEB]: Math.floor(totalMemoryKb / 16), - [DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 16), - [DB_TYPE_DW]: Math.floor(totalMemoryKb / 8), - [DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16), - [DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 16), - }[getDBType.value] - - // 2. Определяем, действует ли строгое ограничение для Windows - const isLegacyWindows = - OS_WINDOWS === getOSType.value && getDBVersion.value <= 17 - - // 3. Задаем лимит памяти (2GB для старых Windows, иначе 8GB) - const maxLimitGb = isLegacyWindows ? 2 : 8 - const memoryLimit = (maxLimitGb * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - - // 4. Если превышен лимит на Windows, вычитаем 1MB, иначе просто возвращаем минимум - return baseMemValue >= memoryLimit && isLegacyWindows - ? memoryLimit - (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] - : Math.min(baseMemValue, memoryLimit) - }) - const getCheckpointSegment = computed(() => [ - { - key: 'min_wal_size', - value: { - [DB_TYPE_WEB]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_OLTP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_DW]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_DESKTOP]: (100 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_MIXED]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - }[getDBType.value], - }, - { - key: 'max_wal_size', - value: { - [DB_TYPE_WEB]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_OLTP]: (8192 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_DW]: (16384 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_DESKTOP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - [DB_TYPE_MIXED]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], - }[getDBType.value], - }, - ]) - const getWalBuffers = computed(() => { - const KB_IN_MB = SIZE_UNIT_MAP['MB'] / SIZE_UNIT_MAP['KB'] - const baseWal = Math.floor((3 * sharedBuffersValue) / 100) - - return Math.max( - 32, - baseWal > 14 * KB_IN_MB - ? 16 * KB_IN_MB - : Math.min(baseWal, 16 * KB_IN_MB), - ) - }) - const getDefaultStatisticsTarget = computed( - () => - ({ - [DB_TYPE_WEB]: 100, - [DB_TYPE_OLTP]: 100, - [DB_TYPE_DW]: 500, - [DB_TYPE_DESKTOP]: 100, - [DB_TYPE_MIXED]: 100, - })[getDBType.value], - ) - const getRandomPageCost = computed(() => { - // Если база помещается в RAM, поиск на диске не важен - if (getDBSize.value === DB_SIZE_LESS_RAM) return 1.1 - - // HDD или аналитические БД (DW), не помещающиеся в RAM, требуют дефолтной стоимости (4.0) - if (getHDType.value === HARD_DRIVE_HDD || getDBType.value === DB_TYPE_DW) - return 4 - - return 1.1 - }) - const getEffectiveIOConcurrency = computed(() => - getOSType.value === OS_LINUX - ? { - [HARD_DRIVE_HDD]: 2, - [HARD_DRIVE_SSD]: 200, - [HARD_DRIVE_SAN]: 300, - [HARD_DRIVE_NVME]: 1000, - }[getHDType.value] - : null, - ) - const getParallelSettings = computed(() => { - const cpuNum = getCPUNum.value - const dbVersion = getDBVersion.value - const dbType = getDBType.value - - if (!cpuNum || cpuNum < 4) return [] - - const halfCpu = Math.ceil(cpuNum / 2) - const cappedWorkers = Math.min(4, halfCpu) // Ограничение в 4 воркера - - const config = [ - { key: 'max_worker_processes', value: cpuNum }, - { - key: 'max_parallel_workers_per_gather', - value: dbType !== DB_TYPE_DW ? cappedWorkers : halfCpu, - }, - ...(dbVersion >= 10 - ? [{ key: 'max_parallel_workers', value: cpuNum }] - : []), - ...(dbVersion >= 11 - ? [{ key: 'max_parallel_maintenance_workers', value: cappedWorkers }] - : []), - ] - - return config - }) - const getWorkMem = computed(() => { - const dbDefaultValues = getDBDefaultValues.value - - const maxWorkerProcesses = getParallelSettings.value.find( - (param) => param['key'] === 'max_worker_processes', - ) - - const parallelForWorkMem = - maxWorkerProcesses?.value > 0 - ? maxWorkerProcesses.value - : dbDefaultValues['max_worker_processes'] > 0 - ? dbDefaultValues['max_worker_processes'] - : 1 - - // Базовая формула расчёта work_mem - const baseWorkMem = - (getTotalMemoryInKb.value - sharedBuffersValue) / - ((maxConnectionsValue + parallelForWorkMem) * 3) - - // Коэффициенты для разных типов БД - const dbTypeCoefficients = { - [DB_TYPE_WEB]: 1, - [DB_TYPE_OLTP]: 1, - [DB_TYPE_DW]: 0.5, - [DB_TYPE_DESKTOP]: 1 / 6, - [DB_TYPE_MIXED]: 0.5, - } - - // Базовая формула с коэффициентом типа БД - const baseWorkMemResult = Math.floor( - baseWorkMem * dbTypeCoefficients[dbType], - ) - - // Корректировка в зависимости от размера БД относительно RAM - const sizeAdjustedWorkMem = - dbSize === DB_SIZE_LESS_RAM - ? Math.floor(baseWorkMemResult * 1.3) - : dbSize === DB_SIZE_GREATER_RAM - ? Math.floor(baseWorkMemResult * 0.9) - : baseWorkMemResult - - // Применяем минимальное значение 4 MB - const minWorkMem = 4 * 1024 // 4 MB в KB - const workMemWithMin = Math.max(sizeAdjustedWorkMem, minWorkMem) - - // Применяем ограничение для Windows 64-bit до PostgreSQL 17 - const winMemoryLimit = 2 * 1024 * 1024 - 1024 // ~2GB - 1MB в KB - const workMemResult = - osType === OS_WINDOWS && dbVersion <= 17 - ? Math.min(workMemWithMin, winMemoryLimit) - : workMemWithMin - - return workMemResult - }) - const getWalLevel = computed(() => - getDBType.value === DB_TYPE_DESKTOP - ? [ - { - key: 'wal_level', - value: 'minimal', - }, - // max_wal_senders must be 0 when wal_level=minimal - { - key: 'max_wal_senders', - value: '0', - }, - ] - : [], - ) - const getJit = computed(() => - getDBVersion.value >= 12 && - [DB_TYPE_WEB, DB_TYPE_OLTP, DB_TYPE_MIXED].includes(getDBType.value) - ? 'off' - : null, - ) - const getWalCompression = computed(() => { - const ver = getDBVersion.value - - return ver >= 15 ? 'lz4' : ver >= 10 ? 'on' : null - }) - const getAutovacuumMaxWorkers = computed(() => { - const cpuNum = getCPUNum.value - - if (!cpuNum) return null - if (cpuNum >= 32) return 5 - if (cpuNum >= 16) return 4 - return null - }) - const getAutovacuumWorkMem = computed(() => { - const threshold = (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - - // Windows 64-bit has a strict 2GB limit up to PostgreSQL 17 - const winMemoryLimit = - (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - - (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] - - const isWindowsWithLowLimit = - getOSType.value === OS_WINDOWS && getDBVersion.value <= 17 - - if (getMaintenanceWorkMem.value < threshold) { - return null - } - - if (isWindowsWithLowLimit) { - return Math.min(threshold, winMemoryLimit) - } - - return threshold - }) - const getIOMethod = computed(() => - getDBVersion.value < 18 - ? null - : getOSType.value === OS_LINUX - ? 'io_uring' - : 'worker', - ) - const getIOWorkers = computed(() => { - if (dbVersion < 18 || !cpuNum || ioMethod === 'io_uring') return null - - return Math.max(4, Math.min(32, Math.floor(cpuNum / 4))) || null - }) - const getWarningInfoMessages = computed(() => { - const warnings = [] - const totalMemory = getTotalMemoryInBytes.value - - // Memory warnings - if (totalMemory < 256 * SIZE_UNIT_MAP['MB']) { - warnings.push('this tool not being optimal', 'for low memory systems') - } else if (totalMemory > 100 * SIZE_UNIT_MAP['GB']) { - warnings.push( - 'this tool not being optimal', - 'for very high memory systems', - ) - } - - // Advanced features compilation warnings - if (getWalCompression.value === 'lz4') { - if (warnings.length > 0) warnings.push('') - - warnings.push( - 'wal_compression = lz4 requires PostgreSQL', - 'to be compiled with --with-lz4', - ) - } - - if (getIOMethod.value === 'io_uring') { - if (warnings.length > 0) warnings.push('') - - warnings.push( - 'io_method = io_uring requires PostgreSQL', - 'to be compiled with --with-liburing', - ) - } - - // I/O Cost Warning for Analytical DBs - if ( - getDBType.value === DB_TYPE_DW && - getHDType.value !== HARD_DRIVE_HDD && - getDBSize.value !== DB_SIZE_LESS_RAM - ) { - const driveName = { - [HARD_DRIVE_SSD]: 'SSDs', - [HARD_DRIVE_NVME]: 'NVMe drives', - [HARD_DRIVE_SAN]: 'SAN storage', - }[hdType] - - if (warnings.length > 0) warnings.push('') - - warnings.push( - `Cost parameters for Data Warehouses on ${driveName} are left at defaults`, - 'to avoid catastrophic index scan selections', - 'Monitor query planner behavior and adjust random_page_cost if necessary', - ) - } - - return warnings.length > 0 ? ['WARNING', ...warnings] : [] - }) - - function setState(payload) { - const settingsStore = useSettingsStore() - - state.value = { - dbVersion: parseFloat(payload.dbVersion), - osType: payload.osType, - dbType: payload.dbType, - totalMemory: parseInt(payload.totalMemory, 10), - totalMemoryUnit: payload.totalMemoryUnit, - cpuNum: payload.cpuNum ? parseInt(payload.cpuNum, 10) : null, - connectionNum: payload.connectionNum - ? parseInt(payload.connectionNum, 10) - : null, - hdType: payload.hdType, - dbSize: payload.dbSize, - } - - settingsStore.onSubmitConfiguration() - } - - const $reset = () => ({ ...initialState }) - - return { - state, - - // Getters - getDBVersion, - getOSType, - getDBType, - getTotalMemory, - getTotalMemoryUnit, - getCPUNum, - getConnectionNum, - getHDType, - getDBSize, - getTotalMemoryInBytes, - getTotalMemoryInKb, - getDBDefaultValues, - getIsConfigured, - getMaxConnections, - getSharedBuffers, - getHugePages, - getEffectiveCacheSize, - getMaintenanceWorkMem, - getCheckpointSegment, - getWalBuffers, - getDefaultStatisticsTarget, - getRandomPageCost, - getEffectiveIOConcurrency, - getParallelSettings, - getWorkMem, - getWalLevel, - getJit, - getWalCompression, - getAutovacuumMaxWorkers, - getAutovacuumWorkMem, - getIOMethod, - getIOWorkers, - getWarningInfoMessages, - - // Actions - setState, - $reset, - } -}) diff --git a/src/stores/useSettingsStore.js b/src/stores/useSettingsStore.js deleted file mode 100644 index 4abfa2d..0000000 --- a/src/stores/useSettingsStore.js +++ /dev/null @@ -1,42 +0,0 @@ -import { defineStore } from 'pinia' -import { reactive, computed } from 'vue' - -import { TAB_CONFIG, APP_THEMES_LIGHT } from '/src/utils/constants/settings.js' - -const initialState = { - tabState: TAB_CONFIG, - theme: APP_THEMES_LIGHT, -} - -export const useSettingsStore = defineStore('settings', () => { - const state = reactive({ ...initialState }) - - const getSettings = computed(() => state) - const getThemeSettings = computed(() => state.theme) - const getTabSettings = computed(() => state.tabState) - - function toggleTheme() { - state.theme = - APP_THEMES_LIGHT === state.theme ? APP_THEMES_DARK : APP_THEMES_LIGHT - } - function onSubmitConfiguration() { - state.tabState = TAB_CONFIG - } - function $reset() { - state = { ...initialState } - } - return { - // STATE - state, - - // GETTERS - getSettings, - getThemeSettings, - getTabSettings, - toggleTheme, - - // ACTIONS - onSubmitConfiguration, - $reset, - } -}) diff --git a/src/stores/useSwStore.js b/src/stores/useSwStore.js deleted file mode 100644 index ce41ae5..0000000 --- a/src/stores/useSwStore.js +++ /dev/null @@ -1,8 +0,0 @@ -import { defineStore } from 'pinia' -import { ref } from 'vue' - -export const useConfigurationStore = defineStore('sw', () => { - const state = ref() - - return { state } -}) diff --git a/src/utils/calculator.js b/src/utils/calculator.js new file mode 100644 index 0000000..d5ad6c8 --- /dev/null +++ b/src/utils/calculator.js @@ -0,0 +1,576 @@ +import { + DEFAULT_DB_VERSION, + OS_LINUX, + OS_WINDOWS, + OS_MAC, + DB_TYPE_WEB, + DB_TYPE_OLTP, + DB_TYPE_DW, + DB_TYPE_DESKTOP, + DB_TYPE_MIXED, + SIZE_UNIT_GB, + HARD_DRIVE_SSD, + HARD_DRIVE_HDD, + HARD_DRIVE_SAN, + HARD_DRIVE_NVME, + DB_SIZE_LESS_RAM, + DB_SIZE_MID_RAM, + DB_SIZE_GREATER_RAM, +} from './constants/configuration' + +const SIZE_UNIT_MAP = { + KB: 1024, + MB: 1048576, + GB: 1073741824, + TB: 1099511627776, + PB: 1125899906842624, +} + +const DEFAULT_DB_SETTINGS = { + default: { + ['max_worker_processes']: 8, + ['max_parallel_workers_per_gather']: 2, + ['max_parallel_workers']: 8, + }, +} + +export function calculateSettings({ + dbVersion = DEFAULT_DB_VERSION, + osType = OS_LINUX, + dbType = DB_TYPE_WEB, + totalMemory = null, + totalMemoryUnit = SIZE_UNIT_GB, + cpuNum = null, + connectionNum = null, + hdType = HARD_DRIVE_SSD, + dbSize = DB_SIZE_MID_RAM, +} = {}) { + const totalMemoryInBytes = totalMemory * SIZE_UNIT_MAP[totalMemoryUnit] + const totalMemoryInKb = totalMemoryInBytes / SIZE_UNIT_MAP['KB'] + const dbDefaultValues = + DEFAULT_DB_SETTINGS[dbVersion] || DEFAULT_DB_SETTINGS.default + const isConfigured = !!totalMemory + const maxConnections = connectionNum + ? connectionNum + : { + [DB_TYPE_WEB]: 200, + [DB_TYPE_OLTP]: 300, + [DB_TYPE_DW]: 40, + [DB_TYPE_DESKTOP]: 20, + [DB_TYPE_MIXED]: 100, + }[dbType] + const effectiveCacheSize = { + [DB_TYPE_WEB]: Math.floor((totalMemoryInKb * 3) / 4), + [DB_TYPE_OLTP]: Math.floor((totalMemoryInKb * 3) / 4), + [DB_TYPE_DW]: Math.floor((totalMemoryInKb * 3) / 4), + [DB_TYPE_DESKTOP]: Math.floor(totalMemoryInKb / 4), + [DB_TYPE_MIXED]: Math.floor((totalMemoryInKb * 3) / 4), + }[dbType] + const checkpointSegments = [ + { + key: 'min_wal_size', + value: { + [DB_TYPE_WEB]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_OLTP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_DW]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_DESKTOP]: (100 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_MIXED]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + }[dbType], + }, + { + key: 'max_wal_size', + value: { + [DB_TYPE_WEB]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_OLTP]: (8192 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_DW]: (16384 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_DESKTOP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + [DB_TYPE_MIXED]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'], + }[dbType], + }, + ] + const checkpointCompletionTarget = 0.9 + const defaultStatisticsTarget = { + [DB_TYPE_WEB]: 100, + [DB_TYPE_OLTP]: 100, + [DB_TYPE_DW]: 500, + [DB_TYPE_DESKTOP]: 100, + [DB_TYPE_MIXED]: 100, + }[dbType] + const walLevel = + dbType === DB_TYPE_DESKTOP + ? [ + { + key: 'wal_level', + value: 'minimal', + }, + // max_wal_senders must be 0 when wal_level=minimal + { + key: 'max_wal_senders', + value: '0', + }, + ] + : [] + const jit = + dbVersion >= 12 && + [DB_TYPE_WEB, DB_TYPE_OLTP, DB_TYPE_MIXED].includes(dbType) + ? 'off' + : null + const sharedBuffers = selectSharedBuffers( + totalMemoryInKb, + dbType, + osType, + dbVersion, + ) + const hugePages = selectHugePages(sharedBuffers, osType) + const maintenanceWorkMem = selectMaintenanceWorkMem( + totalMemoryInKb, + dbType, + osType, + dbVersion, + ) + const walBuffers = selectWalBuffers(sharedBuffers) + const randomPageCost = selectRandomPageCost(hdType, dbSize, dbType) + const effectiveIoConcurrency = selectEffectiveIoConcurrency(osType, hdType) + const parallelSettings = selectParallelSettings(dbVersion, dbType, cpuNum) + const workMem = selectWorkMem( + totalMemoryInKb, + sharedBuffers, + maxConnections, + parallelSettings, + dbDefaultValues, + dbType, + osType, + dbVersion, + dbSize, + ) + const walCompression = selectWalCompression(dbVersion) + const autovacuumMaxWorkers = selectAutovacuumMaxWorkers(cpuNum) + const autovacuumWorkMem = selectAutovacuumWorkMem( + maintenanceWorkMem, + osType, + dbVersion, + ) + const ioMethod = selectIoMethod(dbVersion, osType) + const ioWorkers = selectIoWorkers(dbVersion, cpuNum, ioMethod) + const warningInfoMessages = selectWarningInfoMessages( + totalMemoryInBytes, + walCompression, + ioMethod, + dbType, + hdType, + dbSize, + ) + + return { + dbVersion, + osType, + dbType, + totalMemory, + totalMemoryUnit, + cpuNum, + connectionNum, + hdType, + maxConnections, + hugePages, + sharedBuffers, + effectiveCacheSize, + maintenanceWorkMem, + checkpointSegments, + checkpointCompletionTarget, + walBuffers, + defaultStatisticsTarget, + randomPageCost, + effectiveIoConcurrency, + parallelSettings, + workMem, + warningInfoMessages, + walLevel, + jit, + walCompression, + autovacuumMaxWorkers, + autovacuumWorkMem, + ioMethod, + ioWorkers, + } +} + +function selectSharedBuffers(totalMemoryKb, dbType, osType, dbVersion) { + let sharedBuffersValue = { + [DB_TYPE_WEB]: Math.floor(totalMemoryKb / 4), + [DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 4), + [DB_TYPE_DW]: Math.floor(totalMemoryKb / 4), + [DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16), + [DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 4), + }[dbType] + if (dbVersion < 10 && OS_WINDOWS === osType) { + // Limit shared_buffers to 512MB on Windows + const winMemoryLimit = (512 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (sharedBuffersValue > winMemoryLimit) { + sharedBuffersValue = winMemoryLimit + } + } + return sharedBuffersValue +} + +function selectHugePages(sharedBuffersValue, osType) { + // macOS does not support PostgreSQL huge pages + if (osType === OS_MAC) { + return 'off' + } + const hugePageThreshold = (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] + return sharedBuffersValue >= hugePageThreshold ? 'try' : 'off' +} + +function selectMaintenanceWorkMem(totalMemoryKb, dbType, osType, dbVersion) { + let maintenanceWorkMemValue = { + [DB_TYPE_WEB]: Math.floor(totalMemoryKb / 16), + [DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 16), + [DB_TYPE_DW]: Math.floor(totalMemoryKb / 8), + [DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16), + [DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 16), + }[dbType] + // Default cap is 8GB on modern servers with lots of memory + let memoryLimit = (8 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] + + // Windows 64-bit has a strict 2GB limit up to PostgreSQL 17 + if (OS_WINDOWS === osType && dbVersion <= 17) { + memoryLimit = (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] + } + + if (maintenanceWorkMemValue >= memoryLimit) { + if (OS_WINDOWS === osType && dbVersion <= 17) { + // 2048MB (2 GB) will raise an error on Windows PG <= 17, so we need to remove 1 MB from it + maintenanceWorkMemValue = + memoryLimit - (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + } else { + maintenanceWorkMemValue = memoryLimit + } + } + return maintenanceWorkMemValue +} + +function selectWalBuffers(sharedBuffersValue) { + // Follow auto-tuning guideline for wal_buffers added in 9.1, where it's + // set to 3% of shared_buffers up to a maximum of 16MB. + let walBuffersValue = Math.floor((3 * sharedBuffersValue) / 100) + const maxWalBuffer = (16 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (walBuffersValue > maxWalBuffer) { + walBuffersValue = maxWalBuffer + } + // It's nice of wal_buffers is an even 16MB if it's near that number. Since + // that is a common case on Windows, where shared_buffers is clipped to 512MB, + // round upwards in that situation + const walBufferNearValue = (14 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (walBuffersValue > walBufferNearValue && walBuffersValue < maxWalBuffer) { + walBuffersValue = maxWalBuffer + } + // if less, than 32 kb, than set it to minimum + if (walBuffersValue < 32) { + walBuffersValue = 32 + } + return walBuffersValue +} + +function selectRandomPageCost(hdType, dbSize, dbType) { + // If the entire database fits in RAM, all read requests will be served + // from the OS page cache. Disk seek time (even for HDD) becomes irrelevant. + if (dbSize === DB_SIZE_LESS_RAM) { + return 1.1 + } + + // HDD always gets 4.0 + if (hdType === HARD_DRIVE_HDD) { + return 4 + } + + // For Data Warehouses that don't fit in RAM, a low random_page_cost is dangerous. + // It causes the planner to pick index scans for large data retrievals, which + // are much slower on physical SSDs than sequential scans. + // Return 4.0 (the Postgres default) to prevent this harm + if (dbType === DB_TYPE_DW) { + return 4 + } + + return 1.1 +} + +function selectEffectiveIoConcurrency(osType, hdType) { + if (osType !== OS_LINUX) { + return null + } + return { + [HARD_DRIVE_HDD]: 2, + [HARD_DRIVE_SSD]: 200, + [HARD_DRIVE_SAN]: 300, + [HARD_DRIVE_NVME]: 1000, + }[hdType] +} + +function selectParallelSettings(dbVersion, dbType, cpuNum) { + if (!cpuNum || cpuNum < 4) { + return [] + } + + let workersPerGather = Math.ceil(cpuNum / 2) + + if (dbType !== DB_TYPE_DW && workersPerGather > 4) { + workersPerGather = 4 // no clear evidence, that each new worker will provide big benefit for each noew core + } + + let config = [ + { + key: 'max_worker_processes', + value: cpuNum, + }, + { + key: 'max_parallel_workers_per_gather', + value: workersPerGather, + }, + ] + + if (dbVersion >= 10) { + config.push({ + key: 'max_parallel_workers', + value: cpuNum, + }) + } + + if (dbVersion >= 11) { + let parallelMaintenanceWorkers = Math.ceil(cpuNum / 2) + + if (parallelMaintenanceWorkers > 4) { + parallelMaintenanceWorkers = 4 // no clear evidence, that each new worker will provide big benefit for each noew core + } + + config.push({ + key: 'max_parallel_maintenance_workers', + value: parallelMaintenanceWorkers, + }) + } + + return config +} + +function selectWorkMem( + totalMemoryKb, + sharedBuffersValue, + maxConnectionsValue, + parallelSettingsValue, + dbDefaultValues, + dbType, + osType, + dbVersion, + dbSize, +) { + const parallelForWorkMem = (() => { + if (parallelSettingsValue.length) { + const maxWorkerProcesses = parallelSettingsValue.find( + (param) => param['key'] === 'max_worker_processes', + ) + if ( + maxWorkerProcesses && + maxWorkerProcesses['value'] && + maxWorkerProcesses['value'] > 0 + ) { + return maxWorkerProcesses['value'] + } + } + if ( + dbDefaultValues['max_worker_processes'] && + dbDefaultValues['max_worker_processes'] > 0 + ) { + return dbDefaultValues['max_worker_processes'] + } + return 1 + })() + + // work_mem is assigned any time a query calls for a sort, or a hash, or any other structure that needs a space allocation, which can happen multiple times per query. So you're better off assuming max_connections * 2 or max_connections * 3 is the amount of RAM that will actually use in reality. At the very least, you need to subtract shared_buffers from the amount you're distributing to connections in work_mem. + // The other thing to consider is that there's no reason to run on the edge of available memory. If you do that, there's a very high risk the out-of-memory killer will come along and start killing PostgreSQL backends. Always leave a buffer of some kind in case of spikes in memory usage. So your maximum amount of memory available in work_mem should be (RAM - shared_buffers) / ((max_connections + max_worker_processes) * 3). + const workMemValue = + (totalMemoryKb - sharedBuffersValue) / + ((maxConnectionsValue + parallelForWorkMem) * 3) + + let workMemResult = { + [DB_TYPE_WEB]: Math.floor(workMemValue), + [DB_TYPE_OLTP]: Math.floor(workMemValue), + [DB_TYPE_DW]: Math.floor(workMemValue / 2), + [DB_TYPE_DESKTOP]: Math.floor(workMemValue / 6), + [DB_TYPE_MIXED]: Math.floor(workMemValue / 2), + }[dbType] + + if (dbSize === DB_SIZE_LESS_RAM) { + // OS Cache is underutilized because the DB fits entirely in RAM. + // We can safely boost work_mem by 30% to speed up sorts/hashes. + workMemResult = Math.floor(workMemResult * 1.3) + } else if (dbSize === DB_SIZE_GREATER_RAM) { + // OS Cache is under massive pressure. We must protect memory. + // Reduce work_mem by 10% to provide a wider safety net against OOM spikes. + workMemResult = Math.floor(workMemResult * 0.9) + } + + // Modern floor limit: 4 MB minimum to prevent disk-spill for simple queries + const minWorkMem = (4 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (workMemResult < minWorkMem) { + workMemResult = minWorkMem + } + + // Windows 64-bit has a strict 2GB limit up to PostgreSQL 17 + if (osType === OS_WINDOWS && dbVersion <= 17) { + const winMemoryLimit = + (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - + (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (workMemResult > winMemoryLimit) { + workMemResult = winMemoryLimit + } + } + + return workMemResult +} + +function selectWalCompression(dbVersion) { + // PG 15+ introduced lz4 and zstd support. lz4 is significantly faster + // and lighter on CPU than the native pglz algorithm. Standard package + // installations compile with lz4 support by default. + if (dbVersion >= 15) { + return 'lz4' + } + // For PG 10 through 14, 'on' safely enables pglz compression. + if (dbVersion >= 10) { + return 'on' + } + return null +} + +function selectAutovacuumMaxWorkers(cpuNum) { + // PG default is 3. We only safely scale it up on heavier servers. + if (!cpuNum) return null + if (cpuNum >= 32) return 5 + if (cpuNum >= 16) return 4 + return null +} + +function selectAutovacuumWorkMem(maintenanceWorkMemValue, osType, dbVersion) { + let autovacuumWorkMemValue = null + + // By default, autovacuum_work_mem is -1 (uses maintenance_work_mem). + // Because we safely raised maintenance_work_mem to up to 8GB, letting 3-5 background + // workers each consume 8GB would cause severe OOM risks. We explicitly cap + // background vacuum memory to 2GB max per worker. + const threshold = (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] + if (maintenanceWorkMemValue >= threshold) { + autovacuumWorkMemValue = threshold + } + + // Windows 64-bit has a strict 2GB limit up to PostgreSQL 17 + if ( + autovacuumWorkMemValue !== null && + osType === OS_WINDOWS && + dbVersion <= 17 + ) { + const winMemoryLimit = + (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] - + (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'] + if (autovacuumWorkMemValue > winMemoryLimit) { + autovacuumWorkMemValue = winMemoryLimit + } + } + + return autovacuumWorkMemValue +} + +function selectIoMethod(dbVersion, osType) { + // Asynchronous I/O was introduced in PostgreSQL 18 + if (dbVersion < 18) { + return null + } + + // If we assume standard modern packages (compiled with --with-liburing), + // io_uring is the absolute best performing AIO method for Linux. + if (osType === OS_LINUX) { + return 'io_uring' + } + + // Windows and macOS do not support io_uring natively, so we fall back + // to the safe cross-platform 'worker' processes. + return 'worker' +} + +function selectIoWorkers(dbVersion, cpuNum, ioMethod) { + // io_workers was introduced in PG18 for the new Asynchronous I/O subsystem + if (dbVersion < 18 || !cpuNum || ioMethod === 'io_uring') { + return null + } + + // Default is 3. We conservatively scale it to ~25% of CPU cores to avoid + // lock contention on the AIO queue, capped at PostgreSQL's hard max of 32 + const ioWorkersValue = Math.min(32, Math.max(3, Math.floor(cpuNum / 4))) + + // Only explicitly configure it if we are recommending more than the default + return ioWorkersValue > 3 ? ioWorkersValue : null +} + +function selectWarningInfoMessages( + totalMemory, + walCompression, + ioMethod, + dbType, + hdType, + dbSize, +) { + let warnings = [] + + // Memory warnings + if (totalMemory < 256 * SIZE_UNIT_MAP['MB']) { + warnings.push( + 'этот инструмент не предназначен', + 'для систем с низким количеством ОЗУ', + ) + } else if (totalMemory > 100 * SIZE_UNIT_MAP['GB']) { + warnings.push( + 'этот инструмент не предназначен', + 'для систем с очень большим количеством ОЗУ', + ) + } + + // Advanced features compilation warnings + if (walCompression === 'lz4') { + if (warnings.length > 0) warnings.push('') + + warnings.push( + 'wal_compression = lz4 требует, чтобы PostgreSQL', + 'был скомпилирован с флагом --with-lz4', + ) + } + + if (ioMethod === 'io_uring') { + if (warnings.length > 0) warnings.push('') + + warnings.push( + 'io_method = io_uring требует, чтобы PostgreSQL', + 'был скомпилирован с флагом --with-liburing', + ) + } + + // I/O Cost Warning for Analytical DBs + if ( + dbType === DB_TYPE_DW && + hdType !== HARD_DRIVE_HDD && + dbSize !== DB_SIZE_LESS_RAM + ) { + const driveName = { + [HARD_DRIVE_SSD]: 'SSD', + [HARD_DRIVE_NVME]: 'NVME', + [HARD_DRIVE_SAN]: 'SAN', + }[hdType] + + if (warnings.length > 0) warnings.push('') + + warnings.push( + `Параметры стоимости для хранилищ данных на ${driveName} оставлены со значениями по умолчанию,`, + 'чтобы избежать катастрофического выбора при сканировании индексов.', + 'Следите за поведением планировщика запросов и при необходимости корректируйте random_page_cost.', + ) + } + + return warnings.length > 0 ? ['ВНИМАНИЕ', ...warnings] : [] +} diff --git a/vite.config.js b/vite.config.js index c5be346..960ba04 100644 --- a/vite.config.js +++ b/vite.config.js @@ -31,7 +31,7 @@ export default defineConfig({ description: 'PgTune - Tuning PostgreSQL config by your hardware', display: 'standalone', start_url: '/', - theme_color: '#0f172b', + theme_color: '#fff', background_color: '#fff', icons: [ {