Merge branch 'release/1.0.0'

This commit is contained in:
2026-09-08 23:54:48 +03:00
24 changed files with 1028 additions and 4143 deletions
+37
View File
@@ -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
+4
View File
@@ -26,3 +26,7 @@ dist-ssr
# Auto-generated type declarations
auto-imports.d.ts
components.d.ts
dev-dist
pgtune.tar.zst
+9 -3
View File
@@ -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 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
[![PGTune](src/assets/images/logo.svg 'PGTune')](https://pgtune.mudrov.tech/)
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
Tuning PostgreSQL config by your hardware. Based on original React version of [pgtune](https://github.com/le0pard/pgtune). Rewritten in Vue 3.
### Build status
| Branch | Status |
| --- | --- |
| Develop | [![Build Status](https://drone.minis.mudrov.tech/api/badges/administrator/pgtune/status.svg?ref=refs/heads/develop)](https://drone.minis.mudrov.tech/administrator/pgtune) |
-3
View File
@@ -21,7 +21,6 @@
"eslint-config-vue": "^2.0.2",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-vue": "^10.10.0",
"pinia": "^4.0.3",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
"vite": "^8.2.2",
@@ -1246,8 +1245,6 @@
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"pinia": ["pinia@4.0.3", "", { "dependencies": { "nostics": "^1.1.4" }, "peerDependencies": { "@vue/devtools-api": "^8.1.5", "typescript": ">=5.6.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA=="],
"pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
-92
View File
@@ -1,92 +0,0 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didn’t register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-f0c192c2'], (function (workbox) { 'use strict';
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.kapsgh4ngf8"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
@@ -7,7 +7,7 @@
<meta name="keywords" content="pgtune, postgresql, postgres, tuning, config, configuration, free, open source" />
<meta name="description" content="PgTune - Tuning PostgreSQL config by your hardware" />
<link type="text/plain" rel="author" href="/humans.txt" />
<title>PGTune - calculate configuration for PostgreSQL based on the maximum performance for a given hardware configuration</title>
<title>PGTune - рассчёт конфигурации PostgreSQL, исходя из максимальной производительности для данной конфигурации оборудования</title>
</head>
<body>
<div id="app" class="isolate"></div>
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "pgtune",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -25,7 +25,6 @@
"eslint-config-vue": "^2.0.2",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-vue": "^10.10.0",
"pinia": "^4.0.3",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
"vite": "^8.2.2",
BIN
View File
Binary file not shown.
+1 -5
View File
@@ -5,10 +5,6 @@ import { useThemeColor } from './composables/useThemeColor.js'
import { onMounted } from 'vue'
import { useUpdateApp } from './composables/useUpdateApp.js'
/*
const register = registerSW()
*/
const route = useRoute()
onMounted(() => {
@@ -22,7 +18,7 @@ onMounted(() => {
<UContainer>
<Header />
<UMain class="p-4">
<RouterView :key="route.fullPath" />
<RouterView :key="route.path" />
</UMain>
</UContainer>
</UApp>
+168 -72
View File
@@ -1,104 +1,200 @@
<script setup>
import { ref } from 'vue'
import { reactive, ref } from 'vue'
import {
DB_TYPE_WEB,
DB_TYPE_OLTP,
DB_TYPE_DW,
DB_TYPE_DESKTOP,
DB_TYPE_MIXED,
DB_VERSIONS,
OS_LINUX,
OS_MAC,
OS_WINDOWS,
SIZE_UNIT_TB,
SIZE_UNIT_GB,
SIZE_UNIT_MB,
HARD_DRIVE_SSD,
HARD_DRIVE_NVME,
HARD_DRIVE_SAN,
HARD_DRIVE_HDD,
DB_SIZE_LESS_RAM,
DB_SIZE_MID_RAM,
DB_SIZE_GREATER_RAM,
} from '../utils/constants/configuration'
import { validationSchema as schema } from '../utils/validation'
import { useRoute, useRouter } from 'vue-router'
const dbVersions = ref(['18', '17', '16', '15', '14', '13', '12', '11', '10'])
const dbVersion = ref('18')
const route = useRoute()
const router = useRouter()
const osTypes = ref(['Linux', 'Windows', 'OS X'])
const osType = ref('Linux')
const query = route.query
const dbVersions = ref(
DB_VERSIONS.map((version) => ({
label: String(version),
value: version,
})),
)
const osTypes = ref([
{
label: 'Linux',
value: OS_LINUX,
},
{
label: 'OS X',
value: OS_MAC,
},
{
label: 'Windows',
value: OS_WINDOWS,
},
])
const dbTypes = ref([
'Web application',
'Online transaction processing system',
'Data warehouse',
'Desktop application',
'Mixed type of application',
{
label: 'Web-приложение',
value: DB_TYPE_WEB,
},
{
label: 'Система обработки онлайн-транзакций',
value: DB_TYPE_OLTP,
},
{
label: 'Хранилище данных',
value: DB_TYPE_DW,
},
{
label: 'Десктопное приложение',
value: DB_TYPE_DESKTOP,
},
{
label: 'Приложение смешанного типа',
value: DB_TYPE_MIXED,
},
])
const dbType = ref('Web application')
const totalRam = ref()
const ramUnits = ref(['TB', 'GB', 'MB'])
const ramUnit = ref('GB')
const totalCPUs = ref(1)
const totalConnections = ref(100)
const ramUnits = ref([SIZE_UNIT_TB, SIZE_UNIT_GB, SIZE_UNIT_MB])
const dataStorages = ref([
'NVME storage',
'SSD storage',
'Network (SAN) storage',
'HDD storage',
{
label: 'Хранилище NVMe',
value: HARD_DRIVE_NVME,
},
{
label: 'Хранилище SSD',
value: HARD_DRIVE_SSD,
},
{
label: 'Сетевое хранилище (SAN)',
value: HARD_DRIVE_SAN,
},
{
label: 'Хранилище HDD',
value: HARD_DRIVE_HDD,
},
])
const dataStorage = ref('SSD storage')
const dataSizes = ref(['Less than RAM', 'RAM - 3xRAM', 'More than 3xRAM'])
const dataSize = ref('Less than RAM')
const dataSizes = ref([
{
label: 'Меньше RAM',
value: DB_SIZE_LESS_RAM,
},
{
label: '1x - 3x RAM',
value: DB_SIZE_MID_RAM,
},
{
label: 'Более 3xRAM',
value: DB_SIZE_GREATER_RAM,
},
])
const state = reactive({
dbVersion: query?.dbVersion ? parseInt(query.dbVersion) : 18,
osType: query?.osType ?? OS_LINUX,
dbType: query?.dbType ?? DB_TYPE_WEB,
totalMemory: query?.totalMemory ? parseInt(query.totalMemory) : undefined,
totalMemoryUnit: query?.totalMemoryUnit ?? SIZE_UNIT_GB,
cpuNum: query?.cpuNum ? parseInt(query.cpuNum) : 1,
connectionNum: query?.connectionNum ? parseInt(query.connectionNum) : 100,
hdType: query?.hdType ?? HARD_DRIVE_NVME,
dbSize: query?.dbSize ?? DB_SIZE_LESS_RAM,
})
const onSubmit = async () => {
await router.push({ name: 'Index', query: { ...state } })
}
</script>
<template>
<UForm class="space-y-4">
<UFormField
label="DB version"
help="PostgreSQL version (find out via 'SELECT version();')"
>
<USelect v-model="dbVersion" :items="dbVersions" class="w-full" />
<UForm class="space-y-4" :state :schema @submit="onSubmit">
<UFormField label="Версия БД" help="Версия PostgreSQL" name="dbVersion">
<USelect v-model="state.dbVersion" :items="dbVersions" class="w-full" />
</UFormField>
<UFormField
label="OS Type"
help="Operation system of the PostgreSQL server host"
label="Тип ОС"
help="Операционная система, запускающая PostgreSQL"
name="osType"
>
<USelect v-model="osType" :items="osTypes" class="w-full" />
<USelect v-model="state.osType" :items="osTypes" class="w-full" />
</UFormField>
<UFormField
label="DB type"
help="What type of application is PostgreSQL used for"
label="Тип БД"
help="Тип приложения, которое обслуживает PostgreSQL"
name="dbType"
>
<USelect v-model="dbType" :items="dbTypes" class="w-full" />
<USelect v-model="state.dbType" :items="dbTypes" class="w-full" />
</UFormField>
<div class="flex items-start gap-4">
<UFormField
label="Оперативная память (RAM)"
help="Объём оперативной памяти, которую PostgreSQL может использовать"
required
name="totalMemory"
class="grow"
>
<UInputNumber v-model="state.totalMemory" class="w-full" />
</UFormField>
<UFormField required name="totalMemoryUnit" label="Е.И.">
<USelect v-model="state.totalMemoryUnit" :items="ramUnits" />
</UFormField>
</div>
<UFormField
label="Количество CPU"
help="Количество потоков CPU, которые PostgreSQL может использовать. CPU = № потоков на ядро * № ядер на сокет * № сокетов"
name="cpuNum"
>
<UInputNumber v-model="state.cpuNum" class="w-full" />
</UFormField>
<UFormField
label="Total memory (RAM)"
help="How much memory can PostgreSQL use"
required
label="Количество соединений"
help="Максимальное количество клиентских соединений"
name="connectionNum"
>
<div class="flex items-center gap-4">
<UInputNumber
v-model="totalRam"
class="grow"
placeholder="Total memory (RAM) (* required)"
/>
<USelect v-model="ramUnit" :items="ramUnits" />
</div>
<UInputNumber v-model="state.connectionNum" class="w-full" />
</UFormField>
<UFormField
label="Number of CPUs"
help="Number of CPUs, which PostgreSQL can use. CPUs = N threads per core * N cores per socket * N sockets"
label="Хранилище данных"
help="Тип устройства хранения данных"
name="hdType"
>
<UInputNumber
v-model="totalCPUs"
class="w-full"
placeholder="Number of CPUs"
/>
<USelect v-model="state.hdType" :items="dataStorages" class="w-full" />
</UFormField>
<UFormField
label="Number of connections"
help="Maximum number of PostgreSQL client connections"
label="Объём базы данных"
help="Приблизительное отношение объёма хранимых данных к объёму оперативной памяти сервера"
name="dbSize"
>
<UInputNumber
v-model="totalConnections"
class="w-full"
placeholder="Number of connections"
/>
<USelect v-model="state.dbSize" :items="dataSizes" class="w-full" />
</UFormField>
<UFormField label="Data storage" help="Type of data storage device">
<USelect v-model="dataStorage" :items="dataStorages" class="w-full" />
</UFormField>
<UFormField
label="Total data size"
help="Estimated total size of your database compared to total server RAM"
>
<USelect v-model="dataSize" :items="dataSizes" class="w-full" />
</UFormField>
<UButton size="xl" label="GENERATE" block type="submit" class="h-16" />
<UButton
size="xl"
label="Рассчитать"
block
type="submit"
class="h-16"
icon="lucide:calculator"
/>
</UForm>
</template>
+149
View File
@@ -0,0 +1,149 @@
<script setup>
import { useRoute } from 'vue-router'
import { computed, ref } from 'vue'
import { calculateSettings } from '../utils/calculator'
import { useClipboard } from '@vueuse/core'
const KB_UNIT_MAP = {
KB_PER_MB: 1024,
KB_PER_GB: 1048576,
}
const route = useRoute()
const toast = useToast()
const tab = ref('0')
const tabs = ref([
{
label: 'postgresql.conf',
},
{
label: 'ALTER SYSTEM',
},
])
const isAlterSystem = computed(() => tab.value === '1')
const query = computed(() => route.query)
const res = computed(() => calculateSettings({ ...query.value }))
const config = computed(() => {
const hwConfig = [
['DB Version', res.value.dbVersion],
['OS Type', res.value.osType],
['DB Type', res.value.dbType],
[
'Total Memory (RAM)',
`${res.value.totalMemory} ${res.value.totalMemoryUnit}`,
],
['CPUs num', res.value.cpuNum],
['Connections num', res.value.connectionNum],
['Data Storage', res.value.hdType],
]
.filter((item) => !!item[1])
.map((item) => `${isAlterSystem.value ? '--' : '#'} ${item[0]}: ${item[1]}`)
.join('\n')
const pgConfig = [
['max_connections', res.value.maxConnections],
['shared_buffers', formatValue(res.value.sharedBuffers)],
['effective_cache_size', formatValue(res.value.effectiveCacheSize)],
['maintenance_work_mem', formatValue(res.value.maintenanceWorkMem)],
['checkpoint_completion_target', res.value.checkpointCompletionTarget],
['wal_buffers', formatValue(res.value.walBuffers)],
['default_statistics_target', res.value.defaultStatisticsTarget],
['random_page_cost', res.value.randomPageCost],
['effective_io_concurrency', res.value.effectiveIoConcurrency],
['work_mem', formatValue(res.value.workMem)],
['huge_pages', res.value.hugePages],
['jit', res.value.jit],
['wal_compression', res.value.walCompression],
['autovacuum_max_workers', res.value.autovacuumMaxWorkers],
[
'autovacuum_work_mem',
res.value.autovacuumWorkMem
? formatValue(res.value.autovacuumWorkMem)
: null,
],
['io_method', res.value.ioMethod],
['io_workers', res.value.ioWorkers],
]
.concat(
res.value.checkpointSegments.map((item) => {
if (item.key === 'checkpoint_segments') {
return [item.key, item.value]
}
return [item.key, formatValue(item.value)]
}),
)
.concat(res.value.parallelSettings.map((item) => [item.key, item.value]))
.concat(res.value.walLevel.map((item) => [item.key, item.value]))
.filter((item) => !!item[1])
.map((item) =>
isAlterSystem.value
? `ALTER SYSTEM SET ${item[0]} = '${item[1]}';`
: `${item[0]} = ${item[1]}`,
)
.join('\n')
const warningInfo = res.value.warningInfoMessages
.map((item) => `${isAlterSystem.value ? '--' : '#'} ${item}`)
.join('\n')
let config = [hwConfig, '', pgConfig]
if (res.value.warningInfoMessages.length > 0) {
config = [warningInfo, '', ...config]
}
return config.join('\n')
})
const { copy } = useClipboard({ source: config })
const formatValue = (value) => {
const result = (() => {
if (value % KB_UNIT_MAP['KB_PER_GB'] === 0) {
return {
value: Math.floor(value / KB_UNIT_MAP['KB_PER_GB']),
unit: 'GB',
}
}
if (value % KB_UNIT_MAP['KB_PER_MB'] === 0) {
return {
value: Math.floor(value / KB_UNIT_MAP['KB_PER_MB']),
unit: 'MB',
}
}
return {
value,
unit: 'kB',
}
})()
// return formatted
return `${result.value}${result.unit}`
}
const onCopyConfig = () => {
copy(config)
toast.add({
title: 'Скопировано',
description: 'Конфигурация скопирована успешно',
duration: 7500,
icon: 'lucide:copy',
})
}
</script>
<template>
<section class="space-y-4 min-w-0 w-full">
<UTabs v-model="tab" :content="false" :items="tabs" class="w-full" />
<pre class="w-full overflow-x-auto whitespace-pre">{{ config }}</pre>
<UButton
label="Копировать конфигурацию"
block
icon="lucide:copy"
class="h-12"
@click="onCopyConfig"
/>
</section>
</template>
+33 -25
View File
@@ -1,52 +1,60 @@
<template>
<article class="prose dark:prose-invert max-w-full">
<article class="prose prose-sm dark:prose-invert max-w-full">
<p>
Please provide basic information about the configuration of your hardware
on which the PostgreSQL database is running. Results will be calculated
after clicking the "Generate" button
Пожалуйста, предоставьте основную информацию о конфигурации вашего
оборудования, на котором работает база данных PostgreSQL. Результаты будут
рассчитаны после нажатия кнопки "Рассчитать".
</p>
<p>More information about "DB Type" setting:</p>
<p>Дополнительная информация о настройке "Тип БД":</p>
<ul>
<li>
<p>Web Application (web)</p>
<p>Web-приложение</p>
<ul>
<li>Typically CPU-bound</li>
<li>DB much smaller than RAM</li>
<li>90% or more simple queries</li>
<li>Как правило, зависит от процессора</li>
<li>БД намного меньше оперативной памяти</li>
<li>Более 90% запросов - простые выборки</li>
</ul>
</li>
<li>
<p>Online Transaction Processing (oltp)</p>
<p>Система обработки онлайн-транзакций</p>
<ul>
<li>Typically CPU- or I/O-bound</li>
<li>DB slightly larger than RAM to 1TB</li>
<li>20-40% small data write queries</li>
<li>Some long transactions and complex read queries</li>
<li>Как правило, зависит от процессора или системы ввода-вывода</li>
<li>Размер БД - от размера оперативной памяти до 1 ТБ</li>
<li>20-40% запросов - запись небольших объёмов данных</li>
<li>Встречаются длительные транзакции и сложные запросы на чтение</li>
</ul>
</li>
<li>
<p>Data Warehouse (dw)</p>
<p>Хранилище данных</p>
<ul>
<li>Typically I/O- or RAM-bound</li>
<li>Large bulk loads of data</li>
<li>Large complex reporting queries</li>
<li>Also called "Decision Support" or "Business Intelligence"</li>
<li>
Как правило, зависит от системы ввода-вывода или оперативной памяти
</li>
<li>Большие массивы данных</li>
<li>Сложные запросы построения отчётов</li>
<li>
Также называется "Системой принятия решений" или "Системой
бизнес-аналитики"
</li>
</ul>
</li>
<li>
<p>Desktop application</p>
<p>Десктопное приложение</p>
<ul>
<li>Not a dedicated database</li>
<li>A general workstation, perhaps for a developer</li>
<li>Неспециализированная база данных</li>
<li>Обычная рабочая станция, возможно - для разработки</li>
</ul>
</li>
<li>
<p>Mixed type of application</p>
<p>Приложение смешанного типа</p>
<ul>
<li>Mixed DW and OLTP characteristics</li>
<li>wide mixture of queries</li>
<li>
Смешанные характеристики хранилища данных и системы обработки
онлайн-транзакций
</li>
<li>Широкий спектр запросов</li>
</ul>
</li>
</ul>
+3 -3
View File
@@ -4,11 +4,11 @@ import Logo from '/favicon.svg?url'
const items = ref([
{
label: 'Home',
label: 'Главная',
to: '/',
},
{
label: 'How it works',
label: 'Как это работает?',
to: '/about',
},
])
@@ -32,7 +32,7 @@ const items = ref([
<UButton
color="neutral"
variant="ghost"
to="https://gitea.minis.mudrov.tech"
to="https://gitea.minis.mudrov.tech/administrator/pgtune"
target="_blank"
icon="simple-icons:gitea"
aria-label="Gitea"
+7 -9
View File
@@ -14,37 +14,34 @@ export function useUpdateApp() {
})
const toast = useToast()
if (import.meta.env.DEV) console.log({ SWNeedRefresh: needRefresh.value })
async function close(e) {
needRefresh.value = false
e?.stopPropagation()
}
// New version
watch(
() => needRefresh,
needRefresh,
(n) => {
if (import.meta.env.DEV) console.log('NeedRefresh:', n)
if (n) {
toast.add({
title: 'Update',
title: 'Обновление',
description:
'A newer version of the app is available. Click the “Update” button to install.',
'Доступна более новая версия приложения. Нажмите кнопку “Обновить”, чтобы установить его.',
duration: 0,
icon: 'lucide:cloud-download',
close: false,
actions: [
{
icon: 'lucide:arrow-big-up-dash',
label: 'Update',
label: 'Обновить',
onClick: updateServiceWorker,
color: 'error',
},
{
icon: 'lucide:x',
label: 'Dismiss',
label: 'Отложить',
onClick: close,
variant: 'outline',
},
@@ -53,5 +50,6 @@ export function useUpdateApp() {
})
}
},
{ immediate: true },
)
}
+2 -8
View File
@@ -1,14 +1,8 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import '/src/assets/styles/app.css'
import App from './App.vue'
import { router } from '/router.js'
import { router } from './router'
import ui from '@nuxt/ui/vue-plugin'
import { createHead } from '@unhead/vue/client'
createApp(App)
.use(router)
.use(createPinia())
.use(ui)
.use(createHead())
.mount('#app')
createApp(App).use(router).use(ui).use(createHead()).mount('#app')
+9 -9
View File
@@ -6,19 +6,19 @@ import HeroImage from '/src/assets/images/logo.svg'
<article class="prose dark:prose-invert max-w-full">
<img :src="HeroImage" class="w-full h-auto rounded-2xl" alt="PGTune" />
<p>
<strong>PGTune</strong> calculate configuration for PostgreSQL based on
the maximum performance for a given hardware configuration. It isn't a
<strong>PGTune</strong> рассчитывает конфигурацию PostgreSQL, исходя из
максимальной производительности для данной конфигурации оборудования. Это
не
<a
href="https://en.wikipedia.org/wiki/No_Silver_Bullet"
href="https://ru.wikipedia.org/wiki/Серебряной_пули_нет"
target="_blank"
class="font-medium underline"
>silver bullet</a
>универсальное решение</a
>
for the optimization settings of PostgreSQL. Many settings depend not only
on the hardware configuration, but also on the size of the database, the
number of clients and the complexity of queries. An optimal configuration
of the database can only be made given all these parameters are taken into
account.
для оптимизации настроек PostgreSQL. Многие настройки зависят не только от
конфигурации оборудования, но и от размера базы данных, количества
клиентов и сложности запросов. Оптимальная конфигурация базы данных может
быть создана только с учётом этих параметров.
</p>
</article>
</template>
+26 -3
View File
@@ -1,14 +1,37 @@
<script setup>
import { computed, defineAsyncComponent } from 'vue'
import ConfigurationForm from '../components/configurationForm.vue'
import DefaultDescription from '../components/defaultDescription.vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const shouldShowResultView = computed(
() =>
Object.hasOwn(route.query, 'dbVersion') &&
Object.hasOwn(route.query, 'osType') &&
Object.hasOwn(route.query, 'dbType') &&
Object.hasOwn(route.query, 'totalMemory') &&
Object.hasOwn(route.query, 'totalMemoryUnit') &&
Object.hasOwn(route.query, 'cpuNum') &&
Object.hasOwn(route.query, 'connectionNum') &&
Object.hasOwn(route.query, 'hdType') &&
Object.hasOwn(route.query, 'dbSize'),
)
const DefaultDescription = defineAsyncComponent(
() => import('../components/defaultDescription.vue'),
)
const CFV = defineAsyncComponent(
() => import('../components/configurationFormView.vue'),
)
</script>
<template>
<div class="grid lg:grid-cols-2 gap-16">
<div class="space-y-4">
<h3 class="text-xl font-bold">Parameters of your system</h3>
<h3 class="text-xl font-bold">Параметры системы</h3>
<ConfigurationForm />
</div>
<DefaultDescription />
<CFV v-if="shouldShowResultView" />
<DefaultDescription v-else />
</div>
</template>
View File
-479
View File
@@ -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,
}
})
-42
View File
@@ -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,
}
})
-8
View File
@@ -1,8 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useConfigurationStore = defineStore('sw', () => {
const state = ref()
return { state }
})
+576
View File
@@ -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] : []
}
+1 -1
View File
@@ -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: [
{