Skip to content

Commit f72c4ad

Browse files
perf(internal): improve compilation+test speed
1 parent fb2505d commit f72c4ad

10 files changed

Lines changed: 85 additions & 104 deletions

File tree

.github/workflows/publish-sonatype.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
export -- GPG_SIGNING_KEY_ID
3434
printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
3535
GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
36-
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD"
36+
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
3737
env:
3838
SONATYPE_USERNAME: ${{ secrets.ORB_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
3939
SONATYPE_PASSWORD: ${{ secrets.ORB_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}

buildSrc/src/main/kotlin/orb.kotlin.gradle.kts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99

1010
kotlin {
1111
jvmToolchain {
12-
languageVersion.set(JavaLanguageVersion.of(17))
12+
languageVersion.set(JavaLanguageVersion.of(21))
1313
}
1414

1515
compilerOptions {
@@ -19,6 +19,8 @@ kotlin {
1919
// Suppress deprecation warnings because we may still reference and test deprecated members.
2020
// TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
2121
"-nowarn",
22+
// Use as many threads as there are CPU cores on the machine for compilation.
23+
"-Xbackend-threads=0",
2224
)
2325
jvmTarget.set(JvmTarget.JVM_1_8)
2426
languageVersion.set(KotlinVersion.KOTLIN_1_9)
@@ -34,8 +36,7 @@ configure<SpotlessExtension> {
3436
}
3537
}
3638

37-
// Run tests in parallel to some degree.
3839
tasks.withType<Test>().configureEach {
39-
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
40-
forkEvery = 100
40+
systemProperty("junit.jupiter.execution.parallel.enabled", true)
41+
systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
4142
}

gradle.properties

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
org.gradle.caching=true
2+
org.gradle.configuration-cache=true
23
org.gradle.parallel=true
34
org.gradle.daemon=false
4-
org.gradle.jvmargs=-Xmx4g
5-
kotlin.daemon.jvmargs=-Xmx4g
5+
# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
6+
org.gradle.jvmargs=\
7+
-Xms1g \
8+
-Xmx4g \
9+
-XX:+UseParallelGC \
10+
-XX:InitialCodeCacheSize=256m \
11+
-XX:ReservedCodeCacheSize=1G \
12+
-XX:MetaspaceSize=256m \
13+
-XX:TieredStopAtLevel=1 \
14+
-XX:GCTimeRatio=4 \
15+
-XX:CICompilerCount=4 \
16+
-XX:+OptimizeStringConcat \
17+
-XX:+UseStringDeduplication

orb-java-core/src/main/kotlin/com/withorb/api/core/http/RetryingHttpClient.kt

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import kotlin.math.pow
2323
class RetryingHttpClient
2424
private constructor(
2525
private val httpClient: HttpClient,
26+
private val sleeper: Sleeper,
2627
private val clock: Clock,
2728
private val maxRetries: Int,
2829
private val idempotencyHeader: String?,
@@ -62,10 +63,10 @@ private constructor(
6263
null
6364
}
6465

65-
val backoffMillis = getRetryBackoffMillis(retries, response)
66+
val backoffDuration = getRetryBackoffDuration(retries, response)
6667
// All responses must be closed, so close the failed one before retrying.
6768
response?.close()
68-
Thread.sleep(backoffMillis.toMillis())
69+
sleeper.sleep(backoffDuration)
6970
}
7071
}
7172

@@ -111,10 +112,10 @@ private constructor(
111112
}
112113
}
113114

114-
val backoffMillis = getRetryBackoffMillis(retries, response)
115+
val backoffDuration = getRetryBackoffDuration(retries, response)
115116
// All responses must be closed, so close the failed one before retrying.
116117
response?.close()
117-
return sleepAsync(backoffMillis.toMillis()).thenCompose {
118+
return sleeper.sleepAsync(backoffDuration).thenCompose {
118119
executeWithRetries(requestWithRetryCount, requestOptions)
119120
}
120121
}
@@ -179,7 +180,7 @@ private constructor(
179180
// retried.
180181
throwable is IOException || throwable is OrbIoException
181182

182-
private fun getRetryBackoffMillis(retries: Int, response: HttpResponse?): Duration {
183+
private fun getRetryBackoffDuration(retries: Int, response: HttpResponse?): Duration {
183184
// About the Retry-After header:
184185
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
185186
response
@@ -226,33 +227,40 @@ private constructor(
226227

227228
companion object {
228229

229-
private val TIMER = Timer("RetryingHttpClient", true)
230-
231-
private fun sleepAsync(millis: Long): CompletableFuture<Void> {
232-
val future = CompletableFuture<Void>()
233-
TIMER.schedule(
234-
object : TimerTask() {
235-
override fun run() {
236-
future.complete(null)
237-
}
238-
},
239-
millis,
240-
)
241-
return future
242-
}
243-
244230
@JvmStatic fun builder() = Builder()
245231
}
246232

247233
class Builder internal constructor() {
248234

249235
private var httpClient: HttpClient? = null
236+
private var sleeper: Sleeper =
237+
object : Sleeper {
238+
239+
private val timer = Timer("RetryingHttpClient", true)
240+
241+
override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
242+
243+
override fun sleepAsync(duration: Duration): CompletableFuture<Void> {
244+
val future = CompletableFuture<Void>()
245+
timer.schedule(
246+
object : TimerTask() {
247+
override fun run() {
248+
future.complete(null)
249+
}
250+
},
251+
duration.toMillis(),
252+
)
253+
return future
254+
}
255+
}
250256
private var clock: Clock = Clock.systemUTC()
251257
private var maxRetries: Int = 2
252258
private var idempotencyHeader: String? = null
253259

254260
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
255261

262+
@JvmSynthetic internal fun sleeper(sleeper: Sleeper) = apply { this.sleeper = sleeper }
263+
256264
fun clock(clock: Clock) = apply { this.clock = clock }
257265

258266
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
@@ -262,9 +270,17 @@ private constructor(
262270
fun build(): HttpClient =
263271
RetryingHttpClient(
264272
checkRequired("httpClient", httpClient),
273+
sleeper,
265274
clock,
266275
maxRetries,
267276
idempotencyHeader,
268277
)
269278
}
279+
280+
internal interface Sleeper {
281+
282+
fun sleep(duration: Duration)
283+
284+
fun sleepAsync(duration: Duration): CompletableFuture<Void>
285+
}
270286
}

orb-java-core/src/test/kotlin/com/withorb/api/core/PhantomReachableTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal class PhantomReachableTest {
2020
assertThat(closed).isFalse()
2121

2222
System.gc()
23-
Thread.sleep(3000)
23+
Thread.sleep(100)
2424

2525
assertThat(closed).isTrue()
2626
}

orb-java-core/src/test/kotlin/com/withorb/api/core/http/HeadersTest.kt

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.withorb.api.core.http
22

33
import org.assertj.core.api.Assertions.assertThat
4-
import org.assertj.core.api.Assertions.catchThrowable
5-
import org.assertj.core.api.Assumptions.assumeThat
64
import org.junit.jupiter.params.ParameterizedTest
75
import org.junit.jupiter.params.provider.EnumSource
86

@@ -241,34 +239,4 @@ internal class HeadersTest {
241239

242240
assertThat(size).isEqualTo(testCase.expectedSize)
243241
}
244-
245-
@ParameterizedTest
246-
@EnumSource
247-
fun namesAreImmutable(testCase: TestCase) {
248-
val headers = testCase.headers
249-
val headerNamesCopy = headers.names().toSet()
250-
251-
val throwable = catchThrowable {
252-
(headers.names() as MutableSet<String>).add("another name")
253-
}
254-
255-
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
256-
assertThat(headers.names()).isEqualTo(headerNamesCopy)
257-
}
258-
259-
@ParameterizedTest
260-
@EnumSource
261-
fun valuesAreImmutable(testCase: TestCase) {
262-
val headers = testCase.headers
263-
assumeThat(headers.size).isNotEqualTo(0)
264-
val name = headers.names().first()
265-
val headerValuesCopy = headers.values(name).toList()
266-
267-
val throwable = catchThrowable {
268-
(headers.values(name) as MutableList<String>).add("another value")
269-
}
270-
271-
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
272-
assertThat(headers.values(name)).isEqualTo(headerValuesCopy)
273-
}
274242
}

orb-java-core/src/test/kotlin/com/withorb/api/core/http/QueryParamsTest.kt

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.withorb.api.core.http
22

33
import org.assertj.core.api.Assertions.assertThat
4-
import org.assertj.core.api.Assertions.catchThrowable
5-
import org.assertj.core.api.Assumptions.assumeThat
64
import org.junit.jupiter.params.ParameterizedTest
75
import org.junit.jupiter.params.provider.EnumSource
86

@@ -179,34 +177,4 @@ internal class QueryParamsTest {
179177

180178
assertThat(size).isEqualTo(testCase.expectedSize)
181179
}
182-
183-
@ParameterizedTest
184-
@EnumSource
185-
fun keysAreImmutable(testCase: TestCase) {
186-
val queryParams = testCase.queryParams
187-
val queryParamKeysCopy = queryParams.keys().toSet()
188-
189-
val throwable = catchThrowable {
190-
(queryParams.keys() as MutableSet<String>).add("another key")
191-
}
192-
193-
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
194-
assertThat(queryParams.keys()).isEqualTo(queryParamKeysCopy)
195-
}
196-
197-
@ParameterizedTest
198-
@EnumSource
199-
fun valuesAreImmutable(testCase: TestCase) {
200-
val queryParams = testCase.queryParams
201-
assumeThat(queryParams.size).isNotEqualTo(0)
202-
val key = queryParams.keys().first()
203-
val queryParamValuesCopy = queryParams.values(key).toList()
204-
205-
val throwable = catchThrowable {
206-
(queryParams.values(key) as MutableList<String>).add("another value")
207-
}
208-
209-
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
210-
assertThat(queryParams.values(key)).isEqualTo(queryParamValuesCopy)
211-
}
212180
}

orb-java-core/src/test/kotlin/com/withorb/api/core/http/RetryingHttpClientTest.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import com.github.tomakehurst.wiremock.stubbing.Scenario
77
import com.withorb.api.client.okhttp.OkHttpClient
88
import com.withorb.api.core.RequestOptions
99
import java.io.InputStream
10+
import java.time.Duration
1011
import java.util.concurrent.CompletableFuture
1112
import org.assertj.core.api.Assertions.assertThat
1213
import org.junit.jupiter.api.BeforeEach
14+
import org.junit.jupiter.api.parallel.ResourceLock
1315
import org.junit.jupiter.params.ParameterizedTest
1416
import org.junit.jupiter.params.provider.ValueSource
1517

1618
@WireMockTest
19+
@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
1720
internal class RetryingHttpClientTest {
1821

1922
private var openResponseCount = 0
@@ -24,6 +27,7 @@ internal class RetryingHttpClientTest {
2427
val okHttpClient = OkHttpClient.builder().baseUrl(wmRuntimeInfo.httpBaseUrl).build()
2528
httpClient =
2629
object : HttpClient {
30+
2731
override fun execute(
2832
request: HttpRequest,
2933
requestOptions: RequestOptions,
@@ -40,6 +44,7 @@ internal class RetryingHttpClientTest {
4044
private fun trackClose(response: HttpResponse): HttpResponse {
4145
openResponseCount++
4246
return object : HttpResponse {
47+
4348
private var isClosed = false
4449

4550
override fun statusCode(): Int = response.statusCode()
@@ -66,7 +71,7 @@ internal class RetryingHttpClientTest {
6671
@ValueSource(booleans = [false, true])
6772
fun execute(async: Boolean) {
6873
stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
69-
val retryingClient = RetryingHttpClient.builder().httpClient(httpClient).build()
74+
val retryingClient = retryingHttpClientBuilder().build()
7075

7176
val response =
7277
retryingClient.execute(
@@ -88,11 +93,7 @@ internal class RetryingHttpClientTest {
8893
.willReturn(ok())
8994
)
9095
val retryingClient =
91-
RetryingHttpClient.builder()
92-
.httpClient(httpClient)
93-
.maxRetries(2)
94-
.idempotencyHeader("X-Some-Header")
95-
.build()
96+
retryingHttpClientBuilder().maxRetries(2).idempotencyHeader("X-Some-Header").build()
9697

9798
val response =
9899
retryingClient.execute(
@@ -134,8 +135,7 @@ internal class RetryingHttpClientTest {
134135
.willReturn(ok())
135136
.willSetStateTo("COMPLETED")
136137
)
137-
val retryingClient =
138-
RetryingHttpClient.builder().httpClient(httpClient).maxRetries(2).build()
138+
val retryingClient = retryingHttpClientBuilder().maxRetries(2).build()
139139

140140
val response =
141141
retryingClient.execute(
@@ -181,8 +181,7 @@ internal class RetryingHttpClientTest {
181181
.willReturn(ok())
182182
.willSetStateTo("COMPLETED")
183183
)
184-
val retryingClient =
185-
RetryingHttpClient.builder().httpClient(httpClient).maxRetries(2).build()
184+
val retryingClient = retryingHttpClientBuilder().maxRetries(2).build()
186185

187186
val response =
188187
retryingClient.execute(
@@ -220,8 +219,7 @@ internal class RetryingHttpClientTest {
220219
.willReturn(ok())
221220
.willSetStateTo("COMPLETED")
222221
)
223-
val retryingClient =
224-
RetryingHttpClient.builder().httpClient(httpClient).maxRetries(1).build()
222+
val retryingClient = retryingHttpClientBuilder().maxRetries(1).build()
225223

226224
val response =
227225
retryingClient.execute(
@@ -234,6 +232,20 @@ internal class RetryingHttpClientTest {
234232
assertNoResponseLeaks()
235233
}
236234

235+
private fun retryingHttpClientBuilder() =
236+
RetryingHttpClient.builder()
237+
.httpClient(httpClient)
238+
// Use a no-op `Sleeper` to make the test fast.
239+
.sleeper(
240+
object : RetryingHttpClient.Sleeper {
241+
242+
override fun sleep(duration: Duration) {}
243+
244+
override fun sleepAsync(duration: Duration): CompletableFuture<Void> =
245+
CompletableFuture.completedFuture(null)
246+
}
247+
)
248+
237249
private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse =
238250
if (async) executeAsync(request).get() else execute(request)
239251

orb-java-core/src/test/kotlin/com/withorb/api/services/ErrorHandlingTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import org.assertj.core.api.Assertions.entry
2828
import org.junit.jupiter.api.BeforeEach
2929
import org.junit.jupiter.api.Test
3030
import org.junit.jupiter.api.assertThrows
31+
import org.junit.jupiter.api.parallel.ResourceLock
3132

3233
@WireMockTest
34+
@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
3335
internal class ErrorHandlingTest {
3436

3537
companion object {

orb-java-core/src/test/kotlin/com/withorb/api/services/ServiceParamsTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import com.withorb.api.core.JsonValue
1818
import com.withorb.api.models.CustomerCreateParams
1919
import org.junit.jupiter.api.BeforeEach
2020
import org.junit.jupiter.api.Test
21+
import org.junit.jupiter.api.parallel.ResourceLock
2122

2223
@WireMockTest
24+
@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
2325
internal class ServiceParamsTest {
2426

2527
private lateinit var client: OrbClient

0 commit comments

Comments
 (0)