mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-04 03:34:28 -05:00
checkCommandQueue runs processCommand on its own thread, which read `now` and frame_count and wrote last_frame_sent (CMD_PAUSE) and got_command while the streaming loop writes or reads them. ThreadSanitizer reports these as data races (zm_monitorstream.cpp processCommand and runStream's got_command read against the command thread, and checkCommandQueue's `now` read against EventStream::runStream). Make frame_count, last_frame_sent and got_command atomic, as #4939 did for the playback state, and have the command thread read the clock itself instead of the loop's `now`. The status reply's fps and delay are then measured against the current time rather than the start of the loop's last pass. Test: [races] case for the frame bookkeeping, for TSan builds. fixes #5230 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.3 KiB
C++
156 lines
5.3 KiB
C++
/*
|
|
* This file is part of the ZoneMinder Project. See AUTHORS file for contributors.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_catch2.h"
|
|
|
|
#include "zm_stream.h"
|
|
|
|
#include <atomic>
|
|
#include <thread>
|
|
|
|
// zms runs two threads over one StreamBase: the command thread
|
|
// (checkCommandQueue -> processCommand) writes the playback state, and the
|
|
// streaming loop (runStream) reads it every iteration. Nothing synchronises
|
|
// them, so before zoneminder/zoneminder#4939 every one of these fields was a
|
|
// data race, which is undefined behaviour regardless of what it does on x86.
|
|
//
|
|
// This exercises those exact members from two threads. It passes trivially in a
|
|
// normal build; its value is under ThreadSanitizer:
|
|
//
|
|
// cmake -DTSAN=ON -DBUILD_TEST_SUITE=ON .. && cmake --build .
|
|
// ./tests/tests "[races]"
|
|
//
|
|
// With the fields plain, TSan reports a write/read race on each. With them
|
|
// atomic it is clean.
|
|
|
|
namespace {
|
|
|
|
class PlaybackStateProbe : public StreamBase {
|
|
public:
|
|
// The writes MonitorStream::processCommand performs on the command thread.
|
|
void applyCommands(int iterations) {
|
|
for (int i = 0; i < iterations; i++) {
|
|
stopped = false;
|
|
paused = true;
|
|
replay_rate = ZM_RATE_BASE * ((i % 5) + 1);
|
|
scale = 100 + (i % 100);
|
|
zoom = 100 + (i % 50);
|
|
x = static_cast<unsigned short>(i % 1000);
|
|
y = static_cast<unsigned short>(i % 800);
|
|
step = (i % 3) - 1;
|
|
send_twice = (i % 2) == 0;
|
|
maxfps = 1.0 + (i % 30);
|
|
frame_type = (i % 2) ? FRAME_ANALYSIS : FRAME_NORMAL;
|
|
paused = false;
|
|
}
|
|
}
|
|
|
|
// What processCommand does with the frame bookkeeping on the command thread:
|
|
// CMD_PAUSE stores last_frame_sent, every status reply reads frame_count and
|
|
// last_frame_sent, and checkCommandQueue sets got_command.
|
|
int64 commandFrameState(int iterations) {
|
|
int64 sink = 0;
|
|
for (int i = 0; i < iterations; i++) {
|
|
if (i % 4 == 0) last_frame_sent = std::chrono::steady_clock::now();
|
|
got_command = true; // checkCommandQueue, after each command
|
|
sink += frame_count;
|
|
const TimePoint sent = last_frame_sent;
|
|
sink += sent.time_since_epoch().count() & 1;
|
|
}
|
|
return sink;
|
|
}
|
|
|
|
// And runStream()'s side: count each frame sent and stamp it, and check
|
|
// got_command for the paused keepalive.
|
|
int64 streamFrameState(int iterations) {
|
|
int64 sink = 0;
|
|
for (int i = 0; i < iterations; i++) {
|
|
frame_count++;
|
|
last_frame_sent = std::chrono::steady_clock::now();
|
|
sink += got_command ? 1 : 0;
|
|
const TimePoint sent = last_frame_sent;
|
|
sink += (std::chrono::steady_clock::now() - sent).count() & 1;
|
|
}
|
|
return sink;
|
|
}
|
|
|
|
// The reads runStream() performs on the streaming thread. The accumulator
|
|
// exists so none of this is optimised away.
|
|
int64 readPlaybackState(int iterations) {
|
|
int64 sink = 0;
|
|
for (int i = 0; i < iterations; i++) {
|
|
sink += paused ? 1 : 0;
|
|
sink += stopped ? 1 : 0;
|
|
sink += replay_rate;
|
|
sink += scale;
|
|
sink += zoom;
|
|
sink += x;
|
|
sink += y;
|
|
sink += step;
|
|
sink += send_twice ? 1 : 0;
|
|
sink += static_cast<int64>(maxfps);
|
|
sink += (frame_type == FRAME_ANALYSIS) ? 1 : 0;
|
|
}
|
|
return sink;
|
|
}
|
|
|
|
protected:
|
|
void processCommand(const CmdMsg *) override {}
|
|
void runStream() override {}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("StreamBase playback state is shared safely between the command and stream threads", "[races]") {
|
|
// connkey defaults to 0, so ~StreamBase does no socket teardown here.
|
|
PlaybackStateProbe probe;
|
|
|
|
constexpr int kIterations = 20000;
|
|
std::atomic<int64> observed{0};
|
|
|
|
std::thread command_thread([&probe]() { probe.applyCommands(kIterations); });
|
|
std::thread stream_thread([&probe, &observed]() {
|
|
observed = probe.readPlaybackState(kIterations);
|
|
});
|
|
|
|
command_thread.join();
|
|
stream_thread.join();
|
|
|
|
// The exact total is timing dependent and deliberately not asserted. What
|
|
// matters is that both threads completed and that, under TSan, the run
|
|
// produced no race report.
|
|
REQUIRE(observed.load() >= 0);
|
|
}
|
|
|
|
TEST_CASE("StreamBase frame bookkeeping is shared safely between the command and stream threads", "[races]") {
|
|
// processCommand reads frame_count and last_frame_sent for its status reply
|
|
// and CMD_PAUSE stores last_frame_sent, while runStream() updates both for
|
|
// every frame. Like the case above, this only shows something under TSan.
|
|
PlaybackStateProbe probe;
|
|
|
|
constexpr int kIterations = 20000;
|
|
std::atomic<int64> observed{0};
|
|
|
|
std::thread command_thread([&probe, &observed]() { observed += probe.commandFrameState(kIterations); });
|
|
std::thread stream_thread([&probe, &observed]() { observed += probe.streamFrameState(kIterations); });
|
|
|
|
command_thread.join();
|
|
stream_thread.join();
|
|
|
|
REQUIRE(observed.load() >= 0);
|
|
}
|