Skip to content
Closed
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
22 changes: 12 additions & 10 deletions gix-odb/src/store_impls/dynamic/load_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicU16, Ordering},
atomic::{AtomicUsize, Ordering},
},
time::SystemTime,
};
Expand Down Expand Up @@ -123,14 +123,18 @@ impl super::Store {
'retry_with_changed_index: loop {
let previous_state_id = index.state_id();
'retry_with_next_slot_index: loop {
// Announce the load *before* claiming a slot. Every claimed slot is then covered by an increment
// that happened before the claim, and the decrement only happens once loading is done (or failed).
// Hence a thread that finds nothing left to claim and observes this counter at zero afterwards knows
// that no claimed slot is still being loaded, without having to yield first to widen its window.
let ongoing_operation = IncOnNewAndDecOnDrop::new(&index.num_indices_currently_being_loaded);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent the in-flight-operation counter from wrapping

When 65,536 load_next_index() calls overlap, this new placement counts every caller before it knows whether a slot can be claimed, including callers that will immediately take the miss path. AtomicU16::fetch_add then wraps to zero even if another caller is still loading an index, so a losing caller can skip the wait loop and return false with a snapshot that lacks that index. Use a non-wrapping-sized counter (or otherwise prevent overflow) for this expanded concurrency scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point: with the increment moved in front of the claim, the bound on the counter is the number of threads in this path rather than the number of slots. Widened it to a usize in f4cadb1 (internal type only; tests, clippy and fmt re-run).

match index
.next_index_to_load
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
(current != index.slot_indices.len()).then_some(current + 1)
}) {
Ok(slot_map_index) => {
// This slot-map index is in bounds and was only given to us.
let _ongoing_operation = IncOnNewAndDecOnDrop::new(&index.num_indices_currently_being_loaded);
let slot = &self.files[index.slot_indices[slot_map_index]];
let _lock = slot.write.lock();
if slot.generation.load(Ordering::SeqCst) > index.generation {
Expand Down Expand Up @@ -161,16 +165,14 @@ impl super::Store {
}
}
Err(_nothing_more_to_load) => {
// We are not loading anything, so don't make anyone wait for us.
drop(ongoing_operation);
// There can be contention as many threads start working at the same time and take all the
// slots to load indices for. Some threads might just be left-over and have to wait for something
// to change.
// TODO: potentially hot loop - could this be a condition variable?
// This is a timing-based fix for the case that the `num_indices_being_loaded` isn't yet incremented,
// and we might break out here without actually waiting for the loading operation. Then we'd fail to
// observe a change and the underlying handler would not have all the indices it needs at its disposal.
// Yielding means we will definitely loose enough time to observe the ongoing operation,
// or its effects.
std::thread::yield_now();
// Once all indices are loaded, every miss ends up here with nothing to wait for, which must stay
// syscall-free: a `contains()` miss is the hottest read path of this store.
while index.num_indices_currently_being_loaded.load(Ordering::SeqCst) != 0 {
std::thread::yield_now();
}
Expand Down Expand Up @@ -716,9 +718,9 @@ fn is_multipack_index(path: &Path) -> bool {
path.file_name() == Some(OsStr::new("multi-pack-index"))
}

struct IncOnNewAndDecOnDrop<'a>(&'a AtomicU16);
struct IncOnNewAndDecOnDrop<'a>(&'a AtomicUsize);
impl<'a> IncOnNewAndDecOnDrop<'a> {
pub fn new(v: &'a AtomicU16) -> Self {
pub fn new(v: &'a AtomicUsize) -> Self {
v.fetch_add(1, Ordering::SeqCst);
Self(v)
}
Expand Down
4 changes: 2 additions & 2 deletions gix-odb/src/store_impls/dynamic/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicU16, AtomicU32, AtomicUsize, Ordering},
atomic::{AtomicU32, AtomicUsize, Ordering},
},
time::SystemTime,
};
Expand Down Expand Up @@ -104,7 +104,7 @@ pub struct SlotMapIndex {
pub(crate) loaded_indices: Arc<AtomicUsize>,
/// The amount of indices that are currently being loaded.
/// Zero if no loading operation is currently happening, or more otherwise.
pub(crate) num_indices_currently_being_loaded: Arc<AtomicU16>,
pub(crate) num_indices_currently_being_loaded: Arc<AtomicUsize>,
}

impl SlotMapIndex {
Expand Down
Loading