├── settings.gradle ├── src └── main │ ├── resources │ └── META-INF │ │ └── gradle-plugins │ │ └── com.sdklite.trace.properties │ └── groovy │ └── com │ └── sdklite │ └── trace │ └── gradle │ ├── TracePlugin.groovy │ ├── TraceExtension.groovy │ ├── TraceOptions.groovy │ ├── TraceTransform.groovy │ └── TraceCompiler.groovy ├── .gitignore ├── gradle.properties └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'trace' 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/com.sdklite.trace.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.sdklite.trace.gradle.TracePlugin 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by MacOSX 2 | .DS_Store 3 | 4 | # General files 5 | *.bak 6 | *.log 7 | 8 | # Generated by VIM 9 | .*.swp 10 | 11 | # Generated by IDEA 12 | .idea 13 | *.iml 14 | 15 | # Generated by Eclipse 16 | .classpath 17 | .project 18 | .metadata 19 | .settings 20 | 21 | # Generated by gradle 22 | .gradle 23 | build/ 24 | /gradle 25 | gradlew 26 | gradlew.bat 27 | wrapper/ 28 | 29 | # Generated by ctags 30 | tags 31 | 32 | # Files generated by Maven 33 | target/ 34 | pom.xml.tag 35 | pom.xml.releaseBackup 36 | pom.xml.versionsBackup 37 | pom.xml.next 38 | release.properties 39 | dependency-reduced-pom.xml 40 | buildNumber.properties 41 | 42 | local.properties 43 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | POM_GROUP_ID=com.sdklite.trace 2 | POM_ARTIFACT_ID=gradle-plugin 3 | POM_PACKAGING=jar 4 | POM_VERSION=0.0.3 5 | 6 | POM_PROJECT_NAME=Trace Gradle Plugin 7 | POM_PROJECT_DESCRIPTION=Gradle plugin for method invocation tracing 8 | POM_PROJECT_URL=https://github.com/sdklite/trace 9 | POM_PROJECT_INCEPTION_YEAR=2016 10 | 11 | POM_ORG_NAME=SDKLite 12 | POM_ORG_URL=http://sdklite.com 13 | 14 | POM_SCM_URL=https://github.com/sdklite/trace.git 15 | POM_SCM_CONNECTION=scm:git:https://github.com/sdklite/trace.git 16 | POM_SCM_DEV_CONNECTION=scm:git:git@github.com:sdklite/trace.git 17 | POM_SCM_TAG=HEAD 18 | 19 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 20 | POM_LICENSE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 21 | POM_LICENSE_DIST=repo 22 | -------------------------------------------------------------------------------- /src/main/groovy/com/sdklite/trace/gradle/TracePlugin.groovy: -------------------------------------------------------------------------------- 1 | package com.sdklite.trace.gradle; 2 | 3 | import com.android.build.gradle.AppExtension; 4 | import com.android.build.gradle.LibraryExtension; 5 | import com.android.build.gradle.LibraryPlugin; 6 | import org.gradle.api.Plugin; 7 | import org.gradle.api.Project; 8 | 9 | /** 10 | * @author johnsonlee 11 | */ 12 | public class TracePlugin implements Plugin { 13 | 14 | static final String TRACE_EXTENSION = 'trace'; 15 | 16 | @Override 17 | public void apply(final Project project) { 18 | project.extensions.create(TRACE_EXTENSION, TraceExtension, project); 19 | 20 | def isLibrary = project.plugins.hasPlugin(LibraryPlugin); 21 | def android = project.extensions.getByType(isLibrary ? LibraryExtension : AppExtension); 22 | android.registerTransform(new TraceTransform(project)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/groovy/com/sdklite/trace/gradle/TraceExtension.groovy: -------------------------------------------------------------------------------- 1 | package com.sdklite.trace.gradle 2 | 3 | import org.gradle.api.Project; 4 | 5 | public class TraceExtension { 6 | 7 | private final Project project; 8 | 9 | private boolean enabled; 10 | 11 | private boolean verbose; 12 | 13 | private String[] includes; 14 | 15 | private String[] excludes; 16 | 17 | public TraceExtension(final Project project) { 18 | this.project = project; 19 | } 20 | 21 | public boolean getEnabled() { 22 | return this.enabled; 23 | } 24 | 25 | public void setEnabled(final boolean enabled) { 26 | this.enabled = enabled; 27 | } 28 | 29 | public boolean getVerbose() { 30 | return this.verbose; 31 | } 32 | 33 | public void setVerbose(final boolean verbose) { 34 | this.verbose = verbose; 35 | } 36 | 37 | public String[] getIncludes() { 38 | return this.includes; 39 | } 40 | 41 | public void setIncludes(final String... includes) { 42 | this.includes = includes; 43 | } 44 | 45 | public String[] getExcludes() { 46 | return this.excludes; 47 | } 48 | 49 | public void setExcludes(final String... excludes) { 50 | this.excludes = excludes; 51 | } 52 | 53 | @Override 54 | String toString() { 55 | return "{ enabled:$enabled, verbose:$verbose, includes:$includes, excludes:$excludes }"; 56 | } 57 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trace 2 | 3 | Trace is a gradle plugin for android project to trace java method invocation, it uses the [Transform API](http://tools.android.com/tech-docs/new-build-system/transform-api) and [Javassist](http://jboss-javassist.github.io/javassist/) to manipulate the bytecode, and print the java method with elapsed time to logcat. 4 | 5 | ## Getting Started 6 | 7 | Configure this `build.gradle` like this: 8 | 9 | ```groovy 10 | buildscript { 11 | repositories { 12 | mavenLocal() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:1.5.0' // Or higher version 17 | classpath 'com.sdklite.trace:gradle-plugin:0.0.2' // HERE 18 | } 19 | } 20 | ``` 21 | 22 | Then apply the trace plugin below the android plugin 23 | 24 | ```groovy 25 | apply plugin: 'com.sdklite.trace' 26 | ``` 27 | 28 | Finally, build your project and install the application to your android device, then you can filter the logcat like this: 29 | 30 | ```bash 31 | adb logcat -s trace 32 | ``` 33 | 34 | ## Performance optimization 35 | 36 | Trace gradle plugin is useful and convinient for performance optimization by 37 | dumping the trace log via logcat: 38 | 39 | 1. Clean logcat 40 | 41 | ```bash 42 | adb logcat -c 43 | ``` 44 | 2. Launch app 45 | 3. Dump trace log 46 | 47 | ```bash 48 | adb logcat -d -s trace | awk -F: '{print $NF}' | awk '{printf "%s, %s\n", $1, substr($2, 2)}' 49 | ``` 50 | 51 | or filter by elapsed time 52 | 53 | ```bash 54 | adb logcat -d -s trace | awk -F: '{print $NF}' | awk '{et=strtonum(substr($2,2)); if (et > 30) { printf "%s, %s\n", $1, et}}' 55 | ``` 56 | -------------------------------------------------------------------------------- /src/main/groovy/com/sdklite/trace/gradle/TraceOptions.groovy: -------------------------------------------------------------------------------- 1 | package com.sdklite.trace.gradle; 2 | 3 | /** 4 | * @author johnsonlee 5 | */ 6 | class TraceOptions { 7 | 8 | private final List inputs; 9 | 10 | private final List references; 11 | 12 | private final File output; 13 | 14 | private TraceOptions(final List inputs, final List references, final File output) { 15 | this.inputs = inputs; 16 | this.references = references; 17 | this.output = output; 18 | } 19 | 20 | public Collection getInputs() { 21 | return Collections.unmodifiableCollection(this.inputs); 22 | } 23 | 24 | public Collection getReferences() { 25 | return Collections.unmodifiableCollection(this.references); 26 | } 27 | 28 | public File getOutput() { 29 | return this.output; 30 | } 31 | 32 | public static final class Builder { 33 | 34 | private final List inputs = new ArrayList(); 35 | 36 | private final List references = new ArrayList(); 37 | 38 | private final File output; 39 | 40 | public Builder(final File output) { 41 | this.output = output; 42 | } 43 | 44 | public Builder inputs(final List inputs) { 45 | this.inputs.addAll(inputs); 46 | return this; 47 | } 48 | 49 | public Builder inputs(final File input) { 50 | this.inputs.add(input); 51 | return this; 52 | } 53 | 54 | public Builder references(final File reference) { 55 | this.references.add(reference); 56 | return this; 57 | } 58 | 59 | public Builder references(final List references) { 60 | this.references.addAll(references); 61 | return this; 62 | } 63 | 64 | public TraceOptions build() { 65 | return new TraceOptions(this.inputs, this.references, this.output); 66 | } 67 | } 68 | 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/main/groovy/com/sdklite/trace/gradle/TraceTransform.groovy: -------------------------------------------------------------------------------- 1 | package com.sdklite.trace.gradle; 2 | 3 | import com.android.build.gradle.AppExtension; 4 | import com.android.build.gradle.LibraryExtension; 5 | import com.android.build.gradle.LibraryPlugin; 6 | import com.android.build.api.transform.* 7 | import org.gradle.api.Project; 8 | 9 | /** 10 | * @author johnsonlee 11 | */ 12 | public class TraceTransform extends Transform { 13 | 14 | private static final Set CONTENT_CLASS = Collections.unmodifiableSet(new HashSet([ 15 | QualifiedContent.DefaultContentType.CLASSES 16 | ])); 17 | 18 | private static final Set SCOPE_APP_PROJECT = Collections.unmodifiableSet(new HashSet([ 19 | QualifiedContent.Scope.PROJECT, 20 | QualifiedContent.Scope.PROJECT_LOCAL_DEPS, 21 | QualifiedContent.Scope.SUB_PROJECTS, 22 | QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS, 23 | QualifiedContent.Scope.EXTERNAL_LIBRARIES 24 | ])); 25 | 26 | private static final Set SCOPE_LIB_PROJECT = Collections.unmodifiableSet(new HashSet([ 27 | QualifiedContent.Scope.PROJECT, 28 | QualifiedContent.Scope.PROJECT_LOCAL_DEPS, 29 | ])); 30 | 31 | private static final Set SCOPE_REF_PROJECT = Collections.unmodifiableSet(new HashSet([ 32 | QualifiedContent.Scope.PROJECT_LOCAL_DEPS, 33 | QualifiedContent.Scope.SUB_PROJECTS, 34 | QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS, 35 | QualifiedContent.Scope.EXTERNAL_LIBRARIES, 36 | QualifiedContent.Scope.PROVIDED_ONLY 37 | ])); 38 | 39 | private final Project project; 40 | 41 | private final boolean isLibrary; 42 | 43 | private final TraceCompiler compiler; 44 | 45 | public TraceTransform(final Project project) { 46 | this.project = project; 47 | this.isLibrary = project.plugins.hasPlugin(LibraryPlugin); 48 | this.compiler = new TraceCompiler(project); 49 | } 50 | 51 | @Override 52 | String getName() { 53 | return "trace"; 54 | } 55 | 56 | @Override 57 | Set getInputTypes() { 58 | return CONTENT_CLASS; 59 | } 60 | 61 | @Override 62 | Set getScopes() { 63 | return this.isLibrary ? SCOPE_LIB_PROJECT : SCOPE_APP_PROJECT; 64 | } 65 | 66 | @Override 67 | Set getReferencedScopes() { 68 | return SCOPE_REF_PROJECT; 69 | } 70 | 71 | @Override 72 | boolean isIncremental() { 73 | return false; 74 | } 75 | 76 | @Override 77 | void transform(final Context context, final Collection inputs, final Collection references, final TransformOutputProvider outputProvider, final boolean isIncremental) throws IOException, TransformException, InterruptedException { 78 | if (!project.extensions.getByType(TraceExtension).enabled) { 79 | return; 80 | } 81 | 82 | // clean output 83 | outputProvider.deleteAll(); 84 | 85 | final def android = this.project.extensions.findByType(this.isLibrary ? LibraryExtension : AppExtension); 86 | final List classes = new ArrayList(); 87 | final List libraries = new ArrayList(); 88 | 89 | inputs.each { 90 | classes.addAll(it.directoryInputs); 91 | classes.addAll(it.jarInputs); 92 | } 93 | 94 | references.each { 95 | libraries.addAll(it.directoryInputs) 96 | libraries.addAll(it.jarInputs); 97 | } 98 | 99 | final def input = classes.find { it instanceof DirectoryInput }; 100 | final def output = outputProvider.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY); 101 | final def builder = new TraceOptions.Builder(output) 102 | .inputs(classes*.file) 103 | .references(android.bootClasspath) 104 | .references(libraries*.file); 105 | this.compiler.compile(builder.build()); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/groovy/com/sdklite/trace/gradle/TraceCompiler.groovy: -------------------------------------------------------------------------------- 1 | package com.sdklite.trace.gradle; 2 | 3 | import javassist.ClassPool; 4 | import javassist.CtClass; 5 | import javassist.Modifier; 6 | import org.apache.commons.io.IOUtils; 7 | import org.gradle.api.Project; 8 | 9 | import java.util.jar.JarEntry; 10 | import java.util.jar.JarFile; 11 | 12 | /** 13 | * @author johnsonlee 14 | */ 15 | class TraceCompiler { 16 | 17 | private final Project project; 18 | 19 | private final TraceExtension extension; 20 | 21 | public TraceCompiler(final Project project) { 22 | this.project = project; 23 | this.extension = project.extensions.getByType(TraceExtension); 24 | } 25 | 26 | /** 27 | * Compile with the specified options 28 | * 29 | * @param options 30 | * @throws Exception 31 | */ 32 | public void compile(final TraceOptions options) throws Exception { 33 | final ClassPool pool = ClassPool.default; 34 | 35 | for (final File ref : options.references) { 36 | if (this.extension.verbose) { 37 | this.project.logger.println(" * $ref"); 38 | } 39 | 40 | pool.appendClassPath(ref.absolutePath); 41 | } 42 | 43 | for (final File input : options.inputs) { 44 | if (this.extension.verbose) { 45 | this.project.logger.info(" + $input.absolutePath"); 46 | } 47 | 48 | if (input.directory) { 49 | this.compileDir(pool, input, options.output); 50 | } else { 51 | this.compileJar(pool, input, options.output); 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Compile files in the specified directory 58 | * 59 | * @param pool 60 | * @param input 61 | * @param output 62 | * @throws Exception 63 | */ 64 | private void compileDir(final ClassPool pool, final File input, final File output) throws Exception { 65 | this.project.fileTree(dir: input).visit { 66 | final File file = it.file; 67 | 68 | if (file.directory) { 69 | return; 70 | } 71 | 72 | if (!file.name.endsWith('.class')) { 73 | this.compileFile(this.project, input, new File(file.absolutePath.replace(input.absolutePath, output.absolutePath))); 74 | return; 75 | } 76 | 77 | final InputStream is = new FileInputStream(file); 78 | 79 | try { 80 | this.compileClass(pool, is, output, false); 81 | } finally { 82 | IOUtils.closeQuietly(is); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Compile jar file 89 | * 90 | * @param pool 91 | * @param input 92 | * @param output 93 | * @throws Exception 94 | */ 95 | private void compileJar(final ClassPool pool, final File input, final File output) throws Exception { 96 | final JarFile jar = new JarFile(input); 97 | 98 | for (final Enumeration entries = jar.entries(); entries.hasMoreElements();) { 99 | final JarEntry entry = entries.nextElement(); 100 | if (entry.directory) { 101 | new File(output, entry.name).mkdirs(); 102 | continue; 103 | } 104 | 105 | final InputStream is = jar.getInputStream(entry); 106 | 107 | try { 108 | if (entry.name.endsWith(".class")) { 109 | this.compileClass(pool, is, output, false); 110 | } else { 111 | this.compileFile(is, new File(output, entry.name)); 112 | } 113 | } finally { 114 | IOUtils.closeQuietly(is); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Compile class from stream 121 | * 122 | * @param pool 123 | * @param is 124 | * @param output 125 | * @param ignore 126 | * @throws Exception 127 | */ 128 | private void compileClass(final ClassPool pool, final InputStream is, final File output, final boolean ignore) throws Exception { 129 | final CtClass klass = pool.makeClass(is, false); 130 | 131 | if (this.extension.verbose) { 132 | this.project.logger.println(" - ${klass.name}"); 133 | } 134 | 135 | if (!ignore) { 136 | if (!(klass.isAnnotation() || klass.isArray() || klass.isEnum() || klass.isInterface() || klass.isPrimitive() || klass.isFrozen())) { 137 | klass.declaredMethods.findAll { !Modifier.isAbstract(it.modifiers) && !java.lang.reflect.Modifier.isNative(it.modifiers) }.each { m -> 138 | try { 139 | m.addLocalVariable("${m.name}ElapsedTime", CtClass.longType); 140 | m.insertBefore("{${m.name}ElapsedTime = System.nanoTime();}"); 141 | m.insertAfter("{android.util.Log.v(\"trace\", \"${klass.name}#${m.name}${m.signature} (\" + ((System.nanoTime() - ${m.name}ElapsedTime) / 1000000f) + \" ms)\");}"); 142 | 143 | if (this.extension.verbose) { 144 | this.project.logger.println(" - ${klass.name}#${m.name}${m.signature}"); 145 | } 146 | } catch (final Exception e) { 147 | } 148 | } 149 | } 150 | } 151 | 152 | klass.writeFile(output.absolutePath); 153 | } 154 | 155 | /** 156 | * Compile normal file 157 | * 158 | * @param input 159 | * @param output 160 | * @throws IOException 161 | */ 162 | private void compileFile(final File input, final File output) throws IOException { 163 | if (!output.exists()) { 164 | output.createNewFile(); 165 | } else { 166 | this.project.logger.println("`$output` already exists"); 167 | } 168 | 169 | final FileInputStream fis = new FileInputStream(input); 170 | 171 | try { 172 | this.compileFile(input, output); 173 | } finally { 174 | fis.close(); 175 | } 176 | } 177 | 178 | /** 179 | * Compile stream as file 180 | * 181 | * @param input 182 | * @param output 183 | * @throws IOException 184 | */ 185 | private void compileFile(final InputStream input, final File output) throws IOException { 186 | if (!output.exists()) { 187 | output.getParentFile().mkdirs(); 188 | output.createNewFile(); 189 | } else { 190 | this.project.logger.println("`$output` alread exists"); 191 | } 192 | 193 | final OutputStream os = new FileOutputStream(output); 194 | 195 | try { 196 | IOUtils.copy(input, os); 197 | } finally { 198 | IOUtils.closeQuietly(os); 199 | } 200 | } 201 | } 202 | 203 | --------------------------------------------------------------------------------