Repository :
https://github.com/colorer/Colorer-library
On branches: master,revert_thread
Link :
https://github.com/colorer/Colorer-library/commit/f4e08896e777629cefa947e4511884d1511c79c5
>---------------------------------------------------------------
commit f4e08896e777629cefa947e4511884d1511c79c5
Author: Aleksey Dobrunov <
cta...@ctapmex.com>
Date: Sun Sep 6 20:04:41 2026 +0500
Drop thread_local to avoid TLS overhead in shared-library builds
>---------------------------------------------------------------
f4e08896e777629cefa947e4511884d1511c79c5
.agents/skills/colorer-hrc/core-parse.md | 2 +-
src/colorer/cregexp/cregexp.cpp | 2 +-
src/colorer/cregexp/cregexp.h | 10 ++--
src/colorer/xml/libxml2/LibXmlReader.cpp | 16 +++---
src/colorer/xml/libxml2/SharedXmlInputSource.cpp | 6 +--
src/colorer/xml/libxml2/SharedXmlInputSource.h | 2 +-
tests/unit/test_baseeditor.cpp | 43 +++------------
tests/unit/test_cregexp.cpp | 69 ------------------------
tests/unit/test_xmlreader.cpp | 49 -----------------
9 files changed, 26 insertions(+), 173 deletions(-)
diff --git a/.agents/skills/colorer-hrc/core-parse.md b/.agents/skills/colorer-hrc/core-parse.md
index 85d4743..dc79bb5 100644
--- a/.agents/skills/colorer-hrc/core-parse.md
+++ b/.agents/skills/colorer-hrc/core-parse.md
@@ -130,7 +130,7 @@ Each attempt resets `\m`/`\M` and all capture slots so a failed offset cannot le
### lowParse
-Explicit backtracking stack (`insert_stack` / `check_stack`), not C++ recursion. `RegExpStack` is `thread_local` and **reused** across all `CRegExp` on that thread; `count_elem` is reset each `parseRE`. Do not clear it from `ParserFactory` teardown — a short-lived probe factory used to wipe a live editor parse on another object.
+Explicit backtracking stack (`insert_stack` / `check_stack`), not C++ recursion. `RegExpStack` is process-wide and **reused** across all `CRegExp`; `count_elem` is reset each `parseRE`. Do not parse concurrently. Do not clear it from `ParserFactory` teardown — a short-lived probe factory used to wipe a live editor parse on another object.
`parseStepLimit` (default 1e6) counts NFA steps in one `parse()`. Exceeded → match fails (not a wall-clock quantum). Pathological HRC (`FuncOutline`-style lazy overlap × `[^;]*`) hits this; the right fix is usually the scheme, not raising the limit.
diff --git a/src/colorer/cregexp/cregexp.cpp b/src/colorer/cregexp/cregexp.cpp
index ca3a46c..fbc94c6 100644
--- a/src/colorer/cregexp/cregexp.cpp
+++ b/src/colorer/cregexp/cregexp.cpp
@@ -2,7 +2,7 @@
#include <algorithm>
#include <climits>
-thread_local std::vector<StackElem> CRegExp::RegExpStack;
+std::vector<StackElem> CRegExp::RegExpStack;
/////////////////////////////////////////////////////////////////////////////
diff --git a/src/colorer/cregexp/cregexp.h b/src/colorer/cregexp/cregexp.h
index 557af78..8fe9f28 100644
--- a/src/colorer/cregexp/cregexp.h
+++ b/src/colorer/cregexp/cregexp.h
@@ -253,10 +253,10 @@ struct StackElem
parse() pins parseBuf to UnicodeString::getBuffer() so the NFA does
not index the string per step. Each offset resets \\m/\\M and captures.
- The backtracking stack is thread_local and shared by every CRegExp on
- that thread; count_elem is reset per parseRE. Do not clear it from
- ParserFactory teardown. parseStepLimit (default 1e6) counts NFA steps
- in one parse(); exceeding it fails the match.
+ The backtracking stack is process-wide and reused by every CRegExp;
+ count_elem is reset per parseRE. Do not parse concurrently. Do not
+ clear the stack from ParserFactory teardown. parseStepLimit (default
+ 1e6) counts NFA steps in one parse(); exceeding it fails the match.
TextParser calls mayMatch() with the same pos/eol/schemeStart/line
mask before parse() to avoid entering the NFA.
@@ -456,7 +456,7 @@ class CRegExp
void insert_stack(SRegInfo*& re, SRegInfo*& prev, int& toParse, bool& leftenter, ReAction ifTrueReturn,
ReAction ifFalseReturn, SRegInfo* re2, SRegInfo* prev2, int toParse2);
- static thread_local std::vector<StackElem> RegExpStack;
+ static std::vector<StackElem> RegExpStack;
static bool isLineBreak(wchar c)
{
diff --git a/src/colorer/xml/libxml2/LibXmlReader.cpp b/src/colorer/xml/libxml2/LibXmlReader.cpp
index 89d99fa..97d9b26 100644
--- a/src/colorer/xml/libxml2/LibXmlReader.cpp
+++ b/src/colorer/xml/libxml2/LibXmlReader.cpp
@@ -25,18 +25,18 @@ struct XmlLoadContext
bool is_first_call = true;
};
-thread_local XmlLoadContext* tls_load = nullptr;
+XmlLoadContext* current_load = nullptr;
class XmlLoadCurrent
{
public:
- explicit XmlLoadCurrent(XmlLoadContext* load) : previous(tls_load)
+ explicit XmlLoadCurrent(XmlLoadContext* load) : previous(current_load)
{
- tls_load = load;
+ current_load = load;
}
~XmlLoadCurrent()
{
- tls_load = previous;
+ current_load = previous;
}
XmlLoadCurrent(const XmlLoadCurrent&) = delete;
XmlLoadCurrent& operator=(const XmlLoadCurrent&) = delete;
@@ -47,8 +47,8 @@ class XmlLoadCurrent
XmlLoadContext* loadContext(xmlParserCtxtPtr ctxt)
{
- if (tls_load != nullptr) {
- return tls_load;
+ if (current_load != nullptr) {
+ return current_load;
}
if (ctxt != nullptr && ctxt->_private != nullptr) {
return static_cast<XmlLoadContext*>(ctxt->_private);
@@ -56,8 +56,8 @@ XmlLoadContext* loadContext(xmlParserCtxtPtr ctxt)
return nullptr;
}
-thread_local char xml_err_buf[4096];
-thread_local int xml_err_slen = 0;
+char xml_err_buf[4096];
+int xml_err_slen = 0;
} // namespace
diff --git a/src/colorer/xml/libxml2/SharedXmlInputSource.cpp b/src/colorer/xml/libxml2/SharedXmlInputSource.cpp
index 05990f7..bea254c 100644
--- a/src/colorer/xml/libxml2/SharedXmlInputSource.cpp
+++ b/src/colorer/xml/libxml2/SharedXmlInputSource.cpp
@@ -3,7 +3,7 @@
#include "colorer/Exception.h"
#include "colorer/utils/Environment.h"
-thread_local XmlJarCache* XmlJarCache::current_ = nullptr;
+XmlJarCache* XmlJarCache::current_ = nullptr;
XmlJarCache::Current::Current(XmlJarCache& cache) : previous(current_)
{
@@ -17,8 +17,8 @@ XmlJarCache::Current::~Current()
XmlJarCache& XmlJarCache::fallback()
{
- static thread_local XmlJarCache tls_fallback;
- return tls_fallback;
+ static XmlJarCache process_fallback;
+ return process_fallback;
}
XmlJarCache& XmlJarCache::active()
diff --git a/src/colorer/xml/libxml2/SharedXmlInputSource.h b/src/colorer/xml/libxml2/SharedXmlInputSource.h
index 5ed75e1..a43ad4e 100644
--- a/src/colorer/xml/libxml2/SharedXmlInputSource.h
+++ b/src/colorer/xml/libxml2/SharedXmlInputSource.h
@@ -29,7 +29,7 @@ class XmlJarCache
private:
std::unordered_map<UnicodeString, std::unique_ptr<SharedXmlInputSource>> entries;
- static thread_local XmlJarCache* current_;
+ static XmlJarCache* current_;
static XmlJarCache& fallback();
};
diff --git a/tests/unit/test_baseeditor.cpp b/tests/unit/test_baseeditor.cpp
index 765b5b4..45239e6 100644
--- a/tests/unit/test_baseeditor.cpp
+++ b/tests/unit/test_baseeditor.cpp
@@ -1,6 +1,4 @@
-#include <atomic>
#include <catch2/catch_amalgamated.hpp>
-#include <thread>
#include <vector>
#include "colorer/HrcLibrary.h"
#include "colorer/LineSource.h"
@@ -306,7 +304,7 @@ TEST_CASE("Probe ParserFactory on the same thread does not leak types into maste
REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
}
-TEST_CASE("two editors parse one HrcLibrary concurrently", "[baseeditor]")
+TEST_CASE("two editors parse one HrcLibrary on the same thread", "[baseeditor]")
{
ParserFactory factory;
loadTryLine(factory);
@@ -316,52 +314,25 @@ TEST_CASE("two editors parse one HrcLibrary concurrently", "[baseeditor]")
auto editor_a = makeEditor(factory, source_a);
auto editor_b = makeEditor(factory, source_b);
- std::atomic<int> hits {0};
- auto paint = [&hits](BaseEditor* editor) {
- for (int i = 0; i < 200; i++) {
- REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
- hits++;
- }
- };
-
- std::thread t1([&] { paint(editor_a.get()); });
- std::thread t2([&] { paint(editor_b.get()); });
- t1.join();
- t2.join();
- REQUIRE(hits == 400);
+ for (int i = 0; i < 200; i++) {
+ REQUIRE(hasRegion(editor_a->getLineRegions(0), "try_line:Kw"));
+ REQUIRE(hasRegion(editor_b->getLineRegions(0), "try_line:Kw"));
+ }
}
-TEST_CASE("loading another type does not disturb a concurrent parse", "[baseeditor]")
+TEST_CASE("loading another type does not disturb an existing editor", "[baseeditor]")
{
ParserFactory factory;
loadTryLine(factory);
MutableLines source({UnicodeString(u"int a")});
auto editor = makeEditor(factory, source);
-
- std::atomic<bool> parsing {true};
- std::atomic<int> paints {0};
- std::thread painter([&] {
- while (parsing.load()) {
- REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
- paints++;
- std::this_thread::yield();
- }
- });
-
- // loadHrcPath of a tiny HRC can finish before the painter is scheduled.
- while (paints.load() == 0) {
- std::this_thread::yield();
- }
+ REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
auto block_path = fs::path(__FILE__).parent_path() / "data" / "type_block.hrc";
UnicodeString block_location(block_path.c_str());
factory.loadHrcPath(&block_location);
- parsing = false;
- painter.join();
-
- REQUIRE(paints > 0);
REQUIRE(hasRegion(editor->getLineRegions(0), "try_line:Kw"));
REQUIRE(factory.getHrcLibrary().getFileType(UnicodeString("bl_quote")) != nullptr);
REQUIRE(factory.getHrcLibrary().getFileType(UnicodeString("try_line")) != nullptr);
diff --git a/tests/unit/test_cregexp.cpp b/tests/unit/test_cregexp.cpp
index 78ac1d7..4a27109 100644
--- a/tests/unit/test_cregexp.cpp
+++ b/tests/unit/test_cregexp.cpp
@@ -1,5 +1,3 @@
-#include <atomic>
-#include <thread>
#include <colorer/cregexp/cregexp.h>
#include <catch2/catch_amalgamated.hpp>
#include "colorer/ParserFactory.h"
@@ -1046,73 +1044,6 @@ TEST_CASE("ParserFactory on the same thread does not clear the matcher stack", "
REQUIRE(match.e[0] == 6);
}
-TEST_CASE("ParserFactory destructor does not wipe a live matcher", "[cregexp]")
-{
- const auto pre = ustr(u"/(a|b)+c/");
- const auto str = ustr(u"aaabbc");
- std::atomic<bool> stop{false};
- std::atomic<int> matches{0};
- std::atomic<int> failures{0};
-
- std::thread matcher([&] {
- CRegExp re(&pre);
- if (!re.isOk()) {
- failures++;
- return;
- }
- SMatches match;
- while (!stop.load(std::memory_order_relaxed)) {
- if (!re.parse(&str, &match) || match.s[0] != 0 || match.e[0] != 6) {
- failures++;
- return;
- }
- matches.fetch_add(1, std::memory_order_relaxed);
- }
- });
-
- while (matches.load(std::memory_order_relaxed) == 0 && failures.load(std::memory_order_relaxed) == 0) {
- std::this_thread::yield();
- }
- for (int i = 0; i < 50; i++) {
- ParserFactory probe;
- }
- stop.store(true, std::memory_order_relaxed);
- matcher.join();
- REQUIRE(failures.load() == 0);
- REQUIRE(matches.load() > 0);
-}
-
-TEST_CASE("CRegExp matching is independent per thread", "[cregexp]")
-{
- const auto pre = ustr(u"/(a|b)+c/");
- const auto str = ustr(u"aaabbc");
- std::atomic<int> ok{0};
- std::atomic<int> failures{0};
-
- auto worker = [&] {
- CRegExp re(&pre);
- if (!re.isOk()) {
- failures++;
- return;
- }
- SMatches match;
- for (int i = 0; i < 1000; i++) {
- if (!re.parse(&str, &match) || match.s[0] != 0 || match.e[0] != 6) {
- failures++;
- return;
- }
- }
- ok++;
- };
-
- std::thread t1(worker);
- std::thread t2(worker);
- t1.join();
- t2.join();
- REQUIRE(failures.load() == 0);
- REQUIRE(ok.load() == 2);
-}
-
TEST_CASE("CRegExp \\Y{name} copies named group case-insensitively", "[cregexp]")
{
const auto start_re = ustr(u"/(x)(?{n}Foo)/");
diff --git a/tests/unit/test_xmlreader.cpp b/tests/unit/test_xmlreader.cpp
index e0a4db3..01eb998 100644
--- a/tests/unit/test_xmlreader.cpp
+++ b/tests/unit/test_xmlreader.cpp
@@ -1,5 +1,3 @@
-#include <atomic>
-#include <thread>
#include <catch2/catch_amalgamated.hpp>
#include "colorer/utils/Environment.h"
#include "colorer/xml/XmlReader.h"
@@ -140,50 +138,3 @@ TEST_CASE("Sequential XML parses on one thread keep independent entity bases", "
parse_catalog(u"data/catalog.xml");
REQUIRE(logger->message_print() == false);
}
-
-TEST_CASE("Overlapping XML parses keep independent entity bases", "[xmlreader]")
-{
- logger->clean_messages();
- auto work_dir = fs::current_path();
- colorer::Environment::setOSEnv("CUR_DIR", work_dir.c_str());
-
- std::atomic<int> ok{0};
- std::atomic<int> failures{0};
-
- auto parse_one = [&](const char16_t* path) {
- try {
- UnicodeString p(path);
- XmlInputSource is(p);
- XmlReader reader(is);
- if (!reader.parse()) {
- failures++;
- return;
- }
- XMLNodeList nodes;
- reader.getNodes(nodes);
- if (nodes.empty() || nodes.begin()->name != UnicodeString(u"catalog")) {
- failures++;
- return;
- }
- ok++;
- } catch (...) {
- failures++;
- }
- };
-
- std::thread t1([&] {
- for (int i = 0; i < 40; i++) {
- parse_one(u"data/catalog.xml");
- }
- });
- std::thread t2([&] {
- for (int i = 0; i < 40; i++) {
- parse_one(u"data/catalog-env.xml");
- }
- });
- t1.join();
- t2.join();
- REQUIRE(failures.load() == 0);
- REQUIRE(ok.load() == 80);
- REQUIRE(logger->message_print() == false);
-}