Skip to content

Commit 9dcac46

Browse files
authored
fix: Fix stack overflow in degenerate cases (#137)
* fix: Fix Stack Overflow in degenerate cases * Refactor swap impl * Apply upstream fix * Bump flatbush version in tests
1 parent fc2e61e commit 9dcac46

File tree

10 files changed

+124
-61
lines changed

10 files changed

+124
-61
lines changed

benches/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
If the download link is dead, find the most recent download link here: https://github.com/microsoft/USBuildingFootprints
2+
13
```
24
wget https://usbuildingdata.blob.core.windows.net/usbuildings-v2/Utah.geojson.zip
35
pip install -U geopandas pyogrio

fixtures/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixtures data for test
2+
3+
- Run `npm run generate_fixtures` to generate data from the upstream `flatbush` library.

fixtures/data1_flatbush_js.raw

0 Bytes
Binary file not shown.

package-lock.json

Lines changed: 10 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{
22
"type": "module",
3-
"scripts": {},
3+
"scripts": {
4+
"generate_fixtures": "node fixtures/flatbush_generate.js"
5+
},
46
"volta": {
57
"node": "20.9.0"
68
},
79
"devDependencies": {
810
"@tsconfig/node20": "^20.1.2",
9-
"flatbush": "^4.2.0"
11+
"flatbush": "^4.5.0"
1012
}
1113
}

src/rtree/index.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,47 @@ impl<'a, N: IndexableNum> RTreeRef<'a, N> {
223223

224224
#[cfg(test)]
225225
mod tests {
226+
use crate::rtree::sort::{HilbertSort, STRSort, Sort};
227+
use crate::rtree::{RTreeBuilder, RTreeIndex};
228+
226229
use super::*;
227230

228231
#[test]
229232
fn rejects_short_buffers() {
230233
assert!(RTreeMetadata::<f64>::from_slice(&[]).is_err());
231234
assert!(RTreeMetadata::<f64>::from_slice(&[0; 7]).is_err());
232235
}
236+
237+
fn linspace(start: usize, stop: usize, num: usize, endpoint: bool) -> Vec<f64> {
238+
let div = if endpoint { num - 1 } else { num };
239+
let step = (stop - start) as f64 / div as f64;
240+
(0..num).map(|i| start as f64 + step * i as f64).collect()
241+
}
242+
243+
#[test]
244+
// https://github.com/mourner/flatbush/pull/65
245+
fn quicksort_should_work_with_an_inbalanced_dataset() {
246+
_quicksort_should_work_with_an_inbalanced_dataset::<HilbertSort>();
247+
_quicksort_should_work_with_an_inbalanced_dataset::<STRSort>();
248+
}
249+
250+
fn _quicksort_should_work_with_an_inbalanced_dataset<S: Sort<f64>>() {
251+
let n = 15000;
252+
let mut builder = RTreeBuilder::new(2 * n);
253+
254+
let items = linspace(0, 1000, n as usize, true);
255+
let items2 = linspace(0, 1000, n as usize, true);
256+
257+
for item in items {
258+
builder.add(item, 0., item, 0.);
259+
}
260+
261+
for item in items2 {
262+
builder.add(item, 0., item, 0.);
263+
}
264+
265+
let index = builder.finish::<S>();
266+
267+
index.search(-100., -1., 15000., 1.);
268+
}
233269
}

src/rtree/sort/hilbert.rs

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::indices::MutableIndices;
22
use crate::r#type::IndexableNum;
3+
use crate::rtree::sort::util::swap;
34
use crate::rtree::sort::{Sort, SortParams};
45

56
/// An implementation of hilbert sorting.
@@ -70,8 +71,22 @@ fn sort<N: IndexableNum>(
7071
return;
7172
}
7273

73-
let midpoint = (left + right) / 2;
74-
let pivot = values[midpoint];
74+
// apply median of three method
75+
let start = values[left];
76+
let mid = values[(left + right) >> 1];
77+
let end = values[right];
78+
79+
let x = start.max(mid);
80+
let pivot = if end > x {
81+
x
82+
} else if x == start {
83+
mid.max(end)
84+
} else if x == mid {
85+
start.max(end)
86+
} else {
87+
end
88+
};
89+
7590
let mut i = left.wrapping_sub(1);
7691
let mut j = right.wrapping_add(1);
7792

@@ -101,27 +116,6 @@ fn sort<N: IndexableNum>(
101116
sort(values, boxes, indices, j.wrapping_add(1), right, node_size);
102117
}
103118

104-
/// Swap two values and two corresponding boxes.
105-
#[inline]
106-
fn swap<N: IndexableNum>(
107-
values: &mut [u32],
108-
boxes: &mut [N],
109-
indices: &mut MutableIndices,
110-
i: usize,
111-
j: usize,
112-
) {
113-
values.swap(i, j);
114-
115-
let k = 4 * i;
116-
let m = 4 * j;
117-
boxes.swap(k, m);
118-
boxes.swap(k + 1, m + 1);
119-
boxes.swap(k + 2, m + 2);
120-
boxes.swap(k + 3, m + 3);
121-
122-
indices.swap(i, j);
123-
}
124-
125119
// Taken from static_aabb2d_index under the mit/apache license
126120
// https://github.com/jbuckmccready/static_aabb2d_index/blob/9e6add59d77b74d4de0ac32159db47fbcb3acc28/src/static_aabb2d_index.rs#L486C1-L544C2
127121
#[inline]

src/rtree/sort/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod hilbert;
44
mod str;
55
mod r#trait;
6+
mod util;
67

78
pub use hilbert::HilbertSort;
89
pub use r#str::STRSort;

src/rtree/sort/str.rs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterato
33

44
use crate::indices::MutableIndices;
55
use crate::r#type::IndexableNum;
6+
use crate::rtree::sort::util::swap;
67
use crate::rtree::sort::{Sort, SortParams};
78

89
/// An implementation of sort-tile-recursive (STR) sorting.
@@ -98,6 +99,17 @@ impl<N: IndexableNum> Sort<N> for STRSort {
9899
}
99100
}
100101

102+
/// Max is only implemented for `Ord` types, but float types do not implement `Ord`.
103+
///
104+
/// So we use this as a hack to get the maximum of two PartialOrd values.
105+
fn partial_ord_max<N: IndexableNum>(a: N, b: N) -> N {
106+
if a > b {
107+
a
108+
} else {
109+
b
110+
}
111+
}
112+
101113
/// Custom quicksort that partially sorts bbox data alongside their sort values.
102114
// Partially taken from static_aabb2d_index under the MIT/Apache license
103115
fn sort<N: IndexableNum>(
@@ -114,8 +126,22 @@ fn sort<N: IndexableNum>(
114126
return;
115127
}
116128

117-
let midpoint = (left + right) / 2;
118-
let pivot = values[midpoint];
129+
// apply median of three method
130+
let start = values[left];
131+
let mid = values[(left + right) >> 1];
132+
let end = values[right];
133+
134+
let x = partial_ord_max(start, mid);
135+
let pivot = if end > x {
136+
x
137+
} else if x == start {
138+
partial_ord_max(mid, end)
139+
} else if x == mid {
140+
partial_ord_max(start, end)
141+
} else {
142+
end
143+
};
144+
119145
let mut i = left.wrapping_sub(1);
120146
let mut j = right.wrapping_add(1);
121147

@@ -144,24 +170,3 @@ fn sort<N: IndexableNum>(
144170
sort(values, boxes, indices, left, j, node_size);
145171
sort(values, boxes, indices, j.wrapping_add(1), right, node_size);
146172
}
147-
148-
/// Swap two values and two corresponding boxes.
149-
#[inline]
150-
fn swap<N: IndexableNum>(
151-
values: &mut [N],
152-
boxes: &mut [N],
153-
indices: &mut MutableIndices,
154-
i: usize,
155-
j: usize,
156-
) {
157-
values.swap(i, j);
158-
159-
let k = 4 * i;
160-
let m = 4 * j;
161-
boxes.swap(k, m);
162-
boxes.swap(k + 1, m + 1);
163-
boxes.swap(k + 2, m + 2);
164-
boxes.swap(k + 3, m + 3);
165-
166-
indices.swap(i, j);
167-
}

src/rtree/sort/util.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::indices::MutableIndices;
2+
use crate::IndexableNum;
3+
4+
/// Swap two values and two corresponding boxes.
5+
#[inline]
6+
pub(super) fn swap<V: IndexableNum, N: IndexableNum>(
7+
values: &mut [V],
8+
boxes: &mut [N],
9+
indices: &mut MutableIndices,
10+
i: usize,
11+
j: usize,
12+
) {
13+
values.swap(i, j);
14+
15+
let k = 4 * i;
16+
let m = 4 * j;
17+
boxes.swap(k, m);
18+
boxes.swap(k + 1, m + 1);
19+
boxes.swap(k + 2, m + 2);
20+
boxes.swap(k + 3, m + 3);
21+
22+
indices.swap(i, j);
23+
}

0 commit comments

Comments
 (0)