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

测试前

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

相关文章

来自专栏海说

深入理解计算机系统(3.1)---走进汇编的世界

  本系列拖了蛮久了,主要是因为LZ写的时候其实刚看到第二章,因此这一段时间快速看了下第三章,并花了点时间沉淀了一下,这才耽误了下来。

823
来自专栏从流域到海域

《笨办法学Python》 第26课手记

《笨办法学Python》 第26课手记 本节课的任务是找出代码中的错误,所以就不贴结果了。大家认真找,尝试完全靠自己来修正代码中的错误,如果实在找不到,就休息一...

2008
来自专栏Flutter入门到实战

那些年遇到的后台返回的奇葩json数据

然而:错误数据返回null不说,错误信息居然返回一个一个url?就这么一点错误信息,还要我再去请求一次服务器获取这个错误信息吗。。 服务器流量不要钱的吧。。。...

4092
来自专栏极乐技术社区

微信小游戏初体验

前言 上周【跳一跳】小游戏刷遍了朋友圈,也代表了微信小程序拥有了搭载游戏的功能(早该往这方面发展了,这才是应该有的形态嘛)。作为一个前端er,我的大刀早已经饥渴...

1.2K7
来自专栏小樱的经验随笔

CTF---Web入门第十题 Once More

Once More分值:10 来源: iFurySt 难度:易 参与人数:4782人 Get Flag:2123人 答题人数:2166人 解题通过率:98%...

2936
来自专栏Java3y

纳税服务系统七(投诉管理模块)【显示投诉信息、处理回复、我要投诉、Quartz自动受理、统计图FusionCharts】

投诉受理管理模块 接下来,就是来开发我们的投诉受理管理模块了…..我们来看看原型图与需求吧: 查询用户提交的投诉信息,可以根据投诉部门(部门A/B)、投诉时间段...

5707
来自专栏逻辑熊猫带你玩Python

Python | “万年历——日期查询”

这是一个简单小程序,从这个程序说明,对于编程而言,有一定的数学基础是比较重要的,除此之外锻炼逻辑思维能力可以提高编程能力。

2581
来自专栏平凡文摘

成为java高级程序员需要掌握哪些

1323
来自专栏互联网杂技

如何去了解JavaScript引擎的工作原理

1. 什么是JavaScript解析引擎? 简单地说,JavaScript解析引擎就是能够“读懂”JavaScript代码,并准确地给出代码运行结果的一段程序。...

3717
来自专栏ImportSource

设计模式-搞个接口,留有余地,让你我不再尴尬

设计模式,Design Patterns,Pattern,翻译为“模式”总感觉不够接地气,用今天的话来说可以叫“套路”。设计模式就是写代码的过程中一些常规打法和...

36011

扫码关注云+社区