Skip to content

Commit 6b2527e

Browse files
Implement a topological sort for queue
Guarantees that commits are never benchmarked before their parents. The previous algorithm only ensured that the parents of try commits had this ordering, but in doing so promoted regular master commits into the wrong order in some cases.
1 parent 183c0b4 commit 6b2527e

File tree

1 file changed

+145
-53
lines changed

1 file changed

+145
-53
lines changed

site/src/load.rs

Lines changed: 145 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,39 @@ pub enum MissingReason {
2828
},
2929
Try {
3030
pr: u32,
31+
parent_sha: String,
3132
include: Option<String>,
3233
exclude: Option<String>,
3334
runs: Option<i32>,
3435
},
3536
InProgress(Option<Box<MissingReason>>),
3637
}
3738

39+
impl MissingReason {
40+
fn pr(&self) -> Option<u32> {
41+
let mut this = self;
42+
loop {
43+
match this {
44+
MissingReason::Master { pr, .. } => return Some(*pr),
45+
MissingReason::Try { pr, .. } => return Some(*pr),
46+
MissingReason::InProgress(Some(s)) => this = s,
47+
MissingReason::InProgress(None) => return None,
48+
}
49+
}
50+
}
51+
fn parent_sha(&self) -> Option<&str> {
52+
let mut this = self;
53+
loop {
54+
match this {
55+
MissingReason::Master { parent_sha, .. } => return Some(parent_sha.as_str()),
56+
MissingReason::Try { parent_sha, .. } => return Some(parent_sha.as_str()),
57+
MissingReason::InProgress(Some(s)) => this = s,
58+
MissingReason::InProgress(None) => return None,
59+
}
60+
}
61+
}
62+
}
63+
3864
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
3965
pub struct TryCommit {
4066
pub sha: String,
@@ -201,7 +227,7 @@ fn calculate_missing_from(
201227
mut all_commits: HashSet<String>,
202228
time: chrono::DateTime<chrono::Utc>,
203229
) -> Vec<(Commit, MissingReason)> {
204-
let mut master_commits = master_commits
230+
let mut queue = master_commits
205231
.into_iter()
206232
.filter(|c| time.signed_duration_since(c.time) < Duration::days(29))
207233
.map(|c| {
@@ -219,8 +245,10 @@ fn calculate_missing_from(
219245
)
220246
})
221247
.collect::<Vec<_>>();
222-
master_commits.reverse();
223-
let mut missing = Vec::with_capacity(queued_pr_commits.len() * 2 + master_commits.len());
248+
let master_commits = queue
249+
.iter()
250+
.map(|(mc, _)| mc.sha.clone())
251+
.collect::<HashSet<_>>();
224252
for database::QueuedCommit {
225253
sha,
226254
parent_sha,
@@ -231,77 +259,135 @@ fn calculate_missing_from(
231259
} in queued_pr_commits
232260
.into_iter()
233261
// filter out any queued PR master commits (leaving only try commits)
234-
.filter(|c| !master_commits.iter().any(|(mc, _)| mc.sha == c.sha))
262+
.filter(|c| !master_commits.contains(&c.sha))
235263
{
236-
// Enqueue the `TryParent` commit before the `TryCommit` itself, so that
237-
// all of the `try` run's data is complete when the benchmark results
238-
// of that commit are available.
239-
if let Some((try_parent, metadata)) = master_commits
240-
.iter()
241-
.find(|(m, _)| m.sha == parent_sha.as_str())
242-
{
243-
let (pr, parent_sha) = if let MissingReason::Master {
244-
pr,
245-
parent_sha,
246-
is_try_parent: _,
247-
} = &metadata
248-
{
249-
(*pr, parent_sha.clone())
264+
// Mark the parent commit as a try_parent.
265+
if let Some((_, metadata)) = queue.iter_mut().find(|(m, _)| m.sha == parent_sha.as_str()) {
266+
if let MissingReason::Master { is_try_parent, .. } = metadata {
267+
*is_try_parent = true;
250268
} else {
251-
unreachable!(
252-
"non-master missing reason in master_commits: {:?}",
253-
metadata
254-
);
269+
unreachable!("try commit has non-master parent {:?}", metadata);
255270
};
256-
missing.push((
257-
try_parent.clone(),
258-
MissingReason::Master {
259-
pr,
260-
parent_sha,
261-
is_try_parent: true,
262-
},
263-
));
264271
}
265-
missing.push((
272+
queue.push((
266273
Commit {
267274
sha: sha.to_string(),
268275
date: Date::ymd_hms(2001, 01, 01, 0, 0, 0),
269276
},
270277
MissingReason::Try {
271278
pr,
279+
parent_sha,
272280
include,
273281
exclude,
274282
runs,
275283
},
276284
));
277285
}
278-
missing.extend(master_commits);
279286
for aid in in_progress_artifacts {
280287
match aid {
281288
ArtifactId::Commit(c) => {
282-
let previous = missing
289+
let previous = queue
283290
.iter()
284291
.find(|(i, _)| i.sha == c.sha)
285292
.map(|v| Box::new(v.1.clone()));
286293
all_commits.remove(&c.sha);
287-
missing.insert(0, (c, MissingReason::InProgress(previous)));
294+
queue.insert(0, (c, MissingReason::InProgress(previous)));
288295
}
289296
ArtifactId::Tag(_) => {
290297
// do nothing, for now, though eventually we'll want an artifact queue
291298
}
292299
}
293300
}
294-
let mut already_tested = HashSet::with_capacity(all_commits.len());
295-
already_tested.extend(all_commits);
301+
let mut already_tested = all_commits.clone();
296302
let mut i = 0;
297-
while i != missing.len() {
298-
if !already_tested.insert(missing[i].0.sha.clone()) {
299-
missing.remove(i);
303+
while i != queue.len() {
304+
if !already_tested.insert(queue[i].0.sha.clone()) {
305+
queue.remove(i);
300306
} else {
301307
i += 1;
302308
}
303309
}
304-
missing
310+
sort_queue(all_commits.clone(), queue)
311+
}
312+
313+
fn sort_queue(
314+
mut done: HashSet<String>,
315+
mut unordered_queue: Vec<(Commit, MissingReason)>,
316+
) -> Vec<(Commit, MissingReason)> {
317+
// A topological sort, where each "level" is additionally altered such that
318+
// try commits come first, and then sorted by PR # (as a rough heuristic for
319+
// earlier requests).
320+
321+
let mut finished = 0;
322+
while finished < unordered_queue.len() {
323+
// The next level is those elements in the unordered queue which
324+
// are ready to be benchmarked (i.e., those with parent in done or no
325+
// parent).
326+
let level_len = partition_in_place(unordered_queue[finished..].iter_mut(), |(_, mr)| {
327+
mr.parent_sha().map_or(true, |parent| done.contains(parent))
328+
});
329+
assert!(
330+
level_len != 0,
331+
"at least one commit is ready done={:#?}, {:?}",
332+
done,
333+
&unordered_queue[finished..]
334+
);
335+
let level = &mut unordered_queue[finished..][..level_len];
336+
level.sort_unstable_by_key(|(c, mr)| {
337+
(
338+
// InProgress MR go first (false < true)
339+
mr.parent_sha().is_some(),
340+
mr.pr().unwrap_or(0),
341+
c.sha.clone(),
342+
)
343+
});
344+
for (c, _) in level {
345+
done.insert(c.sha.clone());
346+
}
347+
finished += level_len;
348+
}
349+
unordered_queue
350+
}
351+
352+
// Copy of Iterator::partition_in_place, which is currently unstable.
353+
fn partition_in_place<'a, I, T: 'a, P>(mut iter: I, ref mut predicate: P) -> usize
354+
where
355+
I: Sized + DoubleEndedIterator<Item = &'a mut T>,
356+
P: FnMut(&T) -> bool,
357+
{
358+
// FIXME: should we worry about the count overflowing? The only way to have more than
359+
// `usize::MAX` mutable references is with ZSTs, which aren't useful to partition...
360+
361+
// These closure "factory" functions exist to avoid genericity in `Self`.
362+
363+
#[inline]
364+
fn is_false<'a, T>(
365+
predicate: &'a mut impl FnMut(&T) -> bool,
366+
true_count: &'a mut usize,
367+
) -> impl FnMut(&&mut T) -> bool + 'a {
368+
move |x| {
369+
let p = predicate(&**x);
370+
*true_count += p as usize;
371+
!p
372+
}
373+
}
374+
375+
#[inline]
376+
fn is_true<T>(predicate: &mut impl FnMut(&T) -> bool) -> impl FnMut(&&mut T) -> bool + '_ {
377+
move |x| predicate(&**x)
378+
}
379+
380+
// Repeatedly find the first `false` and swap it with the last `true`.
381+
let mut true_count = 0;
382+
while let Some(head) = iter.find(is_false(predicate, &mut true_count)) {
383+
if let Some(tail) = iter.rfind(is_true(predicate)) {
384+
std::mem::swap(head, tail);
385+
true_count += 1;
386+
} else {
387+
break;
388+
}
389+
}
390+
true_count
305391
}
306392

307393
/// One decimal place rounded percent
@@ -381,8 +467,8 @@ mod tests {
381467
},
382468
];
383469
let in_progress_artifacts = vec![];
384-
// Have not benchmarked anything yet.
385-
let all_commits = HashSet::new();
470+
let mut all_commits = HashSet::new();
471+
all_commits.insert("c".into());
386472

387473
let expected = vec![
388474
(
@@ -414,6 +500,7 @@ mod tests {
414500
},
415501
MissingReason::Try {
416502
pr: 3,
503+
parent_sha: "a".into(),
417504
include: None,
418505
exclude: None,
419506
runs: None,
@@ -479,8 +566,23 @@ mod tests {
479566
let in_progress_artifacts = vec![];
480567
let mut all_commits = HashSet::new();
481568
all_commits.insert(master_commits[1].sha.clone());
569+
// Parent trailers
570+
all_commits.insert(master_commits[0].parent_sha.clone());
571+
all_commits.insert(master_commits[1].parent_sha.clone());
572+
all_commits.insert(master_commits[2].parent_sha.clone());
482573

483574
let expected = vec![
575+
(
576+
Commit {
577+
sha: "123".into(),
578+
date: database::Date(time),
579+
},
580+
MissingReason::Master {
581+
pr: 11,
582+
parent_sha: "345".into(),
583+
is_try_parent: false,
584+
},
585+
),
484586
(
485587
Commit {
486588
sha: "foo".into(),
@@ -499,22 +601,12 @@ mod tests {
499601
},
500602
MissingReason::Try {
501603
pr: 101,
604+
parent_sha: "foo".into(),
502605
include: None,
503606
exclude: None,
504607
runs: None,
505608
},
506609
),
507-
(
508-
Commit {
509-
sha: "123".into(),
510-
date: database::Date(time),
511-
},
512-
MissingReason::Master {
513-
pr: 11,
514-
parent_sha: "345".into(),
515-
is_try_parent: false,
516-
},
517-
),
518610
];
519611
assert_eq!(
520612
expected,

0 commit comments

Comments
 (0)