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.
This commit is contained in:
testercengdong
2026-05-12 09:38:02 +08:00
parent 202050b35f
commit ad89ec6aec
2 changed files with 30 additions and 4 deletions

View File

@@ -38,6 +38,13 @@ void main() async {
class ProcessInfoUtils {
static final processInfoCache = ExpiringCache<String, ProcessInfo>(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<String, int>(const Duration(seconds: 30));
static Future<ProcessInfo?> 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);

View File

@@ -98,6 +98,16 @@ class MacosProcessInfo {
final pidBuf = calloc<Uint8>(pidBufSize);
final sockBuf = calloc<Uint8>(_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;