Skip to content

Commit 8e3dc3a

Browse files
committed
add logging for memory.
1 parent 8623d3f commit 8e3dc3a

3 files changed

Lines changed: 200 additions & 1 deletion

File tree

ios/Tunnel/PacketTunnelProvider.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,101 @@
33
// LanternTunnel
44
//
55

6+
import Darwin
67
import NetworkExtension
78
import System
89
import os
910

1011
class PacketTunnelProvider: ExtensionProvider {
12+
private var memoryLogTimer: DispatchSourceTimer?
13+
private var memoryPressureSource: DispatchSourceMemoryPressure?
14+
private var currentMemoryPressure: DispatchSource.MemoryPressureEvent = .normal
15+
1116
override func startTunnel(options: [String: NSObject]?) async throws {
1217
try await super.startTunnel(options: options)
18+
startMemoryLogger()
1319
}
1420

1521
override func stopTunnel(with reason: NEProviderStopReason) async {
22+
stopMemoryLogger()
1623
try? await super.stopTunnel(with: reason)
1724
}
1825

26+
private func startMemoryLogger(interval: TimeInterval = 10) {
27+
stopMemoryLogger()
28+
startMemoryPressureMonitor()
29+
let timer = DispatchSource.makeTimerSource(queue: .global(qos: .utility))
30+
timer.schedule(deadline: .now(), repeating: interval)
31+
timer.setEventHandler { [weak self] in
32+
self?.logMemoryUsage()
33+
}
34+
memoryLogTimer = timer
35+
timer.resume()
36+
}
37+
38+
private func stopMemoryLogger() {
39+
memoryLogTimer?.cancel()
40+
memoryLogTimer = nil
41+
memoryPressureSource?.cancel()
42+
memoryPressureSource = nil
43+
currentMemoryPressure = .normal
44+
}
45+
46+
// Subscribes to kernel memory-pressure notifications. The OS publishes one
47+
// of three levels (normal / warning / critical) — we mirror the latest into
48+
// currentMemoryPressure so each log line carries the system's own label.
49+
private func startMemoryPressureMonitor() {
50+
let source = DispatchSource.makeMemoryPressureSource(
51+
eventMask: [.normal, .warning, .critical],
52+
queue: .global(qos: .utility)
53+
)
54+
source.setEventHandler { [weak self, weak source] in
55+
guard let self = self, let source = source else { return }
56+
self.currentMemoryPressure = source.data
57+
}
58+
memoryPressureSource = source
59+
source.resume()
60+
}
61+
62+
63+
private func logMemoryUsage() {
64+
var info = task_vm_info_data_t()
65+
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<integer_t>.size)
66+
let kerr = withUnsafeMutablePointer(to: &info) {
67+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
68+
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
69+
}
70+
}
71+
guard kerr == KERN_SUCCESS else {
72+
appLogger.error("PacketTunnelProvider failed to read memory info: \(kerr)")
73+
return
74+
}
75+
let mb = 1024.0 * 1024.0
76+
let footprintMB = Double(info.phys_footprint) / mb
77+
let residentMB = Double(info.resident_size) / mb
78+
let peakMB = Double(info.resident_size_peak) / mb
79+
let virtualMB = Double(info.virtual_size) / mb
80+
let dirtyMB = Double(info.internal) / mb
81+
let compressedMB = Double(info.compressed) / mb
82+
// os_proc_available_memory: bytes remaining before iOS jetsams this process.
83+
// Returns 0 on platforms / processes without a limit (e.g. macOS host app).
84+
let availableBytes = os_proc_available_memory()
85+
let availableMB = Double(availableBytes) / mb
86+
let message = String(
87+
format:
88+
"PacketTunnelProvider memory [pressure=%u] — footprint: %.2f MB, available: %.2f MB, resident: %.2f MB, peak: %.2f MB, dirty: %.2f MB, compressed: %.2f MB, virtual: %.2f MB",
89+
currentMemoryPressure.rawValue, footprintMB, availableMB, residentMB, peakMB, dirtyMB, compressedMB, virtualMB
90+
)
91+
switch currentMemoryPressure {
92+
case .critical:
93+
appLogger.info("\(message)")
94+
case .warning:
95+
appLogger.info("\(message)")
96+
default:
97+
appLogger.info("\(message)")
98+
}
99+
}
100+
19101
public override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?)
20102
{
21103
appLogger.info("PacketTunnelProvider received app message with data: \(messageData)")
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package mobile
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"math"
7+
"runtime"
8+
runtimeDebug "runtime/debug"
9+
"sync"
10+
"time"
11+
)
12+
13+
// memoryLogInterval matches the iOS PacketTunnelProvider Swift memory logger
14+
// cadence so the Go-side and Swift-side log lines interleave at the same rate.
15+
const memoryLogInterval = 10 * time.Second
16+
17+
const bytesPerMB = 1024.0 * 1024.0
18+
19+
var (
20+
memLogMu sync.Mutex
21+
memLogCancel context.CancelFunc
22+
)
23+
24+
// startMemoryLogger begins periodic Go-runtime memory logging for the tunnel
25+
// (extension) process. It is the Go counterpart to the Swift memory logger in
26+
// PacketTunnelProvider: that one reports the whole extension process footprint
27+
// via Mach task_vm_info (phys_footprint), while this reports the Go runtime's
28+
// view from inside that same process. The Go heap is the largest mutable
29+
// contributor to phys_footprint, and iOS jetsams the entire Network Extension
30+
// when the footprint exceeds its tight memory cap — so correlating the two log
31+
// streams shows whether Go is what's pushing the process toward that limit.
32+
//
33+
// Started from StartIPCServer (tunnel start) and stopped from CloseIPCServer
34+
// (tunnel stop), mirroring the Swift logger's startTunnel/stopTunnel hooks.
35+
// Safe to call repeatedly: a start while already running is a no-op until the
36+
// matching stop.
37+
func startMemoryLogger() {
38+
memLogMu.Lock()
39+
defer memLogMu.Unlock()
40+
if memLogCancel != nil {
41+
return
42+
}
43+
ctx, cancel := context.WithCancel(context.Background())
44+
memLogCancel = cancel
45+
go func() {
46+
ticker := time.NewTicker(memoryLogInterval)
47+
defer ticker.Stop()
48+
logMemStats() // log immediately, like the Swift timer's deadline: .now()
49+
for {
50+
select {
51+
case <-ctx.Done():
52+
slog.Debug("Stopping tunnel memory logger")
53+
return
54+
case <-ticker.C:
55+
logMemStats()
56+
}
57+
}
58+
}()
59+
}
60+
61+
// stopMemoryLogger stops the goroutine started by startMemoryLogger. Safe to
62+
// call when the logger isn't running.
63+
func stopMemoryLogger() {
64+
memLogMu.Lock()
65+
defer memLogMu.Unlock()
66+
if memLogCancel != nil {
67+
memLogCancel()
68+
memLogCancel = nil
69+
}
70+
}
71+
72+
// mbFromBytes converts a byte count to megabytes rounded to 2 decimals, matching
73+
// the "%.2f MB" formatting on the Swift side.
74+
func mbFromBytes(b uint64) float64 {
75+
return math.Round(float64(b)/bytesPerMB*100) / 100
76+
}
77+
78+
// logMemStats emits one Go-runtime memory snapshot. Field meanings:
79+
// - heap_alloc: live heap objects (the working set Go can't release)
80+
// - heap_inuse/heap_idle: bytes in in-use vs idle spans
81+
// - heap_released: idle bytes already returned to the OS (lowers footprint)
82+
// - stack_inuse: goroutine stacks
83+
// - sys: total obtained from the OS (Go's contribution to process footprint)
84+
// - next_gc: heap size that will trigger the next GC
85+
// - mem_limit: the soft memory limit in effect (libbox.SetMemoryLimit sets
86+
// this on mobile); 0 means no limit
87+
func logMemStats() {
88+
var m runtime.MemStats
89+
runtime.ReadMemStats(&m)
90+
91+
// SetMemoryLimit(-1) reads the current soft limit without changing it;
92+
// math.MaxInt64 is the sentinel for "no limit".
93+
var limitMB float64
94+
if limit := runtimeDebug.SetMemoryLimit(-1); limit != math.MaxInt64 {
95+
limitMB = math.Round(float64(limit)/bytesPerMB*100) / 100
96+
}
97+
98+
slog.Info("[memory stats]",
99+
"heap_alloc_mb", mbFromBytes(m.HeapAlloc),
100+
"heap_inuse_mb", mbFromBytes(m.HeapInuse),
101+
"heap_idle_mb", mbFromBytes(m.HeapIdle),
102+
"heap_released_mb", mbFromBytes(m.HeapReleased),
103+
"stack_inuse_mb", mbFromBytes(m.StackInuse),
104+
"sys_mb", mbFromBytes(m.Sys),
105+
"next_gc_mb", mbFromBytes(m.NextGC),
106+
"mem_limit_mb", limitMB,
107+
"num_gc", m.NumGC,
108+
"num_goroutine", runtime.NumGoroutine(),
109+
)
110+
}

lantern-core/mobile/mobile.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,13 @@ func StartIPCServer(platform utils.PlatformInterface, opts *utils.Opts) error {
305305
ipcBackend = be
306306
ipcServer = ipc.NewServer(be, !common.IsMobile())
307307
ipcClient = newLoopbackClient(be)
308-
return struct{}{}, ipcServer.Start()
308+
if err := ipcServer.Start(); err != nil {
309+
return struct{}{}, err
310+
}
311+
// Start the Go-side memory logger for the tunnel process, mirroring the
312+
// Swift PacketTunnelProvider logger. Stopped from CloseIPCServer.
313+
startMemoryLogger()
314+
return struct{}{}, nil
309315
})
310316
return err
311317
}
@@ -314,6 +320,7 @@ func CloseIPCServer() error {
314320
_, err := utils.RunOffCgoStack(func() (struct{}, error) {
315321
ipcMu.Lock()
316322
defer ipcMu.Unlock()
323+
stopMemoryLogger()
317324
if ipcBackend != nil {
318325
ipcBackend.Close()
319326
ipcBackend = nil

0 commit comments

Comments
 (0)