Tracing.java
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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
*
* http://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 androidx.test.platform.tracing;
import static androidx.test.internal.util.Checks.checkNotNull;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.annotation.ExperimentalTestApi;
import androidx.test.platform.tracing.Tracer.Span;
import com.google.errorprone.annotations.MustBeClosed;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* {@link Tracing} is a singleton to interact with registered {@link Tracer} implementations.
*
* <p>Support for actual tracing libraries is done by implementing wrappers following the {@link
* Tracer} interface. The actual {@link Tracer} implementations must be registered using {@link
* Tracing#registerTracer(Tracer)}.
*
* <p>The {@link Tracing} singleton is also an entry point used to create the root span for any
* tests by invoking the {@link #beginSpan(String)} method. The returned {@link Span} object must be
* properly closed by invoking the {@link Span#close()} method or wrapping them in a try-resource
* block.
*/
@ExperimentalTestApi
public final class Tracing {
private static final String TAG = Tracing.class.getSimpleName();
private static final Tracing singleton = new Tracing();
private final List<Tracer> tracers = Collections.synchronizedList(new ArrayList<>());
private Tracing() {
// The Android Tracing API only exists starting with JB MR2 (API 18).
if (Build.VERSION.SDK_INT >= 18) {
registerTracer(new AndroidXTracer().enableTracing());
}
}
/**
* Static getter for external access to the singleton. <br>
* Preferred method for internal Espresso is getting this singleton via dagger injection.
*/
@NonNull
public static Tracing getInstance() {
return singleton;
}
/**
* Registers a new tracer.
*
* <p>Once a tracer is registered, it starts being invoked for new root {@link #beginSpan(String)}
* calls. Existing in-flight spans do not invoke the new tracer.
*
* @param tracer A non-null Tracer object. A previously registered instance is ignored.
*/
public void registerTracer(@NonNull Tracer tracer) {
checkNotNull(tracer, "Tracer cannot be null.");
if (tracers.contains(tracer)) {
Log.w(TAG, "Tracer already present: " + tracer.getClass());
} else {
Log.i(TAG, "Tracer added: " + tracer.getClass());
tracers.add(tracer);
}
}
/**
* Unregisters a tracer.
*
* <p>Once a tracer is unregistered, it will stop being invoked for any new root {@link
* #beginSpan(String)} calls made or any new {@link Span#beginChildSpan(String)}. However the
* tracer is still be called for any in-flight spans being closed.
*
* @param tracer A Tracer object. A null reference or non-registered instance is ignored.
*/
public void unregisterTracer(Tracer tracer) {
tracers.remove(tracer);
Log.i(TAG, "Tracer removed: " + (tracer == null ? null : tracer.getClass()));
}
/**
* Returns a new Span as a managed resource in a try{} block. {@link Span#close()} is
* automatically called when the resource is released.
*
* @param name A non-null name describing this span.
*/
@NonNull
@MustBeClosed
public Span beginSpan(@NonNull String name) {
checkNotNull(name);
Map<Tracer, Span> spans;
synchronized (tracers) {
spans = new HashMap<>(tracers.size());
for (Tracer tracer : tracers) {
spans.put(tracer, createUnmanagedSpan(tracer, name));
}
}
return new TracerSpan(spans);
}
/**
* TracerSpan is an internal implementation class of a parent span.
*
* <p>Each Tracer creates its own Span instance wrapping a specific trace event; TracerSpan keeps
* track of which Tracer creates which Span. <br>
* When creating child spans, only tracers involved in the original parent span are called if they
* are still registered.
*/
class TracerSpan implements Span {
private final Map<Tracer, Span> spans;
private TracerSpan(@NonNull Map<Tracer, Span> spans) {
this.spans = spans;
}
/**
* Creates a child span for any tracer that participated in the parent span if that tracer is
* still registered.
*/
@NonNull
@Override
public Span beginChildSpan(@NonNull String name) {
checkNotNull(name);
Map<Tracer, Span> childSpans;
synchronized (tracers) {
childSpans = new HashMap<>(tracers.size());
for (Tracer tracer : tracers) {
Span parentSpan = spans.get(tracer);
if (parentSpan != null) {
childSpans.put(tracer, createUnmanagedChildSpan(parentSpan, name));
}
}
}
return new TracerSpan(childSpans);
}
/** All spans are closed, even if the tracer has been unregistered in between. */
@Override
public void close() {
for (Span span : spans.values()) {
span.close();
}
}
}
/**
* Internal method to create a Span that does not enforce try-resource usage. Caller <em>must</em>
* manually call {@link Span#close()} later on the resource.
*/
@SuppressWarnings("MustBeClosedChecker")
private static Span createUnmanagedSpan(Tracer tracer, String name) {
return tracer.beginSpan(name);
}
/**
* Internal method to create a Span that does not enforce try-resource usage. Caller <em>must</em>
* manually call {@link Span#close()} later on the resource.
*/
@SuppressWarnings("MustBeClosedChecker")
private static Span createUnmanagedChildSpan(Span parentSpan, String name) {
return parentSpan.beginChildSpan(name);
}
}