Skip to content

Commit

Permalink
Trace setValue() and postValue()
Browse files Browse the repository at this point in the history
  • Loading branch information
Chao Zhang committed Apr 24, 2021
1 parent 12cc37f commit 8c3d1a0
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 104 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# LoggingLiveData
# LiveDataDebugger
[![Maven Central](https://img.shields.io/maven-central/v/io.github.chao2zhang.logginglivedata/logginglivedata)](https://search.maven.org/artifact/io.github.chao2zhang.logginglivedata/logginglivedata)

The gradle plugin to make LiveData universally debuggable through bytecode transformation
The gradle plugin to make LiveData universally debuggable through bytecode transformation.
Execution of `LiveData.considerNotify()`, `LiveData.setValue()` and `LiveData.postValue()`
will be logged through logcat with info level and tag `LiveData`.

# Usage
```groovy
apply plugin: 'io.github.chao2zhang.logginglivedata'
apply plugin: 'io.github.chao2zhang.livedatadebugger'
```

# How it works
Expand Down
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'org.jetbrains.kotlin.jvm' version '1.4.31'
id 'org.jetbrains.dokka' version '1.4.30'
id 'java-gradle-plugin'
id 'com.vanniktech.maven.publish' version '0.13.0'
id 'com.vanniktech.maven.publish' version '0.14.2'
}

repositories {
Expand All @@ -17,13 +17,14 @@ repositories {
}


group="io.github.chao2zhang.logginglivedata"
group="io.github.chao2zhang.livedatadebugger"

gradlePlugin {
plugins {
loggingLiveDataPlugin {
id = 'io.github.chao2zhang.logginglivedata'
implementationClass = "io.github.chao2zhang.LoggingLiveDataPlugin"
liveDataDebuggerPlugin {
id = 'io.github.chao2zhang.livedatadebugger'
implementationClass = "io.github.chao2zhang.LiveDataDebuggerPlugin"
description = "The gradle plugin to make LiveData universally debuggable"
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
kotlin.code.style=official

GROUP=io.github.chao2zhang.logginglivedata
POM_ARTIFACT_ID=logginglivedata
VERSION_NAME=0.0.2
GROUP=io.github.chao2zhang.livedatadebugger
POM_ARTIFACT_ID=livedatadebugger
VERSION_NAME=0.0.3

POM_NAME=LoggingLiveData
POM_DESCRIPTION=Enforce named arguments usage for callers of a function through a Kotlin Compiler Plugin.
POM_NAME=LiveDataDebugger
POM_DESCRIPTION=The gradle plugin to make LiveData universally debuggable
POM_INCEPTION_YEAR=2021
POM_URL=https://github.com/chao2zhang/LoggingLiveData
POM_SCM_URL=https://github.com/chao2zhang/LoggingLiveData
POM_SCM_CONNECTION=scm:git:git://github.com/chao2zhang/LoggingLiveData.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/chao2zhang/LoggingLiveData.git
POM_URL=https://github.com/chao2zhang/LiveDataDebugger
POM_SCM_URL=https://github.com/chao2zhang/LiveDataDebugger
POM_SCM_CONNECTION=scm:git:git://github.com/chao2zhang/LiveDataDebugger.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/chao2zhang/LiveDataDebugger.git
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
rootProject.name = 'LoggingLiveData'
rootProject.name = 'LiveDataDebugger'

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.github.chao2zhang

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes.*

/**
* Add the following code after invoking `observer.onChanged`:
* ```
* Log.i("LiveData", "considerNotify() called with LiveData = " + this + " Observer = " + observer.mObserver + " Data = " + this.mData);
* ```
*/
class ConsiderNotifyMethodVisitor(
private val methodVisitor: MethodVisitor
) : MethodVisitor(ASM9, methodVisitor) {

override fun visitMethodInsn(
opcode: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
methodVisitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
if (opcode == INVOKEINTERFACE && owner == "androidx/lifecycle/Observer" && name == "onChanged") {
methodVisitor.visitLdcInsn("LiveData")
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder")
methodVisitor.visitInsn(DUP)
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
methodVisitor.visitLdcInsn("considerNotify() called with LiveData = ")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitVarInsn(ALOAD, 0)
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitLdcInsn(" Observer = ")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitFieldInsn(GETFIELD, "androidx/lifecycle/LiveData\$ObserverWrapper", "mObserver", "Landroidx/lifecycle/Observer;")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitLdcInsn(" Data = ")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitVarInsn(ALOAD, 0)
methodVisitor.visitFieldInsn(GETFIELD, "androidx/lifecycle/LiveData", "mData", "Ljava/lang/Object;")
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false)
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false)
methodVisitor.visitInsn(POP)
}
}

override fun visitMaxs(maxStack: Int, maxLocals: Int) {
methodVisitor.visitMaxs(3, 2)
}
}
45 changes: 45 additions & 0 deletions src/main/kotlin/io/github/chao2zhang/LiveDataClassVisitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.chao2zhang

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes.*


class LiveDataClassVisitor(
private val classVisitor: ClassVisitor
) : ClassVisitor(ASM9, classVisitor) {

override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
visitLogValueMethod(classVisitor.visitMethod(
ACC_PRIVATE,
"logValue",
"(Ljava/lang/String;Ljava/lang/Object;)V",
"(Ljava/lang/String;TT;)V",
null
))
}

override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val methodVisitor = classVisitor.visitMethod(access, name, descriptor, signature, exceptions)
return when (name) {
"considerNotify" -> ConsiderNotifyMethodVisitor(methodVisitor)
"postValue" -> PostValueMethodVisitor(methodVisitor)
"setValue" -> SetValueMethodVisitor(methodVisitor)
else -> methodVisitor
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project

class LoggingLiveDataPlugin : Plugin<Project> {
class LiveDataDebuggerPlugin : Plugin<Project> {

override fun apply(project: Project) {
project.plugins.withType(AppPlugin::class.java) { plugin ->
project.extensions.configure(AppExtension::class.java) { appExtension ->
appExtension.registerTransform(LoggingLiveDataTransform(project.logger))
appExtension.registerTransform(LiveDataTransform(project.logger))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ import com.android.build.api.variant.VariantInfo
import org.gradle.api.logging.Logger
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.util.TraceClassVisitor
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.PrintWriter
import java.io.StringWriter
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

@Suppress("UnstableApiUsage")
class LoggingLiveDataTransform(private val logger: Logger) : Transform() {
class LiveDataTransform(private val logger: Logger) : Transform() {

override fun getName(): String = javaClass.simpleName

Expand Down Expand Up @@ -84,7 +81,7 @@ class LoggingLiveDataTransform(private val logger: Logger) : Transform() {
logger.lifecycle("Transforming $inputEntry to add logging statements in LiveData")
val classReader = ClassReader(inputBytes)
val classWriter = ClassWriter(classReader, 0)
val classVisitor = LoggingLiveDataClassVisitor(classWriter)
val classVisitor = LiveDataClassVisitor(classWriter)
classReader.accept(classVisitor, 0)
outputBytes = classWriter.toByteArray()
}
Expand Down
143 changes: 143 additions & 0 deletions src/main/kotlin/io/github/chao2zhang/LogValueMethodVisitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package io.github.chao2zhang

import org.objectweb.asm.Label
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

/**
* private void logValue(String methodName, T value) {
* StackTraceElement[] traces = Thread.currentThread().getStackTrace();
* if (traces.length > 3) {
* Log.i("LiveData", methodName + "() called with LiveData = " + this + " Value = " + value + " Caller = " + traces[3].toString());
* }
* }
*/
fun visitLogValueMethod(methodVisitor: MethodVisitor) {
methodVisitor.visitCode()
val label0 = Label()
methodVisitor.visitLabel(label0)
methodVisitor.visitLineNumber(302, label0)
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Thread",
"getStackTrace",
"()[Ljava/lang/StackTraceElement;",
false
)
methodVisitor.visitVarInsn(Opcodes.ASTORE, 3)
val label1 = Label()
methodVisitor.visitLabel(label1)
methodVisitor.visitLineNumber(303, label1)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 3)
methodVisitor.visitInsn(Opcodes.ARRAYLENGTH)
methodVisitor.visitInsn(Opcodes.ICONST_5)
val label2 = Label()
methodVisitor.visitJumpInsn(Opcodes.IF_ICMPLE, label2)
val label3 = Label()
methodVisitor.visitLabel(label3)
methodVisitor.visitLineNumber(304, label3)
methodVisitor.visitLdcInsn("LiveData")
methodVisitor.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
methodVisitor.visitInsn(Opcodes.DUP)
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitLdcInsn("() called with LiveData = ")
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/Object;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitLdcInsn(" Value = ")
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 2)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/Object;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitLdcInsn(" Caller = ")
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitVarInsn(Opcodes.ALOAD, 3)
methodVisitor.visitInsn(Opcodes.ICONST_5)
methodVisitor.visitInsn(Opcodes.AALOAD)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StackTraceElement",
"toString",
"()Ljava/lang/String;",
false
)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuilder",
"toString",
"()Ljava/lang/String;",
false
)
methodVisitor.visitMethodInsn(
Opcodes.INVOKESTATIC,
"android/util/Log",
"i",
"(Ljava/lang/String;Ljava/lang/String;)I",
false
)
methodVisitor.visitInsn(Opcodes.POP)
methodVisitor.visitLabel(label2)
methodVisitor.visitLineNumber(306, label2)
methodVisitor.visitFrame(Opcodes.F_APPEND, 1, arrayOf<Any>("[Ljava/lang/StackTraceElement;"), 0, null)
methodVisitor.visitInsn(Opcodes.RETURN)
val label4 = Label()
methodVisitor.visitLabel(label4)
methodVisitor.visitLocalVariable(
"this",
"Landroidx/lifecycle/LiveData;",
"Landroidx/lifecycle/LiveData<TT;>;",
label0,
label4,
0
)
methodVisitor.visitLocalVariable("methodName", "Ljava/lang/String;", null, label0, label4, 1)
methodVisitor.visitLocalVariable("value", "Ljava/lang/Object;", "TT;", label0, label4, 2)
methodVisitor.visitLocalVariable("traces", "[Ljava/lang/StackTraceElement;", null, label1, label4, 3)
methodVisitor.visitMaxs(4, 4)
methodVisitor.visitEnd()
}

This file was deleted.

Loading

0 comments on commit 8c3d1a0

Please sign in to comment.