Files
pangolin/client/ios/PacketTunnel/MemoryMonitor.swift
T
wangjia e129f094c2 feat(ios): M3 PoC — PacketTunnelProvider + entitlement + 内存监控 [tsk_nsobbj_rJdy0]
### 新增文件
- client/ios/PacketTunnel/MemoryMonitor.swift
  周期打点 NE 进程内存(os_proc_available_memory iOS13+ / mach_task_basic_info 兼容
  iOS12);每 10s 采样一次;availableMemory < 5MB 时输出 ⚠️ 警告;提供
  summaryString() 供 handleAppMessage/getMemory IPC 返回。

- client/ios/PacketTunnel/PacketTunnel.entitlements
  Extension 的 NE + App Group entitlement 文件;申请说明写入注释。

- client/ios/Runner/Runner.entitlements
  主 App 的 NE + App Group entitlement 文件;申请说明写入注释。

- doc/ne-entitlement-申请指引.md
  完整申请操作手册:Bundle ID 清单、Apple Developer Portal 步骤、
  专项审批英文申请文案(已脱敏,无红线词)、审批 lead time 说明。

### 修改文件
- client/ios/PacketTunnel/PacketTunnelProvider.swift
  完整 M3 实现(替换原骨架 TODO):
  · startTunnel:解析 configJson(options/App Group 两路回退)→
    buildNetworkSettings → setTunnelNetworkSettings → setupTunBridge
    → startLibbox → startPacketBridging → memoryMonitor.start()
  · stopTunnel:memoryMonitor.stop() → stopLibbox → closeTunBridge
  · handleAppMessage:JSON IPC 协议(getStatus/getMemory/selectOutbound)
  · LibboxPlatformInterface 扩展(#if canImport(Libbox)):
    openTun 返回 socketpair libbox 侧 fd,autoDetectInterfaceControl no-op,
    writeLog 转 os.log,useProcFS 返回 false
  · packetFlow 双向桥接(readFromPacketFlow/writeToPacketFlow 循环)
  · parseTunAddress/parseDNSServers 从 JSON 提取网络参数

- client/ios/Runner/VpnManager.swift
  · localizedDescription "穿山甲 Pangolin VPN" → "Pangolin 加速"(红线词修复)
  · 新增 appGroup 常量(group.com.pangolin.pangolinVpn)
  · start() 写入 App Group UserDefaults 缓存 configJson

- client/ios/Runner/Info.plist
  NSVPNUsageDescription "穿山甲使用 VPN…" → "Pangolin 使用网络加速通道…"(红线词修复)

- client/ios/PacketTunnel/Info.plist
  CFBundleDisplayName "穿山甲 Tunnel" → "Pangolin 加速通道"(红线词修复)

- client/ios/Runner.xcodeproj/project.pbxproj
  · Runner + PacketTunnel 所有 build config 添加 CODE_SIGN_ENTITLEMENTS
  · 新增 MemoryMonitor.swift 到 PacketTunnel Sources 构建阶段
  · 新增 Runner.entitlements / PacketTunnel.entitlements / MemoryMonitor.swift
    文件引用及分组

- app/kernel/build-ios.sh
  添加 M3 内存裁剪决策记录:gVisor→grpc→QUIC 优先裁减顺序及内存估算,
  等待真机 10min 压测结果后更新最终结论

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:44:11 +08:00

141 lines
5.6 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MemoryMonitor.swift NE
//
//
// 1. os_proc_available_memory()iOS 13+ NE
// 2. task_info(MACH_TASK_BASIC_INFO) resident size iOS 12
// 3. availableMemory < warningThreshold
// 4. 10 M3
//
// 使
// let monitor = MemoryMonitor()
// monitor.start() //
// monitor.stop() //
// monitor.peakResidentBytes //
//
// NE jetsam
// A9~15 MB residual limit
// A12~50 MB residual limit
// 线availableMemory < 5 MB
import Darwin
import Foundation
import os.log
// os_proc_available_memory iOS 13+ libSystem 退 mach
// Swift os/proc.h @_silgen_name
@_silgen_name("os_proc_available_memory")
private func _os_proc_available_memory() -> UInt64
// MARK: MemoryMonitor
final class MemoryMonitor {
//
/// 10
let interval: TimeInterval
/// 5 MB
let warningThreshold: UInt64
/// os.log
private let log: OSLog
//
private var timer: DispatchSourceTimer?
private let queue = DispatchQueue(label: "com.pangolin.memory-monitor", qos: .utility)
private(set) var peakResidentBytes: UInt64 = 0
private(set) var minAvailableBytes: UInt64 = UInt64.max
private(set) var sampleCount: Int = 0
init(
interval: TimeInterval = 10,
warningThreshold: UInt64 = 5 * 1024 * 1024,
subsystem: String = "com.pangolin.pangolinVpn.PacketTunnel"
) {
self.interval = interval
self.warningThreshold = warningThreshold
self.log = OSLog(subsystem: subsystem, category: "MemoryMonitor")
}
// MARK:
func start() {
stop()
let t = DispatchSource.makeTimerSource(queue: queue)
t.schedule(deadline: .now(), repeating: interval, leeway: .seconds(1))
t.setEventHandler { [weak self] in self?.tick() }
timer = t
t.resume()
os_log("MemoryMonitor: 开始打点,间隔=%.0fs", log: log, type: .info, interval)
}
func stop() {
timer?.cancel()
timer = nil
os_log(
"MemoryMonitor: 停止打点。总采样 %d 次 | 峰值 RSS=%.1f MB | 最小可用=%.1f MB",
log: log, type: .info,
sampleCount,
Double(peakResidentBytes) / 1_048_576,
minAvailableBytes == UInt64.max ? -1.0 : Double(minAvailableBytes) / 1_048_576
)
}
// MARK:
private func tick() {
let resident = residentBytes()
let available = availableBytes()
sampleCount += 1
if resident > peakResidentBytes { peakResidentBytes = resident }
if available < minAvailableBytes { minAvailableBytes = available }
let level: OSLogType = available < warningThreshold ? .error : .info
os_log(
"MemoryMonitor [#%d] RSS=%.1f MB | 可用=%.1f MB%{public}@",
log: log, type: level,
sampleCount,
Double(resident) / 1_048_576,
Double(available) / 1_048_576,
available < warningThreshold ? " ⚠️ 接近上限,建议裁剪 build tags" : ""
)
}
// MARK:
/// resident sizeRSSiOS 12+
func residentBytes() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
let result: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? UInt64(info.resident_size) : 0
}
/// NE
/// iOS 13+ os_proc_available_memory()
/// iOS 12退 MAX_RSS - resident
func availableBytes() -> UInt64 {
if #available(iOS 13.0, *) {
return _os_proc_available_memory()
} else {
// iOS 12 退 15 MB - RSS
let conservativeLimit: UInt64 = 15 * 1024 * 1024
let resident = residentBytes()
return resident < conservativeLimit ? conservativeLimit - resident : 0
}
}
/// handleAppMessage/getStats
func summaryString() -> String {
let rss = Double(residentBytes()) / 1_048_576
let avail = Double(availableBytes()) / 1_048_576
return String(format: "RSS=%.1fMB avail=%.1fMB peak=%.1fMB samples=%d",
rss, avail,
Double(peakResidentBytes) / 1_048_576,
sampleCount)
}
}