Skip to content

Commit 2777649

Browse files
committed
Replace boost.bimap with std::unordered_map
1 parent ba6bee4 commit 2777649

File tree

4 files changed

+119
-31
lines changed

4 files changed

+119
-31
lines changed

src/stream_info_impl.cpp

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -188,34 +188,56 @@ void stream_info_impl::from_fullinfo_message(const std::string &m) {
188188
read_xml(doc_);
189189
}
190190

191-
/**
192-
* Test whether this stream info matches the given query string.
193-
*/
194-
bool stream_info_impl::matches_query(const string &query) {
191+
/// Test whether this stream info matches the given query string.
192+
bool stream_info_impl::matches_query(const string &query, bool nocache) {
193+
return cached_.matches_query(doc_, query, nocache);
194+
}
195+
196+
bool query_cache::matches_query(const xml_document &doc, const std::string query, bool nocache) {
195197
lslboost::lock_guard<lslboost::mutex> lock(cache_mut_);
196-
query_cache::left_iterator it = cached_.left.find(query);
197-
if (it != cached_.left.end()) {
198-
// found in cache
199-
bool is_match = it->second.second;
198+
199+
decltype(cache)::iterator it;
200+
if (!nocache && (it = cache.find(query)) != cache.end()) {
201+
// the sign bit encodes if the query matches or not
202+
bool matches = it->second > 0;
200203
// update the last-use time stamp
201-
cached_.left.replace_data(it,std::make_pair(lsl_clock(),is_match));
202-
return is_match;
203-
} else {
204-
// not found in cache
205-
try {
206-
// compute whether it matches
207-
string fullquery = (string("/info[") += query) += "]";
208-
bool result = !doc_.select_nodes(fullquery.c_str()).empty();
209-
// insert result into cache
210-
cached_.left.insert(std::make_pair(query,std::make_pair(lsl_clock(),result)));
211-
// remove oldest results until we're within capacity
212-
while ((int)cached_.size() > api_config::get_instance()->max_cached_queries())
213-
cached_.right.erase(cached_.right.begin());
214-
// return result
215-
return result;
216-
} catch(...) {
217-
return false; // error: unsupported query
204+
it->second = ++query_cache_age * (matches ? 1 : -1);
205+
// return cached match
206+
return matches;
207+
}
208+
209+
// not found in cache
210+
try {
211+
// compute whether it matches
212+
bool matched = pugi::xpath_query(query.c_str()).evaluate_boolean(doc.first_child());
213+
214+
auto max_cached = (std::size_t)api_config::get_instance()->max_cached_queries();
215+
if(nocache || max_cached == 0)
216+
return matched;
217+
218+
cache.insert(std::make_pair(query, ++query_cache_age * (matched ? 1 : -1)));
219+
220+
// remove n/2 oldest results to make room for new entries
221+
if (cache.size() > max_cached) {
222+
// Find out the median cache entry age
223+
std::vector<int> agevec;
224+
agevec.reserve(cache.size());
225+
for (auto &val : cache) agevec.push_back(std::abs(val.second));
226+
auto middle = agevec.begin() + max_cached / 2;
227+
std::nth_element(agevec.begin(), middle, agevec.end());
228+
auto oldest_to_keep = *middle;
229+
230+
// Remove all elements older than the median age
231+
for (auto it = cache.begin(); it != cache.end();)
232+
if (abs(it->second) <= oldest_to_keep)
233+
it = cache.erase(it);
234+
else
235+
++it;
218236
}
237+
return matched;
238+
} catch (std::exception &e) {
239+
LOG_F(WARNING, "Query error: %s", e.what());
240+
return false;
219241
}
220242
}
221243

src/stream_info_impl.h

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@
22
#define STREAM_INFO_IMPL_H
33

44
#include "common.h"
5-
#include <boost/bimap.hpp>
6-
#include <boost/thread/mutex.hpp>
75
#include "pugixml/pugixml.hpp"
6+
#include <boost/thread/mutex.hpp>
7+
#include <unordered_map>
88

99
namespace lsl {
1010

11+
/// LRU cache for queries
12+
class query_cache {
13+
std::unordered_map<std::string, int> cache;
14+
int query_cache_age{0};
15+
lslboost::mutex cache_mut_;
16+
public:
17+
bool matches_query(const pugi::xml_document& doc, const std::string query, bool nocache);
18+
};
19+
1120
/**
1221
* Actual implementation of the stream_info class.
1322
* The stream_info class forwards all operations to an instance of this class.
1423
*/
1524
class stream_info_impl {
16-
/// The query cache is a (bidirectional) mapping between query-strings and pairs of (last-use-timestamp, matching-true/false)
17-
typedef lslboost::bimap<std::string,std::pair<double,bool> > query_cache;
1825
public:
1926

2027
/**
@@ -80,7 +87,7 @@ namespace lsl {
8087
* The info "matches" if the given XPath 1.0 query string returns a non-empty node set.
8188
* @return Whether stream info is matched by the query string.
8289
*/
83-
bool matches_query(const std::string &query);
90+
bool matches_query(const std::string &query, bool nocache = false);
8491

8592

8693
//
@@ -255,7 +262,6 @@ namespace lsl {
255262
pugi::xml_document doc_;
256263
// cached query results
257264
query_cache cached_;
258-
lslboost::mutex cache_mut_;
259265
};
260266

261267

testing/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ project(lsltests
55
)
66
cmake_minimum_required (VERSION 3.12)
77
enable_testing()
8+
9+
option(LSL_BENCHMARKS "Enable benchmarks in unit tests" OFF)
10+
811
add_library(catch_main OBJECT catch_main.cpp)
912
target_compile_features(catch_main PUBLIC cxx_std_11)
1013

1114
target_compile_definitions(catch_main PRIVATE LSL_VERSION_INFO=${LSL_VERSION_INFO})
15+
if(LSL_BENCHMARKS)
16+
target_compile_definitions(catch_main PUBLIC CATCH_CONFIG_ENABLE_BENCHMARKING)
17+
endif()
18+
1219
add_executable(lsl_test_exported
1320
DataType.cpp
1421
discovery.cpp
@@ -21,6 +28,7 @@ add_executable(lsl_test_internal
2128
asiocancel.cpp
2229
inireader.cpp
2330
stringfuncs.cpp
31+
streaminfo.cpp
2432
)
2533
target_link_libraries(lsl_test_internal PRIVATE lslobj lslboost catch_main)
2634

testing/streaminfo.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include "../src/stream_info_impl.h"
2+
#include "../src/api_config.h"
3+
#include "catch.hpp"
4+
5+
TEST_CASE("streaminfo matching via XPath", "[basic][streaminfo][xml]") {
6+
lsl::stream_info_impl info(
7+
"streamname", "streamtype", 8, 500, lsl_channel_format_t::cft_string, "sourceid");
8+
auto channels = info.desc().append_child("channels");
9+
for(int i=0; i< 4;++i)
10+
channels.append_child("channel").append_child("type").append_child(pugi::node_pcdata).set_value("EEG");
11+
for(int i=0; i< 4;++i)
12+
channels.append_child("channel").append_child("type").append_child(pugi::node_pcdata).set_value("EOG");
13+
14+
#ifdef CATCH_CONFIG_ENABLE_BENCHMARKING
15+
// Append lots of dummy channels for performance tests
16+
for(int i=0; i<50000; ++i)
17+
channels.append_child("chn").append_child("type").append_child(pugi::node_pcdata).set_value("foobar");
18+
for(int i=0; i<2000; ++i) {
19+
channels = channels.append_child("chn");
20+
channels.append_child(pugi::node_pcdata).set_value("1");
21+
}
22+
23+
BENCHMARK("trivial query") {
24+
return info.matches_query("name='streamname' and type='streamtype'", true);
25+
};
26+
BENCHMARK("complicated query") {
27+
return info.matches_query("count(desc/channels/channel[type='EEG'])>3", true);
28+
};
29+
BENCHMARK("Cached query") {
30+
return info.matches_query("count(desc/channels/channel[type='EEG'])>3", false);
31+
};
32+
33+
// test how well the cache copes with lots of different queries
34+
BENCHMARK("partially cached queries (x1000)") {
35+
int matches = 0;
36+
for (int j = 0; j < 1000; ++j)
37+
matches += info.matches_query(("0<=" + std::to_string(j)).c_str());
38+
return matches;
39+
};
40+
41+
#endif
42+
43+
INFO(info.to_fullinfo_message());
44+
REQUIRE(info.matches_query("name='streamname'"));
45+
REQUIRE(info.matches_query("name='streamname' and type='streamtype'"));
46+
REQUIRE(info.matches_query("channel_count > 5"));
47+
REQUIRE(info.matches_query("nominal_srate >= 499"));
48+
REQUIRE(info.matches_query("count(desc/channels/channel[type='EEG'])>3"));
49+
50+
REQUIRE(!info.matches_query("in'va'lid"));
51+
REQUIRE(!info.matches_query("name='othername'"));
52+
}

0 commit comments

Comments
 (0)