Parallelize DFS, use an AtomicBool for short-circuiting

time:   [73.407 µs 76.145 µs 79.345 µs]
change: [−40.126% −24.679% −3.8062%] (p = 0.04 < 0.05)
This commit is contained in:
2026-02-01 22:53:04 +02:00
parent c9e1d3ab67
commit 98cb9f7f3e
6 changed files with 375 additions and 5 deletions

View File

@@ -22,6 +22,9 @@ test-integration:
test-all: test-all:
cd {{tauri_path}} && cargo test --release -- --nocapture cd {{tauri_path}} && cargo test --release -- --nocapture
bench:
cd {{tauri_path}} && cargo bench
# profile: # profile:
# cd {{tauri_path}} && cargo flamegraph # cd {{tauri_path}} && cargo flamegraph

237
src-tauri/Cargo.lock generated
View File

@@ -43,6 +43,15 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "android_log-sys" name = "android_log-sys"
version = "0.3.2" version = "0.3.2"
@@ -69,6 +78,18 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.100"
@@ -489,6 +510,12 @@ dependencies = [
"toml 0.9.8", "toml 0.9.8",
] ]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.49" version = "1.2.49"
@@ -552,6 +579,58 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.5.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@@ -651,6 +730,41 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "criterion"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools 0.13.0",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
dependencies = [
"cast",
"itertools 0.13.0",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.15"
@@ -660,12 +774,37 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -1523,6 +1662,17 @@ dependencies = [
"syn 2.0.111", "syn 2.0.111",
] ]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@@ -1883,6 +2033,15 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -2509,6 +2668,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]] [[package]]
name = "open" name = "open"
version = "5.3.3" version = "5.3.3"
@@ -2537,6 +2702,16 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -2779,6 +2954,34 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.16" version = "0.17.16"
@@ -3070,6 +3273,26 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rayon"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -3224,10 +3447,12 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"criterion",
"docx-rs", "docx-rs",
"itertools", "itertools 0.14.0",
"log", "log",
"rand 0.9.2", "rand 0.9.2",
"rayon",
"rstest", "rstest",
"serde", "serde",
"serde_json", "serde_json",
@@ -4272,6 +4497,16 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.10.0" version = "1.10.0"

View File

@@ -30,3 +30,19 @@ log = "0.4.29"
rand = "0.9.2" rand = "0.9.2"
docx-rs = "0.4.18" docx-rs = "0.4.18"
anyhow = "1.0.100" anyhow = "1.0.100"
rayon = "1.11"
[dev-dependencies]
criterion = { version = "0.8.1", features = ["html_reports"] }
[[bench]]
name = "rayon"
harness = false
[lints.clippy]
indexing_slicing = "deny"
fallible_impl_from = "deny"
wildcard_enum_match_arm = "deny"
unneeded_field_pattern = "deny"
fn_params_excessive_bools = "deny"
# must_use_candidate = "deny"

View File

@@ -0,0 +1,53 @@
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
use rota_lib::{
config::{ToxicPair, UserConfig},
resident::Resident,
schedule::{MonthlySchedule, ShiftType},
scheduler::Scheduler,
slot::Day,
workload::WorkloadTracker,
};
fn criterion_benchmark(c: &mut Criterion) {
let config = maximal_config();
let scheduler = Scheduler::new_with_config(config);
c.bench_function("scheduler run", |b| {
b.iter(|| {
let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default();
let result = scheduler.run(&mut schedule, &mut tracker);
black_box(result)
})
});
}
fn maximal_config() -> UserConfig {
UserConfig::default()
.with_holidays(vec![2, 3, 10, 11, 12, 25])
.with_residents(vec![
Resident::new(1, "R1").with_max_shifts(3),
Resident::new(2, "R2").with_max_shifts(4),
Resident::new(3, "R3").with_reduced_load(),
Resident::new(4, "R4").with_allowed_types(vec![ShiftType::Closed]),
Resident::new(5, "R5")
.with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]),
Resident::new(6, "R6").with_negative_shifts(vec![Day(5), Day(15), Day(25)]),
Resident::new(7, "R7")
.with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]),
Resident::new(8, "R8"),
Resident::new(9, "R9"),
Resident::new(10, "R10").with_reduced_load(),
])
.with_toxic_pairs(vec![
ToxicPair::new(1, 2),
ToxicPair::new(3, 4),
ToxicPair::new(3, 5),
ToxicPair::new(7, 8),
])
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,3 +1,5 @@
use std::sync::atomic::{AtomicBool, Ordering};
use crate::{ use crate::{
config::UserConfig, config::UserConfig,
resident::ResidentId, resident::ResidentId,
@@ -7,7 +9,10 @@ use crate::{
workload::{WorkloadBounds, WorkloadTracker}, workload::{WorkloadBounds, WorkloadTracker},
}; };
use anyhow::bail;
use log::warn;
use rand::Rng; use rand::Rng;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
pub struct Scheduler { pub struct Scheduler {
pub config: UserConfig, pub config: UserConfig,
@@ -46,7 +51,46 @@ impl Scheduler {
//TODO: add validation //TODO: add validation
self.search(schedule, tracker, Slot::default()) // find first non-manually-filled slot
let slot = (0..=self.config.total_slots)
.find(|&slot_idx| !schedule.0.contains_key(&Slot::from(slot_idx)))
.map(Slot::from)
.ok_or_else(|| anyhow::anyhow!("Schedule is already full"))?;
let resident_ids = self.valid_residents(slot, schedule);
let solved_in_thread = AtomicBool::new(false);
let solved_schedule = resident_ids.par_iter().find_map_any(|&id| {
let mut local_schedule = schedule.clone();
let mut local_tracker = tracker.clone();
local_schedule.insert(slot, id);
local_tracker.insert(id, &self.config, slot);
let solved = self.search(
&mut local_schedule,
&mut local_tracker,
slot.next(),
&solved_in_thread,
);
match solved {
Ok(true) => Some(local_schedule),
Ok(false) => None,
Err(e) => {
warn!("Search error: {}", e);
None
}
}
});
// TODO: can return the schedule instead of a bool
if let Some(solved_schedule) = solved_schedule {
*schedule = solved_schedule;
return Ok(true);
}
Ok(false)
} }
/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month each node is called a slot /// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month each node is called a slot
@@ -57,7 +101,12 @@ impl Scheduler {
schedule: &mut MonthlySchedule, schedule: &mut MonthlySchedule,
tracker: &mut WorkloadTracker, tracker: &mut WorkloadTracker,
slot: Slot, slot: Slot,
solved_in_thread: &AtomicBool,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
if solved_in_thread.load(Ordering::Relaxed) {
bail!("Another thread found the solution")
}
if self.timer.limit_exceeded() { if self.timer.limit_exceeded() {
anyhow::bail!("Time exceeded. Restrictions too tight"); anyhow::bail!("Time exceeded. Restrictions too tight");
} }
@@ -69,11 +118,15 @@ impl Scheduler {
} }
if slot.greater_than(self.config.total_days) { if slot.greater_than(self.config.total_days) {
return Ok(tracker.are_all_thresholds_met(&self.config, &self.bounds)); if tracker.are_all_thresholds_met(&self.config, &self.bounds) {
solved_in_thread.store(true, Ordering::Relaxed);
return Ok(true);
}
return Ok(false);
} }
if schedule.is_slot_manually_assigned(&slot) { if schedule.is_slot_manually_assigned(&slot) {
return self.search(schedule, tracker, slot.next()); return self.search(schedule, tracker, slot.next(), solved_in_thread);
} }
// sort candidates by current workload, add rng for tie breakers // sort candidates by current workload, add rng for tie breakers
@@ -88,7 +141,7 @@ impl Scheduler {
schedule.insert(slot, id); schedule.insert(slot, id);
tracker.insert(id, &self.config, slot); tracker.insert(id, &self.config, slot);
if self.search(schedule, tracker, slot.next())? { if self.search(schedule, tracker, slot.next(), solved_in_thread)? {
return Ok(true); return Ok(true);
} }

View File

@@ -113,6 +113,16 @@ impl Default for Slot {
} }
} }
impl From<u8> for Slot {
fn from(value: u8) -> Self {
let mut slot = Slot::default();
for _ in 0..value {
slot = slot.next()
}
slot
}
}
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)] #[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Day(pub u8); pub struct Day(pub u8);