diff --git a/build.gradle.kts b/build.gradle.kts index 14f017a..6628569 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -165,6 +165,7 @@ dependencies { implementation("org.ow2.asm:asm:8.0.1") implementation("org.ow2.asm:asm-util:8.0.1") implementation("org.ow2.asm:asm-commons:8.0.1") + implementation("io.github.classgraph:classgraph:4.8.138") testImplementation("junit:junit:4.12") testImplementation("com.google.truth:truth:1.0.1") diff --git a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCommand.kt b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCommand.kt index 61b417c..ee89d06 100644 --- a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCommand.kt +++ b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCommand.kt @@ -24,6 +24,8 @@ sealed class CachedValueTracerCommand { /** Remove all cached value stats, and clear filters. */ object Reset: CachedValueTracerCommand() + object Scan: CachedValueTracerCommand() + /** Filter cached values by class name. */ data class Filter(val pattern: String?): CachedValueTracerCommand() @@ -50,6 +52,7 @@ fun parseCachedValueTracerCommand(text: String): CachedValueTracerCommand? { "filter" -> parseFilterCommand(tokens.advance()) "clear-filters" -> CachedValueTracerCommand.ClearFilters "group-by" -> parseGroupByCommand(tokens.advance()) + "scan" -> CachedValueTracerCommand.Scan else -> null } } diff --git a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCompletionProvider.kt b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCompletionProvider.kt index a210143..ee9e78d 100644 --- a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCompletionProvider.kt +++ b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerCompletionProvider.kt @@ -54,7 +54,7 @@ class CachedValueTracerCompletionProvider : TextCompletionProvider { } val elements = when (tokenIndex) { - 0 -> listOf("clear", "reset", "filter", "clear-filters", "group-by") + 0 -> listOf("clear", "reset", "filter", "clear-filters", "group-by", "scan") 1 -> when (command) { is CachedValueTracerCommand.GroupBy -> listOf("class", "stack-trace") else -> emptyList() diff --git a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerController.kt b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerController.kt index f364e9c..8f3d116 100644 --- a/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerController.kt +++ b/src/main/java/com/google/idea/perf/cvtracer/CachedValueTracerController.kt @@ -107,6 +107,10 @@ class CachedValueTracerController( updateUi() } } + is CachedValueTracerCommand.Scan -> { + eventConsumer.clear() + updateUi() + } else -> { LOG.warn("Unknown command: $text") } diff --git a/src/main/java/com/google/idea/perf/tracer/ClassRegistry.kt b/src/main/java/com/google/idea/perf/tracer/ClassRegistry.kt new file mode 100644 index 0000000..b0348ab --- /dev/null +++ b/src/main/java/com/google/idea/perf/tracer/ClassRegistry.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2022 Google LLC + * + * 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 + * + * https://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. + */ +package com.google.idea.perf.tracer + + +import com.google.idea.perf.AgentLoader +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressManager +import io.github.classgraph.ClassGraph +import io.github.classgraph.MethodInfo +import org.objectweb.asm.Type +import java.lang.reflect.Modifier + +object ClassRegistry { + private var classpathClasses: List = emptyList() + + private fun allLoadedClasses(): List> { + val classes = AgentLoader.instrumentation?.allLoadedClasses ?: emptyArray() + + return classes.filter { + with(it.name) { + !startsWith("java.lang.invoke") && + !startsWith("com.sun.proxy") && + !startsWith("jdk.internal.reflect") + } + } + } + + fun allClasses(): List { + //full classpath if available + if (classpathClasses.isNotEmpty()) return classpathClasses + + return allLoadedClasses().mapNotNull { ClassInfo.tryCreate(it) } + + } + + fun scanClassPath() { + val t0 = System.currentTimeMillis() + ClassGraph() + .enableClassInfo() + .rejectPackages("java.lang.invoke", "com.sun.proxy", "jdk.internal.reflect") + .ignoreClassVisibility() + .scan(1) + .use { scanResult -> + classpathClasses = scanResult.allClasses.mapNotNull { ClassInfo.tryCreate(it) } + } + val scanTime = System.currentTimeMillis() - t0 + val logger = Logger.getInstance(ClassRegistry::class.java) + logger.info("Classpath scanned in $scanTime ms") + + if (logger.isDebugEnabled) { + val alreadyLoaded = allLoadedClasses().map { it.name }.toSet() + val notLoadedSample = classpathClasses + .filter { !alreadyLoaded.contains(it.name) } + .take(10) + Logger.getInstance(ClassRegistry::class.java) + .debug("Not yet loaded classes sample $notLoadedSample") + } + } + + fun affectedClasses(traceRequests: Collection): List { + if (traceRequests.isEmpty()) return emptyList() + + if (classpathClasses.isEmpty()) { + return affectedLoadedClasses(traceRequests).mapNotNull { ClassInfo.tryCreate(it) } + } + + // full classpath scanning is slow. + // match classes first, then load full details for that classes and check methods + + val candidates = allClasses() + .filter { classInfo -> + traceRequests.any { it.matcher.mightMatchMethodInClass(classInfo.name) } + } + .map { it.name } + .toTypedArray() + + ProgressManager.checkCanceled() + + ClassGraph() + .acceptClasses(*candidates) + .enableMethodInfo() + .ignoreClassVisibility() + .ignoreFieldVisibility() + .scan(1).use { scanResult -> + val result: MutableList = mutableListOf() + for (clazz in scanResult.allClasses) { + ProgressManager.checkCanceled() + val classDetails = ClassMethods.create(clazz) ?: continue + val matches = traceRequests.any { + it.matcher.matchesMethodInClass(classDetails) + } + val details = ClassInfo.tryCreate(clazz) + if (matches && details != null) { + result.add(details) + } + } + return result + } + } + + + // This may be slow if there are many trace requests or if they use broad glob patterns. + fun affectedLoadedClasses(traceRequests: Collection): List> { + if (traceRequests.isEmpty()) return emptyList() + val result: MutableList> = mutableListOf() + + for (clazz in allLoadedClasses()) { + val classDetails = ClassMethods.tryCreate(clazz) ?: continue + val matches = traceRequests.any { + it.matcher.matchesMethodInClass(classDetails) + } + if (matches) { + result.add(clazz) + } + } + return result + } + + + fun classDetails(classname: String): ClassMethods? { + val loadedClassDetails = loadedClassDetails(classname) + if (loadedClassDetails != null) { + return loadedClassDetails + } + + ClassGraph().acceptClasses(classname) + .enableClassInfo() + .enableMethodInfo() + .ignoreClassVisibility() + .ignoreFieldVisibility() + .scan(1) + .use { scanResult -> + val classInfo = scanResult.getClassInfo(classname) ?: return null + return ClassMethods.create(classInfo) + } + } + + private fun loadedClassDetails(classname: String): ClassMethods? { + val clazz = allLoadedClasses().firstOrNull { it.name == classname } ?: return null + return ClassMethods.tryCreate(clazz) + } +} + + +class Method internal constructor( + private val classGraphMethod: MethodInfo?, + private val javaMethod: java.lang.reflect.Method?, +) { + + val name: String + get() = javaMethod?.name ?: classGraphMethod?.name ?: "" + + val descriptor: String + get() { + if (javaMethod != null) { + return Type.getMethodDescriptor(javaMethod) + } + if (classGraphMethod != null) { + return classGraphMethod.typeDescriptorStr + } + return "" + } + + override fun toString(): String { + return name + } +} + +class ClassMethods internal constructor( + val name: String, + val declaredMethods: List, + val declaredConstructors: List, +) { + + override fun toString(): String { + return name + } + + + companion object { + fun create(c: io.github.classgraph.ClassInfo): ClassMethods? { + try { + return ClassMethods( + name = c.name, + declaredMethods = c.declaredMethodInfo.map { Method(it, null) }, + declaredConstructors = c.declaredConstructorInfo.map { it.typeDescriptorStr }, + ) + } catch (e: Throwable) { + Logger.getInstance(ClassRegistry::class.java) + .warn("Error reading method info for $c", e) + return null + } + } + + fun tryCreate(c: Class<*>): ClassMethods? { + try { + return ClassMethods( + name = c.name, + declaredMethods = c.declaredMethods.map { Method(null, it) }, + declaredConstructors = c.declaredConstructors.map { + Type.getConstructorDescriptor(it) + }, + ) + } catch (e: Throwable) { + Logger.getInstance(ClassRegistry::class.java) + .warn("Error reading method info for $c", e) + return null + } + } + } +} + +class ClassInfo private constructor( + val name: String, + val simpleName: String, + val packageName: String, + val isArray: Boolean, + val isAnonymousClass: Boolean, + val isLocalClass: Boolean, + val isSynthetic: Boolean, + val isInterface: Boolean, + val isAbstract: Boolean, + val isEnum: Boolean, + val isAnnotation: Boolean, + val isThrowable: Boolean, +) { + + + companion object { + fun tryCreate(c: Class<*>): ClassInfo? { + try { + return ClassInfo( + name = c.name, + simpleName = c.simpleName, + packageName = c.packageName, + isArray = c.isArray, + isAnonymousClass = c.isAnonymousClass, + isLocalClass = c.isLocalClass, + isSynthetic = c.isSynthetic, + isInterface = c.isInterface, + isAbstract = Modifier.isAbstract(c.modifiers), + isEnum = c.isEnum, + isAnnotation = c.isAnnotation, + isThrowable = Throwable::class.java.isAssignableFrom(c), + ) + } catch (e: Throwable) { + Logger.getInstance(ClassRegistry::class.java) + .warn("Error reading class info for $c", e) + return null + } + } + + fun tryCreate(c: io.github.classgraph.ClassInfo): ClassInfo? { + try { + return ClassInfo( + name = c.name, + simpleName = c.simpleName, + packageName = c.packageName, + isArray = c.isArrayClass, + isAnonymousClass = c.isAnonymousInnerClass, + isLocalClass = false, + isSynthetic = c.isSynthetic, + isInterface = c.isInterface, + isAbstract = Modifier.isAbstract(c.modifiers), + isEnum = c.isEnum, + isAnnotation = c.isAnnotation, + isThrowable = c.extendsSuperclass(kotlin.Throwable::class.java) || c.extendsSuperclass( + java.lang.Throwable::class.java + ), + ) + } catch (e: Throwable) { + Logger.getInstance(ClassRegistry::class.java) + .warn("Error reading class info for $c", e) + return null + } + } + } + + override fun toString(): String { + return name + } +} \ No newline at end of file diff --git a/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt b/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt index a5b6c17..7e61521 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerCommand.kt @@ -30,6 +30,9 @@ sealed class TracerCommand { /** Zero out all tracepoint data and reset the call tree. */ object Reset: TracerCommand() + /** Scan classpath. */ + object Scan: TracerCommand() + /** Trace or untrace a set of methods. */ data class Trace( val enable: Boolean, @@ -91,6 +94,7 @@ fun parseMethodTracerCommand(text: String): TracerCommand { return when (tokens.first()) { ClearKeyword -> TracerCommand.Clear ResetKeyword -> TracerCommand.Reset + ScanKeyword -> TracerCommand.Scan TraceKeyword -> parseTraceCommand(tokens.advance(), true) UntraceKeyword -> parseTraceCommand(tokens.advance(), false) else -> TracerCommand.Unknown @@ -176,6 +180,7 @@ private object EndOfLine: Token() private object ClearKeyword: Token() private object ResetKeyword: Token() private object TraceKeyword: Token() +private object ScanKeyword: Token() private object UntraceKeyword: Token() private object AllKeyword: Token() private object CountKeyword: Token() @@ -213,6 +218,7 @@ private fun tokenize(text: CharSequence): List { "clear" -> tokens.add(ClearKeyword) "reset" -> tokens.add(ResetKeyword) "trace" -> tokens.add(TraceKeyword) + "scan" -> tokens.add(ScanKeyword) "untrace" -> tokens.add(UntraceKeyword) "all" -> tokens.add(AllKeyword) "count" -> tokens.add(CountKeyword) diff --git a/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt b/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt index 3e39483..ce36036 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerCompletionProvider.kt @@ -66,7 +66,7 @@ class TracerCompletionProvider : TextCompletionProvider, DumbAware { when (tokenIndex) { 0 -> { // We want all commands to be shown regardless of the prefix (for discoverability). - val allCommands = setOf("clear", "reset", "trace", "untrace") + val allCommands = setOf("clear", "reset", "trace", "scan", "untrace") val prefixMatcher = LenientPrefixMatcher(result.prefixMatcher, allCommands) result = result.withPrefixMatcher(prefixMatcher) @@ -77,6 +77,7 @@ class TracerCompletionProvider : TextCompletionProvider, DumbAware { .withTailText(" ") .withInsertHandler(AddSpaceInsertHandler.INSTANCE_WITH_AUTO_POPUP) ) + result.addElement(LookupElementBuilder.create("scan")) result.addElement( LookupElementBuilder.create("untrace") .withTailText(" ") @@ -92,13 +93,12 @@ class TracerCompletionProvider : TextCompletionProvider, DumbAware { result.addElement(wildcard) val traceRequests = TracerConfig.getAllRequests() - val affectedClasses = TracerConfigUtil.getAffectedClasses(traceRequests) + val affectedClasses = ClassRegistry.affectedClasses(traceRequests) for (clazz in affectedClasses) { ProgressManager.checkCanceled() val lookup = TracerCompletionUtil.createClassLookupElement(clazz) - if (lookup != null) { - result.addElement(lookup) - } + result.addElement(lookup) + } } } diff --git a/src/main/java/com/google/idea/perf/tracer/TracerCompletionUtil.kt b/src/main/java/com/google/idea/perf/tracer/TracerCompletionUtil.kt index 67c7b45..22b76f3 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerCompletionUtil.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerCompletionUtil.kt @@ -16,7 +16,6 @@ package com.google.idea.perf.tracer -import com.google.idea.perf.AgentLoader import com.intellij.codeInsight.AutoPopupController import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.InsertionContext @@ -38,7 +37,6 @@ import com.intellij.util.PlatformIcons.INTERFACE_ICON import com.intellij.util.PlatformIcons.METHOD_ICON import com.intellij.util.PlatformIcons.PACKAGE_ICON import com.intellij.util.containers.ArrayListSet -import java.lang.reflect.Modifier import javax.swing.Icon // Things to improve: @@ -51,17 +49,16 @@ import javax.swing.Icon object TracerCompletionUtil { - /** Creates auto-completion results for all loaded classes and their packages. */ + + /** Creates auto-completion results for all classes and their packages. */ fun addLookupElementsForLoadedClasses(result: CompletionResultSet) { - val instrumentation = AgentLoader.instrumentation ?: return val seenPackages = mutableSetOf() val prefixIsEmpty = result.prefixMatcher.prefix.isEmpty() var numResultsForEmptyPrefix = 0 - for (clazz in instrumentation.allLoadedClasses) { + for (classInfo in ClassRegistry.allClasses()) { ProgressManager.checkCanceled() - val classInfo = ClassInfo.tryCreate(clazz) ?: continue // Class name completion: com.example.Class if (!shouldHideClassFromCompletionResults(classInfo)) { @@ -85,15 +82,10 @@ object TracerCompletionUtil { /** Creates auto-completion results for all methods in the given class. */ fun addLookupElementsForMethods(className: String, result: CompletionResultSet) { - val instrumentation = AgentLoader.instrumentation ?: return - val allClasses = instrumentation.allLoadedClasses - val clazz = allClasses.firstOrNull { it.name == className } ?: return + val clazz = ClassRegistry.classDetails(className) ?: return // Declared methods. for (method in clazz.declaredMethods) { - if (Modifier.isAbstract(method.modifiers)) { - continue // Tracing abstract methods is not yet supported. - } result.addElement(LookupElementBuilder.create(method.name).withIcon(METHOD_ICON)) } @@ -142,17 +134,12 @@ object TracerCompletionUtil { } } - fun createClassLookupElement(clazz: Class<*>): LookupElement? { - val classInfo = ClassInfo.tryCreate(clazz) ?: return null - return createClassLookupElement(classInfo) - } - - private fun createClassLookupElement(c: ClassInfo): LookupElement { + fun createClassLookupElement(c: ClassInfo): LookupElement { val shortName = when { - c.simpleName.isBlank() -> c.fqName.substringAfterLast('.') // For anonymous classes. + c.simpleName.isBlank() -> c.name.substringAfterLast('.') // For anonymous classes. else -> c.simpleName } - val contextString = computeClassContextString(c.fqName, shortName) + val contextString = computeClassContextString(c.name, shortName) val icon = when { c.isInterface -> INTERFACE_ICON c.isEnum -> ENUM_ICON @@ -162,7 +149,7 @@ object TracerCompletionUtil { c.isAbstract -> ABSTRACT_CLASS_ICON else -> CLASS_ICON } - return ClassLookupElement(c.fqName, shortName, contextString, icon) + return ClassLookupElement(c.name, shortName, contextString, icon) } private fun computeClassContextString(fqName: String, simpleName: String): String { @@ -209,50 +196,9 @@ object TracerCompletionUtil { c.isLocalClass || c.isSynthetic || c.simpleName.isBlank() || - c.fqName.startsWith("java.lang.invoke.") || - c.fqName.startsWith("com.sun.proxy.") || - c.fqName.startsWith("jdk.internal.reflect.") || - c.fqName.contains("$$") - } - - // Interacting with arbitrary user classes is dangerous, because exceptions like - // NoClassDefFoundError may be thrown in certain corner cases. So we compute any info - // we need upfront and fail gracefully if exceptions are thrown. - private class ClassInfo private constructor( - val fqName: String, - val simpleName: String, - val packageName: String, - val isArray: Boolean, - val isAnonymousClass: Boolean, - val isLocalClass: Boolean, - val isSynthetic: Boolean, - val isInterface: Boolean, - val isAbstract: Boolean, - val isEnum: Boolean, - val isAnnotation: Boolean, - val isThrowable: Boolean - ) { - companion object { - fun tryCreate(c: Class<*>): ClassInfo? { - try { - return ClassInfo( - fqName = c.name, - simpleName = c.simpleName, - packageName = c.packageName, - isArray = c.isArray, - isAnonymousClass = c.isAnonymousClass, - isLocalClass = c.isLocalClass, - isSynthetic = c.isSynthetic, - isInterface = c.isInterface, - isAbstract = Modifier.isAbstract(c.modifiers), - isEnum = c.isEnum, - isAnnotation = c.isAnnotation, - isThrowable = Throwable::class.java.isAssignableFrom(c) - ) - } catch (ignored: Throwable) { - return null - } - } - } + c.name.startsWith("java.lang.invoke.") || + c.name.startsWith("com.sun.proxy.") || + c.name.startsWith("jdk.internal.reflect.") || + c.name.contains("$$") } } diff --git a/src/main/java/com/google/idea/perf/tracer/TracerConfigUtil.kt b/src/main/java/com/google/idea/perf/tracer/TracerConfigUtil.kt index 4fc1162..63c2bf5 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerConfigUtil.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerConfigUtil.kt @@ -16,10 +16,7 @@ package com.google.idea.perf.tracer -import com.google.idea.perf.AgentLoader import com.google.idea.perf.util.GlobMatcher -import com.intellij.openapi.progress.ProgressManager -import org.objectweb.asm.Type class TraceRequest( val matcher: MethodFqMatcher, @@ -58,14 +55,14 @@ class MethodFqMatcher(methodPattern: MethodFqName) { return classMatcher.matches(className) } - fun matchesMethodInClass(clazz: Class<*>): Boolean { + fun matchesMethodInClass(clazz: ClassMethods): Boolean { try { if (!classMatcher.matches(clazz.name)) return false // getDeclaredMethods() is quite slow, but it seems to be the only option. - for (m in clazz.declaredMethods) { - if (methodMatcher.matches(m.name) && - descMatcher.matches(Type.getMethodDescriptor(m)) + for (method in clazz.declaredMethods) { + if (methodMatcher.matches(method.name) && + descMatcher.matches(method.descriptor) ) { return true } @@ -73,7 +70,7 @@ class MethodFqMatcher(methodPattern: MethodFqName) { if (methodMatcher.matches("")) { for (c in clazz.declaredConstructors) { - if (descMatcher.matches(Type.getConstructorDescriptor(c))) { + if (descMatcher.matches(c)) { return true } } @@ -98,16 +95,4 @@ object TracerConfigUtil { return request } - // This may be slow if there are many trace requests or if they use broad glob patterns. - fun getAffectedClasses(traceRequests: Collection): List> { - if (traceRequests.isEmpty()) return emptyList() - val instrumentation = AgentLoader.instrumentation ?: return emptyList() - - fun classMightBeAffected(clazz: Class<*>): Boolean { - ProgressManager.checkCanceled() - return traceRequests.any { it.matcher.matchesMethodInClass(clazz) } - } - - return instrumentation.allLoadedClasses.filter(::classMightBeAffected) - } } diff --git a/src/main/java/com/google/idea/perf/tracer/TracerController.kt b/src/main/java/com/google/idea/perf/tracer/TracerController.kt index 66fa674..0584ff7 100644 --- a/src/main/java/com/google/idea/perf/tracer/TracerController.kt +++ b/src/main/java/com/google/idea/perf/tracer/TracerController.kt @@ -71,6 +71,8 @@ class TracerController( displayWarning("Failed to install instrumentation agent (see idea.log)") } } + + } override fun dispose() { @@ -112,7 +114,7 @@ class TracerController( is TracerCommand.Reset -> { runWithProgress { progress -> val oldRequests = TracerConfig.clearAllRequests() - val affectedClasses = TracerConfigUtil.getAffectedClasses(oldRequests) + val affectedClasses = ClassRegistry.affectedLoadedClasses(oldRequests) retransformClasses(affectedClasses, progress) CallTreeManager.clearCallTrees() } @@ -138,13 +140,23 @@ class TracerController( tracedParams = command.target.parameterIndexes!! ) val request = TracerConfigUtil.appendTraceRequest(methodPattern, config) - val affectedClasses = TracerConfigUtil.getAffectedClasses(listOf(request)) + val affectedClasses = ClassRegistry.affectedLoadedClasses(listOf(request)) retransformClasses(affectedClasses, progress) CallTreeManager.clearCallTrees() } } } } + is TracerCommand.Scan -> { + executor.execute { + runWithProgress { progress -> + progress.text = "Scanning classpath..."; + progress.isIndeterminate = false + progress.fraction = 0.3 + ClassRegistry.scanClassPath() + } + } + } else -> { displayWarning("Command not implemented") }