diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index ad11da75..8476db5e 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -86,10 +87,16 @@ struct StreamPublishEngine::Impl { int64_t captureReport = 0; std::atomic videoStage{"stopped"}; std::atomic videoStageAt{0}; + std::atomic imageStage{"starting"}; + std::atomic imageStageAt{monotonicUs()}; + std::atomic imageFrames{0}; std::atomic convertedFrames{0}, encodedFrames{0}, deliveredFrames{0}, repeatedFrames{0}; void videoProgress(const char* stage) { videoStageAt.store(monotonicUs()); videoStage.store(stage); } + void imageProgress(const char* stage) { + imageStageAt.store(monotonicUs()); imageStage.store(stage); + } bool hasGood = false, rollingBack = false, videoReady = false; uint64_t softwareRequest = 0; int softwareCodec = 0; @@ -149,28 +156,65 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s m_capture.close(); // Контекст нельзя освободить до завершения фонового закрытия устройства. }); #ifdef HAVE_STREAM_CAPTURE - m->imageThread = std::thread(&StreamPublishEngine::imageLoop, this); + m->imageThread = std::thread([this] { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop image worker started tid=%llu", (unsigned long long)(quintptr)QThread::currentThreadId()); + imageLoop(); + m->imageProgress("release-thread-resources"); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop image loop returned; releasing thread resources frames=%llu tid=%llu", + (unsigned long long)m->imageFrames.load(), (unsigned long long)(quintptr)QThread::currentThreadId()); + }); #else m->imageJoined = true; #endif } StreamPublishEngine::~StreamPublishEngine() { + const int64_t destroyAt = monotonicUs(); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction begin id=%llu running=%d stopping=%d image_stage=%s image_frames=%llu", + (unsigned long long)m->id, m->running.load(), m->stopping, m->imageStage.load(), (unsigned long long)m->imageFrames.load()); m->destroying = true; blockSignals(true); stop(); m_timer.stop(); m_previewTimer.stop(); closeSource(); #ifdef HAVE_STREAM_CAPTURE { std::lock_guard guard(m->lock); m->imageStop = true; m->imagePending = false; m->imageInput = {}; } m->imageWake.notify_all(); - m->imageJoinThread = std::thread([this] { m->imageThread.join(); m->imageJoined = true; }); + m->imageJoinThread = std::thread([this] { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop image join begin stage=%s stage_age_ms=%lld", + m->imageStage.load(), (long long)((monotonicUs() - m->imageStageAt.load()) / 1000)); + m->imageThread.join(); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop image join complete; native thread resources released"); + m->imageJoined = true; + }); #endif if (!finishStop() || !m->imageJoined) { // Qt освобождает thread-local графику через GUI. Даже при выходе приложения нельзя блокировать его join(). - DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop publisher destruction waits for worker resource cleanup with GUI events enabled"); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop publisher destruction waits for workers stopping=%d image_joined=%d image_stage=%s", + m->stopping, m->imageJoined.load(), m->imageStage.load()); QEventLoop loop; QTimer timer; - connect(&timer, &QTimer::timeout, &loop, [this, &loop] { if (finishStop() && m->imageJoined) loop.quit(); }); - timer.start(10); loop.exec(QEventLoop::ExcludeUserInputEvents); + int64_t reportAt = destroyAt; + bool firstPoll = true; + connect(&timer, &QTimer::timeout, &loop, [this, &loop, destroyAt, &reportAt, &firstPoll] { + if (firstPoll) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction GUI timer active"); firstPoll = false; + } + if (finishStop() && m->imageJoined) { loop.quit(); return; } + const int64_t now = monotonicUs(); + if (now - reportAt < 5000000) return; + reportAt = now; + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction waiting elapsed_ms=%lld stopping=%d workers_joined=%d " + "video_stage=%s image_joined=%d image_stage=%s image_stage_age_ms=%lld image_frames=%llu microphone_closing=%d", + (long long)((now - destroyAt) / 1000), m->stopping, m->workersJoined.load(), m->videoStage.load(), + m->imageJoined.load(), m->imageStage.load(), (long long)((now - m->imageStageAt.load()) / 1000), + (unsigned long long)m->imageFrames.load(), m_capture.closing()); + }); + timer.start(10); + const int result = loop.exec(QEventLoop::ExcludeUserInputEvents); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction GUI loop exited result=%d stopping=%d image_joined=%d", + result, m->stopping, m->imageJoined.load()); + if (m->stopping || !m->imageJoined) + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction GUI loop exited before worker cleanup completed"); } if (m->imageJoinThread.joinable()) m->imageJoinThread.join(); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "desktop publisher destruction complete elapsed_ms=%lld", (long long)((monotonicUs() - destroyAt) / 1000)); } bool StreamPublishEngine::supported() { #ifdef HAVE_STREAM_CAPTURE @@ -433,6 +477,7 @@ void StreamPublishEngine::imageLoop() { #ifdef HAVE_STREAM_CAPTURE for (;;) { QVideoFrame frame; uint64_t generation; int64_t pts; + m->imageProgress("wait-frame"); { std::unique_lock lock(m->lock); m->imageWake.wait(lock, [this] { return m->imageStop || m->imagePending; }); @@ -440,7 +485,12 @@ void StreamPublishEngine::imageLoop() { frame = std::move(m->imageInput); generation = m->imageGeneration; pts = m->imagePts; m->imagePending = false; } int64_t convertAt = monotonicUs(); + m->imageProgress("convert-frame"); QImage image = frame.toImage(); + // Отметка ставится перед деструкторами image/frame, включая return/continue внутри итерации. + auto releaseFrame = qScopeGuard([this] { m->imageProgress("release-frame"); }); + m->imageFrames++; + m->imageProgress("transform-image"); if (image.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop captured frame conversion failed generation=%llu format=%d size=%dx%d", (unsigned long long)generation, frame.pixelFormat(), frame.width(), frame.height()); @@ -453,6 +503,7 @@ void StreamPublishEngine::imageLoop() { #else if (frame.rotationAngle()) image = image.transformed(QTransform().rotate(frame.rotationAngle())); #endif + m->imageProgress("publish-image"); { std::lock_guard guard(m->lock); if (m->imageStop) return; diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index ae09e415..6fde7e46 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -162,6 +162,7 @@ private slots: debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_DEBUG); + debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO); if (qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) { qunsetenv("WAYLAND_DISPLAY"); qputenv("XDG_SESSION_TYPE", "x11"); QLoggingCategory::setFilterRules("qt.multimedia.*=true");