坎坷之下出新招:记一次应用带宽峰值测试的探索历程

测试前

最近在做一场流量性能测试,期望得到的结果,既不是应用关键场景需要使用的流量总量,也不是应用跑起来后的平均带宽值。而是一个叫带宽峰值的玩意儿,它代表一段时间内,这个应用1s内最高会收发多少数据。有什么特别的呢?

流量测试一般的测试方法:定义关键场景,关键场景前流量值– 场景后流量值= 流量总量。流量总量/时长 = 平均带宽。

测试手段:

  1. 流量卡。主要是记录流量起始点流量卡的流量值,和结束点流量卡的流量值。
  2. GT工具或Emmagee工具。都是每秒都在采样带宽值。
  3. DDMS。需要被测应用是debug包。

在接到需求后,认真分析了上面的测试方法和测试手段,发现只有GT或Emmagee基本能够满足,因为带宽峰值的含义就是一段时间内,带宽值曲线上的最高点。

开始撞墙

一开始我是很信赖GT的,毕竟是我大腾讯同事出品。直到测试开始,它给了我类似以下两组数据(一是负数,二是带宽一点点增加)。

GT输出的带宽测试报告

现有的工具无法满足需求,只能自己动手,丰衣足食了。首先理清楚我们可以从哪些地方拿到实时的流量数据:

  1. 从系统文件(/proc/net/xt_qtaguid/stats)中,可以很方便的拿出每个uid的流量数据,如下图,从0开始,第3列是uid,第五列是接收数据的bytes数,第七列是发送数据的bytes数。

Proc文件中的流量数据

  1. android.net.TrafficStats这个类中提供了一大堆方法用于获取流量数据的方法。其中主要包括:

a) getUidRxBytes(int uid)和getUidTxBytes(int uid);

b) getTotalRxBytes()和getTotalTxBytes();

c) getRxBytes(String iface)和getTxBytes(String iface);

第一次撞墙

需要准确获取一个应用的实时带宽,从字面意义上看,似乎选择以上第1点中的方法,或者第2点中的a更加合适,一个文件读取,一个系统接口获取,都是直接取出了对应uid的流量数据。于是根据1,我迅速的码出了如下代码:

static FileReader mReader = null;
static BufferedReader mBufReader = null;
public static long getTrafficInfo1(int uid) {
    String fileName = "/proc/net/xt_qtaguid/stats";
    long ret = 0;
        try{
mReader = new FileReader(fileName);
            mBufReader = new BufferedReader(mReader);
            String line = null;
            int count = 0;
            while ( (line = mBufReader.readLine()) != null){
                count++;
                if(count == 1 ) continue;
                String[] vecTemp = line.split("[:\\s]+");
                if(vecTemp[1].equals("wlan0") && Integer.valueOf(vecTemp[3]) == uid){
                    ret += Long.valueOf(vecTemp[5]) + Long.valueOf(vecTemp[7]);
                }
            }
        }catch (IOException e){
            Log.e(LOG_TAG,e.getMessage());
        }finally {
try {
if (mBufReader != null) {
mBufReader.close();
                }
if (mReader != null) {
mReader.close();
                }
            }catch (IOException e){
                Log.e(LOG_TAG,e.getMessage());
            }
        }
return ret;
}

然后悲催的发现,虽然通过命令:

adb shell cat /proc/net/xt_qtaguid/stats

能够看到一行行漂亮的数据,在应用程序中却始终都只能读取到title行。这个地方暂时没找到为什么,大概怀疑点是Android权限的问题。

当然我们可以写个运行在PC端的脚本程序,然后adb连接着被测手机,cat出该文件然后再分析,然而带宽峰值的测试,依赖于大样本量,连着ADB跑,这个就局限了,只能我一个人测试,想找其他人帮忙太麻烦。

第二次撞墙

第一种方法不行,马上换。TrafficStats中的getUidRxBytes(int uid)和getUidTxBytes(int uid)这两个接口是否可行呢?看了Emmagee的源码,关于流量的测试,它还真是用这两个接口实现的。

public static long getTrafficInfo(int uid) {
    Log.i(LOG_TAG, "get traffic information");
    long rcvTraffic = UNSUPPORTED;
    long sndTraffic = UNSUPPORTED;
    // Use getUidRxBytes and getUidTxBytes to get network traffic,these API
    // return both tcp and udp usage
    rcvTraffic = TrafficStats.getUidRxBytes(uid);
    sndTraffic = TrafficStats.getUidTxBytes(uid);
    if (rcvTraffic == UNSUPPORTED || sndTraffic == UNSUPPORTED) {
return UNSUPPORTED;
    } else
        return rcvTraffic + sndTraffic;
}

Copy好代码,然后冒出一个问号,为什么要特意标记一个UNSUPPORTED呢?瞄一眼源码,从注释和代码中可以看出,4.3以下这个接口是没有的;7.0及以上,这个接口只能用来拿应用本身的Traffic数据;要拿其他人的?详情请看NetworkStatsManager。

/**
 * Return number of bytes transmitted by the given UID since device boot.
 * Counts packets across all network interfaces, and always increases
 * monotonically since device boot. Statistics are measured at the network
 * layer, so they include both TCP and UDP usage.
 * <p>
 * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
 * return {@link #UNSUPPORTED} on devices where statistics aren't available.
 * <p>
 * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
 * report traffic statistics for the calling UID. It will return
 * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
 * historical network statistics belonging to other UIDs, use
 * {@link NetworkStatsManager}.
 *
 * @see android.os.Process#myUid()
 * @see android.content.pm.ApplicationInfo#uid
 */
public static long getUidTxBytes(int uid) {
// This isn't actually enforcing any security; it just returns the
    // unsupported value. The real filtering is done at the kernel level.
    final int callingUid = android.os.Process.myUid();
    if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
return nativeGetUidStat(uid, TYPE_TX_BYTES);
    } else {
return UNSUPPORTED;
    }
}

看来这是Android版本碎片化带来的坑,瞬间体会到了开发同学做兼容适配的痛苦。

最终方案

由于项目急需用了,最终决定不再纠结于用哪个API。直接使用以下两种方法来做,只是在测试前先杀掉其他应用,如果有手机管家的话,开启管家的禁止联网功能,保证只有被测应用在联网:

  1. getTotalRxBytes()和getTotalTxBytes();
  2. getRxBytes(String iface)和getTxBytes(String iface);

具体代码实现:

第一个比较简单,直接调用即可。第二个因为getRxBytes(String iface)是隐藏方法,所以需要通过反射拿到该方法进行invoke。

static {
try {
        Class<?> claxx = Class.forName("android.net.TrafficStats");
        mgetRxBytesMethod = claxx.getDeclaredMethod("getRxBytes", String.class);
        mgetTxBytesMethod = claxx.getDeclaredMethod("getTxBytes", String.class);
        mgetRxBytesMethod.setAccessible(true);
        mgetTxBytesMethod.setAccessible(true);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static long getRxBytes(String iface){
long ret = -1;
    try {
        ret = Long.valueOf(mgetRxBytesMethod.invoke(null,"wlan0").toString());
    }catch (Exception e){
        e.printStackTrace();
    }
return ret;
}

public static long getTxBytes(String iface){
long ret = -1;
    try {
        ret = Long.valueOf(mgetTxBytesMethod.invoke(null,"wlan0").toString());
    }catch (Exception e){
        e.printStackTrace();
    }
return ret;
}

计算逻辑:

class CountRunnable implements Runnable {
@Override
    public void run() {
        String dataSavePath = DataSaveHandler.getFilePath("band");
        //通知界面刷新
        notifyStart(dataSavePath);

        long current = 0;
        long last = 0;
        long lastSnd = 0;  //上次发
        long lastRcv = 0;  //上次收
        long currentSnd = 0;
        long currentRcv = 0;
        int count = 0;
        while (!isGoingToStop) {
            count++;
            //获取所有网卡的流量
            // currentSnd = TrafficStats.getTotalTxBytes();
            // currentRcv = TrafficStats.getTotalRxBytes();
            //获取wifi网卡上的流量
            currentSnd = TrafficInfo.getRxBytes("wlan0");
            currentRcv = TrafficInfo.getTxBytes("wlan0");
            current = System.currentTimeMillis();
            SystemClock.sleep(100);
            if (count == 1) {
                last = current;
                lastSnd = currentSnd;
                lastRcv = currentRcv;
                continue;  //首次只收集current数据
            }
if (current - last < 1000) {  continue; }
            last = current;
            long sndBand = currentSnd - lastSnd;
            long rcvBand = currentRcv - lastRcv;
            //每次都刷新并保存该文件,能够保证即使不停止,也会有数据
            DataSaveHandler.openFileWriter(dataSavePath);
            float bandInKB = ((float) (sndBand + rcvBand))/1000;
            DecimalFormat df = new DecimalFormat();
            df.applyPattern("0.000");
            DataSaveHandler.writeNewLine(mLogSdf.format(current) + "," + sndBand + "," + rcvBand+","+df.format(bandInKB));
            DataSaveHandler.saveAndCloseFile();

            lastSnd = currentSnd;
            lastRcv = currentRcv;
        }
//通知界面刷新
        notifyStop(dataSavePath);
    }
}

将代码输出为一个应用,实时收集流量数据,写到csv文件中。将这个测试APP发给大家。测试步骤:

  1. 将被测应用外其他应用关闭;
  2. 开启被测应用,进入关键场景;
  3. 打开测试APP,开始收集带宽数据;
  4. 关键场景结束时,打开测试APP,点击结束;
  5. 将结果adb pull出来发给测试;
  6. 分析取带宽峰值平均值。

这个方法虽然没有直接获取被测APP的流量,但是简单快捷,Android各个版本上的兼容性高,能够快速收集到较多的样本。当然也可以再结合NetworkStatsManager来做兼容适配。

参考文献

http://blog.csdn.net/w7849516230/article/details/71705835

文章来源于:腾讯移动品质中心 TMQ

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码字搬砖

spark连接kafka工具类

版权声明:本文为博主原创,欢迎转载,转载请标明出处 Blog Address:http://blog.csdn.net/jsjsjs1789 ...

451
来自专栏Leetcode名企之路

【工程】在线诊断系统设计与实现

本文分享一些在线问题诊断的经验,主要是业务层面,服务层面的在线问题诊断一般需要依赖服务监控系统和报警系统来辅助定位问题。

602
来自专栏kl的专栏

Activiti工作流杂谈-工作机制详解

以下内容为博主在公司内部分享的一个小文档,谈到了activiti的各方面内容,有需要的可留言要word原文件 

5219
来自专栏恰同学骚年

.NET Core微服务之服务间的调用方式(REST and RPC)

  微服务之间的接口调用通常包含两个部分,序列化和通信协议。常见的序列化协议包括json、xml、hession、protobuf、thrift、text、by...

1076
来自专栏岑玉海

Spark源码系列(六)Shuffle的过程解析

Spark大会上,所有的演讲嘉宾都认为shuffle是最影响性能的地方,但是又无可奈何。之前去百度面试hadoop的时候,也被问到了这个问题,直接回答了不知道。...

3706
来自专栏点滴积累

geotrellis使用(三十八)COG 写入和读取

前言 上一篇中简单介绍了 COG 的概念和 Geotrellis 中引入 COG 的原因及简单的原理,本文为大家介绍如何在 Geotrellis 中使用 COG...

2015
来自专栏斑斓

Redux框架reducer对状态的处理

前言 在react+redux项目里,关于reducer处理state的方式,在redux官方文档中有这样一段描述: 不要修改 state。 使用 Objec...

3515
来自专栏跟着阿笨一起玩NET

《我的WCF之旅》博文系列汇总

WCF是构建和运行互联系统的一系列技术的总称,它是建立在Web Service架构上的一个全新的通信平台。你可以把它看成是.NET平台上的新一代的Web Ser...

371
来自专栏点滴积累

geotrellis使用(三十六)瓦片入库更新图层

前言 Geotrellis 是针对大数据量栅格数据进行分布式空间计算的框架,这一点毋庸置疑,并且无论采取何种操作,其实都是先将大块的数据切割成一定大小的小数据(...

3138
来自专栏.NET技术

winserver的consul部署实践与.net core客户端使用(附demo源码)

随着微服务兴起,服务的管理显得极其重要。都知道微服务就是”拆“,把臃肿的单块应用,拆分成多个轻量级的服务,每个服务可以在短周期内重构、迭代、交付。随着微服务的数...

552

扫码关注云+社区