1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
//! Read-ahead of queued trees for the pack-objects object walk.
//!
//! The walk ([`crate::pack_object_select`]) expands trees in the order they were queued,
//! and its queue only ever grows at the back. Inflating a tree does not depend on any
//! other tree, so worker threads can read the next runs of queued trees while the walk
//! parses the current ones. Results are handed back strictly in queue order, so the walk
//! processes every object exactly as if it had read it itself: the object list, its
//! order and the name hashes do not change.
//!
//! Workers only read what is stored locally and never fetch from a promisor remote. An
//! object they cannot read is left to the walk, which reads it itself, fetching it
//! lazily or reporting the error exactly as without read-ahead.
use std::collections::{HashMap, VecDeque};
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread::Scope;
use crate::objects::{Object, ObjectId};
use crate::odb::Odb;
use crate::pack::SingleLocalPackReader;
/// Queue entries handed to a worker at a time: a contiguous run keeps the delta bases
/// of neighbouring trees (successive versions of a directory) in one worker's cache.
const BATCH: usize = 256;
/// Batches in flight per worker, so a worker always has the next run ready to read.
const BATCHES_PER_WORKER: usize = 2;
/// One batch: its sequence number and, for each queue entry it covers, the object to
/// read (`None` for entries the walk reads itself).
type Job = (u64, Vec<Option<ObjectId>>);
/// A finished batch (the objects the worker could read), or the panic payload of the
/// worker that read it.
type Done = (u64, std::thread::Result<Vec<Option<Object>>>);
/// Something queued for the walk: the object id, and whether to read it ahead.
pub(crate) trait ReadAheadEntry {
/// The object to read ahead, or `None` when the walk reads this entry itself.
fn read_ahead_oid(&self) -> Option<ObjectId>;
}
/// Reads the front of the walk's queue ahead on scoped worker threads.
///
/// Workers start only once a full batch of trees is queued, so small walks (a push of
/// a few commits) never spawn threads.
pub(crate) struct TreeReadAhead<'scope, 'env> {
scope: &'scope Scope<'scope, 'env>,
odb: &'env Odb,
workers: usize,
jobs: Option<Sender<Job>>,
done: Option<Receiver<Done>>,
/// Results for the front of the queue, in queue order.
ready: VecDeque<Option<Object>>,
/// Batches that finished out of order, by sequence number.
arrived: HashMap<u64, Vec<Option<Object>>>,
next_seq: u64,
want_seq: u64,
/// Front queue entries covered by `ready` and the batches in flight.
covered: usize,
}
impl<'scope, 'env> TreeReadAhead<'scope, 'env> {
/// A read-ahead that will use up to `workers` threads of `scope`; `0` disables it.
pub fn new(scope: &'scope Scope<'scope, 'env>, odb: &'env Odb, workers: usize) -> Self {
Self {
scope,
odb,
workers,
jobs: None,
done: None,
ready: VecDeque::new(),
arrived: HashMap::new(),
next_seq: 0,
want_seq: 0,
covered: 0,
}
}
/// Issue batches for the queue entries not yet covered, while workers have room.
///
/// `queue` is the walk's queue as it is now; entries before the first uncovered one
/// must not have been popped without [`Self::take_front`].
pub(crate) fn top_up<E: ReadAheadEntry>(&mut self, queue: &VecDeque<E>) {
if self.workers == 0 {
return;
}
let limit = self.workers * BATCHES_PER_WORKER;
while ((self.next_seq - self.want_seq) as usize) < limit
&& queue.len().saturating_sub(self.covered) >= BATCH
{
let batch: Vec<Option<ObjectId>> = queue
.range(self.covered..self.covered + BATCH)
.map(ReadAheadEntry::read_ahead_oid)
.collect();
let seq = self.next_seq;
let Some(jobs) = self.start_workers() else {
return;
};
if jobs.send((seq, batch)).is_err() {
self.stop();
return;
}
self.next_seq += 1;
self.covered += BATCH;
}
}
/// The object for the queue entry about to be popped: `Some` when a worker read it,
/// `None` when the walk must read it itself (not covered, not read ahead, or not
/// readable from local storage).
pub(crate) fn take_front(&mut self) -> Option<Object> {
if self.covered == 0 {
return None;
}
if self.ready.is_empty() && !self.wait_for_next_batch() {
return None;
}
self.covered -= 1;
self.ready.pop_front().flatten()
}
/// Move batch `want_seq` into `ready`, waiting for it. `false` when the workers are
/// gone; everything still covered is then read by the walk instead.
fn wait_for_next_batch(&mut self) -> bool {
loop {
if let Some(batch) = self.arrived.remove(&self.want_seq) {
self.ready.extend(batch);
self.want_seq += 1;
return true;
}
let Some(done) = self.done.as_ref() else {
return false;
};
match done.recv() {
Ok((seq, Ok(batch))) => {
self.arrived.insert(seq, batch);
}
Ok((_, Err(payload))) => resume_unwind(payload),
Err(_) => {
self.stop();
return false;
}
}
}
}
/// Stop reading ahead: nothing more is issued and covered entries are read by the walk.
fn stop(&mut self) {
self.workers = 0;
self.jobs = None;
self.done = None;
self.ready.clear();
self.arrived.clear();
self.covered = 0;
}
fn start_workers(&mut self) -> Option<&Sender<Job>> {
if self.jobs.is_none() {
let (jobs_tx, jobs_rx) = channel::<Job>();
let (done_tx, done_rx) = channel::<Done>();
let jobs_rx = Arc::new(Mutex::new(jobs_rx));
let odb = self.odb;
for _ in 0..self.workers {
let jobs_rx = Arc::clone(&jobs_rx);
let done_tx = done_tx.clone();
self.scope
.spawn(move || read_ahead_worker(odb, &jobs_rx, &done_tx));
}
self.jobs = Some(jobs_tx);
self.done = Some(done_rx);
}
self.jobs.as_ref()
}
}
fn read_ahead_worker(odb: &Odb, jobs: &Mutex<Receiver<Job>>, done: &Sender<Done>) {
let _pack_context = odb.pack_read_context_guard();
let mut reader = odb.open_session_pack_reader().ok().flatten();
loop {
let job = jobs.lock().unwrap_or_else(|e| e.into_inner()).recv();
let Ok((seq, batch)) = job else {
return;
};
let objects = catch_unwind(AssertUnwindSafe(|| {
batch
.iter()
.map(|oid| oid.and_then(|oid| read_local(odb, reader.as_mut(), &oid)))
.collect()
}));
if done.send((seq, objects)).is_err() {
return;
}
}
}
/// Read `oid` through `reader` when it has it, otherwise from `odb`'s local storage;
/// `None` when neither can (the walk then reads it itself).
fn read_local(
odb: &Odb,
reader: Option<&mut SingleLocalPackReader>,
oid: &ObjectId,
) -> Option<Object> {
if let Some(Ok(obj)) = reader.map(|r| r.read_object(oid)) {
return Some(obj);
}
odb.try_read_once(oid).ok().flatten()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::objects::ObjectKind;
use crate::repo::{InitOptions, Repository};
use tempfile::TempDir;
struct Entry(Option<ObjectId>);
impl ReadAheadEntry for Entry {
fn read_ahead_oid(&self) -> Option<ObjectId> {
self.0
}
}
/// Pop every entry the way the walk does, returning for each one whether a worker
/// read it and the payload the walk ends up with.
fn drain(
odb: &Odb,
mut queue: VecDeque<Entry>,
workers: usize,
) -> Vec<(bool, Option<Vec<u8>>)> {
std::thread::scope(|scope| {
let mut read_ahead = TreeReadAhead::new(scope, odb, workers);
read_ahead.top_up(&queue);
let mut out = Vec::new();
while let Some(entry) = {
let front = read_ahead.take_front();
queue.pop_front().map(|entry| (entry, front))
} {
let (entry, front) = entry;
read_ahead.top_up(&queue);
let read_ahead_hit = front.is_some();
let data = front.map(|obj| obj.data).or_else(|| {
entry
.0
.and_then(|oid| odb.read(&oid).ok().map(|obj| obj.data))
});
out.push((read_ahead_hit, data));
}
out
})
}
#[test]
fn objects_come_back_in_queue_order_and_misses_are_left_to_the_walk() {
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path(), &InitOptions::default()).unwrap();
let odb = repo.odb();
let missing = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
let len = 3 * BATCH + 17;
let entries = || -> VecDeque<Entry> {
(0..len)
.map(|i| match i % 10 {
3 => Entry(None),
7 => Entry(Some(missing)),
_ => Entry(Some(
odb.write(ObjectKind::Blob, format!("object {i}").as_bytes())
.unwrap(),
)),
})
.collect()
};
let serial = drain(odb, entries(), 0);
assert!(
serial.iter().all(|(hit, _)| !hit),
"no workers, no read-ahead"
);
let parallel = drain(odb, entries(), 3);
assert_eq!(
parallel.iter().map(|(_, data)| data).collect::<Vec<_>>(),
serial.iter().map(|(_, data)| data).collect::<Vec<_>>()
);
for (i, (hit, data)) in parallel.iter().enumerate() {
match i % 10 {
3 => assert!(!hit && data.is_none(), "entry {i} is not read ahead"),
7 => assert!(
!hit && data.is_none(),
"missing entry {i} is left to the walk"
),
_ if i < len - len % BATCH => {
assert!(hit, "entry {i} lies in a full batch");
assert_eq!(data.as_deref(), Some(format!("object {i}").as_bytes()));
}
_ => assert!(!hit, "the tail shorter than a batch is read by the walk"),
}
}
}
}