lastTimestamp = time(); } public function run(): void { Logger::debug("Profiler started"); echo "\n[Profiler] Started at " . date('c') . "\n"; while (true) { $now = time(); $delta = $now - $this->lastTimestamp; $this->lastTimestamp = $now; $pids = ProcessFinder::findPhpFpmPids(PROC_NAME); foreach (/*array_slice($pids, 0, 5)*/$pids as $pid) { Logger::debug("Profiling PID {$pid}"); $data = $this->collectStats($pid, $delta); if ($data) { Logger::log($data); } } if (empty($pids)) { Logger::debug("No PHP-FPM worker processes found"); } sleep($this->interval); } } private function collectStats(int $pid, int $delta): ?array { $stat = @file_get_contents("/proc/{$pid}/stat"); $status = @file_get_contents("/proc/{$pid}/status"); $environ = @file_get_contents("/proc/{$pid}/environ"); if (!$stat || !$status) return null; $fields = explode(' ', $stat); $utime = (int)$fields[13]; $stime = (int)$fields[14]; $totalCpu = $utime + $stime; $cpuDelta = isset($this->lastCpuTimes[$pid]) ? $totalCpu - $this->lastCpuTimes[$pid] : 0; $this->lastCpuTimes[$pid] = $totalCpu; $memory = (int)ProcessParser::parseKeyValue($status, 'VmRSS'); $threads = (int)ProcessParser::parseKeyValue($status, 'Threads'); $fdCount = count(glob("/proc/{$pid}/fd/*")); $uri = ProcessParser::parseEnv($environ, 'REQUEST_URI'); $stack = GDB::getStack($pid); $this->callCounts[$pid] = ($this->callCounts[$pid] ?? 0) + 1; return [ 'time' => time(), 'delta' => $delta, 'pid' => $pid, 'calls' => $this->callCounts[$pid], 'cpu_time_ticks' => $totalCpu, 'cpu_delta' => $cpuDelta, 'memory_kb' => $memory, 'threads' => $threads, 'fds' => $fdCount, 'request_uri' => $uri, 'stack' => $stack, ]; } } class Logger { public static function log(array $data): void { $json = json_encode($data, JSON_UNESCAPED_SLASHES); file_put_contents(STREAM_FILE, "data: $json\n\n", FILE_APPEND); //echo "[Log] " . date('c') . " - PID {$data['pid']} - Calls: {$data['calls']} - Memory: {$data['memory_kb']} KB - CPU Δ: {$data['cpu_delta']} - FDs: {$data['fds']}, Threads: {$data['threads']}\n"; } public static function debug(string $msg): void { $json = json_encode(['debug' => $msg, 'time' => time()]); file_put_contents(STREAM_FILE, "data: $json\n\n", FILE_APPEND); //echo "[Debug] " . date('c') . " - $msg\n"; } } class GDB { public static function getStack(int $pid): array { $cmdFile = tempnam(sys_get_temp_dir(), 'gdb'); file_put_contents($cmdFile, "set pagination off\nthread apply all bt " . MAX_FRAMES . "\nquit\n"); $output = []; $cmd = "timeout 5s sudo gdb -q -p $pid -x $cmdFile 2>&1"; exec($cmd, $output, $code); unlink($cmdFile); if ($code !== 0) { Logger::debug("GDB failed for PID $pid with exit code $code"); return ["[gdb failed or timed out]"]; } return array_filter($output, fn($l) => str_starts_with(trim($l), '#')); } } class ProcessFinder { public static function findPhpFpmPids(string $name): array { $pids = []; foreach (glob('/proc/[0-9]*/status') as $statusFile) { $pid = (int)basename(dirname($statusFile)); $status = @file_get_contents($statusFile); if ($status && preg_match('/^Name:\s+(.*)$/m', $status, $match)) { $procName = trim($match[1]); if (stripos($procName, 'php-fpm') !== false) { $pids[] = $pid; //Logger::debug("Matched PHP-FPM process: PID {$pid}"); } } } return $pids; } } class ProcessParser { public static function parseKeyValue(string $text, string $key): ?string { if (preg_match("/^$key:\\s+(\\d+)/m", $text, $m)) { return $m[1]; } return null; } public static function parseEnv(string $env, string $key): ?string { if (preg_match("/$key=([^\\x00]+)/", $env, $m)) { return urldecode($m[1]); } return null; } } class StreamServer { public static function start(): void { $server = stream_socket_server("tcp://0.0.0.0:" . PORT, $errno, $errstr); if (!$server) die("Server error: $errstr\n"); echo "[StreamServer] Listening on http://localhost:" . PORT . "\n"; while ($conn = @stream_socket_accept($server)) { stream_set_blocking($conn, false); $request = fread($conn, 1024); if (preg_match('#GET /stream#', $request)) { fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n"); fwrite($conn, ": stream initialized\n\n"); fflush($conn); $pos = 0; $lastPing = time(); while (!feof($conn)) { clearstatcache(); $len = file_exists(STREAM_FILE) ? filesize(STREAM_FILE) : 0; if ($len > $pos) { $f = fopen(STREAM_FILE, 'r'); fseek($f, $pos); while (!feof($f)) { fwrite($conn, fgets($f)); } $pos = ftell($f); fclose($f); } if ((time() - $lastPing) >= 10) { fwrite($conn, ": keep-alive\n\n"); fflush($conn); $lastPing = time(); } usleep(250_000); } fclose($conn); } else { fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"); fwrite($conn, <<