基于 Spring 的 StopWatch 实现统计嵌套任务执行耗时

大家可能都用过 Spring 自带的 StopWatch 工具类,可以统计多个任务的耗时,输出每个任务的耗时占比,但是有个不太好用的地方就是必须手动调用 start 和 stop 方法,并且一旦漏掉 stop 下次再调用 start 的时候就会抛异常啦。此外 它不支持嵌套调用,如果我既想了解一个任务在全局中的耗时情况,同时这个任务可能逻辑比较重,里面又拆分了很多小逻辑,我怎么统计这些小逻辑的耗时呢,我新建一个 StopWatch 来统计不就行啦

基于此,我设计了一个支持嵌套调用,能够格式化输出父子任务的耗时的工具。

由于需要记录嵌套任务,所以设计 Task 的时候,必须支持父子结构,记录一下当前任务的层级对最后格式化输出会很有帮助。

每一个 Task 都能记录自己的总耗时,以及累计子任务的耗时,同时为了防止 OOM 的情况,我们必须忽略超出阈值的子任务详情。

要记录父子任务的耗时,需要一个栈来模拟方法栈的执行过程,同时可以采用 ThreadLocal 来简化获取当前 Task。

为了防止我们漏掉调用 stop 方法,可以结合 Lambda 函数在我们的框架里就基于 try finally 来完成这一套操作。

框架本身不应该影响业务异常,我们需要抛出任务自身调用的异常。

最终代码如下:

NestedTask.java

import lombok.Getter;
import lombok.Setter;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author 24x7
 */
@Getter
public class NestedTask {

    @Setter
    private NestedTask parent;

    private final String name;

    private final long start;

    /**
     * 最多记录任务的数量,防止 oom
     */
    private final int threshold;

    private long duration;

    @Setter
    private int level;


    /**
     * 子任务
     */
    private List<NestedTask> tasks;

    public NestedTask(String name) {
        this.name = name;
        this.start = System.nanoTime();
        this.threshold = 100;
    }

    public void stop() {
        this.duration = System.nanoTime() - this.start;
    }

    public void addTask(NestedTask task) {
        if (Objects.isNull(tasks)) {
            tasks = new ArrayList<>();
        }
        if (tasks.size() < threshold) {
            tasks.add(task);
        }
    }

    public long getTimeMillis() {
        return TimeUnit.NANOSECONDS.toMillis(this.duration);
    }

    public double getTotalTime(TimeUnit timeUnit) {
        return (double) this.duration / TimeUnit.NANOSECONDS.convert(1, timeUnit);
    }

    public String detail() {
        NumberFormat nf = NumberFormat.getNumberInstance(Locale.ENGLISH);
        nf.setMaximumFractionDigits(9);
        nf.setGroupingUsed(false);

        NumberFormat pf = NumberFormat.getPercentInstance(Locale.ENGLISH);
        pf.setMinimumIntegerDigits(2);
        pf.setGroupingUsed(false);

        StringBuilder sb = new StringBuilder(128);
        sb.append("执行耗时分析 '").append(this.name).append("': ");
        String total = nf.format(getTimeMillis());
        sb.append(total).append(" ms");
        int width = Math.max(sb.length(), 40);
        sb.append("\n");

        double allTaskTotalTime = getTotalTime(TimeUnit.SECONDS);
        if (Objects.nonNull(this.tasks) && !this.tasks.isEmpty()) {
            String line = "-".repeat(width) + "\n";
            String unitName = String.format("%-12s", "ms");
            sb.append(line);
            sb.append(unitName).append("  %           Task name\n");
            sb.append(line);

            int digits = total.indexOf('.');
            if (digits < 0) {
                digits = total.length();
            }
            nf.setMinimumIntegerDigits(digits);
            nf.setMaximumFractionDigits(10 - digits);

            for (NestedTask task : this.tasks) {
                detail0(sb, task, allTaskTotalTime, nf, pf);
            }
        }
        else {
            sb.append("No task info kept");
        }

        return sb.toString();
    }

    private static void detail0(StringBuilder sb, NestedTask task, double allTaskTotalTime, NumberFormat nf, NumberFormat pf) {
        int level = task.getLevel();
        sb.append(String.format("%-14s", nf.format(task.getTimeMillis())));
        double selfTime = task.getTotalTime(TimeUnit.SECONDS);
        // 总的任务中的占比
        String p0 = pf.format(selfTime / allTaskTotalTime);
        sb.append(String.format("%-12s", p0));
        if (level > 1) {
            sb.append("  ".repeat(Math.max(0, level - 2)));
            sb.append("|__");
            // 当前任务中的占比
            sb.append(task.getName());
            double totalTime = Objects.isNull(task.parent) ? task.getTotalTime(TimeUnit.SECONDS) : task.parent.getTotalTime(TimeUnit.SECONDS);
            String p1 = pf.format(selfTime / totalTime);
            sb.append(" (").append(p1).append(")");
        } else {
            sb.append(task.getName());
        }
        sb.append('\n');
        List<NestedTask> subTasks = task.getTasks();
        if (Objects.isNull(subTasks) || subTasks.isEmpty()) {
            return;
        }
        for (NestedTask subTask : subTasks) {
            detail0(sb, subTask, allTaskTotalTime, nf, pf);
        }
    }

    public String summary() {
        return "执行耗时分析 '" + this.name + "': " + getTimeMillis() + " ms";
    }

    @Override
    public String toString() {
        return this.name;
    }
}
SimpleNestedProfiler.java

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;
import java.util.concurrent.Callable;

/**
 * @author 24x7
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public final class SimpleNestedProfiler {

    /**
     * 性能分析器栈
     */
    private static final ThreadLocal<LinkedList<NestedTask>> LOCAL_PROFILER_STACK = ThreadLocal.withInitial(LinkedList::new);

    /**
     * 追踪方法耗时的嵌套最大深度
     */
    private static final int MAX_DEPTH = 8;

    public static void clean() {
        LOCAL_PROFILER_STACK.remove();
    }

    private static NestedTask start(String name) {
        LinkedList<NestedTask> profilers = LOCAL_PROFILER_STACK.get();
        NestedTask task = new NestedTask(name);
        if (!profilers.isEmpty()) {
            NestedTask parent = profilers.getLast();
            task.setLevel(parent.getLevel() + 1);
            task.setParent(parent);
            if (profilers.size() < MAX_DEPTH) {
                parent.addTask(task);
            }
        }

        profilers.addLast(task);
        return task;
    }

    private static void stop(NestedTask task) {
        task.stop();
        LinkedList<NestedTask> profilers = LOCAL_PROFILER_STACK.get();
        if (profilers.isEmpty()) {
            return;
        }

        NestedTask removed = profilers.getLast();
        if (removed == task) {
            profilers.removeLast();
            output(task);
        }
    }

    private static void output(NestedTask task) {
        if (task.getLevel() != 0) {
            return;
        }
        try {
            log.info("{}", task.detail());
        } catch (Exception e) {
            log.error("打印耗时分析结果出异常啦, task={}", task, e);
        }
    }

    public static <T> T submit(Callable<T> callable, String taskName) {
        NestedTask task = start(taskName);
        try {
            return callable.call();
        } catch (Exception e) {
            log.error("分析异常, task={}", taskName, e);
            if (e instanceof YourBizRuntimeException) {
                throw (YourBizRuntimeException) e;
            }
            throw new YourBizRuntimeException("system error");
        } finally {
            stop(task);
        }
    }

    public static void execute(Runnable callable, String taskName) {
        NestedTask task = start(taskName);
        try {
            callable.run();
        } finally {
            stop(task);
        }
    }
}
SimpleNestedProfilerTest.java

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @author 24x7
 */
public class SimpleNestedProfilerTest {

    private static int task1() {
        sleep();
        return 1;
    }

    private static void task2() {
        sleep();
    }

    private static void task3() {
        sleep();
        for (int i = 0; i < 5; i++) {
            SimpleNestedProfiler.execute(SimpleNestedProfilerTest::subTask, "子任务 - " + i);
        }
    }

    private static void subTask() {
        sleep();
    }

    private static void sleep() {
        int i = ThreadLocalRandom.current().nextInt(10, 100);
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(i));
    }

    private static void task() {
        int i = SimpleNestedProfiler.submit(SimpleNestedProfilerTest::task1, "任务1"); // NOSONAR
        SimpleNestedProfiler.execute(SimpleNestedProfilerTest::task2, "任务2");
        SimpleNestedProfiler.execute(SimpleNestedProfilerTest::task3, "任务3");
    }

    public static void main(String[] args) {
        try {
            SimpleNestedProfiler.execute(SimpleNestedProfilerTest::task, "总任务");
        } finally {
            SimpleNestedProfiler.clean();
        }
    }
}

测试执行结果如下

23:51:31.148 [main] INFO SimpleNestedProfiler -- 执行耗时分析 '总任务': 625 ms
----------------------------------------
ms            %           Task name
----------------------------------------
077           12%         任务1
063           10%         任务2
483           77%         任务3
060           10%         |__子任务 - 0 (12%)
093           15%         |__子任务 - 1 (19%)
078           12%         |__子任务 - 2 (16%)
091           15%         |__子任务 - 3 (19%)
060           10%         |__子任务 - 4 (13%)