Skip to content

Commit b0e9cf4

Browse files
committed
perf: stream the custom-rule host payload pickle to disk
The parent pickled the whole host payload into one bytes object before writing it, holding a second copy of the payload (about 285 MB for a 10,000-model project) alive during the rules phase. Pickle straight into the partial file instead; the atomic rename and the payload the hosts read are unchanged.
1 parent 13bebe5 commit b0e9cf4

1 file changed

Lines changed: 19 additions & 6 deletions

File tree

‎crates/sqlbuild-python/src/bindings/_helpers/rules/custom_hosts.rs‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::time::Duration;
99

1010
use pyo3::exceptions::PyKeyboardInterrupt;
1111
use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyErr, PyResult, Python};
12-
use pyo3::types::{PyBytes, PyBytesMethods, PyDict, PyDictMethods, PyTuple};
12+
use pyo3::types::{PyDict, PyDictMethods, PyTuple};
1313
use serde_json::{Map, Value, json};
1414
use sqlbuild_rules::custom_host_pool::constants::{
1515
CGROUP_ROOT, HOST_TIMEOUT_MILLIS, MIN_INVOCATIONS_PER_HOST, PROC_CGROUP,
@@ -375,23 +375,36 @@ fn host_outcome(spec: &str) -> HostOutcome {
375375
/// Publish the pickled project and config atomically; hosts started earlier wait for it.
376376
fn write_inputs(py: Python<'_>, path: &Path, request: &HostRequest<'_, '_>) -> PyResult<()> {
377377
let payload: Bound<'_, PyAny> = host_payload_project(request.project)?;
378-
let pickled: Bound<'_, PyAny> = imported(py, "pickle", "dumps")?
379-
.call1((PyTuple::new(py, [payload, request.config.clone()])?,))?;
380-
let bytes: &[u8] = pickled.cast::<PyBytes>()?.as_bytes();
381378
let parent: &Path = path.parent().unwrap_or(path);
382379
let partial: PathBuf = parent.join(format!(
383380
"writing-{}-{}.pickle",
384381
std::process::id(),
385382
INPUT_COUNTER.fetch_add(1, Ordering::Relaxed)
386383
));
387-
if let Err(error) = std::fs::write(&partial, bytes) {
384+
let written: PyResult<()> = pickle_into(
385+
&partial,
386+
&PyTuple::new(py, [payload, request.config.clone()])?,
387+
);
388+
if let Err(error) = written {
388389
remove_file(&partial);
389-
return Err(error.into());
390+
return Err(error);
390391
}
391392
std::fs::rename(&partial, path)?;
392393
Ok(())
393394
}
394395

396+
/// Pickle `value` straight into a new file at `path`, never holding the whole pickle in memory.
397+
fn pickle_into(path: &Path, value: &Bound<'_, PyTuple>) -> PyResult<()> {
398+
let py: Python<'_> = value.py();
399+
let handle: Bound<'_, PyAny> = imported(py, "builtins", "open")?.call1((text(path), "wb"))?;
400+
let dumped: PyResult<Bound<'_, PyAny>> =
401+
imported(py, "pickle", "dump")?.call1((value, &handle));
402+
let closed: PyResult<Bound<'_, PyAny>> = handle.call_method0("close");
403+
dumped?;
404+
closed?;
405+
Ok(())
406+
}
407+
395408
/// The project hosts decode, without model state no rule fact reads.
396409
pub(crate) fn host_payload_project<'py>(
397410
project: &Bound<'py, PyAny>,

0 commit comments

Comments
 (0)