Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions crates/fbuild-build/src/build_fingerprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,47 @@ pub fn hash_watch_set(watches: &[FingerprintWatch]) -> Result<String> {
Ok(format!("{:x}", hasher.finalize()))
}

/// In-memory cache for [`hash_watch_set_stamps`] results across
/// invocations within the same daemon lifetime. The daemon implements
/// this so warm rebuilds within a few seconds of each other can skip
/// the per-build walk over thousands of watched files.
///
/// The cache key is derived by the implementor from the watch set's
/// root paths; the orchestrator just hands the slice in. Callers must
/// expect `get` to return `None` whenever the implementation considers
/// the entry stale (typically a 2–5 s freshness window since the last
/// `put`), so correctness is unaffected by an absent or evicted entry.
pub trait WatchSetStampCache: Send + Sync {
fn get(&self, watches: &[FingerprintWatch]) -> Option<String>;
fn put(&self, watches: &[FingerprintWatch], hash: String);
}

/// [`hash_watch_set_stamps`] with an optional in-memory short-circuit.
///
/// When `cache` is `Some`, a cache hit returns immediately without
/// walking the watch tree (the dominant cost for large projects per
/// `docs/PERF_WARM_BUILD.md`). On miss, the result is recorded
/// before being returned.
///
/// `cache: None` is identical to calling [`hash_watch_set_stamps`]
/// directly — used by code paths (CLI, tests) that don't have a
/// daemon-scoped cache to consult.
pub fn hash_watch_set_stamps_cached(
watches: &[FingerprintWatch],
cache: Option<&dyn WatchSetStampCache>,
) -> Result<String> {
if let Some(c) = cache {
if let Some(hash) = c.get(watches) {
return Ok(hash);
}
}
let hash = hash_watch_set_stamps(watches)?;
if let Some(c) = cache {
c.put(watches, hash.clone());
}
Ok(hash)
}

pub fn hash_watch_set_stamps(watches: &[FingerprintWatch]) -> Result<String> {
let mut ordered = watches.to_vec();
ordered.sort_by(|a, b| a.root.cmp(&b.root).then(a.cache_file.cmp(&b.cache_file)));
Expand Down
17 changes: 13 additions & 4 deletions crates/fbuild-build/src/esp32/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use fbuild_packages::Framework;
use serde::Serialize;

use crate::build_fingerprint::{
hash_watch_set_stamps, load_json, normalize_path, save_json, stable_hash_json,
hash_watch_set_stamps_cached, load_json, normalize_path, save_json, stable_hash_json,
PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION,
};
use crate::flag_overlay::LanguageExtraFlags;
Expand Down Expand Up @@ -374,7 +374,10 @@ impl BuildOrchestrator for Esp32Orchestrator {
} else {
match previous.file_set_hash.as_deref() {
Some(previous_hash) => {
match hash_watch_set_stamps(&fingerprint_watches) {
match hash_watch_set_stamps_cached(
&fingerprint_watches,
params.watch_set_cache.as_deref(),
) {
Ok(current_hash) => current_hash == previous_hash,
Err(e) => {
tracing::warn!("failed to hash watched inputs: {}", e);
Expand All @@ -388,7 +391,10 @@ impl BuildOrchestrator for Esp32Orchestrator {
} else {
match previous.file_set_hash.as_deref() {
Some(previous_hash) => {
match hash_watch_set_stamps(&fingerprint_watches) {
match hash_watch_set_stamps_cached(
&fingerprint_watches,
params.watch_set_cache.as_deref(),
) {
Ok(current_hash) => current_hash == previous_hash,
Err(e) => {
tracing::warn!("failed to hash watched inputs: {}", e);
Expand Down Expand Up @@ -1257,7 +1263,10 @@ impl BuildOrchestrator for Esp32Orchestrator {
let persisted_fingerprint = PersistedBuildFingerprint {
version: BUILD_FINGERPRINT_VERSION,
metadata_hash: metadata_hash.clone(),
file_set_hash: match hash_watch_set_stamps(&fingerprint_watches) {
file_set_hash: match hash_watch_set_stamps_cached(
&fingerprint_watches,
params.watch_set_cache.as_deref(),
) {
Ok(hash) => Some(hash),
Err(e) => {
tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e);
Expand Down
8 changes: 8 additions & 0 deletions crates/fbuild-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ pub struct BuildParams {
/// such as QEMU emulation. These are appended after platformio.ini
/// `build_flags`, so they can intentionally override board/user defaults.
pub extra_build_flags: Vec<String>,
/// Optional daemon-scoped memo for the warm-build fingerprint
/// `hash_watch_set_stamps` walk. When supplied, the orchestrator
/// short-circuits the walk on a fresh cache hit — the dominant
/// non-trivial cost on warm rebuilds of large projects (see
/// `docs/PERF_WARM_BUILD.md`). `None` from the CLI / tests means
/// the orchestrator falls back to walking on every call, which is
/// the pre-existing behaviour.
pub watch_set_cache: Option<std::sync::Arc<dyn build_fingerprint::WatchSetStampCache>>,
}

/// Trait for platform-specific build orchestrators.
Expand Down
3 changes: 3 additions & 0 deletions crates/fbuild-build/tests/avr_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ fn build_uno_minimal() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator;
Expand Down Expand Up @@ -186,6 +187,7 @@ fn compare_with_python_output() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator;
Expand Down Expand Up @@ -270,6 +272,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator;
Expand Down
8 changes: 8 additions & 0 deletions crates/fbuild-build/tests/esp32_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -166,6 +167,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -247,6 +249,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -329,6 +332,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -401,6 +405,7 @@ fn build_esp32s3_fixture() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -465,6 +470,7 @@ fn build_nightdriverstrip_demo() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -555,6 +561,7 @@ fn incremental_build_at(project_dir: &std::path::Path, env_name: &str) {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down Expand Up @@ -645,6 +652,7 @@ fn incremental_nightdriverstrip_one_file_changed() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator;
Expand Down
2 changes: 2 additions & 0 deletions crates/fbuild-build/tests/teensy_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ void loop() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator;
Expand Down Expand Up @@ -125,6 +126,7 @@ fn build_teensy41_fixture() {
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
};

let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator;
Expand Down
2 changes: 2 additions & 0 deletions crates/fbuild-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ tracing-subscriber = { workspace = true }
futures = { workspace = true }
ctrlc = "3.5.2"
blake3 = { workspace = true }
sha2 = { workspace = true }
tempfile = { workspace = true }
Loading
Loading