// Copyright (c) 2023-2025 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::index::Parallelism; use pgrx::{pg_sys, GucContext, GucFlags, GucRegistry, GucSetting}; use std::num::NonZeroUsize; /// Allows the user to toggle the use of our "ParadeDB Custom Scan". The default is `true`. static ENABLE_CUSTOM_SCAN: GucSetting = GucSetting::::new(true); /// The `PER_TUPLE_COST` is an arbitrary value that needs to be really high. In fact, we default /// to one hundred million. /// /// The reason for this is we really do not want Postgres to choose a plan where the `@@@` operator /// is used in a sequential scan, filter, or recheck condition... unless of course there's no /// other way to solve the query. /// /// This value is a multiplier that Postgres applies to the estimated row count any given `@@@` /// query clause will return. In our case, higher is better. /// /// Our IAM impl has its own costing functions that don't use this GUC and provide sensible estimates /// for the overall IndexScan. That plus this help to persuade Postgres to use our IAM whenever /// it logically can. static PER_TUPLE_COST: GucSetting = GucSetting::::new(100_000_000.0); /// Should we log the progress of the CREATE INDEX operation? Default is `false`. static LOG_CREATE_INDEX_PROGRESS: GucSetting = GucSetting::::new(false); /// How many threads should tantivy use during CREATE INDEX? static CREATE_INDEX_PARALLELISM: GucSetting = GucSetting::::new(0); /// How much memory should tantivy use during CREATE INDEX. This value is decided to each indexing /// thread. So if there's 10 threads and this value is 100MB, then a total of 1GB will be allocated. static CREATE_INDEX_MEMORY_BUDGET: GucSetting = GucSetting::::new(1024); /// How many threads should tantivy use during a regular INSERT/UPDATE/COPY statement? static STATEMENT_PARALLELISM: GucSetting = GucSetting::::new(1); /// How much memory should tantivy use during a regular INSERT/UPDATE/COPY statement? This value is decided to each indexing /// thread. So if there's 10 threads and this value is 100MB, then a total of 1GB will be allocated. static STATEMENT_MEMORY_BUDGET: GucSetting = GucSetting::::new(1024); pub fn init() { // Note that Postgres is very specific about the naming convention of variables. // They must be namespaced... we use 'paradedb.' below. GucRegistry::define_bool_guc( "paradedb.enable_custom_scan", "Enable ParadeDB's custom scan", "Enable ParadeDB's custom scan", &ENABLE_CUSTOM_SCAN, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_float_guc( "paradedb.per_tuple_cost", "Arbitrary multiplier for the cost of retrieving a tuple from a USING bm25 index outside of an IndexScan", "Default is 100,000,000.0. It is very expensive to use a USING bm25 index in the wrong query plan", &PER_TUPLE_COST, 0.0, f64::MAX, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_bool_guc( "paradedb.log_create_index_progress", "Log CREATE INDEX progress every 100,000 rows", "", &LOG_CREATE_INDEX_PROGRESS, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_int_guc( "paradedb.create_index_parallelism", "The number of threads to use when creating an index", "Default is 0, which means a thread for as many cores in the machine", &CREATE_INDEX_PARALLELISM, 0, std::thread::available_parallelism() .expect("your computer should have at least one core") .get() .try_into() .expect("your computer has too many cores"), GucContext::Userset, GucFlags::default(), ); GucRegistry::define_int_guc( "paradedb.create_index_memory_budget", "The amount of memory to allocate to 1 thread during indexing", "Default is `1GB`, which is allocated to each thread defined by `paradedb.create_index_parallelism`", &CREATE_INDEX_MEMORY_BUDGET, 0, i32::MAX, GucContext::Userset, GucFlags::UNIT_MB, ); GucRegistry::define_int_guc( "paradedb.statement_parallelism", "The number of threads to use when indexing during an INSERT/UPDATE/COPY statement", "Default is 1. Recommended value is generally 1. Value of zero means a thread for as many cores in the machine", &STATEMENT_PARALLELISM, 0, std::thread::available_parallelism().expect("your computer should have at least one core").get().try_into().expect("your computer has too many cores"), GucContext::Userset, GucFlags::default(), ); GucRegistry::define_int_guc( "paradedb.statement_memory_budget", "The amount of memory to allocate to 1 thread during an INSERT/UPDATE/COPY statement", "Default is `1GB`, which is allocated to each thread defined by `paradedb.statement_parallelism`", &STATEMENT_MEMORY_BUDGET, 0, i32::MAX, GucContext::Userset, GucFlags::UNIT_MB, ); } pub fn enable_custom_scan() -> bool { ENABLE_CUSTOM_SCAN.get() } pub fn per_tuple_cost() -> f64 { PER_TUPLE_COST.get() } pub fn log_create_index_progress() -> bool { LOG_CREATE_INDEX_PROGRESS.get() } pub fn create_index_parallelism() -> NonZeroUsize { adjust_nthreads(CREATE_INDEX_PARALLELISM.get()) } pub fn create_index_memory_budget() -> usize { adjust_budget(CREATE_INDEX_MEMORY_BUDGET.get(), create_index_parallelism()) } pub fn statement_parallelism() -> NonZeroUsize { adjust_nthreads(STATEMENT_PARALLELISM.get()) } pub fn statement_memory_budget() -> usize { adjust_budget(STATEMENT_MEMORY_BUDGET.get(), statement_parallelism()) } fn adjust_nthreads(nthreads: i32) -> NonZeroUsize { let nthreads = if nthreads <= 0 { std::thread::available_parallelism() .expect("your computer should have at least one core") .get() } else { nthreads as usize }; unsafe { // SAFETY: we ensured above that nthreads is > 0 NonZeroUsize::new_unchecked(nthreads) } } fn adjust_budget(per_thread_budget: i32, parallelism: Parallelism) -> usize { // NB: These limits come from [`tantivy::index_writer::MEMORY_BUDGET_NUM_BYTES_MAX`], which is not publicly exposed mod limits { // Size of the margin for the `memory_arena`. A segment is closed when the remaining memory // in the `memory_arena` goes below MARGIN_IN_BYTES. pub const MARGIN_IN_BYTES: usize = 1_000_000; // We impose the memory per thread to be at least 15 MB, as the baseline consumption is 12MB. pub const MEMORY_BUDGET_NUM_BYTES_MIN: usize = ((MARGIN_IN_BYTES as u32) * 15u32) as usize; pub const MEMORY_BUDGET_NUM_BYTES_MAX: usize = u32::MAX as usize - MARGIN_IN_BYTES; } let per_thread_budget = if per_thread_budget <= 0 { // value is unset, so we'll use the maintenance_work_mem, divided between the parallelism value let mwm_as_bytes = unsafe { // SAFETY: Postgres sets maintenance_work_mem when it starts up pg_sys::maintenance_work_mem as usize * 1024 // convert from kilobytes to bytes }; mwm_as_bytes / parallelism.get() } else { per_thread_budget as usize * 1024 * 1024 // convert from megabytes to bytes }; // clamp the per_thread_budget to the min/max values let per_thread_budget = per_thread_budget.clamp( limits::MEMORY_BUDGET_NUM_BYTES_MIN, limits::MEMORY_BUDGET_NUM_BYTES_MAX - 1, ); per_thread_budget * parallelism.get() }