Pury Project Analysis

Hujiawei Bujidao


     

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.

img

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

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的源码就分析到这里吧,感兴趣的建议再扫一遍源码看下,还是会有挺多收获的。

Hujiawei is a mobile developer Guangdong, China http://javayhu.me/ 本博客所有文章均为原创,请勿随意转载,如需转载请联系我 (hujiawei090807 AT gmail.com) 我在小专栏有个移动开发技术专栏,不定期分享移动开发的核心技术,总结移动开发的实战经验
所有文章皆为原创,内容制作精良,保证干货满满,欢迎订阅 (https://xiaozhuanlan.com/u/javayhu)
>>> 我最近在Android面试指南小专栏里面写了一篇稿子 [Android面试——算法面试心得] ,欢迎阅读!<<<
下面的二维码是我个人维护的微信公众号“潇涧技术专栏”,会不定期分享移动开发的核心技术,欢迎关注!