Files
filament/libs/utils/include/utils/RangeMap.h
Philip Rideout e99f13b14d Fix some RangeMap behavior and improve its unit test.
The existing unit test had an incorrect expectation in what happens
after splicing an existing range. Fixing the unit test revealed an
actual bug in the RangeMap implementation. It should never modify the
lower bound of an existing interval, because that invalidates the
iterator. It should also never retain a pointer or reference to a range
when the range is potentially removed by subsequent codelines.
2022-02-15 15:46:34 -08:00

309 lines
11 KiB
C++

/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TNT_UTILS_RANGEMAP_H
#define TNT_UTILS_RANGEMAP_H
#include <utils/Panic.h>
#include <utils/Range.h>
#include <utils/debug.h>
#include <map>
namespace utils {
/**
* Sparse container for a series of ordered non-overlapping intervals.
*
* RangeMap has a low memory footprint if it contains fairly homogeneous data. Internally, the
* intervals are automatically split and merged as elements are added or removed.
*
* Each interval maps to an instance of ValueType, which should support cheap equality checks
* and copy assignment. (simple concrete types are ideal)
*
* KeyType should support operator< because intervals are internally sorted using std::map.
*/
template<typename KeyType, typename ValueType>
class RangeMap {
public:
/**
* Replaces all slots between first (inclusive) and last (exclusive).
*/
void add(KeyType first, KeyType last, const ValueType& value) noexcept {
// First check if an existing range contains "first".
Iterator iter = findRange(first);
if (iter != end()) {
const Range<KeyType> existing = getRange(iter);
// Check if the existing range be extended.
if (getValue(iter) == value) {
if (existing.last < last) {
wipe(existing.last, last);
iter = shrink(iter, existing.first, last);
mergeRight(iter);
}
return;
}
// Split the existing range into two ranges.
if (last < existing.last && first > existing.first) {
iter = shrink(iter, existing.first, first);
insert(first, last, value);
insert(last, existing.last, getValue(iter));
return;
}
clear(first, last);
insert(first, last, value);
return;
}
// Check if an existing range contains the end of the new range.
KeyType back = last;
iter = findRange(--back);
if (iter == end()) {
wipe(first, last);
insert(first, last, value);
return;
}
const Range<KeyType> existing = getRange(iter);
// Check if the existing range be extended.
if (getValue(iter) == value) {
if (existing.first > first) {
wipe(first, existing.first);
iter = shrink(iter, first, existing.last);
mergeLeft(iter);
}
return;
}
// Clip the beginning of the existing range and potentially remove it.
if (last < existing.last) {
shrink(iter, last, existing.last);
}
wipe(first, last);
insert(first, last, value);
}
/**
* Shorthand for the "add" method that inserts a single element.
*/
void set(KeyType key, const ValueType& value) noexcept {
KeyType begin = key;
add(begin, ++key, value);
}
/**
* Checks if a range exists that encompasses the given key.
*/
bool has(KeyType key) const noexcept {
return findRange(key) != mMap.end();
}
/**
* Retrieves the element at the given location, panics if no element exists.
*/
const ValueType& get(KeyType key) const {
ConstIterator iter = findRange(key);
ASSERT_PRECONDITION(iter != end(), "RangeMap: No element exists at the given key.");
return getValue(iter);
}
/**
* Removes all elements between begin (inclusive) and end (exclusive).
*/
void clear(KeyType first, KeyType last) noexcept {
// Check if an existing range contains "first".
Iterator iter = findRange(first);
if (iter != end()) {
const Range<KeyType> existing = getRange(iter);
// Split the existing range into two ranges.
if (last < existing.last && first > existing.first) {
iter = shrink(iter, existing.first, first);
insert(last, existing.last, getValue(iter));
return;
}
// Clip one of the ends of the existing range or remove it.
if (first > existing.first) {
shrink(iter, existing.first, first);
} else if (last < existing.last) {
shrink(iter, last, existing.last);
} else {
wipe(first, last);
}
// There might be another range that intersects the cleared range, so try again.
clear(first, last);
return;
}
// Check if an existing range contains the end of the new range.
KeyType back = last;
iter = findRange(--back);
if (iter == end()) {
wipe(first, last);
return;
}
const Range<KeyType> existing = getRange(iter);
// Clip the beginning of the existing range and potentially remove it.
if (last < existing.last) {
shrink(iter, last, existing.last);
}
wipe(first, last);
}
/**
* Shorthand for the "clear" method that clears a single element.
*/
void reset(KeyType key) noexcept {
KeyType begin = key;
clear(begin, ++key);
}
/**
* Returns the number of internal interval objects (rarely used).
*/
size_t rangeCount() const noexcept { return mMap.size(); }
private:
using Map = std::map<KeyType, std::pair<Range<KeyType>, ValueType>>;
using Iterator = typename Map::iterator;
using ConstIterator = typename Map::const_iterator;
ConstIterator begin() const noexcept { return mMap.begin(); }
ConstIterator end() const noexcept { return mMap.end(); }
Iterator begin() noexcept { return mMap.begin(); }
Iterator end() noexcept { return mMap.end(); }
Range<KeyType>& getRange(Iterator iter) const { return iter->second.first; }
ValueType& getValue(Iterator iter) const { return iter->second.second; }
const Range<KeyType>& getRange(ConstIterator iter) const { return iter->second.first; }
const ValueType& getValue(ConstIterator iter) const { return iter->second.second; }
// Private helper that assumes there is no existing range that overlaps the given range.
void insert(KeyType first, KeyType last, const ValueType& value) noexcept {
assert_invariant(!has(first));
assert_invariant(!has(last - 1));
// Check if there is an adjacent range to the left than can be extended.
KeyType previous = first;
if (Iterator iter = findRange(--previous); iter != end() && getValue(iter) == value) {
getRange(iter).last = last;
mergeRight(iter);
return;
}
// Check if there is an adjacent range to the right than can be extended.
if (Iterator iter = findRange(last); iter != end() && getValue(iter) == value) {
getRange(iter).first = first;
return;
}
mMap[first] = {Range<KeyType> { first, last }, value};
}
// Private helper that erases all intervals that are wholly contained within the given range.
// Note that this is quite different from the public "clear" method.
void wipe(KeyType first, KeyType last) noexcept {
// Find the first range whose beginning is greater than or equal to "first".
Iterator iter = mMap.lower_bound(first);
while (iter != end() && getRange(iter).first < last) {
KeyType existing_last = getRange(iter).last;
if (existing_last > last) {
break;
}
iter = mMap.erase(iter);
}
}
// Checks if there is range to the right that touches the given range.
// If so, erases it, extends the given range rightwards, and returns true.
bool mergeRight(Iterator iter) {
Iterator next = iter;
if (++next == end() || getValue(next) != getValue(iter)) {
return false;
}
if (getRange(next).first != getRange(iter).last) {
return false;
}
getRange(iter).last = getRange(next).last;
mMap.erase(next);
return true;
}
// Checks if there is range to the left that touches the given range.
// If so, erases it, extends the given range leftwards, and returns true.
bool mergeLeft(Iterator iter) {
Iterator prev = iter;
if (--prev == end() || getValue(prev) != getValue(iter)) {
return false;
}
if (getRange(prev).last != getRange(iter).first) {
return false;
}
getRange(iter).first = getRange(prev).first;
mMap.erase(prev);
return true;
}
// Private helper that clips one end of an existing range.
Iterator shrink(Iterator iter, KeyType first, KeyType last) {
assert_invariant(first < last);
assert_invariant(getRange(iter).first == first || getRange(iter).last == last);
std::pair<utils::Range<KeyType>, ValueType> value = {{first, last}, iter->second.second};
mMap.erase(iter);
return mMap.insert({first, value}).first;
}
// If the given key is encompassed by an existing range, returns an iterator for that range.
// If no encompassing range exists, returns end().
ConstIterator findRange(KeyType key) const noexcept {
return findRangeT<ConstIterator>(*this, key);
}
// If the given key is encompassed by an existing range, returns an iterator for that range.
// If no encompassing range exists, returns end().
Iterator findRange(KeyType key) noexcept {
return findRangeT<Iterator>(*this, key);
}
// This template method allows us to avoid code duplication for const and non-const variants of
// findRange. C++17 has "std::as_const()" but that would not be helpful here, as we would still
// need to convert a const iterator to a non-const iterator.
template<typename IteratorType, typename SelfType>
static IteratorType findRangeT(SelfType& instance, KeyType key) noexcept {
// Find the first range whose beginning is greater than or equal to the given key.
IteratorType iter = instance.mMap.lower_bound(key);
if (iter != instance.end() && instance.getRange(iter).contains(key)) {
return iter;
}
// If that was the first range, or if the map is empty, return false.
if (iter == instance.begin()) {
return instance.end();
}
// Check the range immediately previous to the one that was found.
return instance.getRange(--iter).contains(key) ? iter : instance.end();
}
// This maps from the start value of each range to the range itself.
Map mMap;
};
} // namespace utils
#endif // TNT_UTILS_RANGEMAP_H