1pub mod conf_block;
12
13const KIB: u64 = 1;
15const MIB: u64 = 1024 * KIB;
17const GIB: u64 = 1024 * MIB;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Platform {
23 Linux,
24 Windows,
25}
26
27impl Platform {
28 pub const fn current() -> Self {
30 if cfg!(target_os = "windows") {
31 Self::Windows
32 } else {
33 Self::Linux
34 }
35 }
36
37 fn is_windows(self) -> bool {
38 matches!(self, Self::Windows)
39 }
40}
41
42#[derive(Debug, Clone, Copy)]
44pub struct HostResources {
45 pub total_ram_kib: u64,
47 pub cpus: u32,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Budget {
54 pub ram_kib: u64,
56 pub cpus: u32,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct TuneInputs {
63 pub platform: Platform,
64 pub resources: HostResources,
65 pub pg_major: u32,
67 pub max_connections: u32,
69 pub lz4_wal_supported: bool,
72 pub temp_file_limit_kib: Option<u64>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Setting {
80 pub key: &'static str,
81 pub value: String,
82}
83
84impl Setting {
85 fn new(key: &'static str, value: impl Into<String>) -> Self {
86 Self {
87 key,
88 value: value.into(),
89 }
90 }
91}
92
93fn budget_ram_kib(platform: Platform, total_ram_kib: u64) -> u64 {
100 if platform.is_windows() {
101 (total_ram_kib / 2).min(4 * GIB)
102 } else if total_ram_kib >= 8 * GIB {
103 total_ram_kib - 4 * GIB
104 } else {
105 total_ram_kib / 2
106 }
107}
108
109fn budget_cpus(cpus: u32) -> u32 {
111 if cpus >= 8 {
112 cpus - 4
113 } else if cpus >= 5 {
114 4
115 } else if cpus >= 2 {
116 2
117 } else {
118 1
119 }
120}
121
122pub fn budget(platform: Platform, resources: HostResources) -> Budget {
124 Budget {
125 ram_kib: budget_ram_kib(platform, resources.total_ram_kib),
126 cpus: budget_cpus(resources.cpus),
127 }
128}
129
130pub fn expected_shared_buffers_kib(budget: &Budget, platform: Platform, pg_major: u32) -> u64 {
132 let value = budget.ram_kib / 4;
133 if platform.is_windows() && pg_major < 10 {
134 value.min(512 * MIB)
135 } else {
136 value
137 }
138}
139
140pub fn expected_effective_cache_kib(budget: &Budget) -> u64 {
142 (budget.ram_kib * 3) / 4
143}
144
145pub fn expected_maintenance_kib(budget: &Budget, platform: Platform, pg_major: u32) -> u64 {
147 let value = budget.ram_kib / 16;
148 let limit = if platform.is_windows() && pg_major <= 17 {
149 2 * GIB
150 } else {
151 8 * GIB
152 };
153 if value >= limit {
154 if platform.is_windows() && pg_major <= 17 {
155 limit - MIB
156 } else {
157 limit
158 }
159 } else {
160 value
161 }
162}
163
164fn expected_work_mem_kib(
165 budget: &Budget,
166 platform: Platform,
167 pg_major: u32,
168 max_connections: u32,
169) -> u64 {
170 let shared = expected_shared_buffers_kib(budget, platform, pg_major);
171 let parallel = if budget.cpus >= 4 { budget.cpus } else { 8 };
172 let divisor = u64::from(max_connections + parallel) * 3;
173 let mut value = (budget.ram_kib.saturating_sub(shared)) / divisor.max(1);
174 value = value.max(4 * MIB);
175 if platform.is_windows() && pg_major <= 17 {
176 value = value.min(2 * GIB - MIB);
177 }
178 value
179}
180
181fn expected_wal_buffers_kib(shared_buffers_kib: u64) -> u64 {
182 let raw = (shared_buffers_kib * 3) / 100;
183 let capped = raw.clamp(32, 16 * MIB);
184 if capped > 14 * MIB && capped < 16 * MIB {
186 16 * MIB
187 } else {
188 capped
189 }
190}
191
192fn render_kib(kib: u64) -> String {
195 if kib.is_multiple_of(GIB) {
196 format!("{}GB", kib / GIB)
197 } else if kib.is_multiple_of(MIB) {
198 format!("{}MB", kib / MIB)
199 } else {
200 format!("{kib}kB")
201 }
202}
203
204pub fn compute(input: &TuneInputs) -> Vec<Setting> {
206 let TuneInputs {
207 platform,
208 resources,
209 pg_major,
210 max_connections,
211 lz4_wal_supported,
212 temp_file_limit_kib,
213 } = *input;
214 let budget = budget(platform, resources);
215 let mut out = Vec::new();
216
217 let shared_buffers = expected_shared_buffers_kib(&budget, platform, pg_major);
218 out.push(Setting::new("max_connections", max_connections.to_string()));
219 out.push(Setting::new("shared_buffers", render_kib(shared_buffers)));
220 out.push(Setting::new(
221 "effective_cache_size",
222 render_kib(expected_effective_cache_kib(&budget)),
223 ));
224 out.push(Setting::new(
225 "maintenance_work_mem",
226 render_kib(expected_maintenance_kib(&budget, platform, pg_major)),
227 ));
228 out.push(Setting::new("checkpoint_completion_target", "0.9"));
229 out.push(Setting::new(
230 "wal_buffers",
231 render_kib(expected_wal_buffers_kib(shared_buffers)),
232 ));
233 out.push(Setting::new("default_statistics_target", "100"));
234 out.push(Setting::new("random_page_cost", "1.1"));
235
236 if !platform.is_windows() {
238 out.push(Setting::new("effective_io_concurrency", "200"));
239 }
240
241 out.push(Setting::new(
242 "work_mem",
243 render_kib(expected_work_mem_kib(
244 &budget,
245 platform,
246 pg_major,
247 max_connections,
248 )),
249 ));
250 out.push(Setting::new(
251 "huge_pages",
252 if shared_buffers >= 2 * GIB {
253 "try"
254 } else {
255 "off"
256 },
257 ));
258 out.push(Setting::new("min_wal_size", "2GB"));
259 out.push(Setting::new("max_wal_size", "8GB"));
260
261 if budget.cpus >= 4 {
262 let per_gather = budget.cpus.div_ceil(2).min(4);
263 out.push(Setting::new(
264 "max_worker_processes",
265 budget.cpus.to_string(),
266 ));
267 out.push(Setting::new(
268 "max_parallel_workers_per_gather",
269 per_gather.to_string(),
270 ));
271 if pg_major >= 10 {
272 out.push(Setting::new(
273 "max_parallel_workers",
274 budget.cpus.to_string(),
275 ));
276 }
277 if pg_major >= 11 {
278 out.push(Setting::new(
279 "max_parallel_maintenance_workers",
280 per_gather.to_string(),
281 ));
282 }
283 }
284
285 let autovacuum_max_workers = if budget.cpus >= 32 {
286 Some(5)
287 } else if budget.cpus >= 16 {
288 Some(4)
289 } else {
290 None
291 };
292 if let Some(workers) = autovacuum_max_workers {
293 out.push(Setting::new("autovacuum_max_workers", workers.to_string()));
294 }
295 if expected_maintenance_kib(&budget, platform, pg_major) >= 2 * GIB {
296 out.push(Setting::new("autovacuum_work_mem", "2GB"));
297 }
298
299 if pg_major >= 18 {
300 let io_workers = (budget.cpus / 4).clamp(3, 32);
301 if io_workers > 3 {
302 out.push(Setting::new("io_workers", io_workers.to_string()));
303 }
304 if !platform.is_windows() {
306 out.push(Setting::new("io_method", "io_uring"));
307 }
308 }
309
310 if pg_major >= 12 {
311 out.push(Setting::new("jit", "off"));
312 }
313
314 let wal_compression = if pg_major >= 15 {
315 if lz4_wal_supported {
316 Some("lz4")
317 } else {
318 Some("on")
319 }
320 } else if pg_major >= 10 {
321 Some("on")
322 } else {
323 None
324 };
325 if let Some(value) = wal_compression {
326 out.push(Setting::new("wal_compression", value));
327 }
328
329 if let Some(kib) = temp_file_limit_kib {
330 out.push(Setting::new("temp_file_limit", render_kib(kib)));
331 }
332
333 out
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 fn get<'a>(settings: &'a [Setting], key: &str) -> Option<&'a str> {
341 settings
342 .iter()
343 .find(|s| s.key == key)
344 .map(|s| s.value.as_str())
345 }
346
347 fn windows_inputs(total_gib: u64, cpus: u32) -> TuneInputs {
348 TuneInputs {
349 platform: Platform::Windows,
350 resources: HostResources {
351 total_ram_kib: total_gib * GIB,
352 cpus,
353 },
354 pg_major: 16,
355 max_connections: 100,
356 lz4_wal_supported: false,
357 temp_file_limit_kib: None,
358 }
359 }
360
361 #[test]
362 fn windows_budget_is_capped_at_4gib() {
363 assert_eq!(budget_ram_kib(Platform::Windows, 4 * GIB), 2 * GIB);
364 assert_eq!(budget_ram_kib(Platform::Windows, 8 * GIB), 4 * GIB);
365 assert_eq!(budget_ram_kib(Platform::Windows, 16 * GIB), 4 * GIB);
366 assert_eq!(budget_ram_kib(Platform::Windows, 64 * GIB), 4 * GIB);
367 }
368
369 #[test]
370 fn linux_budget_reserves_four_gib() {
371 assert_eq!(budget_ram_kib(Platform::Linux, 32 * GIB), 28 * GIB);
372 assert_eq!(budget_ram_kib(Platform::Linux, 4 * GIB), 2 * GIB);
373 }
374
375 #[test]
376 fn windows_shared_buffers_scale_with_budget() {
377 assert_eq!(
378 get(&compute(&windows_inputs(4, 2)), "shared_buffers"),
379 Some("512MB")
380 );
381 assert_eq!(
382 get(&compute(&windows_inputs(8, 2)), "shared_buffers"),
383 Some("1GB")
384 );
385 assert_eq!(
386 get(&compute(&windows_inputs(16, 4)), "shared_buffers"),
387 Some("1GB")
388 );
389 assert_eq!(
390 get(&compute(&windows_inputs(64, 16)), "shared_buffers"),
391 Some("1GB")
392 );
393 }
394
395 #[test]
396 fn windows_omits_linux_only_settings() {
397 let s = compute(&windows_inputs(16, 8));
398 assert_eq!(get(&s, "effective_io_concurrency"), None);
399 assert_eq!(get(&s, "io_method"), None);
400 }
401
402 #[test]
403 fn windows_16gib_core_values() {
404 let s = compute(&windows_inputs(16, 4));
405 assert_eq!(get(&s, "effective_cache_size"), Some("3GB"));
407 assert_eq!(get(&s, "maintenance_work_mem"), Some("256MB"));
408 assert_eq!(get(&s, "max_wal_size"), Some("8GB"));
409 assert_eq!(get(&s, "random_page_cost"), Some("1.1"));
410 assert_eq!(get(&s, "huge_pages"), Some("off"));
411 }
412
413 #[test]
414 fn parallelism_gated_on_cpu_budget() {
415 let s = compute(&windows_inputs(16, 2));
417 assert_eq!(get(&s, "max_worker_processes"), None);
418 let s = compute(&windows_inputs(16, 8));
420 assert_eq!(get(&s, "max_worker_processes"), Some("4"));
421 assert_eq!(get(&s, "max_parallel_workers_per_gather"), Some("2"));
422 assert_eq!(get(&s, "max_parallel_workers"), Some("4"));
423 assert_eq!(get(&s, "max_parallel_maintenance_workers"), Some("2"));
424 }
425
426 #[test]
427 fn wal_compression_falls_back_without_lz4() {
428 let mut input = windows_inputs(16, 4);
429 input.pg_major = 16;
430 input.lz4_wal_supported = false;
431 assert_eq!(get(&compute(&input), "wal_compression"), Some("on"));
432 input.lz4_wal_supported = true;
433 assert_eq!(get(&compute(&input), "wal_compression"), Some("lz4"));
434 input.pg_major = 13;
435 input.lz4_wal_supported = true;
436 assert_eq!(get(&compute(&input), "wal_compression"), Some("on"));
437 }
438
439 #[test]
440 fn pre_pg10_windows_caps_shared_buffers() {
441 let mut input = windows_inputs(64, 8);
442 input.pg_major = 9;
443 assert_eq!(get(&compute(&input), "shared_buffers"), Some("512MB"));
445 assert_eq!(get(&compute(&input), "jit"), None);
447 assert_eq!(get(&compute(&input), "wal_compression"), None);
448 }
449
450 #[test]
451 fn temp_file_limit_emitted_when_present() {
452 let mut input = windows_inputs(16, 4);
453 input.temp_file_limit_kib = Some(50 * GIB);
454 assert_eq!(get(&compute(&input), "temp_file_limit"), Some("50GB"));
455 input.temp_file_limit_kib = None;
456 assert_eq!(get(&compute(&input), "temp_file_limit"), None);
457 }
458
459 #[test]
460 fn linux_32gib_matches_ops_tuning() {
461 let input = TuneInputs {
462 platform: Platform::Linux,
463 resources: HostResources {
464 total_ram_kib: 32 * GIB,
465 cpus: 8,
466 },
467 pg_major: 16,
468 max_connections: 100,
469 lz4_wal_supported: true,
470 temp_file_limit_kib: None,
471 };
472 let s = compute(&input);
473 assert_eq!(get(&s, "shared_buffers"), Some("7GB"));
475 assert_eq!(get(&s, "effective_cache_size"), Some("21GB"));
476 assert_eq!(get(&s, "random_page_cost"), Some("1.1"));
477 assert_eq!(get(&s, "effective_io_concurrency"), Some("200"));
478 assert_eq!(get(&s, "max_wal_size"), Some("8GB"));
479 assert_eq!(get(&s, "wal_compression"), Some("lz4"));
480 }
481
482 #[test]
483 fn render_kib_uses_largest_exact_unit() {
484 assert_eq!(render_kib(GIB), "1GB");
485 assert_eq!(render_kib(512 * MIB), "512MB");
486 assert_eq!(render_kib(32), "32kB");
487 assert_eq!(render_kib(1536 * MIB), "1536MB");
488 }
489}