|
1 | 1 | use std::path::Path; |
| 2 | +use std::time::Duration; |
2 | 3 | use std::{fmt, io}; |
3 | 4 |
|
| 5 | +use serde::Deserialize; |
4 | 6 | use spacetimedb_lib::ConnectionId; |
5 | 7 | use spacetimedb_paths::cli::{ConfigDir, PrivKeyPath, PubKeyPath}; |
6 | 8 | use spacetimedb_paths::server::{ConfigToml, MetadataTomlPath}; |
@@ -131,6 +133,8 @@ pub struct ConfigFile { |
131 | 133 | pub certificate_authority: Option<CertificateAuthority>, |
132 | 134 | #[serde(default)] |
133 | 135 | pub logs: LogConfig, |
| 136 | + #[serde(default)] |
| 137 | + pub v8_heap_policy: V8HeapPolicyConfig, |
134 | 138 | } |
135 | 139 |
|
136 | 140 | impl ConfigFile { |
@@ -169,6 +173,139 @@ pub struct LogConfig { |
169 | 173 | pub directives: Vec<String>, |
170 | 174 | } |
171 | 175 |
|
| 176 | +#[derive(Clone, Copy, Debug, serde::Deserialize)] |
| 177 | +#[serde(rename_all = "kebab-case")] |
| 178 | +pub struct V8HeapPolicyConfig { |
| 179 | + #[serde(default = "def_req_interval", deserialize_with = "de_nz_u64")] |
| 180 | + pub heap_check_request_interval: Option<u64>, |
| 181 | + #[serde(default = "def_time_interval", deserialize_with = "de_nz_duration")] |
| 182 | + pub heap_check_time_interval: Option<Duration>, |
| 183 | + #[serde(default = "def_gc_trigger", deserialize_with = "de_fraction")] |
| 184 | + pub heap_gc_trigger_fraction: f64, |
| 185 | + #[serde(default = "def_retire", deserialize_with = "de_fraction")] |
| 186 | + pub heap_retire_fraction: f64, |
| 187 | + #[serde(default, rename = "heap-limit-mb", deserialize_with = "de_limit_mb")] |
| 188 | + pub heap_limit_bytes: Option<usize>, |
| 189 | +} |
| 190 | + |
| 191 | +impl Default for V8HeapPolicyConfig { |
| 192 | + fn default() -> Self { |
| 193 | + Self { |
| 194 | + heap_check_request_interval: def_req_interval(), |
| 195 | + heap_check_time_interval: def_time_interval(), |
| 196 | + heap_gc_trigger_fraction: def_gc_trigger(), |
| 197 | + heap_retire_fraction: def_retire(), |
| 198 | + heap_limit_bytes: None, |
| 199 | + } |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +impl V8HeapPolicyConfig { |
| 204 | + pub fn normalized(mut self) -> Self { |
| 205 | + if self.heap_retire_fraction < self.heap_gc_trigger_fraction { |
| 206 | + log::warn!( |
| 207 | + "v8-heap-policy.heap-retire-fraction ({}) is below \ |
| 208 | + v8-heap-policy.heap-gc-trigger-fraction ({}); using the GC trigger fraction for both", |
| 209 | + self.heap_retire_fraction, |
| 210 | + self.heap_gc_trigger_fraction, |
| 211 | + ); |
| 212 | + self.heap_retire_fraction = self.heap_gc_trigger_fraction; |
| 213 | + } |
| 214 | + |
| 215 | + self |
| 216 | + } |
| 217 | +} |
| 218 | + |
| 219 | +/// Default number of requests between V8 heap checks. |
| 220 | +fn def_req_interval() -> Option<u64> { |
| 221 | + Some(65_536) |
| 222 | +} |
| 223 | + |
| 224 | +/// Default wall-clock interval between V8 heap checks. |
| 225 | +fn def_time_interval() -> Option<Duration> { |
| 226 | + Some(Duration::from_secs(30)) |
| 227 | +} |
| 228 | + |
| 229 | +/// Default heap fill fraction that triggers a GC. |
| 230 | +fn def_gc_trigger() -> f64 { |
| 231 | + 0.67 |
| 232 | +} |
| 233 | + |
| 234 | +/// Default heap fill fraction that retires the worker after a GC. |
| 235 | +fn def_retire() -> f64 { |
| 236 | + 0.75 |
| 237 | +} |
| 238 | + |
| 239 | +fn de_nz_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error> |
| 240 | +where |
| 241 | + D: serde::Deserializer<'de>, |
| 242 | +{ |
| 243 | + let value = u64::deserialize(deserializer)?; |
| 244 | + Ok((value != 0).then_some(value)) |
| 245 | +} |
| 246 | + |
| 247 | +fn de_nz_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error> |
| 248 | +where |
| 249 | + D: serde::Deserializer<'de>, |
| 250 | +{ |
| 251 | + #[derive(serde::Deserialize)] |
| 252 | + #[serde(untagged)] |
| 253 | + enum DurationValue { |
| 254 | + String(String), |
| 255 | + Seconds(u64), |
| 256 | + } |
| 257 | + |
| 258 | + let duration = match DurationValue::deserialize(deserializer)? { |
| 259 | + DurationValue::String(value) => humantime::parse_duration(&value).map_err(serde::de::Error::custom)?, |
| 260 | + DurationValue::Seconds(value) => Duration::from_secs(value), |
| 261 | + }; |
| 262 | + |
| 263 | + Ok((!duration.is_zero()).then_some(duration)) |
| 264 | +} |
| 265 | + |
| 266 | +fn de_fraction<'de, D>(deserializer: D) -> Result<f64, D::Error> |
| 267 | +where |
| 268 | + D: serde::Deserializer<'de>, |
| 269 | +{ |
| 270 | + #[derive(serde::Deserialize)] |
| 271 | + #[serde(untagged)] |
| 272 | + enum FractionValue { |
| 273 | + Integer(u64), |
| 274 | + Float(f64), |
| 275 | + } |
| 276 | + |
| 277 | + let value = match FractionValue::deserialize(deserializer)? { |
| 278 | + FractionValue::Integer(value) => value as f64, |
| 279 | + FractionValue::Float(value) => value, |
| 280 | + }; |
| 281 | + |
| 282 | + if value.is_finite() && (0.0..=1.0).contains(&value) { |
| 283 | + Ok(value) |
| 284 | + } else { |
| 285 | + Err(serde::de::Error::custom(format!( |
| 286 | + "expected a fraction between 0.0 and 1.0, got {value}" |
| 287 | + ))) |
| 288 | + } |
| 289 | +} |
| 290 | + |
| 291 | +fn de_limit_mb<'de, D>(deserializer: D) -> Result<Option<usize>, D::Error> |
| 292 | +where |
| 293 | + D: serde::Deserializer<'de>, |
| 294 | +{ |
| 295 | + let value = u64::deserialize(deserializer)?; |
| 296 | + if value == 0 { |
| 297 | + return Ok(None); |
| 298 | + } |
| 299 | + |
| 300 | + let bytes = value |
| 301 | + .checked_mul(1024 * 1024) |
| 302 | + .ok_or_else(|| serde::de::Error::custom("heap-limit-mb is too large"))?; |
| 303 | + |
| 304 | + usize::try_from(bytes) |
| 305 | + .map(Some) |
| 306 | + .map_err(|_| serde::de::Error::custom("heap-limit-mb does not fit in usize")) |
| 307 | +} |
| 308 | + |
172 | 309 | #[cfg(test)] |
173 | 310 | mod tests { |
174 | 311 | use super::*; |
@@ -270,4 +407,41 @@ mod tests { |
270 | 407 | .check_compatibility_and_update(mkmeta_pre(2, 1, 0, "rc1")) |
271 | 408 | .unwrap_err(); |
272 | 409 | } |
| 410 | + |
| 411 | + #[test] |
| 412 | + fn v8_heap_policy_defaults_when_omitted() { |
| 413 | + let config: ConfigFile = toml::from_str("").unwrap(); |
| 414 | + |
| 415 | + assert_eq!(config.v8_heap_policy.heap_check_request_interval, Some(65_536)); |
| 416 | + assert_eq!( |
| 417 | + config.v8_heap_policy.heap_check_time_interval, |
| 418 | + Some(Duration::from_secs(30)) |
| 419 | + ); |
| 420 | + assert_eq!(config.v8_heap_policy.heap_gc_trigger_fraction, 0.67); |
| 421 | + assert_eq!(config.v8_heap_policy.heap_retire_fraction, 0.75); |
| 422 | + assert_eq!(config.v8_heap_policy.heap_limit_bytes, None); |
| 423 | + } |
| 424 | + |
| 425 | + #[test] |
| 426 | + fn v8_heap_policy_parses_from_toml() { |
| 427 | + let toml = r#" |
| 428 | + [v8-heap-policy] |
| 429 | + heap-check-request-interval = 0 |
| 430 | + heap-check-time-interval = "45s" |
| 431 | + heap-gc-trigger-fraction = 0.6 |
| 432 | + heap-retire-fraction = 0.8 |
| 433 | + heap-limit-mb = 256 |
| 434 | + "#; |
| 435 | + |
| 436 | + let config: ConfigFile = toml::from_str(toml).unwrap(); |
| 437 | + |
| 438 | + assert_eq!(config.v8_heap_policy.heap_check_request_interval, None); |
| 439 | + assert_eq!( |
| 440 | + config.v8_heap_policy.heap_check_time_interval, |
| 441 | + Some(Duration::from_secs(45)) |
| 442 | + ); |
| 443 | + assert_eq!(config.v8_heap_policy.heap_gc_trigger_fraction, 0.6); |
| 444 | + assert_eq!(config.v8_heap_policy.heap_retire_fraction, 0.8); |
| 445 | + assert_eq!(config.v8_heap_policy.heap_limit_bytes, Some(256 * 1024 * 1024)); |
| 446 | + } |
273 | 447 | } |
0 commit comments