基于方法耗时统计等场景实现的耗时统计小工具

说到方法耗时统计、性能调试等,开发中经常都会去做,但我们在做这个事情的过程中,似乎很多人都用的很暴力的一种方式去做。哪里需要统计耗时,就直接使用时间戳变量,在最后计算结果,比如下面这样:

private static void work() {
        long time1 = System.currentTimeMillis();
        workA();
        long time2 = System.currentTimeMillis();
        workB();
        long time3 = System.currentTimeMillis();
        workC();
        long time4 = System.currentTimeMillis();
        workEnd();
        long time5 = System.currentTimeMillis();

        long aRunTime = time2 - time1;
        long bRunTime = time3 - time2;
        long cRunTime = time4 - time3;
        long totalRunTime = time5 - time1;
        
        // 统计A、B、C分别耗时和总耗时
        System.out.println("aRunTime:" + aRunTime + " bRunTime:" + bRunTime + " cRunTime:" + cRunTime + " totalRunTime:" + totalRunTime);
    }

有没有点似曾相识?这种做法我们同事都经常干,哈哈哈,简单的写两句图省事,但是往往不省事~唉。

在一个方法内我们可以写几个局部变量统计,多个方法我们写几个成员变量统计,不在一个类,页面跳转之类的又怎么办呢?写几个全局静态变量来统计么?

我个人是不太喜欢这样的写法,感觉会越写越乱,而且变得很麻烦、复杂,也不好控制和查看,所以最后写个简单方便的小工具。

使用效果图如下:

解释一波

  • 第一行包含标签,表示开始。
  • 最后一行统计总耗时,以及添加记录的次数,表示结束。
  • 中间部分就是具体的每次记录。举例:1492 ms message: 执行A。
    • 1492 ms(毫秒)是相对于前一条记录的耗时。
    • “message: 执行A” 中的 “执行A” 是任意添加的消息内容。
    • 任务A执行耗时1492毫秒,任务B执行耗时0毫秒(耗时太少,毫秒单位统计不出来)。
    • 任意两个记录间的耗时可以相加,得到间隔的耗时,比如:从开始到任务C的耗时 = 1492 + 2004 = 3496ms。

代码实现

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 一个实用程序类,用来帮助记录在方法调用中的耗时。
 * 用法如下:
 *
 * <pre>
 *     TimingRecorder.addRecord(label, "work");
 *     // ... do some work A ...
 *     TimingRecorder.addRecord(label, "work A");
 *     // ... do some work B ...
 *     TimingRecorder.addRecord(label, "work B");
 *     // ... do some work C ...
 *     TimingRecorder.addRecord(label, "work C");
 *     TimingRecorder.logTime(label);
 * </pre>
 *
 * <p>logTime调用会后输出示例如下:</p>
 *
 * <pre>
 *     标签: label <开始>
 *        0 ms  message: 开始
 *     1501 ms  message: 执行A
 *      302 ms  message: 执行B
 *     2002 ms  message: 执行完成~
 *     <结束>   总耗时: 3805 ms   记录次数: 4 次
 * </pre>
 *
 * @author LiuFeng
 * @date 2020-10-21
 */
public class TimingRecorder {
    private static String TAG = "TimingRecorder";

    // 是否调用打印
    private static boolean isToLog = true;

    // 单个标签可以添加记录的最大数量
    private static final int RECORD_MAX_NUM = 1000;

    // 记录储存容器
    private static final ConcurrentHashMap<String, List<RecordTime>> recordMap = new ConcurrentHashMap<>();

    /**
     * 添加记录
     *
     * @param label   记录的标签
     * @param message 记录的日志内容。
     */
    public static void addRecord(String label, String message) {
        // 时间记录
        RecordTime recordTime = new RecordTime();
        // Android系统可以使用SystemClock.elapsedRealtime()
        // 代替System.nanoTime() / 1000_000,作用相同,这里是考虑到Java里使用
        recordTime.time = System.nanoTime() / 1000_000;
        recordTime.message = message;

        // 处理存入记录
        handleRecordTimes(label, recordTime);
    }

    /**
     * 处理记录数据集合(数据过多时将释放)
     *
     * @param label
     * @param recordTime
     */
    private static synchronized void handleRecordTimes(String label, RecordTime recordTime) {
        // 取出记录
        List<RecordTime> recordTimes = getRecordTimes(label);

        // 超过记录容量时,调用打印释放,目的避免有未打印记录占用过多内存
        if (recordTimes.size() >= RECORD_MAX_NUM) {
            String desc = label + ": Add too many records! will invoke logTime.";
            println(desc);

            // 保留第一条,是为了释放打印后,新的记录中能计算到从第一条开始的耗时
            RecordTime firstRecordTime = recordTimes.get(0);

            // 执行打印记录,并清除标签
            logTime(label, desc);

            // 存入第一条数据到新容器
            recordTimes = getRecordTimes(label);
            recordTimes.add(firstRecordTime);
        }

        // 存入新记录
        recordTimes.add(recordTime);
    }

    /**
     * 获取标签对应容器
     *
     * @param label
     * @return
     */
    private static List<RecordTime> getRecordTimes(String label) {
        List<RecordTime> recordTimes = recordMap.get(label);

        // 创建容器
        if (recordTimes == null) {
            recordTimes = new ArrayList<>();
            List<RecordTime> tempRecords = recordMap.putIfAbsent(label, recordTimes);
            if (tempRecords != null) {
                recordTimes = tempRecords;
            }
        }
        return recordTimes;
    }

    /**
     * 打印记录时间
     *
     * @param label 记录的标签
     * @return
     */
    public static String logTime(String label) {
        return logTime(label, null);
    }

    /**
     * 打印记录时间
     *
     * @param label 记录的标签
     * @param msg   追加的日志内容。
     * @return
     */
    public static String logTime(String label, String msg) {
        List<RecordTime> recordTimes;

        // 加锁保证添加数据和移除数据操作安全
        synchronized (TimingRecorder.class) {
            // 取出记录,清除标记
            recordTimes = recordMap.remove(label);
        }

        // 在记录容量内,追加一条表示结束的记录
        if (recordTimes != null && recordTimes.size() < RECORD_MAX_NUM) {
            RecordTime recordTime = new RecordTime();
            recordTime.time = System.nanoTime() / 1000_000;
            recordTime.message = msg != null ? msg : "log time end.";
            recordTimes.add(recordTime);
        }

        // 解析记录
        String record = parseRecord(label, recordTimes);

        // 打印
        if (isToLog) {
            println(record);
        }

        return record;
    }

    /**
     * 解析记录数据
     *
     * @param label
     * @param recordTimes
     * @return
     */
    private static String parseRecord(String label, List<RecordTime> recordTimes) {
        // 组装数据
        StringBuilder buffer = new StringBuilder();
        buffer.append("\n标签: ").append(label);

        // 无记录处理
        if (recordTimes == null || recordTimes.isEmpty()) {
            buffer.append(" <没有记录>");
            return buffer.toString();
        }

        buffer.append(" <开始>\n");

        int size = recordTimes.size();

        // 第一次记录时间
        long firstTime = recordTimes.get(0).time;

        // 最后一次记录时间
        long lastTime = recordTimes.get(size - 1).time;

        // 总耗时
        long totalTime = lastTime - firstTime;

        // 计算耗时的前一个时间
        long prevTime = firstTime;

        // 取总耗时位数长度,记作最大的位长,用于空格缩进
        int maxDigitLength = String.valueOf(totalTime).length();

        // 取出数据计算差值并拼接
        for (int i = 0; i < size; i++) {
            RecordTime record = recordTimes.get(i);
            long spaceTime = record.time - prevTime;
            prevTime = record.time;

            // 当前耗时位长
            int currentDigitLength = String.valueOf(spaceTime).length();

            // 缩进空格字符(目的是对齐易看)
            String indentSpaceStr = getSpaceStr(maxDigitLength - currentDigitLength);

            // 拼接
            buffer.append(indentSpaceStr);
            buffer.append(spaceTime).append(" ms");
            buffer.append("  message: ").append(record.message);
            buffer.append("\n");
        }

        buffer.append("<结束>");
        buffer.append("   总耗时: ").append(totalTime).append(" ms");
        buffer.append("   记录次数: ").append(size).append(" 次");


        return buffer.toString();
    }

    /**
     * 记录时间的model
     */
    private static class RecordTime {
        long time;
        String message;
    }

    /**
     * 生成指定数量空格字符串
     *
     * @param spaces
     * @return
     */
    private static String getSpaceStr(int spaces) {
        String number = String.valueOf(spaces <= 0 ? "" : spaces);
        return String.format("%" + number + "s", "");
    }

    /**
     * 调用日志打印
     *
     * @param msg
     */
    private static void println(String msg) {
        // Java里用System.out,Android最好是用Log
        System.out.println(msg);
        //Log.println(Log.INFO, TAG, msg);
    }
}

使用方式

// 执行任务前,调用addRecord,添加记录
TimingRecorder.addRecord("label", "work start");

// ... do some work A ...
TimingRecorder.addRecord("label", "work A");

// ... do some work B ...
TimingRecorder.addRecord("label", "work B");

// ... do some work C ...
TimingRecorder.addRecord("label", "work C");

// 执行结束,调用logTime,打印时间统计
TimingRecorder.logTime("label", "work end");

达到的目的?

  1. 简单、易用,不用手动写时间戳再计算吧。
  2. 可以统计很多方法函数的耗时,想统计几个写几个。
  3. 以标签作为区别不同统计耗时的唯一键,好理解,可跨不同方法、不同类添加记录。
  4. 一个统计记录在一条日志里,美观好看,不用到处查找其他记录。

注意事项

  1. isToLog控制日志打印开关,正式环境不需要打印就置false关掉。
  2. logTime()方法返回统计记录的字符串,可以拿到数据丢到本地文件等。
  3. 单个标签可以添加记录的最大数量是1000条,但不是说超过就不能用了,为了减少占内存,会先打印清空释放,和后续的统计数据可以串起来的。
  4. 代码里记录时间用的System.nanoTime() / 1000_000,所以如果需要更精确的时间,比如微秒,就除以1000。
  5. 调用日志打印:Java里用System.out,Android最好是用Log。

最后

看起来简单的一个小工具,其实考虑的东西还是有不少的,里面也有没考虑到的问题,大家可以根据自己需求修改完善。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343