How to Monitor bannedbook/fanqiang Performance: Profiling the DNS Proxy and Go Core

Monitor bannedbook/fanqiang performance by instrumenting the ChannelMonitor class in LocalDnsServer.kt for DNS latency metrics, enabling pprof endpoints in the Go libcore for CPU and memory profiling, and using Android Studio Profiler to analyze UI thread stalls.

The bannedbook/fanqiang repository implements a sophisticated circumvention VPN combining a Kotlin-based Android DNS proxy with a high-performance Go networking core. Understanding how to monitor bannedbook/fanqiang performance requires observing the non-blocking I/O operations in the ChannelMonitor selector loop and the packet processing routines in the Go libcore. This guide identifies the exact source file locations and instrumentation code needed to capture comprehensive latency, throughput, and resource utilization metrics.

Instrumenting the DNS Proxy ChannelMonitor

The Android DNS proxy relies on LocalDnsServer.kt and ChannelMonitor.kt in fqnews/core/src/main/java/com/github/shadowsocks/net/ to manage non-blocking NIO channel operations. The ChannelMonitor class runs a dedicated selector thread that processes registrations and readiness events, avoiding NetworkOnMainThreadException while handling DNS requests.

The register() method sends channel registration requests to a pendingRegistrations channel. By adding timestamp logging at the entry point of this method and when channels become ready, you can calculate per-operation latency. The class uses a writeCompat extension function to handle API compatibility for WritableByteChannel operations, and printLog(e) for error handling.

Add timestamp instrumentation to the register() method:

// ChannelMonitor.kt – add simple timestamp logging
private suspend fun WritableByteChannel.writeCompat(src: ByteBuffer) =
    if (Build.VERSION.SDK_INT <= 23) withContext(Dispatchers.Default) { write(src) } else write(src)

suspend fun register(channel: SelectableChannel, ops: Int, block: (SelectionKey) -> Unit): SelectionKey {
    val registration = Registration(channel, ops, block)
    pendingRegistrations.send(registration)
    println("[${System.currentTimeMillis()}] REGISTER ${channel} ops=$ops")
    ByteBuffer.allocateDirect(1).also { junk ->
        loop@ while (running) when (registrationPipe.sink().writeCompat(junk)) {
            0 -> kotlinx.coroutines.yield()
            1 -> break@loop
            else -> throw IOException("Failed to register in the channel")
        }
    }
    if (!running) throw CancellationException()
    return registration.result.await()
}

Similarly, instrument the wait() coroutine to emit completion timestamps when channels become ready for OP_READ or OP_WRITE operations.

Profiling the Go Core with pprof

The Go networking components reside in fqnews2/libcore, including the STUN client implementation in stun/client.go and DNS handling in dns_box.go. To monitor bannedbook/fanqiang performance at the network layer, integrate the Go pprof package to expose runtime metrics.

Add a pprof HTTP endpoint in fqnews2/libcore/http.go:

package libcore

import (
    "log"
    "net/http"
    _ "net/http/pprof" // registers /debug/pprof/* handlers
)

func init() {
    go func() {
        log.Println("pprof listening on :6060")
        if err := http.ListenAndServe(":6060", nil); err != nil {
            log.Fatalf("pprof failed: %v", err)
        }
    }()
}

With this endpoint active, capture CPU profiles while the VPN processes traffic:

go tool pprof -seconds 30 http://localhost:6060/debug/pprof/profile

This profiles the exact functions in stun/client.go and dns_box.go that handle packet processing, revealing CPU hotspots and goroutine counts. You can also use expvar hooks to export custom counters for DNS lookup latency and STUN round-trip times.

Capturing Android System Metrics

For the Android UI layer located in app/src/main, use Android Studio Profiler to monitor frame rendering times, memory usage, and battery impact. The DNS proxy uses UDP/TCP under the hood, which appears in the Profiler's Network pane. Ensure AndroidManifest.xml contains the necessary network permissions for profiling that uses network sockets.

Capture runtime logs from the ChannelMonitor thread using adb:


# Run once per test session

adb logcat -v time -s ChannelMonitor > /tmp/channel_monitor.log &

# … after exercising the app …

adb logcat -c               # clear the buffer for next run

Analyzing Latency from ChannelMonitor Logs

Process the captured log files to calculate DNS operation latency. The logs contain REGISTER events (when operations start) and WAIT events (when channels become ready).

Extract latency metrics using this awk script:

awk '
/REGISTER/ { start[$4] = $1 }
/WAIT/ && $4 in start { printf "%s %dms\n", $4, $1 - start[$4] }
' /tmp/channel_monitor.log | sort -k2 -n

This calculates the time delta between registration and readiness for each channel, sorting results to identify slow operations.

Aggregating Metrics for Unified Monitoring

Combine these data sources into a centralized monitoring stack:

  • Logcat ingestion: Pipe Android logs to Fluent Bit → Elasticsearch for searchable, time-series log analysis
  • Go metrics: Export pprof data via a Prometheus exporter such as Pyroscope for continuous profiling
  • Custom Kotlin counters: Expose DNS latency histograms via a lightweight HTTP /metrics endpoint for Prometheus scraping

This unified approach provides a dashboard showing DNS request latency distributions, Go routine CPU usage, and Android UI frame-time histograms.

Summary

  • Instrument ChannelMonitor: Modify fqnews/core/src/main/java/com/github/shadowsocks/net/ChannelMonitor.kt to emit timestamps in register() and wait handlers to capture DNS proxy latency
  • Enable Go pprof: Add net/http/pprof imports to fqnews2/libcore/http.go to expose CPU and memory profiles for the networking core
  • Capture Android logs: Use adb logcat -s ChannelMonitor to collect runtime data and process with awk to calculate per-operation latency
  • Profile system-wide: Use Android Studio Profiler for UI thread analysis and resource monitoring on the Android frontend

Frequently Asked Questions

How do I measure DNS request latency in bannedbook/fanqiang?

Modify the register() method in ChannelMonitor.kt to log System.currentTimeMillis() when channels register and when they become ready. The difference between these timestamps represents the selector latency for each DNS operation, which you can extract using the provided awk script.

Where should I add pprof endpoints in the Go libcore?

Add the pprof HTTP server initialization to fqnews2/libcore/http.go within an init() function. This exposes /debug/pprof/ endpoints for the entire libcore package, including the STUN client routines in stun/client.go and DNS handlers in dns_box.go.

Can I monitor fanqiang performance without modifying the source code?

You can capture basic metrics using Android Studio Profiler and standard adb logcat without code changes, but precise DNS latency measurement requires adding timestamp instrumentation to ChannelMonitor.kt. The Go pprof endpoints also require the minimal code addition shown above to expose runtime metrics.

What tools analyze the ChannelMonitor logs most effectively?

Use awk for quick command-line analysis of latency patterns, or import the log data into Grafana or Elasticsearch for visualization. The printLog function calls in the Kotlin source can be redirected to structured logging systems like ELK for historical trend analysis.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →