diff --git a/CHANGELOG.md b/CHANGELOG.md index a71113f..720b7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Better subclassing support: Determine classes dynamically, so that methods like str() are aware when our types are subclassed. - Removed no-op check and extra list allocation during chop() + - overlaps() on a range no longer scans the entire boundary table; + only boundary points inside the queried range are tested (#127) - Maintainers: - Load issue4 test data via JSON to save on compilation and collection time. diff --git a/intervaltree/intervaltree.py b/intervaltree/intervaltree.py index e273127..8e39b0a 100644 --- a/intervaltree/intervaltree.py +++ b/intervaltree/intervaltree.py @@ -619,10 +619,16 @@ def overlaps_range(self, begin, end): return False elif self.overlaps_point(begin): return True + # only test boundary points inside (begin, end), not the whole + # boundary table (#127); boundaries exactly at begin are already + # covered by the overlaps_point(begin) check above + boundary_table = self.boundary_table + bound_begin = boundary_table.bisect_left(begin) + bound_end = boundary_table.bisect_left(end) # up to, but not including end return any( self.overlaps_point(bound) - for bound in self.boundary_table - if begin < bound < end + for index in xrange(bound_begin, bound_end) + for bound in (boundary_table.keys()[index],) ) def split_overlaps(self): diff --git a/test/issues/issue127_test.py b/test/issues/issue127_test.py new file mode 100644 index 0000000..e8dc063 --- /dev/null +++ b/test/issues/issue127_test.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Regression test for issue #127: overlaps(begin, end) on a range scanned +every boundary point in the tree (O(n) pure-python comparisons per query) +instead of only the points inside (begin, end). + +300k disjoint intervals -> 600k boundary points. A miss-case query in the +unfixed code takes ~4s for 200 queries; with the boundary scan restricted +to the query range it should be under a tenth of a second. +""" +import time + +from intervaltree import IntervalTree + + +def test_overlaps_range_scans_only_query_range(): + tree = IntervalTree.from_tuples((i * 10, i * 10 + 4) for i in range(300000)) + + # sanity: semantics must be unchanged + assert tree.overlaps(500006, 500009) is False # range inside a pure gap + assert tree.overlaps(500006, 500012) is True # range reaches (500010, 500014) + + t0 = time.time() + for _ in range(200): + tree.overlaps(500006, 500009) + # unfixed: ~4s (600k boundary comparisons per query) + # fixed: ~0.01s (only the 0-2 boundaries inside the range) + assert time.time() - t0 < 1.0