|
/* |
|
* Copyright 2025 Duolingo, Inc. |
|
* |
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
* you may not use this file except in compliance with the License. |
|
* You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, software |
|
* distributed under the License is distributed on an "AS IS" BASIS, |
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
* See the License for the specific language governing permissions and |
|
* limitations under the License. |
|
*/ |
|
|
|
/** This plugin adds a method trace to all methods in classes that match the given pattern. */ |
|
internal object MethodTrace { |
|
/** Configures the given [project] and Android [extension] with common testing properties. */ |
|
fun configure(project: Project, extension: AndroidComponentsExtension<*, *, *>) { |
|
val enableMethodTrace = |
|
project.property("com.duolingo.tools.method_trace").toString().toBoolean() |
|
val methodTraceClassPattern = |
|
project.property("com.duolingo.tools.method_trace.class_pattern").toString() |
|
|
|
if (enableMethodTrace && methodTraceClassPattern.isNotEmpty()) { |
|
extension.onVariants { variant -> |
|
variant.instrumentation.transformClassesWith( |
|
MethodTraceTransform::class.java, |
|
InstrumentationScope.PROJECT |
|
) { |
|
it.methodTraceClassPattern.set(methodTraceClassPattern) |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** Parameters for the [MethodTraceTransform] class. */ |
|
interface MethodTraceTransformParams : InstrumentationParameters { |
|
@get:Input val methodTraceClassPattern: Property<String> |
|
} |
|
|
|
/** |
|
* This class adds begin/end method traces to all methods in classes that match the given pattern. |
|
* |
|
* This class is intended to be abstract, see [AsmClassVisitorFactory] for more details. |
|
*/ |
|
abstract class MethodTraceTransform : AsmClassVisitorFactory<MethodTraceTransformParams> { |
|
|
|
override fun createClassVisitor( |
|
classContext: ClassContext, |
|
nextClassVisitor: ClassVisitor |
|
): ClassVisitor { |
|
return object : ClassVisitor(Opcodes.ASM9, nextClassVisitor) { |
|
val className = classContext.currentClassData.className |
|
|
|
override fun visitMethod( |
|
access: Int, |
|
name: String?, |
|
descriptor: String?, |
|
signature: String?, |
|
exceptions: Array<out String>? |
|
): MethodVisitor { |
|
val superMethodVisitor = |
|
super.visitMethod(access, name, descriptor, signature, exceptions) |
|
|
|
val methodVisitor = |
|
object : AdviceAdapter(Opcodes.ASM9, superMethodVisitor, access, name, descriptor) { |
|
override fun onMethodEnter() { |
|
// Add Tracer.beginSection at the beginning of the method |
|
mv.visitFieldInsn( |
|
Opcodes.GETSTATIC, |
|
"com/duolingo/core/log/TracerHolder", |
|
"tracer", |
|
"Lcom/duolingo/core/log/Tracer;" |
|
) |
|
mv.visitLdcInsn("$className.$name".takeLast(MAX_SECTION_NAME_LENGTH)) |
|
mv.visitMethodInsn( |
|
Opcodes.INVOKEVIRTUAL, |
|
"com/duolingo/core/log/Tracer", |
|
"beginSection", |
|
"(Ljava/lang/String;)V", |
|
false |
|
) |
|
} |
|
|
|
override fun onMethodExit(opcode: Int) { |
|
// Add Tracer.endSection at the end of the method |
|
mv.visitFieldInsn( |
|
Opcodes.GETSTATIC, |
|
"com/duolingo/core/log/TracerHolder", |
|
"tracer", |
|
"Lcom/duolingo/core/log/Tracer;" |
|
) |
|
mv.visitLdcInsn("$className.$name".takeLast(MAX_SECTION_NAME_LENGTH)) |
|
mv.visitMethodInsn( |
|
Opcodes.INVOKEVIRTUAL, |
|
"com/duolingo/core/log/Tracer", |
|
"endSection", |
|
"(Ljava/lang/String;)V", |
|
false |
|
) |
|
} |
|
} |
|
|
|
return methodVisitor |
|
} |
|
} |
|
} |
|
|
|
override fun isInstrumentable(classData: ClassData): Boolean { |
|
val isInstrumentable = |
|
classData.className.matches(parameters.get().methodTraceClassPattern.get().toRegex()) |
|
if (isInstrumentable) { |
|
println("Instrumenting method for ${classData.className}") |
|
} |
|
return isInstrumentable |
|
} |
|
|
|
companion object { |
|
private const val MAX_SECTION_NAME_LENGTH = 127 |
|
} |
|
} |
|
} |
I'm coming here from https://blog.duolingo.com/android-app-performance/, and it is a great read.
Can you specify a license to use this gist so that we can learn from it and apply it in other use cases?
Thanks in advance.