mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-24 12:01:51 +02:00
feat(admin): show each stream's producer, CPU and memory on the streams page
A new "Resources" column between Stream Info and Actions: which process is producing the channel (the fanout daemon's native remuxer, ffmpeg, or PHP for the LLOD segmenter / loopback relay), the CPU it is burning and the memory it holds. With the native remuxer now an option per stream, "what does this channel actually cost" and "which backend is it on" are the two questions the list could not answer. ProcessManager reads both from /proc/PID/stat (fields 14/15 and 24, with the page size derived rather than assumed — 64K pages are normal on arm64). CPU there is cumulative, so a percentage needs two readings: cron:streams samples each producer once a pass and folds cpu/mem/producer into the stream's progress_info, carrying the previous reading in the same JSON to subtract from. cpuPercent() returns null rather than a wild figure when the pair says nothing — no previous sample, same instant, or a counter that went backwards because the producer restarted. Only the node running a stream can read its own /proc, so the sampling happens there and reaches the panel in the row the cron already writes; MAIN just renders it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZP7fc2Hz36wbPmuLd9F9o
This commit is contained in:
@@ -173,6 +173,139 @@ class ProcessManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What kind of producer a stream's pid is, for the panel's stream list.
|
||||
*
|
||||
* @param int $pid Process ID
|
||||
* @return string|null 'fanout' (xc_fanout remux), 'ffmpeg', 'php' (the LLOD
|
||||
* segmenter / loopback relay), or null when it cannot be read.
|
||||
*/
|
||||
public static function producerKind($pid) {
|
||||
$pid = (int)$pid;
|
||||
|
||||
if ($pid <= 0 || !self::procExists($pid) || !is_readable('/proc/' . $pid . '/exe')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$exe = @basename(@readlink('/proc/' . $pid . '/exe'));
|
||||
|
||||
if (strpos($exe, 'xc_fanout') === 0) {
|
||||
$cmdline = (string) @file_get_contents('/proc/' . $pid . '/cmdline');
|
||||
return strpos($cmdline, "\0remux\0") !== false ? 'fanout' : null;
|
||||
}
|
||||
if (strpos($exe, 'ffmpeg') === 0) {
|
||||
return 'ffmpeg';
|
||||
}
|
||||
if (strpos($exe, 'php') === 0) {
|
||||
return 'php';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* One CPU/memory reading for a process, from /proc/PID/stat.
|
||||
*
|
||||
* CPU is cumulative (the ticks the process has burned since it started), so a
|
||||
* percentage needs two readings — see cpuPercent(). Memory is resident set
|
||||
* size in bytes, which is the figure that matters for a box running hundreds
|
||||
* of encoders: what they actually hold in RAM.
|
||||
*
|
||||
* @param int $pid Process ID
|
||||
* @return array{ticks:int,rss:int,at:float}|null Null when the process is gone.
|
||||
*/
|
||||
public static function resourceSample($pid) {
|
||||
$pid = (int)$pid;
|
||||
|
||||
if ($pid <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stat = @file_get_contents('/proc/' . $pid . '/stat');
|
||||
if (!is_string($stat) || $stat === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Field 2 (comm) is parenthesised and may itself contain spaces and
|
||||
// brackets — "(ffmpeg (x))" is a legal name — so everything before the
|
||||
// LAST ')' is skipped and the split starts at field 3 (state).
|
||||
$rClose = strrpos($stat, ')');
|
||||
if ($rClose === false) {
|
||||
return null;
|
||||
}
|
||||
$rFields = preg_split('/\s+/', trim(substr($stat, $rClose + 1)));
|
||||
if (!is_array($rFields) || count($rFields) < 22) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// $rFields[$i] is /proc/PID/stat field $i + 3: utime 14, stime 15, rss 24.
|
||||
return [
|
||||
'ticks' => (int) $rFields[11] + (int) $rFields[12],
|
||||
'rss' => (int) $rFields[21] * self::pageSize(),
|
||||
'at' => microtime(true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* CPU use between two resourceSample() readings, in percent of one core.
|
||||
*
|
||||
* @param array $now The newer sample.
|
||||
* @param array $prev The older one (from the previous pass).
|
||||
* @return float|null Null when the pair says nothing: no previous reading, a
|
||||
* restarted process (its counter went backwards), or two
|
||||
* readings from the same instant.
|
||||
*/
|
||||
public static function cpuPercent(array $now, array $prev) {
|
||||
if (!isset($now['ticks'], $now['at'], $prev['ticks'], $prev['at'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rSeconds = (float) $now['at'] - (float) $prev['at'];
|
||||
$rTicks = (int) $now['ticks'] - (int) $prev['ticks'];
|
||||
if ($rSeconds <= 0 || $rTicks < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// /proc reports CPU time in USER_HZ, which is 100 on Linux whatever the
|
||||
// kernel's own tick rate — it is part of the /proc ABI, not CONFIG_HZ.
|
||||
return round(($rTicks / 100) / $rSeconds * 100, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The kernel page size, in bytes — /proc/PID/stat counts RSS in pages.
|
||||
*
|
||||
* Derived from this process's own two views of its memory (statm in pages,
|
||||
* status in kB) rather than assumed, because 64K pages are normal on arm64.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected static function pageSize() {
|
||||
static $rSize = null;
|
||||
if ($rSize !== null) {
|
||||
return $rSize;
|
||||
}
|
||||
|
||||
$rSize = 4096;
|
||||
$rStatm = @file_get_contents('/proc/self/statm');
|
||||
$rStatus = @file_get_contents('/proc/self/status');
|
||||
if (is_string($rStatm) && is_string($rStatus) && preg_match('/^VmRSS:\s+(\d+) kB/m', $rStatus, $rMatch)) {
|
||||
$rPages = (int) (preg_split('/\s+/', trim($rStatm))[1] ?? 0);
|
||||
if ($rPages > 0) {
|
||||
// The two files are read a moment apart, so snap the ratio to a
|
||||
// real page size instead of trusting it to the byte.
|
||||
$rRatio = ((int) $rMatch[1] * 1024) / $rPages;
|
||||
foreach ([4096, 8192, 16384, 65536] as $rCandidate) {
|
||||
if ($rRatio >= $rCandidate * 0.75 && $rRatio <= $rCandidate * 1.25) {
|
||||
$rSize = $rCandidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rSize;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────
|
||||
// Process Control
|
||||
// ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user