-
-
Notifications
You must be signed in to change notification settings - Fork 41
perf(instrumentation): Inject manifest metadata at build time #1401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4c47b46
0ff9af0
6a94d31
5b0ebf9
1a0e822
0d32407
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package io.sentry.android.gradle | ||
|
|
||
| import java.io.File | ||
| import javax.xml.parsers.DocumentBuilderFactory | ||
| import org.w3c.dom.Element | ||
|
|
||
| internal object ManifestMetadataParser { | ||
| fun parse(manifest: File): Map<String, Any>? = | ||
| runCatching { | ||
| val document = | ||
| manifest.inputStream().buffered().use { | ||
| DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(it) | ||
| } | ||
| val application = | ||
| document.getElementsByTagName(TAG_APPLICATION).item(0) ?: return emptyMap() | ||
| val metadata = linkedMapOf<String, Any>() | ||
|
|
||
| for (index in 0 until application.childNodes.length) { | ||
| val element = application.childNodes.item(index) as? Element ?: continue | ||
| if (element.tagName != TAG_META_DATA) continue | ||
|
|
||
| val name = element.getAttribute(ATTR_NAME) | ||
| if (!name.startsWith(SENTRY_PREFIX)) continue | ||
| val value = element.getAttribute(ATTR_VALUE) | ||
| if ( | ||
| element.hasAttribute(ATTR_RESOURCE) || | ||
| !element.hasAttribute(ATTR_VALUE) || | ||
| value.startsWith("@") || | ||
| value.contains("${'$'}{") | ||
| ) { | ||
| SentryPlugin.logger.info( | ||
| "Sentry manifest metadata was not optimized because $name could not be resolved at build time." | ||
| ) | ||
| return null | ||
| } | ||
| metadata[name] = inferType(value) | ||
| } | ||
| metadata | ||
| } | ||
| .onFailure { | ||
| SentryPlugin.logger.info( | ||
| "Sentry manifest metadata could not be parsed for optimization.", | ||
| it, | ||
| ) | ||
| } | ||
| .getOrNull() | ||
|
|
||
| internal fun inferType(value: String): Any = | ||
| when (value) { | ||
| "true" -> true | ||
| "false" -> false | ||
| else -> parseInteger(value) ?: value.toFloatOrNull() ?: value | ||
| } | ||
|
|
||
| private fun parseInteger(value: String): Int? = | ||
| if (value.matches(DECIMAL_INTEGER)) { | ||
| value.toIntOrNull() | ||
| } else if (value.matches(HEX_INTEGER)) { | ||
| value.removePrefix("+").let { Integer.decode(it) } | ||
| } else { | ||
| null | ||
| } | ||
|
|
||
| private const val TAG_APPLICATION = "application" | ||
| private const val TAG_META_DATA = "meta-data" | ||
| private const val ATTR_NAME = "android:name" | ||
| private const val ATTR_VALUE = "android:value" | ||
| private const val ATTR_RESOURCE = "android:resource" | ||
| private const val SENTRY_PREFIX = "io.sentry." | ||
| private val DECIMAL_INTEGER = Regex("[+-]?\\d+") | ||
| private val HEX_INTEGER = Regex("[+-]?0[xX][0-9a-fA-F]+") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package io.sentry.android.gradle.instrumentation | ||
|
|
||
| import io.sentry.android.gradle.SentryPlugin | ||
| import org.objectweb.asm.ClassVisitor | ||
| import org.objectweb.asm.FieldVisitor | ||
| import org.objectweb.asm.MethodVisitor | ||
| import org.objectweb.asm.Opcodes | ||
|
|
||
| /** Injects fully resolved build-time manifest metadata into `ManifestMetadataReader`. */ | ||
| internal class ManifestMetadataClassVisitor( | ||
| apiVersion: Int, | ||
| nextClassVisitor: ClassVisitor, | ||
| private val metadata: Map<String, Any>, | ||
| ) : ClassVisitor(apiVersion, nextClassVisitor) { | ||
| private var hasMetadataField = false | ||
| private var hasStaticInitializer = false | ||
|
|
||
| override fun visitField( | ||
| access: Int, | ||
| name: String?, | ||
| descriptor: String?, | ||
| signature: String?, | ||
| value: Any?, | ||
| ): FieldVisitor? { | ||
| if (name == METADATA_FIELD && descriptor == MAP_DESCRIPTOR) hasMetadataField = true | ||
| return super.visitField(access, name, descriptor, signature, value) | ||
| } | ||
|
|
||
| override fun visitMethod( | ||
| access: Int, | ||
| name: String?, | ||
| descriptor: String?, | ||
| signature: String?, | ||
| exceptions: Array<out String>?, | ||
| ): MethodVisitor { | ||
| val visitor = super.visitMethod(access, name, descriptor, signature, exceptions) | ||
| if (name != STATIC_INITIALIZER || descriptor != VOID_METHOD_DESCRIPTOR) return visitor | ||
|
|
||
| hasStaticInitializer = true | ||
| return object : MethodVisitor(api, visitor) { | ||
| override fun visitInsn(opcode: Int) { | ||
| if (opcode == Opcodes.RETURN && hasMetadataField) injectMetadata(this) | ||
| super.visitInsn(opcode) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun visitEnd() { | ||
| if (!hasMetadataField) { | ||
| SentryPlugin.logger.info( | ||
| "Sentry manifest metadata was not optimized because the current SDK version does not support this optimization." | ||
| ) | ||
| } | ||
| if (hasMetadataField && !hasStaticInitializer) { | ||
| val visitor = | ||
| super.visitMethod( | ||
| Opcodes.ACC_STATIC, | ||
| STATIC_INITIALIZER, | ||
| VOID_METHOD_DESCRIPTOR, | ||
| null, | ||
| null, | ||
| ) | ||
| visitor.visitCode() | ||
| injectMetadata(visitor) | ||
| visitor.visitInsn(Opcodes.RETURN) | ||
| visitor.visitMaxs(0, 0) | ||
| visitor.visitEnd() | ||
| } | ||
| super.visitEnd() | ||
| } | ||
|
|
||
| private fun injectMetadata(visitor: MethodVisitor) { | ||
| visitor.visitTypeInsn(Opcodes.NEW, HASH_MAP_NAME) | ||
| visitor.visitInsn(Opcodes.DUP) | ||
| visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, HASH_MAP_NAME, "<init>", "()V", false) | ||
| visitor.visitFieldInsn(Opcodes.PUTSTATIC, READER_INTERNAL_NAME, METADATA_FIELD, MAP_DESCRIPTOR) | ||
|
|
||
| metadata.forEach { (key, value) -> | ||
| visitor.visitFieldInsn( | ||
| Opcodes.GETSTATIC, | ||
| READER_INTERNAL_NAME, | ||
| METADATA_FIELD, | ||
| MAP_DESCRIPTOR, | ||
| ) | ||
| visitor.visitLdcInsn(key) | ||
| visitor.emitValue(value) | ||
| visitor.visitMethodInsn( | ||
| Opcodes.INVOKEINTERFACE, | ||
| "java/util/Map", | ||
| "put", | ||
| "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", | ||
| true, | ||
| ) | ||
| visitor.visitInsn(Opcodes.POP) | ||
| } | ||
| } | ||
|
|
||
| private fun MethodVisitor.emitValue(value: Any) { | ||
| when (value) { | ||
| is Boolean -> { | ||
| visitInsn(if (value) Opcodes.ICONST_1 else Opcodes.ICONST_0) | ||
| visitMethodInsn( | ||
| Opcodes.INVOKESTATIC, | ||
| "java/lang/Boolean", | ||
| "valueOf", | ||
| "(Z)Ljava/lang/Boolean;", | ||
| false, | ||
| ) | ||
| } | ||
| is Int -> { | ||
| visitLdcInsn(value) | ||
| visitMethodInsn( | ||
| Opcodes.INVOKESTATIC, | ||
| "java/lang/Integer", | ||
| "valueOf", | ||
| "(I)Ljava/lang/Integer;", | ||
| false, | ||
| ) | ||
| } | ||
| is Float -> { | ||
| visitLdcInsn(value) | ||
| visitMethodInsn( | ||
| Opcodes.INVOKESTATIC, | ||
| "java/lang/Float", | ||
| "valueOf", | ||
| "(F)Ljava/lang/Float;", | ||
| false, | ||
| ) | ||
| } | ||
| is String -> visitLdcInsn(value) | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| const val READER_INTERNAL_NAME = "io/sentry/android/core/ManifestMetadataReader" | ||
| const val METADATA_FIELD = "buildTimeMetadata" | ||
| const val MAP_DESCRIPTOR = "Ljava/util/Map;" | ||
| const val HASH_MAP_NAME = "java/util/HashMap" | ||
| const val STATIC_INITIALIZER = "<clinit>" | ||
| const val VOID_METHOD_DESCRIPTOR = "()V" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,38 +4,73 @@ import com.android.build.api.instrumentation.AsmClassVisitorFactory | |
| import com.android.build.api.instrumentation.ClassContext | ||
| import com.android.build.api.instrumentation.ClassData | ||
| import com.android.build.api.instrumentation.InstrumentationParameters | ||
| import io.sentry.android.gradle.ManifestMetadataParser | ||
| import io.sentry.android.gradle.util.SentryModules | ||
| import org.gradle.api.artifacts.ModuleIdentifier | ||
| import org.gradle.api.file.RegularFileProperty | ||
| import org.gradle.api.internal.artifacts.DefaultModuleIdentifier | ||
| import org.gradle.api.provider.MapProperty | ||
| import org.gradle.api.provider.Property | ||
| import org.gradle.api.tasks.Input | ||
| import org.gradle.api.tasks.InputFile | ||
| import org.gradle.api.tasks.PathSensitive | ||
| import org.gradle.api.tasks.PathSensitivity | ||
| import org.objectweb.asm.ClassVisitor | ||
|
|
||
| abstract class SentrySdkOptimizationClassVisitorFactory : | ||
| AsmClassVisitorFactory<SentrySdkOptimizationClassVisitorFactory.SdkOptimizationParameters> { | ||
|
|
||
| interface SdkOptimizationParameters : InstrumentationParameters { | ||
| @get:Input val classAvailability: MapProperty<String, Boolean> | ||
|
|
||
| @get:Input val buildTimeMetadataEnabled: Property<Boolean> | ||
|
|
||
| // The merged manifest cannot be read before dependency transforms are isolated, so use a | ||
| // per-build key to prevent AGP from reusing metadata injected for another app. | ||
| @get:Input val buildTimeMetadataCacheKey: Property<String> | ||
|
|
||
| @get:InputFile @get:PathSensitive(PathSensitivity.NONE) val mergedManifest: RegularFileProperty | ||
| } | ||
|
|
||
| private fun buildTimeMetadata(): Map<String, Any>? = | ||
| if (parameters.get().buildTimeMetadataEnabled.get()) { | ||
| ManifestMetadataParser.parse(parameters.get().mergedManifest.asFile.get()) | ||
| } else { | ||
| null | ||
| } | ||
|
|
||
| override fun createClassVisitor( | ||
| classContext: ClassContext, | ||
| nextClassVisitor: ClassVisitor, | ||
| ): ClassVisitor { | ||
| return LoadClassClassVisitor( | ||
| instrumentationContext.apiVersion.get(), | ||
| nextClassVisitor, | ||
| parameters.get().classAvailability.get(), | ||
| ) | ||
| } | ||
| ): ClassVisitor = | ||
| when (classContext.currentClassData.className) { | ||
| LOAD_CLASS_NAME -> | ||
| LoadClassClassVisitor( | ||
| instrumentationContext.apiVersion.get(), | ||
| nextClassVisitor, | ||
| parameters.get().classAvailability.get(), | ||
| ) | ||
| MANIFEST_METADATA_READER_NAME -> | ||
| ManifestMetadataClassVisitor( | ||
| instrumentationContext.apiVersion.get(), | ||
| nextClassVisitor, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixCache the result of the Prompt for AI AgentAlso affects:
|
||
| checkNotNull(buildTimeMetadata()), | ||
| ) | ||
| else -> nextClassVisitor | ||
| } | ||
|
|
||
| // Empty availability means the runtime classpath is unknown. Skip this transformation so | ||
| // LoadClass falls back to reflection. | ||
| override fun isInstrumentable(classData: ClassData): Boolean = | ||
| classData.className == LOAD_CLASS_NAME && parameters.get().classAvailability.get().isNotEmpty() | ||
| when (classData.className) { | ||
| LOAD_CLASS_NAME -> parameters.get().classAvailability.get().isNotEmpty() | ||
| MANIFEST_METADATA_READER_NAME -> buildTimeMetadata() != null | ||
| else -> false | ||
| } | ||
|
|
||
| internal companion object { | ||
| const val LOAD_CLASS_NAME = "io.sentry.util.LoadClass" | ||
| const val MANIFEST_METADATA_READER_NAME = "io.sentry.android.core.ManifestMetadataReader" | ||
|
|
||
| val CLASS_MODULES: Map<String, Set<ModuleIdentifier>> = | ||
| sortedMapOf( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shared cache key reuses injected metadata
High Severity
buildTimeMetadataCacheKeynow comes fromSentryModulesService, a singleregisterIfAbsentbuild service shared by every project and variant. The isolation key is therefore identical across apps and flavors, so AGP can reuseManifestMetadataReaderbytecode injected for a different variant. Product flavors and debug/release often have differentio.sentry.*metadata, including DSNs.Additional Locations (1)
plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt#L25-L26Reviewed by Cursor Bugbot for commit 0d32407. Configure here.