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,
57 pub cache_ram_kib: u64,
62 pub cpus: u32,
64}
65
66#[derive(Debug, Clone, Copy)]
68pub struct TuneInputs {
69 pub platform: Platform,
70 pub resources: HostResources,
71 pub pg_major: u32,
73 pub max_connections: u32,
75 pub lz4_wal_supported: bool,
78 pub temp_file_limit_kib: Option<u64>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Setting {
86 pub key: &'static str,
87 pub value: String,
88}
89
90impl Setting {
91 fn new(key: &'static str, value: impl Into<String>) -> Self {
92 Self {
93 key,
94 value: value.into(),
95 }
96 }
97}
98
99fn budget_ram_kib(platform: Platform, total_ram_kib: u64) -> u64 {
105 if platform.is_windows() {
106 (total_ram_kib / 2).min(4 * GIB)
107 } else {
108 cache_budget_ram_kib(total_ram_kib)
109 }
110}
111
112fn cache_budget_ram_kib(total_ram_kib: u64) -> u64 {
119 if total_ram_kib >= 8 * GIB {
120 total_ram_kib - 4 * GIB
121 } else {
122 total_ram_kib / 2
123 }
124}
125
126fn budget_cpus(cpus: u32) -> u32 {
128 if cpus >= 8 {
129 cpus - 4
130 } else if cpus >= 5 {
131 4
132 } else if cpus >= 2 {
133 2
134 } else {
135 1
136 }
137}
138
139pub fn budget(platform: Platform, resources: HostResources) -> Budget {
141 Budget {
142 ram_kib: budget_ram_kib(platform, resources.total_ram_kib),
143 cache_ram_kib: cache_budget_ram_kib(resources.total_ram_kib),
144 cpus: budget_cpus(resources.cpus),
145 }
146}
147
148pub fn expected_shared_buffers_kib(budget: &Budget, platform: Platform, pg_major: u32) -> u64 {
150 let value = budget.ram_kib / 4;
151 if platform.is_windows() && pg_major < 10 {
152 value.min(512 * MIB)
153 } else {
154 value
155 }
156}
157
158pub fn expected_effective_cache_kib(budget: &Budget) -> u64 {
162 (budget.cache_ram_kib * 3) / 4
163}
164
165pub fn expected_maintenance_kib(budget: &Budget, platform: Platform, pg_major: u32) -> u64 {
167 let value = budget.ram_kib / 16;
168 let limit = if platform.is_windows() && pg_major <= 17 {
169 2 * GIB
170 } else {
171 8 * GIB
172 };
173 if value >= limit {
174 if platform.is_windows() && pg_major <= 17 {
175 limit - MIB
176 } else {
177 limit
178 }
179 } else {
180 value
181 }
182}
183
184fn expected_work_mem_kib(
185 budget: &Budget,
186 platform: Platform,
187 pg_major: u32,
188 max_connections: u32,
189) -> u64 {
190 let shared = expected_shared_buffers_kib(budget, platform, pg_major);
191 let parallel = if budget.cpus >= 4 { budget.cpus } else { 8 };
192 let divisor = u64::from(max_connections + parallel) * 3;
193 let mut value = (budget.ram_kib.saturating_sub(shared)) / divisor.max(1);
194 value = value.max(4 * MIB);
195 if platform.is_windows() && pg_major <= 17 {
196 value = value.min(2 * GIB - MIB);
197 }
198 value
199}
200
201fn expected_wal_buffers_kib(shared_buffers_kib: u64) -> u64 {
202 let raw = (shared_buffers_kib * 3) / 100;
203 let capped = raw.clamp(32, 16 * MIB);
204 if capped > 14 * MIB && capped < 16 * MIB {
206 16 * MIB
207 } else {
208 capped
209 }
210}
211
212fn render_kib(kib: u64) -> String {
215 if kib.is_multiple_of(GIB) {
216 format!("{}GB", kib / GIB)
217 } else if kib.is_multiple_of(MIB) {
218 format!("{}MB", kib / MIB)
219 } else {
220 format!("{kib}kB")
221 }
222}
223
224pub fn compute(input: &TuneInputs) -> Vec<Setting> {
226 let TuneInputs {
227 platform,
228 resources,
229 pg_major,
230 max_connections,
231 lz4_wal_supported,
232 temp_file_limit_kib,
233 } = *input;
234 let budget = budget(platform, resources);
235 let mut out = Vec::new();
236
237 let shared_buffers = expected_shared_buffers_kib(&budget, platform, pg_major);
238 out.push(Setting::new("max_connections", max_connections.to_string()));
239 out.push(Setting::new("shared_buffers", render_kib(shared_buffers)));
240 out.push(Setting::new(
241 "effective_cache_size",
242 render_kib(expected_effective_cache_kib(&budget)),
243 ));
244 out.push(Setting::new(
245 "maintenance_work_mem",
246 render_kib(expected_maintenance_kib(&budget, platform, pg_major)),
247 ));
248 out.push(Setting::new("checkpoint_completion_target", "0.9"));
249 out.push(Setting::new(
250 "wal_buffers",
251 render_kib(expected_wal_buffers_kib(shared_buffers)),
252 ));
253 out.push(Setting::new("default_statistics_target", "100"));
254 out.push(Setting::new("random_page_cost", "1.1"));
255
256 if !platform.is_windows() {
258 out.push(Setting::new("effective_io_concurrency", "200"));
259 }
260
261 out.push(Setting::new(
262 "work_mem",
263 render_kib(expected_work_mem_kib(
264 &budget,
265 platform,
266 pg_major,
267 max_connections,
268 )),
269 ));
270 out.push(Setting::new(
271 "huge_pages",
272 if shared_buffers >= 2 * GIB {
273 "try"
274 } else {
275 "off"
276 },
277 ));
278 out.push(Setting::new("min_wal_size", "2GB"));
279 out.push(Setting::new("max_wal_size", "8GB"));
280
281 if budget.cpus >= 4 {
282 let per_gather = budget.cpus.div_ceil(2).min(4);
283 out.push(Setting::new(
284 "max_worker_processes",
285 budget.cpus.to_string(),
286 ));
287 out.push(Setting::new(
288 "max_parallel_workers_per_gather",
289 per_gather.to_string(),
290 ));
291 if pg_major >= 10 {
292 out.push(Setting::new(
293 "max_parallel_workers",
294 budget.cpus.to_string(),
295 ));
296 }
297 if pg_major >= 11 {
298 out.push(Setting::new(
299 "max_parallel_maintenance_workers",
300 per_gather.to_string(),
301 ));
302 }
303 }
304
305 let autovacuum_max_workers = if budget.cpus >= 32 {
306 Some(5)
307 } else if budget.cpus >= 16 {
308 Some(4)
309 } else {
310 None
311 };
312 if let Some(workers) = autovacuum_max_workers {
313 out.push(Setting::new("autovacuum_max_workers", workers.to_string()));
314 }
315 if expected_maintenance_kib(&budget, platform, pg_major) >= 2 * GIB {
316 out.push(Setting::new("autovacuum_work_mem", "2GB"));
317 }
318
319 if pg_major >= 18 {
320 let io_workers = (budget.cpus / 4).clamp(3, 32);
321 if io_workers > 3 {
322 out.push(Setting::new("io_workers", io_workers.to_string()));
323 }
324 if !platform.is_windows() {
326 out.push(Setting::new("io_method", "io_uring"));
327 }
328 }
329
330 if pg_major >= 12 {
331 out.push(Setting::new("jit", "off"));
332 }
333
334 let wal_compression = if pg_major >= 15 {
335 if lz4_wal_supported {
336 Some("lz4")
337 } else {
338 Some("on")
339 }
340 } else if pg_major >= 10 {
341 Some("on")
342 } else {
343 None
344 };
345 if let Some(value) = wal_compression {
346 out.push(Setting::new("wal_compression", value));
347 }
348
349 if let Some(kib) = temp_file_limit_kib {
350 out.push(Setting::new("temp_file_limit", render_kib(kib)));
351 }
352
353 out
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn get<'a>(settings: &'a [Setting], key: &str) -> Option<&'a str> {
361 settings
362 .iter()
363 .find(|s| s.key == key)
364 .map(|s| s.value.as_str())
365 }
366
367 fn windows_inputs(total_gib: u64, cpus: u32) -> TuneInputs {
368 TuneInputs {
369 platform: Platform::Windows,
370 resources: HostResources {
371 total_ram_kib: total_gib * GIB,
372 cpus,
373 },
374 pg_major: 16,
375 max_connections: 100,
376 lz4_wal_supported: false,
377 temp_file_limit_kib: None,
378 }
379 }
380
381 #[test]
382 fn windows_budget_is_capped_at_4gib() {
383 assert_eq!(budget_ram_kib(Platform::Windows, 4 * GIB), 2 * GIB);
384 assert_eq!(budget_ram_kib(Platform::Windows, 8 * GIB), 4 * GIB);
385 assert_eq!(budget_ram_kib(Platform::Windows, 16 * GIB), 4 * GIB);
386 assert_eq!(budget_ram_kib(Platform::Windows, 64 * GIB), 4 * GIB);
387 }
388
389 #[test]
390 fn linux_budget_reserves_four_gib() {
391 assert_eq!(budget_ram_kib(Platform::Linux, 32 * GIB), 28 * GIB);
392 assert_eq!(budget_ram_kib(Platform::Linux, 4 * GIB), 2 * GIB);
393 }
394
395 #[test]
396 fn cache_budget_ignores_windows_ceiling() {
397 assert_eq!(cache_budget_ram_kib(4 * GIB), 2 * GIB);
400 assert_eq!(cache_budget_ram_kib(8 * GIB), 4 * GIB);
401 assert_eq!(cache_budget_ram_kib(16 * GIB), 12 * GIB);
402 assert_eq!(cache_budget_ram_kib(64 * GIB), 60 * GIB);
403 }
404
405 #[test]
406 fn windows_effective_cache_tracks_whole_box() {
407 assert_eq!(
410 get(&compute(&windows_inputs(16, 4)), "effective_cache_size"),
411 Some("9GB")
412 );
413 assert_eq!(
414 get(&compute(&windows_inputs(64, 16)), "effective_cache_size"),
415 Some("45GB")
416 );
417 }
418
419 #[test]
420 fn windows_shared_buffers_scale_with_budget() {
421 assert_eq!(
422 get(&compute(&windows_inputs(4, 2)), "shared_buffers"),
423 Some("512MB")
424 );
425 assert_eq!(
426 get(&compute(&windows_inputs(8, 2)), "shared_buffers"),
427 Some("1GB")
428 );
429 assert_eq!(
430 get(&compute(&windows_inputs(16, 4)), "shared_buffers"),
431 Some("1GB")
432 );
433 assert_eq!(
434 get(&compute(&windows_inputs(64, 16)), "shared_buffers"),
435 Some("1GB")
436 );
437 }
438
439 #[test]
440 fn windows_omits_linux_only_settings() {
441 let s = compute(&windows_inputs(16, 8));
442 assert_eq!(get(&s, "effective_io_concurrency"), None);
443 assert_eq!(get(&s, "io_method"), None);
444 }
445
446 #[test]
447 fn windows_16gib_core_values() {
448 let s = compute(&windows_inputs(16, 4));
449 assert_eq!(get(&s, "effective_cache_size"), Some("9GB"));
451 assert_eq!(get(&s, "maintenance_work_mem"), Some("256MB"));
452 assert_eq!(get(&s, "max_wal_size"), Some("8GB"));
453 assert_eq!(get(&s, "random_page_cost"), Some("1.1"));
454 assert_eq!(get(&s, "huge_pages"), Some("off"));
455 }
456
457 #[test]
458 fn parallelism_gated_on_cpu_budget() {
459 let s = compute(&windows_inputs(16, 2));
461 assert_eq!(get(&s, "max_worker_processes"), None);
462 let s = compute(&windows_inputs(16, 8));
464 assert_eq!(get(&s, "max_worker_processes"), Some("4"));
465 assert_eq!(get(&s, "max_parallel_workers_per_gather"), Some("2"));
466 assert_eq!(get(&s, "max_parallel_workers"), Some("4"));
467 assert_eq!(get(&s, "max_parallel_maintenance_workers"), Some("2"));
468 }
469
470 #[test]
471 fn wal_compression_falls_back_without_lz4() {
472 let mut input = windows_inputs(16, 4);
473 input.pg_major = 16;
474 input.lz4_wal_supported = false;
475 assert_eq!(get(&compute(&input), "wal_compression"), Some("on"));
476 input.lz4_wal_supported = true;
477 assert_eq!(get(&compute(&input), "wal_compression"), Some("lz4"));
478 input.pg_major = 13;
479 input.lz4_wal_supported = true;
480 assert_eq!(get(&compute(&input), "wal_compression"), Some("on"));
481 }
482
483 #[test]
484 fn pre_pg10_windows_caps_shared_buffers() {
485 let mut input = windows_inputs(64, 8);
486 input.pg_major = 9;
487 assert_eq!(get(&compute(&input), "shared_buffers"), Some("512MB"));
489 assert_eq!(get(&compute(&input), "jit"), None);
491 assert_eq!(get(&compute(&input), "wal_compression"), None);
492 }
493
494 #[test]
495 fn temp_file_limit_emitted_when_present() {
496 let mut input = windows_inputs(16, 4);
497 input.temp_file_limit_kib = Some(50 * GIB);
498 assert_eq!(get(&compute(&input), "temp_file_limit"), Some("50GB"));
499 input.temp_file_limit_kib = None;
500 assert_eq!(get(&compute(&input), "temp_file_limit"), None);
501 }
502
503 #[test]
504 fn linux_32gib_matches_ops_tuning() {
505 let input = TuneInputs {
506 platform: Platform::Linux,
507 resources: HostResources {
508 total_ram_kib: 32 * GIB,
509 cpus: 8,
510 },
511 pg_major: 16,
512 max_connections: 100,
513 lz4_wal_supported: true,
514 temp_file_limit_kib: None,
515 };
516 let s = compute(&input);
517 assert_eq!(get(&s, "shared_buffers"), Some("7GB"));
519 assert_eq!(get(&s, "effective_cache_size"), Some("21GB"));
520 assert_eq!(get(&s, "random_page_cost"), Some("1.1"));
521 assert_eq!(get(&s, "effective_io_concurrency"), Some("200"));
522 assert_eq!(get(&s, "max_wal_size"), Some("8GB"));
523 assert_eq!(get(&s, "wal_compression"), Some("lz4"));
524 }
525
526 #[test]
527 fn render_kib_uses_largest_exact_unit() {
528 assert_eq!(render_kib(GIB), "1GB");
529 assert_eq!(render_kib(512 * MIB), "512MB");
530 assert_eq!(render_kib(32), "32kB");
531 assert_eq!(render_kib(1536 * MIB), "1536MB");
532 }
533}