函数的耗时统计
函数的耗时统计是应用性能管理(Application Performance Management)中的一个重要方面,可能会影响到用户体验,比如说页面的卡顿,操作的不流畅,甚至可能会发生ANR。统计方法的方案大体上可以分为两种:
- 代码耗时统计法。
- 字节码插桩耗时统计法。
代码耗时统计法侵入性强,需要修改每个需要统计的方法,在相应的方法体中增加相应的代码逻辑;字节码插桩耗时统计法无侵入性,需要配置相应的参数,编写相应的ASM逻辑。字节码插桩适合数量繁重,以及步骤统一的任务。函数做耗时统计就是这类任务。
ASM耗时统计的几种方式
- 增加属性字段,在插桩方法的开始时候,相应的属性字段设置为当前时刻的值,在插桩方法的结束之前重新获取当前时刻的值,减去属性的值,计算结果就是函数耗时的值。
- 不需要增加属性字段,在插桩方法的开始的时候,分配一个临时变量来存储当前时刻的值,在插桩方法结束之前重新获取当前时刻的值,减去之前分配的临时变量的值,计算结果就是函数耗时的值。 方法1和方法2,各自有各自的优缺点。方法1的优点就是简单,插桩方法都使用统一的变量进行计算;方法1的缺点就是插桩方法使用了统一的变量(类变量或者实力变量)可能因为多线程导致计算结果不准确。方法2的缺点是开发复杂,优点就是准确。
ASM使用属性字段统计耗时
事例代码如下所示:
typescript
import static org.objectweb.asm.Opcodes.*;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class AddTimerClassVisitor extends ClassVisitor {
private String mOwner;
public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
mOwner = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (methodVisitor != null && !name.equals("<init>") && !"<clinit>".equals(name)) {
MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) {
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
// mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// mv.visitLdcInsn("Entering ASM");
// mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}
};
return newMethodVisitor;
}
return methodVisitor;
}
@Override
public void visitEnd() {
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer",
"J", null, null);
if (fv != null) {
fv.visitEnd();
}
cv.visitEnd();
}
事例代码中需要讲解,或者说需要注意的地方。
- AddTimerClassVisitor继承ClassVisitor。
- 需要增加属性,需要重写AddTimerClassVisitor的visitEnd方法。在visitEnd方法调用之前执行新增相应的属性,调用visitField。visitField的参数包括属性访问控制修饰符,属性名称,描述符,签名(泛型),默认值。
- 重写visitMethod方法,需要通过判断如果不是构造方法才进行相应的方法插桩。visitMehod方法的返回值是MethodVisitor,如果返回null代表方法删除;如果返回新建的其他MethodVisitor,代表方法的重写(尽量取一个不常用的变量名称,避免和类的其他变量名称冲突)。
- 新建的MethodVisitor需要重新visitCode方法,在方法开始之前取得当前时刻赋值给timer。
- 新建的MethodVisitor需要重新visitInsn方法,在方法结束之前取得当前时刻,并减去timer的值计算方法耗时。
- 新建的MethodVisitor需要重新visitMaxs方法,因为 long占2个槽值,计算两个long的减法最多可能多占用4个槽值,所以比最大值可能多站4个槽值。
- 如果只是计算耗时这种逻辑,建议使用SystemClock获取自Android设备启动以来的时刻,而不是使用时间戳(手机手动被调整时间导致计算时间有差错)。
ASM使用临时变量统计耗时
MethodVisitor子类事例代码如下所示:
java
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.LocalVariablesSorter;
import org.objectweb.asm.*;
public class TimingMethodVisitor extends MethodVisitor {
private LocalVariablesSorter lvs;
private int startTimeVarIndex;
public TimingMethodVisitor(int api, MethodVisitor mv) {
super(api, mv);
}
public void setLVS(LocalVariablesSorter lvs) {
this.lvs = lvs;
}
@Override
public void visitCode() {
super.visitCode();
// Insert: long startTime = System.nanoTime();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
startTimeVarIndex = lvs.newLocal(Type.LONG_TYPE); // Allocate local variable
mv.visitVarInsn(Opcodes.LSTORE, startTimeVarIndex);
}
@Override
public void visitInsn(int opcode) {
// Check if the opcode is a return instruction
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
// Insert: long elapsedTime = System.nanoTime() - startTime;
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(Opcodes.LLOAD, startTimeVarIndex);
mv.visitInsn(Opcodes.LSUB);
// Print elapsed time
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitInsn(Opcodes.SWAP); // Swap PrintStream and elapsedTime
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
super.visitInsn(opcode);
}
}
MethodVisitor需要注意的地方:
- 重写visitCode方法,需要记录时间戳,放在startTimeVarIndex索引位置上。本地变量用LocalVariablesSorter来申请。
- 重写visitInsn方法,获取当前时间戳,减去startTimeVarIndex索引位置上的时间戳,差值就是方法耗时。
ClassVisitor子类的事例代码:
java
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.LocalVariablesSorter;
public class TimingClassVisitor extends ClassVisitor {
public TimingClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !name.equals("<init>")) { // Avoid modifying constructors
LocalVariablesSorter lvs = new LocalVariablesSorter(access, descriptor, mv);
TimingMethodVisitor tmv = new TimingMethodVisitor(api, lvs);
tmv.setLVS(lvs);
return tmv;
}
return mv;
}
}
ClassVisitor子类的注意事项:
- 重写visitMethod方法。构造方法不需要统计方法耗时,所以需要过滤。
- LocalVariablesSorter的作用是分配本地变量,所以它应该作为TimingMethodVisitor属性,而不应该作为visitMethod的返回值,返回值应该是TimingMethodVisitor。
运行结果如下图所示:
耗时统计基本功能已经做完了。
函数耗时还有需要做的地方
- 函数的类名和方法名需要展示出来。
- 耗时的阈值需要设定,可以从本地设定,也外部配置平台获取。
- 超过阈值的耗时函数的相关信息需要上报到APM系统,这样方便研发人员进行分析优化。
- 统计函数耗时需要制定规则,哪类需要过滤,哪类不需要过滤需要制定规则。
总结
ASM可以做的事情很多,统计函数耗时是一个基本功能,我今天实现一个简单的耗时统计的功能。希望文章对您有所帮助。