##// END OF EJS Templates
rust: peek_mut optim for lazy ancestors...
Georges Racinet -
r40847:e13ab4ac default
parent child Browse files
Show More
@@ -1,270 +1,273
1 1 // ancestors.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Rust versions of generic DAG ancestors algorithms for Mercurial
9 9
10 10 use super::{Graph, GraphError, Revision, NULL_REVISION};
11 11 use std::collections::{BinaryHeap, HashSet};
12 12
13 13 /// Iterator over the ancestors of a given list of revisions
14 14 /// This is a generic type, defined and implemented for any Graph, so that
15 15 /// it's easy to
16 16 ///
17 17 /// - unit test in pure Rust
18 18 /// - bind to main Mercurial code, potentially in several ways and have these
19 19 /// bindings evolve over time
20 20 pub struct AncestorsIterator<G: Graph> {
21 21 graph: G,
22 22 visit: BinaryHeap<Revision>,
23 23 seen: HashSet<Revision>,
24 24 stoprev: Revision,
25 25 }
26 26
27 27 impl<G: Graph> AncestorsIterator<G> {
28 28 /// Constructor.
29 29 ///
30 30 /// if `inclusive` is true, then the init revisions are emitted in
31 31 /// particular, otherwise iteration starts from their parents.
32 32 pub fn new<I>(
33 33 graph: G,
34 34 initrevs: I,
35 35 stoprev: Revision,
36 36 inclusive: bool,
37 37 ) -> Result<Self, GraphError>
38 38 where
39 39 I: IntoIterator<Item = Revision>,
40 40 {
41 41 let filtered_initrevs = initrevs.into_iter().filter(|&r| r >= stoprev);
42 42 if inclusive {
43 43 let visit: BinaryHeap<Revision> = filtered_initrevs.collect();
44 44 let seen = visit.iter().map(|&x| x).collect();
45 45 return Ok(AncestorsIterator {
46 46 visit: visit,
47 47 seen: seen,
48 48 stoprev: stoprev,
49 49 graph: graph,
50 50 });
51 51 }
52 52 let mut this = AncestorsIterator {
53 53 visit: BinaryHeap::new(),
54 54 seen: HashSet::new(),
55 55 stoprev: stoprev,
56 56 graph: graph,
57 57 };
58 58 this.seen.insert(NULL_REVISION);
59 59 for rev in filtered_initrevs {
60 this.conditionally_push_parents(rev)?;
60 let parents = this.graph.parents(rev)?;
61 this.conditionally_push_rev(parents.0);
62 this.conditionally_push_rev(parents.1);
61 63 }
62 64 Ok(this)
63 65 }
64 66
65 67 #[inline]
66 68 fn conditionally_push_rev(&mut self, rev: Revision) {
67 69 if self.stoprev <= rev && !self.seen.contains(&rev) {
68 70 self.seen.insert(rev);
69 71 self.visit.push(rev);
70 72 }
71 73 }
72 74
73 #[inline]
74 fn conditionally_push_parents(
75 &mut self,
76 rev: Revision,
77 ) -> Result<(), GraphError> {
78 let parents = self.graph.parents(rev)?;
79 self.conditionally_push_rev(parents.0);
80 self.conditionally_push_rev(parents.1);
81 Ok(())
82 }
83
84 75 /// Consumes partially the iterator to tell if the given target
85 76 /// revision
86 77 /// is in the ancestors it emits.
87 78 /// This is meant for iterators actually dedicated to that kind of
88 79 /// purpose
89 80 pub fn contains(&mut self, target: Revision) -> bool {
90 81 if self.seen.contains(&target) && target != NULL_REVISION {
91 82 return true;
92 83 }
93 84 for rev in self {
94 85 if rev == target {
95 86 return true;
96 87 }
97 88 if rev < target {
98 89 return false;
99 90 }
100 91 }
101 92 false
102 93 }
103 94 }
104 95
105 96 /// Main implementation.
106 97 ///
107 98 /// The algorithm is the same as in `_lazyancestorsiter()` from `ancestors.py`
108 99 /// with a few non crucial differences:
109 100 ///
110 101 /// - there's no filtering of invalid parent revisions. Actually, it should be
111 102 /// consistent and more efficient to filter them from the end caller.
112 /// - we don't use the equivalent of `heapq.heapreplace()`, but we should, for
113 /// the same reasons (using `peek_mut`)
114 /// - we don't have the optimization for adjacent revs (case where p1 == rev-1)
103 /// - we don't have the optimization for adjacent revs
104 /// (case where p1 == rev-1), because it amounts to update the first element
105 /// of the heap without sifting, which Rust's BinaryHeap doesn't let us do.
115 106 /// - we save a few pushes by comparing with `stoprev` before pushing
116 107 ///
117 108 /// Error treatment:
118 109 /// We swallow the possible GraphError of conditionally_push_parents() to
119 110 /// respect the Iterator trait in a simple manner: never emitting parents
120 111 /// for the returned revision. We finds this good enough for now, because:
121 112 ///
122 113 /// - there's a good chance that invalid revisionss are fed from the start,
123 114 /// and `new()` doesn't swallow the error result.
124 115 /// - this is probably what the Python implementation produces anyway, due
125 116 /// to filtering at each step, and Python code is currently the only
126 117 /// concrete caller we target, so we shouldn't need a finer error treatment
127 118 /// for the time being.
128 119 impl<G: Graph> Iterator for AncestorsIterator<G> {
129 120 type Item = Revision;
130 121
131 122 fn next(&mut self) -> Option<Revision> {
132 let current = match self.visit.pop() {
123 let current = match self.visit.peek() {
133 124 None => {
134 125 return None;
135 126 }
136 Some(i) => i,
127 Some(c) => *c,
137 128 };
138 self.conditionally_push_parents(current).unwrap_or(());
129 let parents = self
130 .graph
131 .parents(current)
132 .unwrap_or((NULL_REVISION, NULL_REVISION));
133 let p1 = parents.0;
134 if p1 < self.stoprev || self.seen.contains(&p1) {
135 self.visit.pop();
136 } else {
137 *(self.visit.peek_mut().unwrap()) = p1;
138 self.seen.insert(p1);
139 };
140
141 self.conditionally_push_rev(parents.1);
139 142 Some(current)
140 143 }
141 144 }
142 145
143 146 #[cfg(test)]
144 147 mod tests {
145 148
146 149 use super::*;
147 150
148 151 #[derive(Clone, Debug)]
149 152 struct Stub;
150 153
151 154 /// This is the same as the dict from test-ancestors.py
152 155 impl Graph for Stub {
153 156 fn parents(
154 157 &self,
155 158 rev: Revision,
156 159 ) -> Result<(Revision, Revision), GraphError> {
157 160 match rev {
158 161 0 => Ok((-1, -1)),
159 162 1 => Ok((0, -1)),
160 163 2 => Ok((1, -1)),
161 164 3 => Ok((1, -1)),
162 165 4 => Ok((2, -1)),
163 166 5 => Ok((4, -1)),
164 167 6 => Ok((4, -1)),
165 168 7 => Ok((4, -1)),
166 169 8 => Ok((-1, -1)),
167 170 9 => Ok((6, 7)),
168 171 10 => Ok((5, -1)),
169 172 11 => Ok((3, 7)),
170 173 12 => Ok((9, -1)),
171 174 13 => Ok((8, -1)),
172 175 r => Err(GraphError::ParentOutOfRange(r)),
173 176 }
174 177 }
175 178 }
176 179
177 180 fn list_ancestors<G: Graph>(
178 181 graph: G,
179 182 initrevs: Vec<Revision>,
180 183 stoprev: Revision,
181 184 inclusive: bool,
182 185 ) -> Vec<Revision> {
183 186 AncestorsIterator::new(graph, initrevs, stoprev, inclusive)
184 187 .unwrap()
185 188 .collect()
186 189 }
187 190
188 191 #[test]
189 192 /// Same tests as test-ancestor.py, without membership
190 193 /// (see also test-ancestor.py.out)
191 194 fn test_list_ancestor() {
192 195 assert_eq!(list_ancestors(Stub, vec![], 0, false), vec![]);
193 196 assert_eq!(
194 197 list_ancestors(Stub, vec![11, 13], 0, false),
195 198 vec![8, 7, 4, 3, 2, 1, 0]
196 199 );
197 200 assert_eq!(list_ancestors(Stub, vec![1, 3], 0, false), vec![1, 0]);
198 201 assert_eq!(
199 202 list_ancestors(Stub, vec![11, 13], 0, true),
200 203 vec![13, 11, 8, 7, 4, 3, 2, 1, 0]
201 204 );
202 205 assert_eq!(list_ancestors(Stub, vec![11, 13], 6, false), vec![8, 7]);
203 206 assert_eq!(
204 207 list_ancestors(Stub, vec![11, 13], 6, true),
205 208 vec![13, 11, 8, 7]
206 209 );
207 210 assert_eq!(list_ancestors(Stub, vec![11, 13], 11, true), vec![13, 11]);
208 211 assert_eq!(list_ancestors(Stub, vec![11, 13], 12, true), vec![13]);
209 212 assert_eq!(
210 213 list_ancestors(Stub, vec![10, 1], 0, true),
211 214 vec![10, 5, 4, 2, 1, 0]
212 215 );
213 216 }
214 217
215 218 #[test]
216 219 /// Corner case that's not directly in test-ancestors.py, but
217 220 /// that happens quite often, as demonstrated by running the whole
218 221 /// suite.
219 222 /// For instance, run tests/test-obsolete-checkheads.t
220 223 fn test_nullrev_input() {
221 224 let mut iter =
222 225 AncestorsIterator::new(Stub, vec![-1], 0, false).unwrap();
223 226 assert_eq!(iter.next(), None)
224 227 }
225 228
226 229 #[test]
227 230 fn test_contains() {
228 231 let mut lazy =
229 232 AncestorsIterator::new(Stub, vec![10, 1], 0, true).unwrap();
230 233 assert!(lazy.contains(1));
231 234 assert!(!lazy.contains(3));
232 235
233 236 let mut lazy =
234 237 AncestorsIterator::new(Stub, vec![0], 0, false).unwrap();
235 238 assert!(!lazy.contains(NULL_REVISION));
236 239 }
237 240
238 241 /// A corrupted Graph, supporting error handling tests
239 242 struct Corrupted;
240 243
241 244 impl Graph for Corrupted {
242 245 fn parents(
243 246 &self,
244 247 rev: Revision,
245 248 ) -> Result<(Revision, Revision), GraphError> {
246 249 match rev {
247 250 1 => Ok((0, -1)),
248 251 r => Err(GraphError::ParentOutOfRange(r)),
249 252 }
250 253 }
251 254 }
252 255
253 256 #[test]
254 257 fn test_initrev_out_of_range() {
255 258 // inclusive=false looks up initrev's parents right away
256 259 match AncestorsIterator::new(Stub, vec![25], 0, false) {
257 260 Ok(_) => panic!("Should have been ParentOutOfRange"),
258 261 Err(e) => assert_eq!(e, GraphError::ParentOutOfRange(25)),
259 262 }
260 263 }
261 264
262 265 #[test]
263 266 fn test_next_out_of_range() {
264 267 // inclusive=false looks up initrev's parents right away
265 268 let mut iter =
266 269 AncestorsIterator::new(Corrupted, vec![1], 0, false).unwrap();
267 270 assert_eq!(iter.next(), Some(0));
268 271 assert_eq!(iter.next(), None);
269 272 }
270 273 }
General Comments 0
You need to be logged in to leave comments. Login now