OkHttp uses synchronized
as an allocation free mutex. Our concurrency model is tricky enough that we’ve documented the rules! And if we forget the rules, we also use runtime assertions to catch mistakes:
@Override void flush() {
// Make sure we don’t hold a lock while doing I/O!
assert (!Thread.holdsLock(Http2Stream.this));
...
}
When we migrated to Kotlin in 4.0, our assertions migrated too:
override fun flush() {
// Make sure we don’t hold a lock while doing I/O!
assert(!Thread.holdsLock(this@Http2Stream))
...
}
This seems like a boring change but unfortunately it wasn’t! One of our users reported a performance regression introduced with the Kotlin conversion.
To better understand the regression, let’s look at how the Java assert
keyword works. The Java code above is equivalent to this source code:
@Override void flush() {
if (Http2Stream.class.desiredAssertionStatus()) {
if (!Thread.holdsLock(Http2Stream.this) == false) {
throw new AssertionError();
}
}
...
}
It first checks to see if assertions are enabled. (You can enable them with the -ea
flag to the Java process.) Only if they are does it execute the potentially-expensive assert
expression. And if that is false then our program crashes with an AssertionError
.
The Kotlin assert
keyword works differently:
override fun flush() {
if (!Thread.holdsLock(this@Http2Stream) == false) {
if (Http2Stream::class.java.desiredAssertionStatus()) {
throw AssertionError()
}
}
...
}
Kotlin always executes the assert expression. Then it uses the -ea
flag to decide whether to throw an AssertionError
.
In OkHttp the cost of the Thread.holdsLock()
call was too slow to run all the time. Our mitigation was to create our own assertion-like class that defers evaluation.
If you’re migrating a performance-sensitive codebase from Java to Kotlin, look out for assert
!
Update, 2020-03-24: The Kotlin compiler flag -Xassertions=jvm
will generate bytecode that works like Java. See KT-22292 for details. (I don’t personally like compiler flags that change the runtime behavior.)