| name | thread-safety-analyzer |
| description | Analyze MemCapture C++17 code for thread safety issues including race conditions on shared metrics maps, deadlocks in metric shutdown, improper std::atomic usage for mQuit flags, and collection thread lifecycle problems. Use when reviewing concurrent metric code or debugging threading issues. |
Thread Safety Analysis for MemCapture C++17
Purpose
Systematically analyze MemCapture's multi-threaded metric collection code for thread safety
issues that can cause race conditions, deadlocks, or data corruption during capture.
Usage
Invoke this skill when:
- Reviewing new
IMetric subclass implementations
- Debugging a hang at end of capture (stuck
StopCollection())
- Investigating data corruption in
report.json (garbled values)
- Adding a new field to
mLinuxMemoryMeasurements map
- Changing the
ConditionVariable or mQuit flag logic
- Reviewing
JsonReportGenerator access patterns
MemCapture Threading Architecture
main thread
│
├── MemoryMetric thread (mThread)
│ ├── reads /proc/meminfo, /proc/brcm/, sysfs GPU nodes
│ ├── writes mLinuxMemoryMeasurements (map<string, Measurement>)
│ └── waits on mCv (ConditionVariable)
│
├── ProcessMetric thread (mThread)
│ ├── reads /proc/<pid>/smaps via Procrank
│ ├── writes mProcessMeasurements
│ └── waits on its own ConditionVariable
│
└── CpuIdleMetric thread (mThread)
├── reads /sys/devices/system/cpu/cpu0/cpuidle/
├── writes mCpuIdleMeasurements
└── waits on its own ConditionVariable
After all StopCollection() calls (sequential, from main thread):
└── SaveResults() called for each metric — single-threaded, safe
Analysis Process
Step 1: Identify All Shared Data
Search for data accessed from both the main thread and a collection thread:
mQuit flag — must be std::atomic<bool> or protected by mMutex
mLinuxMemoryMeasurements, mProcessMeasurements — owned by collection thread only
JsonReportGenerator — accessed only by SaveResults() (main thread, post-stop)
For each shared variable, verify:
- Collection thread writes are never concurrent with
SaveResults() reads
mQuit set by main thread is immediately visible to collection thread
- No raw pointer aliases exist to collection-thread-owned data
Step 2: Verify mQuit Flag Safety
class MemoryMetric : public IMetric {
std::atomic<bool> mQuit{false};
std::thread mThread;
std::mutex mMutex;
std::condition_variable mCv;
};
void MemoryMetric::StopCollection() {
mQuit = true;
mCv.notify_one();
if (mThread.joinable())
mThread.join();
}
void MemoryMetric::CollectData(std::chrono::seconds freq) {
std::unique_lock<std::mutex> lock(mMutex);
while (!mQuit) {
mCv.wait_for(lock, freq);
if (mQuit) break;
lock.unlock();
lock.lock();
}
}
bool mQuit = false;
Step 3: Verify Collection Thread Lifecycle
~MemoryMetric() {
StopCollection();
}
void MemoryMetric::StartCollection(std::chrono::seconds freq) {
mQuit = false;
mThread = std::thread(&MemoryMetric::CollectData, this, freq);
}
void MemoryMetric::StartCollection(std::chrono::seconds freq) {
if (mThread.joinable()) {
LOG_WARN("Collection already running — ignoring StartCollection");
return;
}
}
Step 4: Verify Measurement Map Thread Safety
mLinuxMemoryMeasurements is a std::map<std::string, Measurement>:
- Only the collection thread writes to it (via
AddSample)
- Only the main thread reads from it (via
SaveResults(), after join)
- No concurrent access = no mutex needed
BUT: Violations to watch for:
void BadUsage() {
metric.StartCollection(freq);
auto& m = metric.GetMeasurement("MemTotal");
}
metric.StopCollection();
metric.SaveResults();
Step 5: Check ConditionVariable Usage
mCv.wait_for(lock, freq, [this] { return mQuit.load(); });
while (!mQuit) {
auto status = mCv.wait_for(lock, freq);
if (mQuit) break;
}
mCv.wait_for(lock, freq);
if (mQuit) return;
Step 6: Check JsonReportGenerator Concurrency
JsonReportGenerator is not internally thread-safe. The design contract is:
- It is constructed once (main thread, before any
StartCollection())
- It is written to only in
SaveResults() (main thread, after all StopCollection())
- Collection threads must not call any
JsonReportGenerator methods
void MemoryMetric::CollectData(std::chrono::seconds freq) {
while (!mQuit) {
mCv.wait_for(lock, freq);
Collect();
mReportGenerator->SaveResults();
}
}
metric.StopCollection();
metric.SaveResults();
Running Thread Safety Analysis
ThreadSanitizer (fastest, catches most races)
mkdir build_tsan && cd build_tsan
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=thread -g" \
-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake \
..
cmake --build . --parallel $(nproc)
./MemCapture --platform AMLOGIC --duration 30 \
--output-dir /tmp/tsan_test/ 2>&1 | tee tsan.log
Helgrind (deeper lock-order analysis)
valgrind --tool=helgrind \
--track-lockorders=yes \
./MemCapture --platform AMLOGIC --duration 10 \
--output-dir /tmp/helgrind_test/ 2>&1 | tee helgrind.log
Common Issues and Fixes
Issue: mQuit is a plain bool
bool mQuit = false;
std::atomic<bool> mQuit{false};
Issue: Collection thread not joined in destructor
~MemoryMetric() {
mQuit = true;
}
~MemoryMetric() {
StopCollection();
}
Issue: Map reference held across collection cycle
auto& ref = mLinuxMemoryMeasurements["MemTotal"];
mLinuxMemoryMeasurements.emplace("MemFree", Measurement("MemFree"));
ref.AddSample(value);
mLinuxMemoryMeasurements.emplace("MemTotal", Measurement("MemTotal"));
mLinuxMemoryMeasurements.emplace("MemFree", Measurement("MemFree"));
mLinuxMemoryMeasurements.at("MemTotal").AddSample(value);
Output Format
## Thread Safety Analysis — MemCapture
### Critical Issues (must fix)
1. [MemoryMetric.h:42] mQuit is plain bool — use std::atomic<bool>
2. [CpuIdleMetric.cpp:88] SaveResults() called from collection thread — race condition
### Warnings (should fix)
1. [ProcessMetric.cpp:120] Map reference held across emplace — potential invalidation
### Verified Safe
- mLinuxMemoryMeasurements: only written by collection thread, read by SaveResults() after join ✅
- JsonReportGenerator: constructed before threads start, written only from main thread ✅
### Suggested Fixes
[Specific code changes for each issue]
Verification
After fixes:
- ThreadSanitizer reports no data races on a 60-second capture
- Helgrind reports no lock ordering violations
- All platforms build without warnings (
-Wall -Wextra)
- JSON report output matches baseline after refactor