From ad89ec6aecd391f8529a36a49562dd220ee56498 Mon Sep 17 00:00:00 2001 From: testercengdong Date: Tue, 12 May 2026 09:38:02 +0800 Subject: [PATCH] fix(macos,process_info): address Copilot review feedback - Add 30s (host:port) -> pid cache in ProcessInfoUtils so the synchronous libproc scan stays off the request hot path for keep-alive flows (typical HTTP client reuses the same source port across requests in a connection). - Lift sockView ByteData out of the inner fd loop so the buffer wrapper is reused across all fds in a scan (saves per-fd allocations). - Bail out of the scan after 16 consecutive proc_pidfdinfo size mismatches, in case a future macOS bumps the socket_fdinfo layout -- avoids wasted syscalls instead of scanning every fd just to return null at the end. Re-verified end-to-end on macOS 26.4: 500 concurrent curl, 0 fork leaks, 9099 freed immediately on app exit, no errors in logs. --- lib/network/util/process_info.dart | 16 ++++++++++++++-- lib/network/util/process_info_macos.dart | 18 ++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/network/util/process_info.dart b/lib/network/util/process_info.dart index 62d1496..088d6fc 100644 --- a/lib/network/util/process_info.dart +++ b/lib/network/util/process_info.dart @@ -38,6 +38,13 @@ void main() async { class ProcessInfoUtils { static final processInfoCache = ExpiringCache(const Duration(minutes: 5)); + // (host:port) -> pid short cache. Keeps the FFI / Process.run lookup off + // the request hot path for the typical HTTP keep-alive case where many + // requests share a single client TCP connection (and thus a single + // remote socket address). Greatly reduces how often the synchronous + // libproc scan runs on the main isolate. + static final _pidCache = ExpiringCache(const Duration(seconds: 30)); + static Future getProcessByPort(InetSocketAddress socketAddress, String cacheKeyPre) async { try { if (Platform.isAndroid) { @@ -51,8 +58,13 @@ class ProcessInfoUtils { return null; } - var pid = await _getPid(socketAddress); - if (pid == null) return null; + var addrKey = "${socketAddress.host}:${socketAddress.port}"; + var pid = _pidCache.get(addrKey); + if (pid == null) { + pid = await _getPid(socketAddress); + if (pid == null) return null; + _pidCache.set(addrKey, pid); + } String cacheKey = "$cacheKeyPre:$pid"; var processInfo = processInfoCache.get(cacheKey); diff --git a/lib/network/util/process_info_macos.dart b/lib/network/util/process_info_macos.dart index b65ce3d..7ee02f8 100644 --- a/lib/network/util/process_info_macos.dart +++ b/lib/network/util/process_info_macos.dart @@ -98,6 +98,16 @@ class MacosProcessInfo { final pidBuf = calloc(pidBufSize); final sockBuf = calloc(_kSizeofSocketFdInfo); + // Reuse the same ByteData view across all fds; the underlying native + // buffer is overwritten in place by each proc_pidfdinfo call. + final sockView = ByteData.sublistView(sockBuf.asTypedList(_kSizeofSocketFdInfo)); + + // If proc_pidfdinfo keeps returning a size smaller than expected, the + // struct layout has changed (e.g. a future macOS bumped the size) and + // continuing the scan can't possibly find anything. Bail out early + // after enough consecutive mismatches to avoid wasting syscalls. + var consecutiveLayoutMismatch = 0; + const layoutMismatchThreshold = 16; try { final actual = _procListPids(_kProcAllPids, 0, pidBuf.cast(), pidBufSize); @@ -107,6 +117,7 @@ class MacosProcessInfo { for (final pid in pidView) { if (pid <= 0) continue; + if (consecutiveLayoutMismatch >= layoutMismatchThreshold) break; final fdSize = _procPidInfo(pid, _kProcPidListFds, 0, nullptr, 0); if (fdSize <= 0) continue; @@ -127,9 +138,12 @@ class MacosProcessInfo { final fd = fdView.getInt32(entryOff + _kOffProcFd, Endian.host); final n = _procPidFdInfo(pid, fd, _kProcPidFdSocketInfo, sockBuf.cast(), _kSizeofSocketFdInfo); - if (n < _kSizeofSocketFdInfo) continue; + if (n < _kSizeofSocketFdInfo) { + consecutiveLayoutMismatch++; + continue; + } + consecutiveLayoutMismatch = 0; - final sockView = ByteData.sublistView(sockBuf.asTypedList(_kSizeofSocketFdInfo)); final soiKind = sockView.getInt32(_kOffSoiKind, Endian.host); if (soiKind != _kSockInfoTcp) continue;