Skip to content

Performance Gradle plugin instruments synthetic bridge methods, duplicating @AddTrace traces #8579

Description

@D3uf

[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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions