/build/source/nativelink-worker/src/capacity.rs
Line | Count | Source |
1 | | // Copyright 2026 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | //! What this worker can see of its own CPU and memory, and what it should |
16 | | //! advertise from that. |
17 | | //! |
18 | | //! Every deployment so far typed the advertisement by hand next to the pod's |
19 | | //! limits, and the two drifted: chinchilla advertised 16 cores and 60 GiB |
20 | | //! inside 15-core, 56 GiB limits. The limit is the number the kernel |
21 | | //! enforces, so the advertisement is derived from it here. |
22 | | |
23 | | use core::hash::BuildHasher; |
24 | | use std::collections::HashMap; |
25 | | use std::path::{Path, PathBuf}; |
26 | | |
27 | | use nativelink_config::cas_server::{ |
28 | | CapacityConfig, CpuUnit, MemoryEnforcement, ResourceEnforcementConfig, WorkerProperty, |
29 | | }; |
30 | | use nativelink_error::{Code, Error, make_err}; |
31 | | use tracing::{info, warn}; |
32 | | |
33 | | /// CPU and memory as the cgroup (or, failing a limit, the host) reports them. |
34 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
35 | | pub struct ObservedCapacity { |
36 | | pub cpu_millicores: u64, |
37 | | pub memory_kb: u64, |
38 | | } |
39 | | |
40 | | /// `cpu.max` is `"<quota> <period>"` in microseconds, or `"max <period>"` when |
41 | | /// unlimited; unlimited means the host's cores. |
42 | 4 | pub fn parse_cpu_max(cpu_max: &str, host_cpus: u64) -> Option<u64> { |
43 | 4 | let mut fields = cpu_max.split_whitespace(); |
44 | 4 | let quota = fields.next()?0 ; |
45 | 4 | if quota == "max" { |
46 | 1 | return Some(host_cpus.saturating_mul(1000)); |
47 | 3 | } |
48 | 3 | let quota2 : u642 = quota.parse().ok()?1 ; |
49 | 2 | let period: u64 = fields.next()?0 .parse().ok()?0 ; |
50 | 2 | if period == 0 { |
51 | 1 | return None; |
52 | 1 | } |
53 | 1 | Some(quota.saturating_mul(1000) / period) |
54 | 4 | } |
55 | | |
56 | | /// `memory.max` is bytes, or `"max"` when unlimited; unlimited means the |
57 | | /// host's memory. |
58 | 3 | pub fn parse_memory_max(memory_max: &str, host_memory_kb: u64) -> Option<u64> { |
59 | 3 | let value = memory_max.trim(); |
60 | 3 | if value == "max" { |
61 | 1 | return Some(host_memory_kb); |
62 | 2 | } |
63 | 2 | value.parse::<u64>().ok().map(|bytes| bytes1 / 1024) |
64 | 3 | } |
65 | | |
66 | | /// The number after `key` on the first line that starts with it, as |
67 | | /// `/proc/meminfo` (`MemTotal: 65536000 kB`) and a cgroup's `memory.stat` |
68 | | /// (`inactive_file 18672467968`) both lay them out. A key without its own |
69 | | /// colon must be followed by whitespace, so `inactive_file` does not match |
70 | | /// `inactive_file_extra`. |
71 | 69.0k | fn keyed_value(text: &str, key: &str) -> Option<u64> { |
72 | 206k | text.lines()69.0k .find_map69.0k (|line| { |
73 | 206k | let rest69.0k = line.strip_prefix(key)?137k ; |
74 | 69.0k | if !key.ends_with(':') && !rest.starts_with(char::is_whitespace)16 { |
75 | 2 | return None; |
76 | 69.0k | } |
77 | 69.0k | rest.split_whitespace().next()?0 .parse().ok() |
78 | 206k | }) |
79 | 69.0k | } |
80 | | |
81 | | /// `MemTotal` from `/proc/meminfo`, in KiB. |
82 | 85 | pub fn parse_meminfo_total_kb(meminfo: &str) -> Option<u64> { |
83 | 85 | keyed_value(meminfo, "MemTotal:") |
84 | 85 | } |
85 | | |
86 | | /// The headroom the advertisement is divided by: the block's own figure |
87 | | /// when set, else the enforcement headroom while memory enforcement is on, |
88 | | /// so the two numbers cannot drift apart, else nothing. |
89 | 4 | pub fn memory_headroom_percent( |
90 | 4 | config: &CapacityConfig, |
91 | 4 | enforcement: Option<&ResourceEnforcementConfig>, |
92 | 4 | ) -> u64 { |
93 | 4 | config.memory_headroom_percent.unwrap_or_else(|| {3 |
94 | 3 | enforcement |
95 | 3 | .filter(|enforcement| enforcement.memory2 == MemoryEnforcement::Soft2 ) |
96 | 3 | .map_or(0, |enforcement| enforcement.memory_headroom_percent) |
97 | 3 | }) |
98 | 4 | } |
99 | | |
100 | | /// The observed capacity less the worker's own share, memory divided by |
101 | | /// `memory_headroom_percent`. Returns `(cpu, memory_kb)` with the CPU on |
102 | | /// the scale `cpu_unit` names: whole cores rounded down, so a 14-core pod |
103 | | /// keeping one core back advertises `13`, not `13000`, to a scheduler |
104 | | /// whose actions ask for `cpu_count=1`. |
105 | 9 | pub const fn advertised( |
106 | 9 | observed: ObservedCapacity, |
107 | 9 | config: &CapacityConfig, |
108 | 9 | memory_headroom_percent: u64, |
109 | 9 | ) -> (u64, u64) { |
110 | 9 | let cpu_millicores = observed |
111 | 9 | .cpu_millicores |
112 | 9 | .saturating_sub(config.overhead_cpu_millicores); |
113 | 9 | let cpu = match config.cpu_unit { |
114 | 7 | CpuUnit::Cores => cpu_millicores / 1000, |
115 | 2 | CpuUnit::Millicores => cpu_millicores, |
116 | | }; |
117 | 9 | let memory = observed |
118 | 9 | .memory_kb |
119 | 9 | .saturating_sub(config.overhead_memory_kb) |
120 | 9 | .saturating_mul(100) |
121 | 9 | / (100 + memory_headroom_percent); |
122 | 9 | (cpu, memory) |
123 | 9 | } |
124 | | |
125 | | /// The largest memory reservation the scheduler can match to this worker: |
126 | | /// what it advertises under `property`, from the cgroup when the capacity |
127 | | /// block sets that property and the cgroup was read, else from the static |
128 | | /// platform properties. `None` when the worker advertises no such figure. |
129 | 5 | pub fn advertised_memory_kb<S: BuildHasher>( |
130 | 5 | capacity: Option<&CapacityConfig>, |
131 | 5 | memory_headroom_percent: u64, |
132 | 5 | observed: Option<ObservedCapacity>, |
133 | 5 | properties: &HashMap<String, WorkerProperty, S>, |
134 | 5 | property: &str, |
135 | 5 | ) -> Option<u64> { |
136 | 5 | if let (Some(config3 ), Some(observed3 )) = (capacity, observed) |
137 | 3 | && config.memory_property_name == property |
138 | | { |
139 | 2 | return Some(advertised(observed, config, memory_headroom_percent).1); |
140 | 3 | } |
141 | 3 | match properties.get(property)?1 { |
142 | 2 | WorkerProperty::Values(values) => values.iter().filter_map(|v| v.parse().ok()).max(), |
143 | 0 | WorkerProperty::QueryCmd(_) => None, |
144 | | } |
145 | 5 | } |
146 | | |
147 | | /// What to do when the cgroup cannot be read: the configured properties |
148 | | /// stand if they carry both numbers, since an operator who kept them has a |
149 | | /// worker that still takes work; a worker with neither would register and |
150 | | /// then satisfy no action that asks for CPU or memory, idling for good on |
151 | | /// one warning line, so that is refused. |
152 | 3 | pub fn without_cgroup<S: BuildHasher>( |
153 | 3 | config: &CapacityConfig, |
154 | 3 | properties: &HashMap<String, WorkerProperty, S>, |
155 | 3 | ) -> Result<(), Error> { |
156 | 3 | let missing: Vec<&str> = [&config.cpu_property_name, &config.memory_property_name] |
157 | 3 | .into_iter() |
158 | 6 | .filter3 (|name| !properties.contains_key(name.as_str())) |
159 | 3 | .map(String::as_str) |
160 | 3 | .collect(); |
161 | 3 | if missing.is_empty() { |
162 | 1 | warn!( |
163 | | "capacity is configured but no cgroup v2 limit applies to the worker (see the line above for the cgroup looked at); advertising the configured platform_properties instead" |
164 | | ); |
165 | 1 | return Ok(()); |
166 | 2 | } |
167 | 2 | Err(make_err!( |
168 | 2 | Code::FailedPrecondition, |
169 | 2 | "capacity is configured but no cgroup v2 limit applies to the worker (not Linux, cgroup v1, no permission, or no limit at any level; see the line above), and platform_properties carries no {}: set them, or drop the capacity block", |
170 | 2 | missing.join(" or ") |
171 | 2 | )) |
172 | 3 | } |
173 | | |
174 | | /// The cgroup v2 path of this process from `/proc/self/cgroup`: the `0::` |
175 | | /// line, which is `/` inside a container with its own cgroup namespace and |
176 | | /// the full `/kubepods.slice/.../cri-containerd-<id>.scope` path in one that |
177 | | /// shares the host's, as a privileged container does. |
178 | 71.0k | pub fn parse_self_cgroup(self_cgroup: &str) -> Option<&str> { |
179 | 71.0k | self_cgroup |
180 | 71.0k | .lines() |
181 | 71.0k | .find_map(|line| line.strip_prefix("0::")) |
182 | 71.0k | .map(str::trim) |
183 | 71.0k | .filter(|path| path71.0k .starts_with71.0k ('/')) |
184 | 71.0k | } |
185 | | |
186 | | /// Where this process's own `cpu.max`, `memory.max` and `memory.current` |
187 | | /// live: the mount root joined with the path from `/proc/self/cgroup` when |
188 | | /// that directory exists under the mount, the mount root otherwise. A |
189 | | /// container with a private cgroup namespace sees itself at the root, so its |
190 | | /// path is `/` and both agree. A privileged container sees the host's tree, |
191 | | /// where the root's files are the host's (or absent, as on a systemd host), |
192 | | /// and its own numbers sit further down. |
193 | 71.0k | pub fn resolve_cgroup_dir( |
194 | 71.0k | mount_root: &Path, |
195 | 71.0k | self_cgroup: Option<&str>, |
196 | 71.0k | is_dir: impl Fn(&Path) -> bool, |
197 | 71.0k | ) -> PathBuf { |
198 | 71.0k | if let Some(path71.0k ) = self_cgroup.and_then(parse_self_cgroup) { |
199 | 71.0k | let own = mount_root.join(path.trim_start_matches('/')); |
200 | 71.0k | if is_dir(&own) { |
201 | 2 | return own; |
202 | 71.0k | } |
203 | 1 | } |
204 | 71.0k | mount_root.to_path_buf() |
205 | 71.0k | } |
206 | | |
207 | | /// This process's cgroup directory on the live system. |
208 | 71.0k | fn cgroup_dir() -> PathBuf { |
209 | 71.0k | let self_cgroup = std::fs::read_to_string("/proc/self/cgroup").ok(); |
210 | 71.0k | resolve_cgroup_dir(Path::new("/sys/fs/cgroup"), self_cgroup.as_deref(), |dir| { |
211 | 71.0k | dir.join("memory.max").is_file() |
212 | 71.0k | }) |
213 | 71.0k | } |
214 | | |
215 | | /// The `avg10` of the `full` line of a cgroup v2 `memory.pressure` file: |
216 | | /// the share of the last ten seconds (0 to 100) during which every task in the |
217 | | /// cgroup was stalled on memory. `None` when the text has no such line. |
218 | 4 | pub fn parse_pressure_full_avg10(text: &str) -> Option<f64> { |
219 | 4 | text.lines().find_map(|line| { |
220 | 4 | let rest2 = line.strip_prefix("full")?2 ; |
221 | 2 | rest.split_whitespace() |
222 | 2 | .find_map(|field| field.strip_prefix("avg10=")) |
223 | 2 | .and_then(|value| value.parse().ok()) |
224 | 4 | }) |
225 | 4 | } |
226 | | |
227 | | /// The `memory.pressure` file that describes this worker: the one in the |
228 | | /// nearest limited cgroup (the limit the kernel enforces, see |
229 | | /// [`find_limit`]), else the one in the worker's own cgroup directory. |
230 | | /// `None` where neither exists: cgroup v1, a kernel without PSI, or a |
231 | | /// cgroup root, which has no `memory.pressure`. |
232 | | #[cfg(target_os = "linux")] |
233 | 3 | pub fn memory_pressure_file() -> Option<PathBuf> { |
234 | 3 | let mount_root = Path::new("/sys/fs/cgroup"); |
235 | 3 | let own = cgroup_dir(); |
236 | 3 | let limited = find_limit(&own, mount_root, "memory.max", read_file).map(|(dir, _)| dir); |
237 | 3 | [limited, Some(own)] |
238 | 3 | .into_iter() |
239 | 3 | .flatten() |
240 | 3 | .map(|dir| dir.join("memory.pressure")) |
241 | 3 | .find(|path| path.is_file()) |
242 | 3 | } |
243 | | |
244 | | /// Whether a `cpu.max` or `memory.max` value is "no limit": `max`, or for |
245 | | /// `cpu.max` `max <period>`. |
246 | 8 | fn is_unlimited(value: &str) -> bool { |
247 | 8 | value.split_whitespace().next() == Some("max") |
248 | 8 | } |
249 | | |
250 | | /// The first limit for `file` from `dir` up to and including `mount_root`, |
251 | | /// with the directory it was found in. The process's own cgroup is often |
252 | | /// an unlimited child of a limited parent (a systemd scope in a pod, a |
253 | | /// split cgroup, a privileged container under `kubepods.slice`), and the |
254 | | /// parent's limit is the one the kernel enforces on it. `None` when every |
255 | | /// level says `max` or nothing is readable: no limit applies. |
256 | 69.0k | pub fn find_limit( |
257 | 69.0k | dir: &Path, |
258 | 69.0k | mount_root: &Path, |
259 | 69.0k | file: &str, |
260 | 69.0k | read: impl Fn(&Path) -> Option<String>, |
261 | 69.0k | ) -> Option<(PathBuf, String)> { |
262 | 69.0k | let mut at = dir; |
263 | | loop { |
264 | 69.1k | if let Some(value8 ) = read(&at.join(file)) |
265 | 8 | && !is_unlimited(value.trim()) |
266 | | { |
267 | 3 | return Some((at.to_path_buf(), value)); |
268 | 69.0k | } |
269 | 69.0k | if at == mount_root || !9 at9 .starts_with(mount_root) { |
270 | 69.0k | return None; |
271 | 9 | } |
272 | 9 | at = at.parent()?0 ; |
273 | | } |
274 | 69.0k | } |
275 | | |
276 | 142k | fn read_file(path: &Path) -> Option<String> { |
277 | 142k | std::fs::read_to_string(path).ok() |
278 | 142k | } |
279 | | |
280 | | /// Reads the limits that apply to the worker: for CPU and for memory, the |
281 | | /// nearest limit from its own cgroup up to the mount root. `None` where no |
282 | | /// limit applies at any level (not Linux, cgroup v1, no permission, or a |
283 | | /// host where nothing is limited), so the configured properties stand. |
284 | 82 | pub fn observe_cgroup() -> Option<ObservedCapacity> { |
285 | 82 | let host_cpus = std::thread::available_parallelism().map_or(1, |n| n.get() as u64); |
286 | 82 | let host_memory_kb = std::fs::read_to_string("/proc/meminfo") |
287 | 82 | .ok() |
288 | 82 | .and_then(|meminfo| parse_meminfo_total_kb(&meminfo)) |
289 | 82 | .unwrap_or(0); |
290 | 82 | let mount_root = Path::new("/sys/fs/cgroup"); |
291 | 82 | let dir = cgroup_dir(); |
292 | 82 | let cpu = find_limit(&dir, mount_root, "cpu.max", read_file); |
293 | 82 | let memory = find_limit(&dir, mount_root, "memory.max", read_file); |
294 | 82 | if cpu.is_none() && memory.is_none() { |
295 | 82 | warn!( |
296 | 82 | cgroup = %dir.display(), |
297 | | "no cgroup v2 cpu.max or memory.max limit from this cgroup up to the mount root; keeping the configured platform_properties" |
298 | | ); |
299 | 82 | return None; |
300 | 0 | } |
301 | 0 | let cpu_max = cpu.map_or_else(|| "max".to_string(), |(_, value)| value); |
302 | 0 | let memory_max = memory.map_or_else(|| "max".to_string(), |(_, value)| value); |
303 | | Some(ObservedCapacity { |
304 | 0 | cpu_millicores: parse_cpu_max(&cpu_max, host_cpus)?, |
305 | 0 | memory_kb: parse_memory_max(&memory_max, host_memory_kb)?, |
306 | | }) |
307 | 82 | } |
308 | | |
309 | | /// Sets the CPU and memory properties to what the cgroup allows. When |
310 | | /// nothing can be read, the configured properties stand if they carry both, |
311 | | /// and the worker fails to start if they do not. Returns what was |
312 | | /// advertised. |
313 | 0 | pub fn apply<S: BuildHasher>( |
314 | 0 | config: &CapacityConfig, |
315 | 0 | memory_headroom_percent: u64, |
316 | 0 | properties: &mut HashMap<String, WorkerProperty, S>, |
317 | 0 | ) -> Result<Option<(u64, u64)>, Error> { |
318 | 0 | let Some(observed) = observe_cgroup() else { |
319 | 0 | without_cgroup(config, properties)?; |
320 | 0 | return Ok(None); |
321 | | }; |
322 | 0 | let (cpu, memory_kb) = advertised(observed, config, memory_headroom_percent); |
323 | 0 | properties.insert( |
324 | 0 | config.cpu_property_name.clone(), |
325 | 0 | WorkerProperty::Values(vec![cpu.to_string()]), |
326 | | ); |
327 | 0 | properties.insert( |
328 | 0 | config.memory_property_name.clone(), |
329 | 0 | WorkerProperty::Values(vec![memory_kb.to_string()]), |
330 | | ); |
331 | 0 | info!( |
332 | | observed_cpu_millicores = observed.cpu_millicores, |
333 | | observed_memory_kb = observed.memory_kb, |
334 | | cpu, |
335 | | cpu_unit = ?config.cpu_unit, |
336 | | memory_kb, |
337 | | memory_headroom_percent, |
338 | | "Advertising capacity from the cgroup" |
339 | | ); |
340 | 0 | Ok(Some((cpu, memory_kb))) |
341 | 0 | } |
342 | | |
343 | 18 | pub fn parse_memory_current_kb(memory_current: &str) -> Option<u64> { |
344 | 18 | memory_current |
345 | 18 | .trim() |
346 | 18 | .parse::<u64>() |
347 | 18 | .ok() |
348 | 18 | .map(|bytes| bytes16 / 1024) |
349 | 18 | } |
350 | | |
351 | | /// `MemAvailable` from `/proc/meminfo`, in KiB. |
352 | 68.9k | pub fn parse_meminfo_available_kb(meminfo: &str) -> Option<u64> { |
353 | 68.9k | keyed_value(meminfo, "MemAvailable:") |
354 | 68.9k | } |
355 | | |
356 | | /// The page cache in a cgroup's `memory.stat` the kernel can drop at once, |
357 | | /// in KiB: `inactive_file`, the cache it reclaims first and what kubelet |
358 | | /// leaves out of the working set it evicts on, less `file_dirty` and |
359 | | /// `file_writeback`, which cannot be dropped until they reach the disk |
360 | | /// (kubelet does not take those off, so this reads up to that much lower). |
361 | | /// Dirty pages may sit on the active list instead; taking all of them off |
362 | | /// the inactive one errs towards less free, never more. `None` without an |
363 | | /// `inactive_file` line. |
364 | 11 | pub fn parse_memory_stat_reclaimable_kb(memory_stat: &str) -> Option<u64> { |
365 | 11 | let inactive_file9 = keyed_value(memory_stat, "inactive_file")?2 ; |
366 | 9 | let dirty = keyed_value(memory_stat, "file_dirty").unwrap_or(0); |
367 | 9 | let writeback = keyed_value(memory_stat, "file_writeback").unwrap_or(0); |
368 | 9 | Some( |
369 | 9 | inactive_file |
370 | 9 | .saturating_sub(dirty) |
371 | 9 | .saturating_sub(writeback) |
372 | 9 | / 1024, |
373 | 9 | ) |
374 | 11 | } |
375 | | |
376 | | /// Memory the worker could still give an action: the cgroup limit less its |
377 | | /// working set (current usage less the page cache the kernel can drop) |
378 | | /// when there is a limit, the host's `MemAvailable` when there is not, |
379 | | /// nothing when neither can be read. `memory.current` charges every page |
380 | | /// of file cache the worker's I/O has touched, and the kernel only reclaims |
381 | | /// it near the limit, so a busy worker's usage sits just under its limit |
382 | | /// with little of it in use. |
383 | 11 | pub const fn free_memory_kb_from( |
384 | 11 | limit_kb: Option<u64>, |
385 | 11 | current_kb: Option<u64>, |
386 | 11 | reclaimable_kb: Option<u64>, |
387 | 11 | available_kb: Option<u64>, |
388 | 11 | ) -> Option<u64> { |
389 | 11 | match (limit_kb, current_kb) { |
390 | 9 | (Some(limit), Some(current)) => { |
391 | 9 | let reclaimable = match reclaimable_kb { |
392 | 6 | Some(kb) => kb, |
393 | 3 | None => 0, |
394 | | }; |
395 | 9 | Some(limit.saturating_sub(current.saturating_sub(reclaimable))) |
396 | | } |
397 | 2 | _ => available_kb, |
398 | | } |
399 | 11 | } |
400 | | |
401 | | /// Free memory measured against `limited`, the nearest limited cgroup and |
402 | | /// its `memory.max` as `find_limit` returns them, with `read` for the |
403 | | /// files; the host's `MemAvailable` when there is no limit. |
404 | 68.9k | pub fn free_memory_kb_with( |
405 | 68.9k | limited: Option<&(PathBuf, String)>, |
406 | 68.9k | read: impl Fn(&Path) -> Option<String>, |
407 | 68.9k | ) -> Option<u64> { |
408 | 68.9k | let Some((dir4 , max4 )) = limited else { |
409 | 68.9k | return read(Path::new("/proc/meminfo")) |
410 | 68.9k | .and_then(|meminfo| parse_meminfo_available_kb(&meminfo)); |
411 | | }; |
412 | 4 | let read_current = |
413 | 8 | || read(&dir.join("memory.current")).and_then(|current| parse_memory_current_kb6 (¤t6 )); |
414 | | // The usage is read on both sides of the cache, and the larger taken. |
415 | | // That mitigates the common races, cache that grows during the read or |
416 | | // is reclaimed during it, but the three reads are not one snapshot: |
417 | | // cache that grows at the `memory.stat` read and is reclaimed before |
418 | | // the second usage read is missed by both usage reads yet subtracted, |
419 | | // so a transient cache peak can report more free than there is. The |
420 | | // cache figure also comes from per-CPU counters the kernel flushes in |
421 | | // batches, so it can run ahead of what is there by a few MiB per CPU. |
422 | 4 | let current_before = read_current(); |
423 | 4 | let reclaimable_kb = |
424 | 4 | read(&dir.join("memory.stat")).and_then(|stat| parse_memory_stat_reclaimable_kb(&stat)); |
425 | 4 | let current_kb = current_before.max(read_current()); |
426 | 4 | match (parse_memory_current_kb(max.trim()), current_kb) { |
427 | 3 | (Some(limit_kb), Some(current_kb)) => { |
428 | 3 | free_memory_kb_from(Some(limit_kb), Some(current_kb), reclaimable_kb, None) |
429 | | } |
430 | 1 | _ => read(Path::new("/proc/meminfo")) |
431 | 1 | .and_then(|meminfo| parse_meminfo_available_kb(&meminfo)), |
432 | | } |
433 | 68.9k | } |
434 | | |
435 | | /// Whether `dir`, the worker's own cgroup, carries a memory limit of its |
436 | | /// own with readable usage: then nothing outside the worker gives memory |
437 | | /// back, so what it reads free while idle is the most it will read. A limit |
438 | | /// only on an ancestor (a shared `kubepods.slice`, say) is shared with |
439 | | /// other workloads, which can release memory, and without readable usage |
440 | | /// free memory falls back to the host's `MemAvailable`; neither counts. |
441 | 2.03k | pub fn own_memory_limit_in(dir: &Path, read: impl Fn(&Path) -> Option<String>) -> bool { |
442 | 2.03k | let limited = read(&dir.join("memory.max")) |
443 | 2.03k | .is_some_and(|max| parse_memory_current_kb3 (max.trim()).is_some3 ()); |
444 | 2.03k | let usage = read(&dir.join("memory.current")) |
445 | 2.03k | .is_some_and(|current| parse_memory_current_kb(¤t)3 .is_some3 ()); |
446 | 2.03k | limited && usage2 |
447 | 2.03k | } |
448 | | |
449 | | /// `own_memory_limit_in` for this process's cgroup. |
450 | | #[cfg(target_os = "linux")] |
451 | 2.03k | pub fn memory_is_limited() -> bool { |
452 | 2.03k | own_memory_limit_in(&cgroup_dir(), read_file) |
453 | 2.03k | } |
454 | | |
455 | | #[cfg(not(target_os = "linux"))] |
456 | | pub const fn memory_is_limited() -> bool { |
457 | | false |
458 | | } |
459 | | |
460 | | /// What the worker reports on each keepalive. Linux only; elsewhere the |
461 | | /// worker reports nothing and is never vetoed. |
462 | | #[cfg(target_os = "linux")] |
463 | 68.9k | pub fn free_memory_kb() -> Option<u64> { |
464 | | // The limit and the usage counted against it come from the same level: |
465 | | // the nearest limited ancestor, which charges everything below it. |
466 | 68.9k | let limited = find_limit( |
467 | 68.9k | &cgroup_dir(), |
468 | 68.9k | Path::new("/sys/fs/cgroup"), |
469 | 68.9k | "memory.max", |
470 | | read_file, |
471 | | ); |
472 | 68.9k | free_memory_kb_with(limited.as_ref(), read_file) |
473 | 68.9k | } |
474 | | |
475 | | #[cfg(not(target_os = "linux"))] |
476 | | pub const fn free_memory_kb() -> Option<u64> { |
477 | | None |
478 | | } |