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

测试前

最近在做一场流量性能测试,期望得到的结果,既不是应用关键场景需要使用的流量总量,也不是应用跑起来后的平均带宽值。而是一个叫带宽峰值的玩意儿,它代表一段时间内,这个应用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

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

如有侵权,请联系 zhuanlan_guanli@qq.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏码神联盟

碎片化 | 第四阶段-48-hibernate概述和配置-视频

本套视频从Java基础到架构模式以及AI算法,整体视频以“碎片化”学习的模式,提供给大家 ,并配备实际项目为案例,让大家在坐车、吃饭、午休、蹲坑的时候,都可以学...

3146
来自专栏一个会写诗的程序员的博客

《MongoDB极简教程》第一章 NoSQL简史 & MongoDB安装&环境配置NoSQLNoSQL 简史CAP定理(CAP theorem)BASEMongoDB 特性&优势文档参考安装&环境配置

MongoDB 是一款开源的文档数据库,并且是业内领先的 NoSQL 数据库,用 C++ 编写而成。

583
来自专栏向治洪

android获取设备唯一标示

概述 有时需要对用户设备进行标识,所以希望能够得到一个稳定可靠并且唯一的识别码。虽然Android系统中提供了这样设备识别码,但是由于Android系统版本、...

6286
来自专栏程序你好

一个微服务架构的简单示例

903
来自专栏PingCAP的专栏

三篇文章了解 TiDB 技术内幕:谈调度

前两篇文章介绍了 TiKV、TiDB 的基本概念以及一些核心功能的实现原理,这两个组件一个负责 KV 存储,一个负责 SQL 引擎。本篇文章将介绍一下 PD(P...

2030
来自专栏逸鹏说道

前后端分离了,然后呢?

  前言   前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离,并不是传统行业中的按部门划分,一部分人纯做前端(HTML/CSS/JavaSc...

2767
来自专栏24K纯开源

DirectShow Filter的开发实践

一、介绍      摄像头图像采集处理在业界有着多种成熟的方案。从老的DirectShow、Grabber技术,到新的Windows Media Foundat...

2118
来自专栏码农笔录

微信扫码支付、网站接入微信支付-Java

1232
来自专栏phodal

后端技能清单(草稿)

昨天也顺手整理了一下我所需要的后端技能清单。不过,由于我离非常有经验的后端开发者有点距离,希望大家可以给点意见哈。 入门 HTML / CSS 编程语言:Ja...

1855
来自专栏haifeiWu与他朋友们的专栏

Netty实战之第一个应用

作为一个正在Java路上摸爬滚打的小菜鸡,之前在项目中也用过Netty,也因为Netty报名阿里的中间件大赛,但终究功力太浅,最终不了了之,最近工作中又遇到了N...

572

扫描关注云+社区