1use std::error::Error;
2use std::future::Future;
3
4use polars_buffer::Buffer;
5use polars_core::config::verbose;
6use polars_core::runtime::RAYON;
7use polars_utils::relaxed_cell::RelaxedCell;
8use tokio::sync::Semaphore;
9
10static CONCURRENCY_BUDGET: std::sync::OnceLock<(Semaphore, u32)> = std::sync::OnceLock::new();
11pub(super) const MAX_BUDGET_PER_REQUEST: usize = 10;
12
13pub trait GetSize {
14 fn size(&self) -> u64;
15}
16
17impl GetSize for Buffer<u8> {
18 fn size(&self) -> u64 {
19 self.len() as u64
20 }
21}
22
23impl<T: GetSize> GetSize for Vec<T> {
24 fn size(&self) -> u64 {
25 self.iter().map(|v| v.size()).sum()
26 }
27}
28
29impl<T: GetSize, E: Error> GetSize for Result<T, E> {
30 fn size(&self) -> u64 {
31 match self {
32 Ok(v) => v.size(),
33 Err(_) => 0,
34 }
35 }
36}
37
38#[cfg(feature = "cloud")]
39pub(crate) struct Size(u64);
40
41#[cfg(feature = "cloud")]
42impl GetSize for Size {
43 fn size(&self) -> u64 {
44 self.0
45 }
46}
47#[cfg(feature = "cloud")]
48impl From<u64> for Size {
49 fn from(value: u64) -> Self {
50 Self(value)
51 }
52}
53
54enum Optimization {
55 Step,
56 Accept,
57 Finished,
58}
59
60struct SemaphoreTuner {
61 previous_download_speed: u64,
62 last_tune: std::time::Instant,
63 downloaded: RelaxedCell<u64>,
64 download_time: RelaxedCell<u64>,
65 opt_state: Optimization,
66 increments: u32,
67}
68
69impl SemaphoreTuner {
70 fn new() -> Self {
71 Self {
72 previous_download_speed: 0,
73 last_tune: std::time::Instant::now(),
74 downloaded: RelaxedCell::from(0),
75 download_time: RelaxedCell::from(0),
76 opt_state: Optimization::Step,
77 increments: 0,
78 }
79 }
80 fn should_tune(&self) -> bool {
81 match self.opt_state {
82 Optimization::Finished => false,
83 _ => self.last_tune.elapsed().as_millis() > 350,
84 }
85 }
86
87 fn add_stats(&self, downloaded_bytes: u64, download_time: u64) {
88 self.downloaded.fetch_add(downloaded_bytes);
89 self.download_time.fetch_add(download_time);
90 }
91
92 fn increment(&mut self, semaphore: &Semaphore) {
93 semaphore.add_permits(1);
94 self.increments += 1;
95 }
96
97 fn tune(&mut self, semaphore: &'static Semaphore) -> bool {
98 let bytes_downloaded = self.downloaded.load();
99 let time_elapsed = self.download_time.load();
100 let download_speed = bytes_downloaded
101 .checked_div(time_elapsed)
102 .unwrap_or_default();
103
104 let increased = download_speed > self.previous_download_speed;
105 self.previous_download_speed = download_speed;
106 match self.opt_state {
107 Optimization::Step => {
108 self.increment(semaphore);
109 self.opt_state = Optimization::Accept
110 },
111 Optimization::Accept => {
112 if increased {
114 self.increment(semaphore);
116 }
118 else {
120 self.opt_state = Optimization::Finished;
121 FINISHED_TUNING.store(true);
122 if verbose() {
123 eprintln!(
124 "concurrency tuner finished after adding {} steps",
125 self.increments
126 )
127 }
128 return true;
130 }
131 },
132 Optimization::Finished => {},
133 }
134 self.last_tune = std::time::Instant::now();
135 false
137 }
138}
139static INCR: RelaxedCell<u64> = RelaxedCell::new_u64(0);
140static FINISHED_TUNING: RelaxedCell<bool> = RelaxedCell::new_bool(false);
141static PERMIT_STORE: std::sync::OnceLock<tokio::sync::RwLock<SemaphoreTuner>> =
142 std::sync::OnceLock::new();
143
144fn get_semaphore() -> &'static (Semaphore, u32) {
145 CONCURRENCY_BUDGET.get_or_init(|| {
146 let permits = std::env::var("POLARS_CONCURRENCY_BUDGET")
147 .map(|s| {
148 let budget = s.parse::<usize>().expect("integer");
149 FINISHED_TUNING.store(true);
150 budget
151 })
152 .unwrap_or_else(|_| std::cmp::max(RAYON.current_num_threads(), MAX_BUDGET_PER_REQUEST));
153 (Semaphore::new(permits), permits as u32)
154 })
155}
156
157pub fn get_concurrency_limit() -> u32 {
158 get_semaphore().1
159}
160
161pub async fn tune_with_concurrency_budget<F, Fut>(requested_budget: u32, callable: F) -> Fut::Output
162where
163 F: FnOnce() -> Fut,
164 Fut: Future,
165 Fut::Output: GetSize,
166{
167 let (semaphore, initial_budget) = get_semaphore();
168
169 assert!(requested_budget <= *initial_budget);
171
172 let _permit_acq = semaphore.acquire_many(requested_budget).await.unwrap();
175
176 let now = std::time::Instant::now();
177 let res = callable().await;
178
179 if FINISHED_TUNING.load() || res.size() == 0 {
180 return res;
181 }
182
183 let duration = now.elapsed().as_millis() as u64;
184 let permit_store = PERMIT_STORE.get_or_init(|| tokio::sync::RwLock::new(SemaphoreTuner::new()));
185
186 let Ok(tuner) = permit_store.try_read() else {
187 return res;
188 };
189 tuner.add_stats(res.size(), duration);
191
192 if !tuner.should_tune() {
194 return res;
195 }
196 drop(tuner);
198
199 if !INCR.fetch_add(1).is_multiple_of(5) {
201 return res;
202 }
203 let Ok(mut tuner) = permit_store.try_write() else {
205 return res;
206 };
207 let finished = tuner.tune(semaphore);
208 if finished {
209 drop(_permit_acq);
210 let undo = semaphore.acquire().await.unwrap();
212 std::mem::forget(undo)
213 }
214 res
215}
216
217pub async fn with_concurrency_budget<F, Fut>(requested_budget: u32, callable: F) -> Fut::Output
218where
219 F: FnOnce() -> Fut,
220 Fut: Future,
221{
222 let (semaphore, initial_budget) = get_semaphore();
223
224 assert!(requested_budget <= *initial_budget);
226
227 let _permit_acq = semaphore.acquire_many(requested_budget).await.unwrap();
230
231 callable().await
232}