AI帮我实现网络性能统计功能

143 阅读7分钟

背景

最近我们产品 FinClip 需要针对网络请求模块的API(request/downloadFile)增加 网络性能相关(domainLookUpStart、domainLookUpEnd、connectStart、connectEnd等)的统计。

调研

我们先去调研了微信,而微信的实现情况如下:

Android

  • request 接口从客户端 7.0.10 版本开始使用 Chromium 内网络相关部分封装的底层组件 (cronet), 之前版本使用 HttpURLConnection 系统组件 (系统组件依赖系统实现会有平台兼容性问题, 我们建议用新版本微信来进行调试)
  • download 接口从客户端 7.0.12 版本开始使用 cronet 组件, 之前版本使用 HttpURLConnection 组件
  • upload 接口目前仍在使用 HttpURLConnection 组件
  • websocket 接口从客户端 7.0.4 版本开始使用微信底层组件 wcwss, 并在 7.0.10 版本优化了调用性能

iOS

  • request/download 接口从客户端 8.0.3 版本开始使用 cronet 组件, 之前版本使用 NSURLSession 系统组件
  • upload 接口目前仍在使用 NSURLSession 组件
  • websocket 接口从客户端 7.0.20 版本开始使用微信底层组件 wcwss, 之前版本使用 SRWebSocket 组件

cronet 是什么?

Cronet是Chromium网络引擎对不同操作系统做的封装,实现了移动端应用层、表示层、会话层协议,支持HTTP1/2、SPDY、QUIC、WebSocket、FTP、DNS、TLS等协议标准。支持Android、IOS、Chrome OS、Fuchsia,部分支持Linux、MacOS、Windows桌面操作系统。实现了Brotli数据压缩、预连接、DNS缓存、session复用等策略优化以及TCP fast open等系统优化。

更多cronet相关知识可查看如下文章:

Cronet网络库系列(一):用例与原理实现详解  zhuanlan.zhihu.com/p/93362473

集成参考:juejin.cn/post/727369…

可以直接使用google编译好的库:console.cloud.google.com/storage/bro…

cronet 编译成库之后,体积是多大呢?

2024-07-22 版本体积

ios_108.0.5325.0版本:纯真机的动态库是5.9M,静态库28M;  纯模拟器动态库6.4M

image.png

image.png

android_128.0.6610.0版本:25.4M

image.png

而我们SDK体积大小,对于客户来说非常敏感,这个体积大小完全无法集成。

那么有没有可能使用其他方式来实现呢?

AI 给出的方案

随后,我向Cursor 抛出问题?

1. 请帮我整理出 (https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html)
返回profile中所有的属性名。

2. iOS端如何统计这些属性?

3. Android端如何统计这些属性?

AI给出的回复:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
    NSURLSessionTaskTransactionMetrics *transactionMetrics = metrics.transactionMetrics.lastObject;
    
    NSMutableDictionary *profile = [NSMutableDictionary dictionary];
    
    // 重定向相关【iOS不支持】
    // profile[@"redirectStart"] = @([self timeIntervalSince1970:transactionMetrics.redirectStartDate]);
    // profile[@"redirectEnd"] = @([self timeIntervalSince1970:transactionMetrics.redirectEndDate]);
    
    // 获取资源相关
    profile[@"fetchStart"] = @([self timeIntervalSince1970:transactionMetrics.fetchStartDate]);
    
    // DNS查询相关
    profile[@"domainLookUpStart"] = @([self timeIntervalSince1970:transactionMetrics.domainLookupStartDate]);
    profile[@"domainLookUpEnd"] = @([self timeIntervalSince1970:transactionMetrics.domainLookupEndDate]);
    
    // 连接建立相关
    profile[@"connectStart"] = @([self timeIntervalSince1970:transactionMetrics.connectStartDate]);
    profile[@"connectEnd"] = @([self timeIntervalSince1970:transactionMetrics.connectEndDate]);
    
    // SSL连接相关
    profile[@"SSLconnectionStart"] = @([self timeIntervalSince1970:transactionMetrics.secureConnectionStartDate]);
    profile[@"SSLconnectionEnd"] = @([self timeIntervalSince1970:transactionMetrics.secureConnectionEndDate]);
    
    // 请求响应相关
    profile[@"requestStart"] = @([self timeIntervalSince1970:transactionMetrics.requestStartDate]);
    profile[@"requestEnd"] = @([self timeIntervalSince1970:transactionMetrics.requestEndDate]);
    profile[@"responseStart"] = @([self timeIntervalSince1970:transactionMetrics.responseStartDate]);
    profile[@"responseEnd"] = @([self timeIntervalSince1970:transactionMetrics.responseEndDate]);
    
    // RTT (Round Trip Time)
    NSTimeInterval rtt = [transactionMetrics.responseStartDate timeIntervalSinceDate:transactionMetrics.requestStartDate];
    profile[@"rtt"] = @(rtt * 1000); // 转换为毫秒
    
    // 网络类型估计 (这需要额外的逻辑或使用Reachability库)
    profile[@"estimate_nettype"] = @([self estimateNetworkType:rtt]);
    
    // HTTP RTT 估计
    profile[@"httpRttEstimate"] = @(rtt * 1000);
    
    // 传输层 RTT 估计 (这个值在iOS中难以准确获取,这里使用HTTP RTT作为近似)
    profile[@"transportRttEstimate"] = @(rtt * 1000);
    
    // 下行吞吐量估计 (这个值在iOS中难以准确获取,需要额外计算)
    profile[@"downstreamThroughputKbpsEstimate"] = @([self estimateThroughputWithTask:task metrics:metrics]);
    
    // 实际吞吐量 (与估计值相同,因为我们没有更精确的方法)
    profile[@"throughputKbps"] = profile[@"downstreamThroughputKbpsEstimate"];
    
    // 对等IP (在iOS中无法直接获取)
    profile[@"peerIP"] = transactionMetrics.remoteAddress ?: @"";
    
    // 端口 (在iOS中无法直接获取)
    profile[@"port"] = @(0);
    
    // 是否复用连接
    profile[@"socketReused"] = @(transactionMetrics.isReusedConnection);
    
    // 发送和接收的字节数
    profile[@"sendBytesCount"] = @(task.countOfBytesSent);
    profile[@"receivedBytedCount"] = @(task.countOfBytesReceived);
    
    // 协议
    profile[@"protocol"] = transactionMetrics.networkProtocolName ?: @"unknown";
    
    // 是否使用高性能模式 (iOS中没有直接对应的概念)
    profile[@"usingHighPerformanceMode"] = @NO;
    
    NSLog(@"Request Profile: %@", profile);
}

- (NSTimeInterval)timeIntervalSince1970:(NSDate *)date {
    return date ? [date timeIntervalSince1970] * 1000 : 0;
}

- (NSInteger)estimateNetworkType:(NSTimeInterval)rtt {
    if (rtt < 0.05) return 5; // 4G
    if (rtt < 0.1) return 4;  // 3G
    if (rtt < 0.3) return 3;  // 2G
    return 2; // Slow 2G
}

- (double)estimateThroughputWithTask:(NSURLSessionTask *)task metrics:(NSURLSessionTaskMetrics *)metrics {
    NSTimeInterval duration = [metrics.taskInterval duration];
    if (duration > 0) {
        return (task.countOfBytesReceived * 8.0) / (1024.0 * duration);
    }
    return 0;
}

这个实现尽可能地模拟了微信小程序 wx.request 的 profile 对象中的字段。但是有一些限制和注意事项:

  • 某些字段(如 peerIP 和 port)在 iOS 中无法直接获取。
  • 网络类型估计(estimate_nettype)是基于 RTT 的简化估计,实际上需要更复杂的逻辑或使用 Reachability 库。
  • 吞吐量估计是基于接收到的数据量和时间计算的,可能不如设备 API 提供的信息准确。
  • usingHighPerformanceMode 在 iOS 中没有直接对应的概念。
  • 所有时间戳都转换为毫秒级别以匹配微信 API 的格式。
  • 某些字段(如 transportRttEstimate)在 iOS 中难以准确获取,所以使用了近似值。

这样功能已经实现了七八成,再对一些属性的取值做一些优化就差不多了。

而Android 下实现,Cursor 同样给出的方案:

import okhttp3.*
import okhttp3.EventListener
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong

class DetailedPerformanceEventListener : EventListener() {
    private val timeStamps = mutableMapOf<String, Long>()
    private var startNanos: Long = 0
    private var dnsStartNanos: Long = 0
    private var connectStartNanos: Long = 0
    private var secureConnectStartNanos: Long = 0
    private var requestStartNanos: Long = 0
    private var responseStartNanos: Long = 0
    private var responseEndNanos: Long = 0
    private var peerAddress: InetSocketAddress? = null
    private var protocol: Protocol? = null
    private val sendBytesCount = AtomicLong(0)
    private val receivedBytesCount = AtomicLong(0)
    private var socketReused = false

    override fun callStart(call: Call) {
        startNanos = System.nanoTime()
        timeStamps["fetchStart"] = startNanos
    }

    override fun dnsStart(call: Call, domainName: String) {
        dnsStartNanos = System.nanoTime()
        timeStamps["domainLookUpStart"] = dnsStartNanos
    }

    override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
        timeStamps["domainLookUpEnd"] = System.nanoTime()
    }

    override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
        connectStartNanos = System.nanoTime()
        timeStamps["connectStart"] = connectStartNanos
        peerAddress = inetSocketAddress
    }

    override fun secureConnectStart(call: Call) {
        secureConnectStartNanos = System.nanoTime()
        timeStamps["SSLconnectionStart"] = secureConnectStartNanos
    }

    override fun secureConnectEnd(call: Call, handshake: Handshake?) {
        timeStamps["SSLconnectionEnd"] = System.nanoTime()
    }

    override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
        timeStamps["connectEnd"] = System.nanoTime()
        this.protocol = protocol
    }

    override fun requestHeadersStart(call: Call) {
        requestStartNanos = System.nanoTime()
        timeStamps["requestStart"] = requestStartNanos
    }

    override fun requestHeadersEnd(call: Call, request: Request) {
        // 请求头结束
    }

    override fun requestBodyStart(call: Call) {
        // 请求体开始
    }

    override fun requestBodyEnd(call: Call, byteCount: Long) {
        sendBytesCount.addAndGet(byteCount)
        timeStamps["requestEnd"] = System.nanoTime()
    }

    override fun responseHeadersStart(call: Call) {
        responseStartNanos = System.nanoTime()
        timeStamps["responseStart"] = responseStartNanos
    }

    override fun responseHeadersEnd(call: Call, response: Response) {
        // 响应头结束
    }

    override fun responseBodyStart(call: Call) {
        // 响应体开始
    }

    override fun responseBodyEnd(call: Call, byteCount: Long) {
        receivedBytesCount.addAndGet(byteCount)
        responseEndNanos = System.nanoTime()
        timeStamps["responseEnd"] = responseEndNanos
    }

    override fun callEnd(call: Call) {
        val profile = generateProfile()
        println("Request Profile: $profile")
    }

    private fun generateProfile(): Map<String, Any> {
        val rtt = calculateRTT()
        return mapOf(
            "redirectStart" to 0, // OkHttp自动处理重定向,无法直接获取
            "redirectEnd" to 0,
            "fetchStart" to (timeStamps["fetchStart"] ?: 0) / 1_000_000,
            "domainLookUpStart" to (timeStamps["domainLookUpStart"] ?: timeStamps["fetchStart"] ?: 0) / 1_000_000,
            "domainLookUpEnd" to (timeStamps["domainLookUpEnd"] ?: timeStamps["domainLookUpStart"] ?: 0) / 1_000_000,
            "connectStart" to (timeStamps["connectStart"] ?: 0) / 1_000_000,
            "connectEnd" to (timeStamps["connectEnd"] ?: 0) / 1_000_000,
            "SSLconnectionStart" to (timeStamps["SSLconnectionStart"] ?: 0) / 1_000_000,
            "SSLconnectionEnd" to (timeStamps["SSLconnectionEnd"] ?: 0) / 1_000_000,
            "requestStart" to (timeStamps["requestStart"] ?: 0) / 1_000_000,
            "requestEnd" to (timeStamps["requestEnd"] ?: 0) / 1_000_000,
            "responseStart" to (timeStamps["responseStart"] ?: 0) / 1_000_000,
            "responseEnd" to (timeStamps["responseEnd"] ?: 0) / 1_000_000,
            "rtt" to rtt,
            "estimate_nettype" to estimateNetworkType(rtt),
            "httpRttEstimate" to rtt, // 简化估计
            "transportRttEstimate" to rtt, // 简化估计
            "downstreamThroughputKbpsEstimate" to estimateThroughput(),
            "throughputKbps" to calculateActualThroughput(),
            "peerIP" to (peerAddress?.address?.hostAddress ?: ""),
            "port" to (peerAddress?.port ?: 0),
            "socketReused" to socketReused,
            "sendBytesCount" to sendBytesCount.get(),
            "receivedBytedCount" to receivedBytesCount.get(),
            "protocol" to (protocol?.toString() ?: "unknown"),
            "usingHighPerformanceMode" to false // OkHttp没有直接对应的概念
        )
    }

    private fun calculateRTT(): Long {
        return (responseStartNanos - requestStartNanos) / 1_000_000
    }

    private fun estimateNetworkType(rtt: Long): Int {
        // 简化的网络类型估计
        return when {
            rtt < 50 -> 5 // 4G
            rtt < 100 -> 4 // 3G
            rtt < 300 -> 3 // 2G
            else -> 2 // Slow 2G
        }
    }

    private fun estimateThroughput(): Long {
        val duration = (responseEndNanos - responseStartNanos) / 1_000_000_000.0
        return (receivedBytesCount.get() * 8 / 1024 / duration).toLong()
    }

    private fun calculateActualThroughput(): Long {
        val duration = (responseEndNanos - requestStartNanos) / 1_000_000_000.0
        return ((sendBytesCount.get() + receivedBytesCount.get()) * 8 / 1024 / duration).toLong()
    }
}

class PerformanceInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = chain.proceed(request)
        
        // 这里可以访问一些OkHttp内部信息
        val connection = chain.connection()
        if (connection != null) {
            println("Socket reused: ${connection.socket().isReused}")
        }

        return response
    }
}

// 使用示例
fun main() {
    val client = OkHttpClient.Builder()
        .addInterceptor(PerformanceInterceptor())
        .eventListener(DetailedPerformanceEventListener())
        .build()

    val request = Request.Builder()
        .url("https://api.github.com/users/octocat")
        .build()

    client.newCall(request).execute().use { response ->
        println("Response code: ${response.code}")
    }
}

这个示例尽可能模拟了微信小程序 wx.request 的 profile 对象中的字段。但是有一些限制和注意事项:

  • 某些字段(如 redirectStart 和 redirectEnd)在 OkHttp 中难以直接获取,因为 OkHttp 自动处理重定向。
  • 网络类型估计(estimate_nettype)是基于 RTT 的简化估计,实际上需要更复杂的逻辑或依赖设备 API。
  • 吞吐量估计是基于接收到的数据量和时间计算的,可能不如设备 API 提供的信息准确。
  • socketReused 信息需要通过 Interceptor 获取,因为 EventListener 没有直接提供这个信息。
  • 某些字段(如 usingHighPerformanceMode)在 OkHttp 中没有直接对应的概念。
  • 时间戳都转换为毫秒级别以匹配微信 API 的格式。
  • 要使用这个性能监控系统,你需要在创建 OkHttpClient 时添加 DetailedPerformanceEventListener 和 PerformanceInterceptor。这将为每个请求生成详细的性能报告。
  • 请注意,这种详细的性能监控可能会对应用程序的性能产生轻微影响,建议仅在调试或性能分析阶段使用。

我们优化一下部分属性的取值、存储、以及根据request的参数控制是否开启,做一些优化即可。

后续

我们基于AI给出的方案,做了调整比如网络类型的取值、一些阶段的起止时间,kps的计算等等。实际上花费了大概两天时间就把功能实现了,后续做了详细的测试,解决了一些特殊场景问题、参数配置因素,也在一个迭代内就完成了。

总结

其实,该功能在23年我们预估基于cronet实现可能要花好几周,而且体积还很大,基本是得放弃。 但是今年的公司提倡使用AI提升效率后,使用AI做了一次咨询,最终只花了两天就把主题功能实现了。

FinClip 是一个小程序容器SDK,我们已经推出了iOS、Android、鸿蒙、MacOS、Windows、Flutter、React Native等多个版本的SDK,是让移动 App 或跨端设备运行小程序最快,最简单的方法。