Pury Project Analysis

本文总结下对Android平台的性能分析工具Pury的源码分析。

Pury的源码:https://github.com/NikitaKozlov/Pury

Pury is a profiling library for measuring time between multiple independent events. Events can be triggered with one of the annotations or with a method call. All events for a single scenario are united into one report.

感兴趣的话可以先阅读关于Pury作者为啥开发Pury的介绍,最精彩的是关于Pury的内部设计架构和它的局限性的介绍:

Performance measurements are done by Profilers. Each Profiler contains a list of Runs. Multiple Profilers can work in parallel, but only a single Run per each Profiler can be active. Once all Runs in a single Profiler are finished, result is reported. Amount of runs defines by runsCounter parameter.

Run has a root Stage inside. Each Stage has a name, an order number and an arbitrary amount of nested Stages. Stage can have only one active nested Stage. If you stop a parent Stage, then all nested Stages are also stopped.

以下是我的源码阅读总结:

1. 源码结构

1.1 annotations:纯Java应用,已发布到maven上,名称是pury-annotations,其中主要是定义了MethodProfilingStartProfilingStopProfiling三个注解

1.2 pury:核心工程,依赖了annotations和aspectj,已发布到maven上,名称是pury

compile 'com.nikitakozlov.pury:annotations:1.0.1'
compile 'org.aspectj:aspectjrt:1.8.6'

1.3 example:应用示例,依赖了pury,演示了几个场景下的几个方法的监控示例

2. 使用方法

注解形式所支持的5个参数

profilerName — name of the profiler is displayed in the result. Along with runsCounter identifies the Profiler. runsCounter — amount of runs for Profiler to wait for. Result is available only after all runs are stopped. stageName — identifies a stage to start. Name is displayed in the result. stageOrder — stage order reflects the hierarchy of stages. In order to start a new stage, it must be bigger then order of current most nested active stage. Stage order is a subject to one more limitation: first start event must have order number equal zero. enabled — if set to false, an annotation is skipped.

Profiler is identified by combination of profilerName and runsCounter. So if you are using same profilerName, but different runsCounter, then you will get two separate results, instead of a combined one.

profiler对应一个需要监控的场景,runsCounter是指监控场景需要执行的次数 stage对应这个场景下需要监控的方法,stageOrder是指监控方法的对应层级 需要注意的是Profiler是由profilerName和runsCounter两个共同决定的,也就是说如果profilerName相同但是runsCounter不同的话是两个不同的监控场景,最终会得到两个独立的结果。

下面是一个采用注解的方式实现监控的例子,它监控了数据加载这个事件。

@StartProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.SPLASH_LOAD_DATA,
        stageOrder = StartApp.SPLASH_LOAD_DATA_ORDER)
private void loadData() {
    new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {

        @Override
        public void run() {
            onDataLoaded();
            startMainActivity();
        }
    }, 1000);
}

@StopProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.SPLASH_SCREEN)
private void onDataLoaded() {

}

监控App Start场景下的方法调用时长的输出示例,它表示监控的场景名(ProfilerName)是App Start,这个场景总共耗时1182ms,这个场景下有6个stage,分别是App StartSplash ScreenSplash Load DataMain Activity LaunchonCreate()onStart(),下面的输出显示了每个stage的运行时间。

Profiling results for App Start:
App Start --> 0ms
  Splash Screen --> 5ms
    Splash Load Data --> 37ms
    Splash Load Data <-- 1042ms, execution = 1005ms
  Splash Screen <-- 1042ms, execution = 1037ms
  Main Activity Launch --> 1043ms 
    onCreate() --> 1077ms 
    onCreate() <-- 1100ms, execution = 23ms
    onStart() --> 1101ms 
    onStart() <-- 1131ms, execution = 30ms
  Main Activity Launch <-- 1182ms, execution = 139ms
App Start <-- 1182ms

监控Pagination场景下的方法调用时长的输出示例,它统计了Pagination这个场景下的3个stage,分别是Get Next PageLoadProcess,每个stage都会运行5次并统计avg、min和max用时。

Profiling results for Pagination:
Get Next Page --> 0ms
  Load --> avg = 1.80ms, min = 1ms, max = 3ms, for 5 runs
  Load <-- avg = 258.40ms, min = 244ms, max = 278ms, for 5 runs
  Process --> avg = 261.00ms, min = 245ms, max = 280ms, for 5 runs
  Process <-- avg = 114.20ms, min = 99ms, max = 129ms, for 5 runs
Get Next Page <-- avg = 378.80ms, min = 353ms, max = 411ms, for 5 runs

3. 核心代码分析

3.1 annotations工程中的注解

annotations中的注解有6个,分别是MethodProfilingMethodProfilingsStartProfilingStartProfilingsStopProfilingStopProfilings,因为有些方法可能存在多个注解,所以每个都对应会有一个复数形式的。这些注解作用的对象可以是普通的方法,也可以是类的构造器。

/**
 * Combination of {@link StartProfiling} and {@link StopProfiling}. If stage name is empty, then stage name from method's name and class will be generated.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface MethodProfiling {
    /**
     * Profiler Name, used in results.
     */
    String profilerName() default "";

    /**
     * Name of stage to start. Used in results. If stage name is empty, then stage name from method's name and class will be generated.
     */
    String stageName() default "";

    /**
     * Stage order must be bigger then order of current most nested active stage.
     * First profiling must starts with value 0.
     */
    int stageOrder() default 0;

    /**
     * Amount of runs to average. Result will be available only after all runs are stopped.
     */
    int runsCounter() default 1;

    /**
     * Set to false if you want to skip this annotation.
     */
    boolean enabled() default true;
}

3.2 pury工程中的注解处理类

pury工程中的注解处理类有3个,分别是ProfileMethodAspectStartProfilingAspectStopProfilingAspect类。

下面是ProfileMethodAspect的源码,其中定义了4个PointCut以及1个Around Advice。方法weaveJoinPoint是核心方法,它的主要执行流程是:假设我们对方法M提供了MethodProfiling注解,weaveJointPoint先会根据注解提供的参数去获取并启动所有相关的stage,也就是该方法所在的所有场景(profiler)下的对应stage,然后调用方法M使其执行,最后再停止所有的stage。

@Aspect
public class ProfileMethodAspect {
    private static final String POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfiling * *(..))";

    private static final String POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfiling *.new(..))";


    private static final String GROUP_ANNOTATION_POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfilings * *(..))";

    private static final String GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfilings *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void method() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructor() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_METHOD)
    public void methodWithMultipleAnnotations() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR)
    public void constructorWithMultipleAnnotations() {
    }

    @Around("constructor() || method() || methodWithMultipleAnnotations() || constructorWithMultipleAnnotations()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        ProfilingManager profilingManager = ProfilingManager.getInstance();
        List<StageId> stageIds = getStageIds(joinPoint);
        for (StageId stageId : stageIds) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .startStage(stageId.getStageName(), stageId.getStageOrder());
        }

        Object result = joinPoint.proceed();

        for (StageId stageId : stageIds) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .stopStage(stageId.getStageName());
        }

        return result;
    }

    private List<StageId> getStageIds(ProceedingJoinPoint joinPoint) {
        if (!Pury.isEnabled()) {
            return Collections.emptyList();
        }

        Annotation[] annotations =
                ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotations();
        List<StageId> stageIds = new ArrayList<>();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == MethodProfiling.class) {
                StageId stageId = getStageId((MethodProfiling) annotation, joinPoint);
                if (stageId != null) {
                    stageIds.add(stageId);
                }
            }
            if (annotation.annotationType() == MethodProfilings.class) {
                for (MethodProfiling methodProfiling : ((MethodProfilings) annotation).value()) {
                    StageId stageId = getStageId(methodProfiling, joinPoint);
                    if (stageId != null) {
                        stageIds.add(stageId);
                    }
                }
            }
        }
        return stageIds;
    }

    private StageId getStageId(MethodProfiling annotation, ProceedingJoinPoint joinPoint) {
        if (!annotation.enabled()) {
            return null;
        }
        ProfilerId profilerId = new ProfilerId(annotation.profilerName(), annotation.runsCounter());
        String stageName = annotation.stageName();
        if (stageName.isEmpty()) {
            CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
            String className = codeSignature.getDeclaringType().getSimpleName();
            String methodName = codeSignature.getName();
            stageName = className + "." + methodName;
        }

        return new StageId(profilerId, stageName, annotation.stageOrder());
    }
}

StartProfilingAspectStopProfilingAspect与之类似,只不过前者定义的是@Before advice,而后者定义的是@After advice。

3.3 核心类Pury

Pury是pury工程的核心工具类,除了可以设置自定义的Logger以及设置enabled状态之外,它还提供了startProfilingstopProfiling两个方法来实现代码调用的方法来对方法进行监控。

public final class Pury {
    static volatile Logger sLogger;
    static volatile boolean sEnabled = true;

    public static void setLogger(Logger logger) {
        sLogger = logger;
    }

    public synchronized static Logger getLogger() {
        if (sLogger == null) {
            sLogger = new DefaultLogger();
        }
        return sLogger;
    }

    public static boolean isEnabled() {
        return sEnabled;
    }

    public synchronized static void setEnabled(boolean enabled) {
        if (!enabled) {
            ProfilingManager.getInstance().clear();
        }
        sEnabled = enabled;
    }

    /**
     *
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName Name of stage to start. Used in results.
     * @param stageOrder Stage order must be bigger then order of current most nested active stage.
     *                   First profiling must starts with value 0.
     * @param runsCounter used to identify profiler. Amount of runs to average.
     *                    Result will be available only after all runs are stopped.
     */
    public static void startProfiling(String profilerName, String stageName, int stageOrder, int runsCounter) {
        ProfilerId profilerId = new ProfilerId(profilerName, runsCounter);
        ProfilingManager.getInstance().getProfiler(profilerId).startStage(stageName, stageOrder);
    }

    /**
     *
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName  Name of stage to stop. Used in results.
     * @param runsCounter used to identify profiler. Amount of runs to average.
     *                    Result will be available only after all runs are stopped.
     */
    public static void stopProfiling(String profilerName, String stageName, int runsCounter) {
        ProfilerId profilerId = new ProfilerId(profilerName, runsCounter);
        ProfilingManager.getInstance().getProfiler(profilerId).stopStage(stageName);
    }
}

Profiler是由profilerName和runsCounter两个共同决定的

ProfilerId profilerId = new ProfilerId(profilerName, runsCounter);

在某个监控场景下启动和停止某个方法的监控

ProfilingManager.getInstance().getProfiler(profilerId).startStage(stageName, stageOrder);

ProfilingManager.getInstance().getProfiler(profilerId).stopStage(stageName, stageOrder);

3.4 其他包和类

pury工程的其他类都存放在internal.profile包和internal.result两个包中,前者定义了ProfilerStageStopWatch等相关类,后者定义了ProfileResultProcessorProfileResult等各种处理结果和相应的处理类。

4. 其他内容

4.1 Pury的优缺点

个人认为,pury提供了方法调用和注解两种使用形式,实现了对某个场景及该场景下方法级别的监控,甚至可以设置场景的出现次数并自动计算场景下方法的min/avg/max三种执行时长,其功能足以满足一般的应用的场景响应时间监控的需求。不同于Hugo项目,后者只是对一个方法的监控,不能做到Pury这样针对场景的监控。

Pury存在一个明显的缺点就是方法的层级必须指定,而且必须正确指定。一般来说,方法调用的堆栈往往可能会很深,明确指定方法的层级有时候会比较麻烦,当方法的调用流程发生变化的时候不易于维护。实际上,通过分析方法调用的情况来自动配置方法层级应该是可以做到的(类似TraceView工具)。

4.2 Pury使用的gradle插件

发布到maven使用的gradle插件是https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle 实现注解解析的gradle插件是com.nikitakozlov.weaverlite,这个是作者自己封装的插件WeaverLite

Pury的源码就分析到这里吧,感兴趣的建议再扫一遍源码看下,还是会有挺多收获的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android开发实战

谷歌官方Android应用架构库——LiveData

LiveData 是一个数据持有者类,它持有一个值并允许观察该值。不同于普通的可观察者,LiveData 遵守应用程序组件的生命周期,以便 Observer 可...

1203
来自专栏Flutter知识集

Flutter 实践 MVVM

在做Android或iOS开发时,经常会了解到MVC,MVP和MVVM。MVVM在移动端一度被非常推崇,虽然也有不少反对的声音,不过MVVM确实是不错的设计架构...

3.1K3
来自专栏Java开发者杂谈

XSS事件(一)

​ 最近做的一个项目因为安全审计需要,需要做安全改造。其中自然就包括XSS和CSRF漏洞安全整改。关于这两个网络安全漏洞的详细说明,可以参照我本篇博客最后的参考...

1934
来自专栏青青天空树

趣味题:恺撒Caesar密码(c++实现)

描述:Julius Caesar 生活在充满危险和阴谋的年代。为了生存,他首次发明了密码,用于军队的消息传递。假设你是Caesar 军团中的一名军官,需要把Ca...

812
来自专栏DOTNET

ASP.NET MVC编程——单元测试

1自动化测试基本概念 自动化测试分为:单元测试,集成测试,验收测试。 单元测试 检验被测单元的功能,被测单元一般为低级别的组件,如一个类或类方法。 单元测试要满...

5325
来自专栏dotnet & java

讲一下Asp.net core MVC2.1 里面的 ApiControllerAttribute

ASP.NET Core MVC 2.1 特意为构建 HTTP API 提供了一些小特性,今天主角就是 ApiControllerAttribute. (注:文...

1362
来自专栏JavaWeb

基于Spring自定义标签

3704
来自专栏JackieZheng

探秘Tomcat——启动篇

tomcat作为一款web服务器本身很复杂,代码量也很大,但是模块化很强,最核心的模块还是连接器Connector和容器Container。具体请看下图: ? ...

4977
来自专栏西二旗一哥

iOS - autoreleasepool and @autoreleasepool

+ 在一个自动引用计数的环境中(并不是垃圾回收机制),一个包含了多个对象的 NSAutoreleasePool 对象能够接收 autorelease 消息并且...

1694
来自专栏hotqin888的专栏

engineercms利用pdf.js制作连续看图功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

1831

扫码关注云+社区

领取腾讯云代金券