test: pre-fill libstdc++ ctype caches before threads exist

- std::ctype<char> caches narrow()/widen() results per character in
  plain char arrays of the global classic-locale facet, written without
  synchronization from header-inlined code (locale_facets.h); two
  threads exercising an uncached character concurrently (e.g. compiling
  a std::regex in Channel::Validate) constitute a true data race that
  ThreadSanitizer rightfully reports
- the stores are real and unsynchronized, so a tsan-instrumented
  libstdc++ cannot help here; instead fill the caches before any thread
  is spawned, which turns every later access into a pure read
- warm the lazily-installed num_put/num_get caches used by stream
  insertion/extraction as well, via a small format/parse round-trip
- wire the warm-up into the gtest runner main() and, via a static
  initializer, into the test device runner
This commit is contained in:
Dennis Klein
2026-06-10 16:14:15 +02:00
committed by Dennis Klein
parent fc69b5e7ae
commit 2bd9a072a9
4 changed files with 71 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ endif()
add_testhelper(runTestDevice add_testhelper(runTestDevice
SOURCES SOURCES
helper/runTestDevice.cxx helper/runTestDevice.cxx
helper/LocaleWarmup.h
helper/devices/TestPairLeft.h helper/devices/TestPairLeft.h
helper/devices/TestPairRight.h helper/devices/TestPairRight.h
helper/devices/TestPollIn.h helper/devices/TestPollIn.h

View File

@@ -0,0 +1,63 @@
/********************************************************************************
* Copyright (C) 2026 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH *
* *
* This software is distributed under the terms of the *
* GNU Lesser General Public Licence (LGPL) version 3, *
* copied verbatim in the file "LICENSE" *
********************************************************************************/
#ifndef FAIR_MQ_TEST_LOCALEWARMUP_H
#define FAIR_MQ_TEST_LOCALEWARMUP_H
#include <locale>
#include <sstream>
namespace fair::mq::test {
/// libstdc++'s std::ctype<char> caches narrow()/widen() results per character
/// in plain char arrays of the global classic-locale facet -- by design with
/// unsynchronized writes, emitted from header-inlined code (locale_facets.h).
/// Whenever two threads exercise an uncached character concurrently (e.g. by
/// compiling a std::regex), ThreadSanitizer rightfully reports the write as a
/// data race. The stores are real and unsynchronized, so instrumenting
/// libstdc++ cannot help; instead, fill the caches before any thread exists,
/// which turns every later access into a pure read.
inline void WarmUpLocaleCaches()
{
auto const& ct = std::use_facet<std::ctype<char>>(std::locale::classic());
for (int c = 1; c < 256; ++c) {
auto const ch = static_cast<char>(c);
ct.narrow(ch, '\0');
ct.widen(ch);
}
// Only the range overload runs _M_narrow_init(), which sets the
// all-cached flag (_M_narrow_ok); the single-char loop above fills the
// cache without it. (widen() needs no equivalent: the single-char
// overload already runs _M_widen_init().)
char from[256];
char to[256];
for (int c = 0; c < 256; ++c) {
from[c] = static_cast<char>(c);
}
ct.narrow(from, from + 256, '?', to);
// num_put/num_get caches used by stream insertion/extraction
std::ostringstream os;
os << 4711 << ' ' << 3.14;
std::istringstream is("42 3.14");
int i = 0;
double d = 0.;
is >> i >> d;
}
/// For binaries that do not own main(): define a namespace-scope instance to
/// run the warm-up during static initialization, before main().
struct LocaleWarmup
{
LocaleWarmup() { WarmUpLocaleCaches(); }
};
} // namespace fair::mq::test
#endif /* FAIR_MQ_TEST_LOCALEWARMUP_H */

View File

@@ -6,6 +6,7 @@
* copied verbatim in the file "LICENSE" * * copied verbatim in the file "LICENSE" *
********************************************************************************/ ********************************************************************************/
#include "LocaleWarmup.h"
#include "devices/TestPairLeft.h" #include "devices/TestPairLeft.h"
#include "devices/TestPairRight.h" #include "devices/TestPairRight.h"
#include "devices/TestPollIn.h" #include "devices/TestPollIn.h"
@@ -30,6 +31,10 @@
namespace bpo = boost::program_options; namespace bpo = boost::program_options;
namespace {
[[maybe_unused]] fair::mq::test::LocaleWarmup const gLocaleWarmup{};
}
auto addCustomOptions(bpo::options_description& options) -> void auto addCustomOptions(bpo::options_description& options) -> void
{ {
options.add_options() options.add_options()

View File

@@ -10,6 +10,7 @@
#include <TestEnvironment.h> #include <TestEnvironment.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <helper/LocaleWarmup.h>
#include <string> #include <string>
@@ -24,6 +25,7 @@ string runTestDevice = "@RUN_TEST_DEVICE@";
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
fair::mq::test::WarmUpLocaleCaches();
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);
::testing::FLAGS_gtest_death_test_style = "threadsafe"; ::testing::FLAGS_gtest_death_test_style = "threadsafe";
setenv("FAIRMQ_PATH", FAIRMQ_TEST_ENVIRONMENT, 0); setenv("FAIRMQ_PATH", FAIRMQ_TEST_ENVIRONMENT, 0);