diff --git a/android-test-app/build.gradle.kts b/android-test-app/build.gradle.kts index 54accdb6722b..ec9491d0bf52 100644 --- a/android-test-app/build.gradle.kts +++ b/android-test-app/build.gradle.kts @@ -40,7 +40,7 @@ android { dependencies { implementation(libs.playservices.safetynet) - implementation(projects.okhttp) + "friendsImplementation"(projects.okhttp) implementation(libs.androidx.activity) androidTestImplementation(libs.androidx.junit) diff --git a/android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity.kt b/android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity.kt index cf896359b4b1..bf95d1ceb50a 100644 --- a/android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity.kt +++ b/android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity.kt @@ -19,10 +19,13 @@ import android.os.Bundle import androidx.activity.ComponentActivity import okhttp3.Call import okhttp3.Callback +import okhttp3.ConnectionPool import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okhttp3.android.tracing.AndroidxTracingConnectionListener +import okhttp3.android.tracing.AndroidxTracingInterceptor import okhttp3.internal.platform.AndroidPlatform import okio.IOException @@ -30,7 +33,12 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val client = OkHttpClient() + val client = + OkHttpClient + .Builder() + .connectionPool(ConnectionPool(connectionListener = AndroidxTracingConnectionListener())) + .addNetworkInterceptor(AndroidxTracingInterceptor()) + .build() // Ensure we are compiling against the right variant println(AndroidPlatform.isSupported) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ef8b1ff0a02..20fe36726970 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +androidx-tracing = "1.3.0" # 7.0.0 is JDK 17+ https://github.com/bndtools/bnd/wiki/Changes-in-7.0.0 biz-aQute-bnd = "7.1.0" checkStyle = "10.26.1" @@ -29,6 +30,7 @@ androidx-junit = "androidx.test.ext:junit:1.2.1" androidx-lint-gradle = { module = "androidx.lint:lint-gradle", version.ref = "lintGradle" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" } androidx-test-runner = "androidx.test:runner:1.6.2" +androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version.ref = "androidx-tracing" } animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.24" aqute-resolve = { module = "biz.aQute.bnd:biz.aQute.resolve", version.ref = "biz-aQute-bnd" } assertk = "com.willowtreeapps.assertk:assertk:0.28.1" diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt index 3f5ce36c3378..b1fa4fadd455 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt @@ -144,11 +144,13 @@ internal open class RecordingConnectionListener( } override fun connectStart( + connectionId: Long, route: Route, call: Call, ) = logEvent(ConnectionEvent.ConnectStart(System.nanoTime(), route, call)) override fun connectFailed( + connectionId: Long, route: Route, call: Call, failure: IOException, diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 3198289c19f4..09ce3cfdc9e3 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -95,6 +95,7 @@ kotlin { compileOnly(libs.conscrypt.openjdk) implementation(libs.androidx.annotation) implementation(libs.androidx.startup.runtime) + implementation(libs.androidx.tracing.ktx) } } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingConnectionListener.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingConnectionListener.kt new file mode 100644 index 000000000000..49ff5c47ceb7 --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingConnectionListener.kt @@ -0,0 +1,74 @@ +package okhttp3.android.tracing + +import androidx.tracing.Trace +import okhttp3.Call +import okhttp3.Connection +import okhttp3.Route +import okhttp3.internal.connection.ConnectionListener +import okio.IOException + +/** + * Tracing implementation of ConnectionListener that marks the lifetime of each connection + * in Perfetto traces. + */ +class AndroidxTracingConnectionListener( + private val delegate: ConnectionListener = NONE, + val traceLabel: (Route) -> String = { it.defaultTracingLabel }, +) : ConnectionListener() { + override fun connectStart( + connectionId: Long, + route: Route, + call: Call, + ) { + Trace.beginAsyncSection(labelForTrace(route), connectionId.toInt()) + delegate.connectStart(connectionId, route, call) + } + + override fun connectFailed( + connectionId: Long, + route: Route, + call: Call, + failure: IOException, + ) { + Trace.endAsyncSection(labelForTrace(route), connectionId.toInt()) + delegate.connectFailed(connectionId, route, call, failure) + } + + override fun connectEnd( + connection: Connection, + route: Route, + call: Call, + ) { + delegate.connectEnd(connection, route, call) + } + + override fun connectionClosed(connection: Connection) { + Trace.endAsyncSection(labelForTrace(connection.route()), connection.id.toInt()) + delegate.connectionClosed(connection) + } + + private fun labelForTrace(route: Route): String = traceLabel(route).take(AndroidxTracingInterceptor.Companion.MAX_TRACE_LABEL_LENGTH) + + override fun connectionAcquired( + connection: Connection, + call: Call, + ) { + delegate.connectionAcquired(connection, call) + } + + override fun connectionReleased( + connection: Connection, + call: Call, + ) { + delegate.connectionReleased(connection, call) + } + + override fun noNewExchanges(connection: Connection) { + delegate.noNewExchanges(connection) + } + + companion object { + val Route.defaultTracingLabel: String + get() = this.address.url.host + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingInterceptor.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingInterceptor.kt new file mode 100644 index 000000000000..32cbfcf7aecf --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/tracing/AndroidxTracingInterceptor.kt @@ -0,0 +1,28 @@ +package okhttp3.android.tracing + +import androidx.tracing.trace +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response + +/** + * Tracing implementation of Interceptor that marks each Call in a Perfetto + * trace. Typically used as a network interceptor. + */ +class AndroidxTracingInterceptor( + val traceLabel: (Request) -> String = { it.defaultTracingLabel }, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response = + trace(traceLabel(chain.request()).take(MAX_TRACE_LABEL_LENGTH)) { + chain.proceed(chain.request()) + } + + companion object { + internal const val MAX_TRACE_LABEL_LENGTH = 127 + + val Request.defaultTracingLabel: String + get() { + return url.encodedPath + } + } +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt index 20dda3de9dc7..af7e558004fb 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt @@ -67,6 +67,10 @@ import java.net.Socket * been found. But only complete the stream once its data stream has been exhausted. */ interface Connection { + /** Unique id of this connection, assigned at the time of the attempt. */ + val id: Long + get() = 0L + /** Returns the route used by this connection. */ fun route(): Route diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionPool.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionPool.kt index 95add28eb064..ed500b06b804 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionPool.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionPool.kt @@ -84,7 +84,7 @@ class ConnectionPool internal constructor( ) // Internal until we promote ConnectionListener to be a public API. - internal constructor( + constructor( maxIdleConnections: Int = 5, keepAliveDuration: Long = 5, timeUnit: TimeUnit = TimeUnit.MINUTES, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/CallConnectionUser.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/CallConnectionUser.kt index 5f2c7a3158b8..7b125e932cd1 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/CallConnectionUser.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/CallConnectionUser.kt @@ -50,18 +50,22 @@ internal class CallConnectionUser( call.client.routeDatabase.connected(route) } - override fun connectStart(route: Route) { + override fun connectStart( + connectionId: Long, + route: Route, + ) { eventListener.connectStart(call, route.socketAddress, route.proxy) - poolConnectionListener.connectStart(route, call) + poolConnectionListener.connectStart(connectionId, route, call) } override fun connectFailed( + connectionId: Long, route: Route, protocol: Protocol?, e: IOException, ) { eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e) - poolConnectionListener.connectFailed(route, call, e) + poolConnectionListener.connectFailed(connectionId, route, call, e) } override fun secureConnectStart() { diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt index 9d4b19627251..3a7b586f5931 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt @@ -24,6 +24,7 @@ import java.net.Socket import java.net.UnknownServiceException import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket import okhttp3.CertificatePinner @@ -79,6 +80,8 @@ class ConnectPlan( internal val isTlsFallback: Boolean, ) : RoutePlanner.Plan, ExchangeCodec.Carrier { + private val id = idGenerator.incrementAndGet() + /** True if this connect was canceled; typically because it lost a race. */ @Volatile private var canceled = false @@ -135,7 +138,7 @@ class ConnectPlan( // Tell the call about the connecting call so async cancels work. user.addPlanToCancel(this) try { - user.connectStart(route) + user.connectStart(id, route) connectSocket() success = true @@ -149,7 +152,7 @@ class ConnectPlan( e, ) } - user.connectFailed(route, null, e) + user.connectFailed(id, route, null, e) return ConnectResult(plan = this, throwable = e) } finally { user.removePlanToCancel(this) @@ -231,6 +234,7 @@ class ConnectPlan( sink = sink, pingIntervalMillis = pingIntervalMillis, connectionListener = connectionPool.connectionListener, + id = id, ) this.connection = connection connection.start() @@ -240,7 +244,7 @@ class ConnectPlan( success = true return ConnectResult(plan = this) } catch (e: IOException) { - user.connectFailed(route, null, e) + user.connectFailed(id, route, null, e) if (!retryOnConnectionFailure || !retryTlsHandshake(e)) { retryTlsConnection = null @@ -333,7 +337,7 @@ class ConnectPlan( ProtocolException( "Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS", ) - user.connectFailed(route, null, failure) + user.connectFailed(id, route, null, failure) return ConnectResult(plan = this, throwable = failure) } } @@ -565,5 +569,7 @@ class ConnectPlan( companion object { private const val NPE_THROW_WITH_NULL = "throw with null exception" private const val MAX_TUNNEL_ATTEMPTS = 21 + + private val idGenerator = AtomicLong(0) } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionListener.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionListener.kt index 0157820594b0..435702285f2f 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionListener.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionListener.kt @@ -27,11 +27,12 @@ import okio.IOException * attempt to mutate the event parameters, or be reentrant back into the client. * Any IO - writing to files or network should be done asynchronously. */ -internal abstract class ConnectionListener { +abstract class ConnectionListener { /** * Invoked as soon as a call causes a connection to be started. */ open fun connectStart( + connectionId: Long, route: Route, call: Call, ) {} @@ -40,6 +41,7 @@ internal abstract class ConnectionListener { * Invoked when a connection fails to be established. */ open fun connectFailed( + connectionId: Long, route: Route, call: Call, failure: IOException, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionUser.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionUser.kt index d6856c0d118f..a09f16ee59d0 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionUser.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionUser.kt @@ -35,7 +35,10 @@ interface ConnectionUser { fun updateRouteDatabaseAfterSuccess(route: Route) - fun connectStart(route: Route) + fun connectStart( + connectionId: Long, + route: Route, + ) fun secureConnectStart() @@ -52,6 +55,7 @@ interface ConnectionUser { ) fun connectFailed( + connectionId: Long, route: Route, protocol: Protocol?, e: IOException, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/PoolConnectionUser.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/PoolConnectionUser.kt index cccdb50aa02e..8bae6b129560 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/PoolConnectionUser.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/PoolConnectionUser.kt @@ -39,7 +39,10 @@ object PoolConnectionUser : ConnectionUser { override fun updateRouteDatabaseAfterSuccess(route: Route) { } - override fun connectStart(route: Route) { + override fun connectStart( + connectionId: Long, + route: Route, + ) { } override fun secureConnectStart() { @@ -61,6 +64,7 @@ object PoolConnectionUser : ConnectionUser { } override fun connectFailed( + connectionId: Long, route: Route, protocol: Protocol?, e: IOException, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt index ee00fef1c7fd..30037d3b75f5 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt @@ -83,6 +83,7 @@ class RealConnection internal constructor( private val sink: BufferedSink, private val pingIntervalMillis: Int, internal val connectionListener: ConnectionListener, + override val id: Long, ) : Http2Connection.Listener(), Connection, ExchangeCodec.Carrier, @@ -502,6 +503,7 @@ class RealConnection internal constructor( }.buffer(), pingIntervalMillis = 0, ConnectionListener.NONE, + 0L, ) result.idleAtNs = idleAtNs return result