[READ] Step 1: Are you in the right place?
Yes: this is a bug in firebase-perf-gradle, the Firebase Performance Gradle plugin hosted in this repository.
[REQUIRED] Step 2: Describe your environment
- Android Studio version: N/A (reproduced from the command line)
- Firebase Component: Performance Monitoring (Gradle plugin bytecode instrumentation)
- Component version: Gradle plugin 2.0.2 / com.google.firebase:firebase-perf:22.0.6
[REQUIRED] Step 3: Describe the problem
The plugin instruments every method carrying @AddTrace, including compiler-generated ACC_BRIDGE / ACC_SYNTHETIC methods. When a Kotlin method that implements a generic interface is
annotated, the erasure bridge carries a copy of the annotation, so the plugin emits FirebasePerformance.startTrace(...) / Trace.stop() in both the real method and the bridge.
Since a call through the interface goes through the bridge, which then calls the real method, a single call reports the trace twice, the bridge trace wrapping the real one. Consequences: trace counts are doubled in the Firebase console (durations stay correct).
Kotlin copies method annotations onto erasure bridges as of language version 2.4 (KT-82655, requested by KT-38983), which is why this surfaces now. Copying the annotation is intended Kotlin behaviour, the bug is that the instrumentation does not skip bridges.
Real-world impact: our app annotates the create method of about fifteen androidx.startup.Initializer<Unit> implementations, and every one of those startup traces is now reported twice.
Steps to reproduce:
Minimal reproduction project attached firebase-perf-bridge-repro.zip
./gradlew :app:assembleDebug
javap -v -p -c app/build/intermediates/classes/debug/transformDebugClassesWithAsm/dirs/com/example/bridgerepro/UnitFactory.class
Actual result, both create()V and its synthetic bridge create()Ljava/lang/Object; are instrumented:
public void create();
flags: (0x0001) ACC_PUBLIC
2: invokestatic // FirebasePerformance.startTrace:(String)Trace
11: invokevirtual // Trace.stop:()V
com.google.firebase.perf.metrics.AddTrace(name="unit_factory_create")
public java.lang.Object create();
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
2: invokestatic // FirebasePerformance.startTrace:(String)Trace
14: invokevirtual // Trace.stop:()V
com.google.firebase.perf.metrics.AddTrace(name="unit_factory_create")
The reproduction is intentionally build-time only (no Activity, no google-services.json), since the duplication is already unambiguous in the bytecode. At runtime, a call through Factory<Unit> enters the bridge, which starts a trace and then calls the real method, which starts a second trace under the same name.
Expected result, only create()V is instrumented and the synthetic bridge is left untouched.
Control, uncommenting freeCompilerArgs.add("-language-version=2.3") in app/build.gradle.kts makes the duplication disappear: with language version 2.3 the annotation is not copied onto the bridge, so only one method is instrumented. This isolates the plugin's handling of bridges as the cause.
The reproduction also contains a Factory<String> implementation, showing the behaviour is not specific to Unit.
Relevant Code:
The whole reproduction is one file:
interface Factory<T> {
fun create(): T
}
class UnitFactory : Factory<Unit> {
@AddTrace(name = "unit_factory_create")
override fun create() {
Thread.sleep(1)
}
}
On the plugin side, InstrumentationVisitor.visitMethod already receives access, but only forwards it to FirebasePerfMethodVisitor, nothing filters on it (InstrumentationVisitor.java):
final MethodVisitor rootMethodVisitor =
classVisitor.visitMethod(access, methodName, methodDesc, signature, exceptions);
return new FirebasePerfMethodVisitor(
classInfo.type.getDescriptor(), api, rootMethodVisitor, access, methodName, methodDesc, instrConfig);
FirebasePerfMethodVisitor.visitAnnotation then dispatches purely on the annotation descriptor, and FirebaseTimerAnnotationProcessor.onMethodEnter only reads the annotation's enabled attribute, so nothing rejects a bridge.
Bridge methods contain no user code, so returning the uninstrumented visitor for them would fix it:
if ((access & Opcodes.ACC_BRIDGE) != 0 || (access & Opcodes.ACC_SYNTHETIC) != 0) {
return rootMethodVisitor;
}
Filtering on ACC_BRIDGE alone is enough for this case; also skipping ACC_SYNTHETIC is safe hardening, since @AddTrace cannot be applied to compiler-generated code in the first place.
[READ] Step 1: Are you in the right place?
Yes: this is a bug in firebase-perf-gradle, the Firebase Performance Gradle plugin hosted in this repository.
[REQUIRED] Step 2: Describe your environment
[REQUIRED] Step 3: Describe the problem
The plugin instruments every method carrying
@AddTrace, including compiler-generatedACC_BRIDGE/ACC_SYNTHETICmethods. When a Kotlin method that implements a generic interface isannotated, the erasure bridge carries a copy of the annotation, so the plugin emits
FirebasePerformance.startTrace(...)/Trace.stop()in both the real method and the bridge.Since a call through the interface goes through the bridge, which then calls the real method, a single call reports the trace twice, the bridge trace wrapping the real one. Consequences: trace counts are doubled in the Firebase console (durations stay correct).
Kotlin copies method annotations onto erasure bridges as of language version 2.4 (KT-82655, requested by KT-38983), which is why this surfaces now. Copying the annotation is intended Kotlin behaviour, the bug is that the instrumentation does not skip bridges.
Real-world impact: our app annotates the
createmethod of about fifteenandroidx.startup.Initializer<Unit>implementations, and every one of those startup traces is now reported twice.Steps to reproduce:
Minimal reproduction project attached firebase-perf-bridge-repro.zip
Actual result, both
create()Vand its synthetic bridgecreate()Ljava/lang/Object;are instrumented:The reproduction is intentionally build-time only (no
Activity, nogoogle-services.json), since the duplication is already unambiguous in the bytecode. At runtime, a call throughFactory<Unit>enters the bridge, which starts a trace and then calls the real method, which starts a second trace under the same name.Expected result, only
create()Vis instrumented and the synthetic bridge is left untouched.Control, uncommenting
freeCompilerArgs.add("-language-version=2.3")inapp/build.gradle.ktsmakes the duplication disappear: with language version 2.3 the annotation is not copied onto the bridge, so only one method is instrumented. This isolates the plugin's handling of bridges as the cause.The reproduction also contains a
Factory<String>implementation, showing the behaviour is not specific toUnit.Relevant Code:
The whole reproduction is one file:
On the plugin side,
InstrumentationVisitor.visitMethodalready receivesaccess, but only forwards it toFirebasePerfMethodVisitor, nothing filters on it (InstrumentationVisitor.java):FirebasePerfMethodVisitor.visitAnnotationthen dispatches purely on the annotation descriptor, andFirebaseTimerAnnotationProcessor.onMethodEnteronly reads the annotation'senabledattribute, so nothing rejects a bridge.Bridge methods contain no user code, so returning the uninstrumented visitor for them would fix it:
Filtering on
ACC_BRIDGEalone is enough for this case; also skippingACC_SYNTHETICis safe hardening, since@AddTracecannot be applied to compiler-generated code in the first place.