1717******************************************************************************/
1818
1919#include " nodeobs_autoconfig.h"
20+ #include " nodeobs_autoconfig_resource_sampler.h"
2021#include < algorithm>
2122#include < array>
2223#include < future>
@@ -158,6 +159,12 @@ struct AutoconfigRun {
158159 };
159160 std::vector<SelectionDetail> selectionDetails;
160161
162+ // Per-phase resource samples (CPU%, process RAM, optionally GPU VRAM).
163+ // Captured by ResourceSampler around the bandwidth and encoder test phases;
164+ // surfaced via the resource_usage event + summary IPC. Pure telemetry — no
165+ // influence on the selection heuristics in this version.
166+ std::vector<autoConfig::ResourceWindow> resourceWindows;
167+
161168 // Per-canvas video-context decision. Captured in applyResults.
162169 struct VideoDecision {
163170 void *contextPtr = nullptr ;
@@ -234,6 +241,65 @@ void autoConfig::WaitPendingTests(double timeout)
234241 }
235242}
236243
244+ // Serialize a ResourceWindow to JSON and emit a resource_usage event. The window
245+ // is also pushed onto runContext.resourceWindows so GetAutoConfigSummary can
246+ // re-emit it later. Frontends can consume either the event stream or the summary.
247+ static std::string resourceWindowToJson (const autoConfig::ResourceWindow &w)
248+ {
249+ obs_data_t *root = obs_data_create ();
250+ obs_data_set_string (root, " phase" , w.phase .c_str ());
251+ obs_data_set_int (root, " sampleCount" , w.sampleCount );
252+ obs_data_set_int (root, " durationMs" , w.durationMs );
253+
254+ // p50 is the typical value during the window; p95 is the sustained ceiling
255+ // after dropping single-sample outliers (a background process briefly using
256+ // CPU shouldn't dominate the report).
257+ auto putPct = [&](const char *key, double p50, double p95) {
258+ obs_data_t *o = obs_data_create ();
259+ obs_data_set_double (o, " p50" , p50);
260+ obs_data_set_double (o, " p95" , p95);
261+ obs_data_set_obj (root, key, o);
262+ obs_data_release (o);
263+ };
264+ auto putPctInt = [&](obs_data_t *parent, const char *key, uint64_t p50, uint64_t p95) {
265+ obs_data_t *o = obs_data_create ();
266+ obs_data_set_int (o, " p50" , (long long )p50);
267+ obs_data_set_int (o, " p95" , (long long )p95);
268+ obs_data_set_obj (parent, key, o);
269+ obs_data_release (o);
270+ };
271+
272+ putPct (" cpuPct" , w.p50Sample .cpuPct , w.p95Sample .cpuPct );
273+ putPct (" procRamMB" , w.p50Sample .procRamMB , w.p95Sample .procRamMB );
274+
275+ obs_data_t *gpu = obs_data_create ();
276+ obs_data_set_bool (gpu, " available" , w.gpuAvailable );
277+ if (w.gpuAvailable ) {
278+ putPctInt (gpu, " vramUsedMB" , w.p50Sample .gpuVramUsedMB , w.p95Sample .gpuVramUsedMB );
279+ // Budget is platform-driven and effectively constant across a window —
280+ // surface a single number rather than a percentile pair.
281+ obs_data_set_int (gpu, " vramBudgetMB" , (long long )w.p95Sample .gpuVramBudgetMB );
282+ }
283+ obs_data_set_obj (root, " gpu" , gpu);
284+ obs_data_release (gpu);
285+
286+ std::string json = obs_data_get_json (root);
287+ obs_data_release (root);
288+ return json;
289+ }
290+
291+ static void recordResourceWindow (const autoConfig::ResourceWindow &w)
292+ {
293+ if (w.sampleCount <= 0 )
294+ return ;
295+
296+ runContext.resourceWindows .push_back (w);
297+
298+ std::string payload = resourceWindowToJson (w);
299+ std::lock_guard<std::mutex> lock (eventsMutex);
300+ events.push (AutoConfigInfo (" resource_usage" , w.phase , 100 , payload));
301+ }
302+
237303void autoConfig::TestHardwareEncoding (void )
238304{
239305 size_t idx = 0 ;
@@ -518,6 +584,23 @@ void autoConfig::GetAutoConfigSummary(void *data, const int64_t id, const std::v
518584 obs_data_release (sel);
519585 }
520586
587+ // resourceUsage — per-phase CPU/RAM (and Windows-only GPU VRAM) samples
588+ // captured during the bandwidth and encoder test phases. Same JSON shape
589+ // as the resource_usage event payload.
590+ {
591+ obs_data_array_t *windows = obs_data_array_create ();
592+ for (auto &w : runContext.resourceWindows ) {
593+ std::string s = resourceWindowToJson (w);
594+ obs_data_t *item = obs_data_create_from_json (s.c_str ());
595+ if (item) {
596+ obs_data_array_push_back (windows, item);
597+ obs_data_release (item);
598+ }
599+ }
600+ obs_data_set_array (root, " resourceUsage" , windows);
601+ obs_data_array_release (windows);
602+ }
603+
521604 std::string json = obs_data_get_json_pretty (root);
522605 obs_data_release (root);
523606
@@ -538,6 +621,15 @@ void autoConfig::InitializeAutoConfig(void *data, const int64_t id, const std::v
538621 runContext = AutoconfigRun{};
539622 cancel = false ;
540623
624+ // Drain leftover events from a prior run. Otherwise a stopping_step queued
625+ // by an aborted bandwidth thread (e.g. after TerminateAutoConfig) leaks into
626+ // the next session's first drainUntil() and confuses callers.
627+ {
628+ std::lock_guard<std::mutex> lock (eventsMutex);
629+ while (!events.empty ())
630+ events.pop ();
631+ }
632+
541633 if (!args.empty ()) {
542634 const std::vector<char > &bin = args[0 ].value_bin ;
543635 size_t n = bin.size () / sizeof (uint64_t );
@@ -846,9 +938,11 @@ void autoConfig::TestBandwidthThreadV2(void)
846938 // the loop above exits as soon as signals are drained (often
847939 // < 100 ms after the RTMP connection opens) and totalBytes is 0.
848940 int dataWaitMs = 0 ;
941+ autoConfig::ResourceSampler sampler;
849942 if (!gotError && allConnected) {
850943 const int targetWaitMs = 5000 ;
851944 auto dataStart = std::chrono::steady_clock::now ();
945+ sampler.start (" bandwidth" );
852946 while (std::chrono::steady_clock::now () - dataStart < std::chrono::milliseconds (targetWaitMs)) {
853947 std::unique_lock<std::mutex> ul (m);
854948 if (cancel) {
@@ -857,8 +951,10 @@ void autoConfig::TestBandwidthThreadV2(void)
857951 }
858952 ul.unlock ();
859953 std::this_thread::sleep_for (std::chrono::milliseconds (250 ));
954+ sampler.sample ();
860955 }
861956 dataWaitMs = (int )std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now () - dataStart).count ();
957+ recordResourceWindow (sampler.stop ());
862958 }
863959
864960 if (!gotError) {
@@ -1693,6 +1789,9 @@ void autoConfig::TestStreamEncoderThread()
16931789 events.push (AutoConfigInfo (" starting_step" , " runContext.streamingEncoder_test" , 0 ));
16941790 eventsMutex.unlock ();
16951791
1792+ autoConfig::ResourceSampler sampler;
1793+ sampler.start (" stream_encoder" , std::chrono::milliseconds (250 ));
1794+
16961795 TestHardwareEncoding ();
16971796
16981797 if (!runContext.softwareTested ) {
@@ -1738,6 +1837,8 @@ void autoConfig::TestStreamEncoderThread()
17381837 events.push (AutoConfigInfo (" encoder_detection" , " summary" , 100 , payload));
17391838 }
17401839
1840+ recordResourceWindow (sampler.stop ());
1841+
17411842 eventsMutex.lock ();
17421843 events.push (AutoConfigInfo (" stopping_step" , " runContext.streamingEncoder_test" , 100 ));
17431844 eventsMutex.unlock ();
@@ -1749,6 +1850,9 @@ void autoConfig::TestRecordingEncoderThread()
17491850 events.push (AutoConfigInfo (" starting_step" , " runContext.recordingEncoder_test" , 0 ));
17501851 eventsMutex.unlock ();
17511852
1853+ autoConfig::ResourceSampler sampler;
1854+ sampler.start (" recording_encoder" , std::chrono::milliseconds (250 ));
1855+
17521856 TestHardwareEncoding ();
17531857
17541858 if (!runContext.hardwareEncodingAvailable && !runContext.softwareTested ) {
@@ -1785,6 +1889,8 @@ void autoConfig::TestRecordingEncoderThread()
17851889 }
17861890 }
17871891
1892+ recordResourceWindow (sampler.stop ());
1893+
17881894 eventsMutex.lock ();
17891895 events.push (AutoConfigInfo (" stopping_step" , " runContext.recordingEncoder_test" , 100 ));
17901896 eventsMutex.unlock ();
0 commit comments