Skip to content

Commit

Permalink
Cache lock changes (#5608)
Browse files Browse the repository at this point in the history
* Add ApolloStore concurrency micro benchmarks

* Remove lock from ApolloStore - now individual NormalizedCaches must be thread-safe

* Make the call to the next cache be transactional

* Add some 'integration test' benchmarks that execute queries
  • Loading branch information
BoD authored and martinbonnin committed Feb 27, 2024
1 parent 45c180f commit b72716d
Show file tree
Hide file tree
Showing 24 changed files with 815 additions and 272 deletions.
4 changes: 2 additions & 2 deletions benchmark/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="INTERNET" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Storage permissions required to run the macrobenchmark on my Pixel 3, not sure why -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Expand All @@ -17,4 +17,4 @@
</intent-filter>
</activity>
</application>
</manifest>
</manifest>
2 changes: 2 additions & 0 deletions benchmark/microbenchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ dependencies {

androidTestImplementation(libs.benchmark.junit4)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation("com.apollographql.apollo3:apollo-mockserver")
androidTestImplementation("com.apollographql.apollo3:apollo-testing-support")
}

configure<com.android.build.gradle.LibraryExtension> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.apollographql.apollo3.benchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.platform.app.InstrumentationRegistry
import com.apollographql.apollo3.api.json.jsonReader
import com.apollographql.apollo3.api.parseJsonResponse
import com.apollographql.apollo3.benchmark.Utils.dbName
import com.apollographql.apollo3.benchmark.Utils.operationBasedQuery
import com.apollographql.apollo3.benchmark.Utils.resource
import com.apollographql.apollo3.benchmark.test.R
import com.apollographql.apollo3.cache.normalized.incubating.ApolloStore
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.FieldPolicyCacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.incubating.api.NormalizedCacheFactory
import com.apollographql.apollo3.cache.normalized.incubating.api.TypePolicyCacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.sql.SqlNormalizedCacheFactory
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import java.lang.reflect.Method
import java.util.concurrent.Executors

class ApolloStoreIncubatingTests {
@get:Rule
val benchmarkRule = BenchmarkRule()

@Test
fun concurrentReadWritesMemory() {
concurrentReadWrites(MemoryCacheFactory())
}

@Test
fun concurrentReadWritesSql() {
Utils.dbFile.delete()
// Pass context explicitly here because androidx.startup fails due to relocation
val cacheFactory = SqlNormalizedCacheFactory(InstrumentationRegistry.getInstrumentation().context, dbName)
concurrentReadWrites(cacheFactory)
}

@Test
fun concurrentReadWritesMemoryThenSql() {
Utils.dbFile.delete()
val cacheFactory = MemoryCacheFactory().chain(SqlNormalizedCacheFactory(InstrumentationRegistry.getInstrumentation().context, dbName))
concurrentReadWrites(cacheFactory)
}

private fun concurrentReadWrites(cacheFactory: NormalizedCacheFactory) {
val apolloStore = createApolloStore(cacheFactory)
val query = operationBasedQuery
val data = query.parseJsonResponse(resource(R.raw.calendar_response_simple).jsonReader()).data!!
val threadPool = Executors.newFixedThreadPool(CONCURRENCY)
benchmarkRule.measureRepeated {
val futures = (1..CONCURRENCY).map {
threadPool.submit {
// Let each thread execute a few writes/reads
repeat(WORK_LOAD) {
apolloStore.writeOperation(query, data)
val data2 = apolloStore.readOperation(query)
Assert.assertEquals(data, data2)
}
}
}
// Wait for all threads to finish
futures.forEach { it.get() }
}
}

private fun createApolloStore(cacheFactory: NormalizedCacheFactory): ApolloStore {
return createApolloStoreMethod.invoke(
null,
cacheFactory,
TypePolicyCacheKeyGenerator,
FieldPolicyCacheResolver,
) as ApolloStore
}


companion object {
private const val CONCURRENCY = 10
private const val WORK_LOAD = 5

/**
* There doesn't seem to be a way to relocate Kotlin metadata and kotlin_module files so we rely on reflection to call top-level
* methods
* See https://discuss.kotlinlang.org/t/what-is-the-proper-way-to-repackage-shade-kotlin-dependencies/10869
*/
private val apolloStoreKtClass = Class.forName("com.apollographql.apollo3.cache.normalized.incubating.ApolloStoreKt")
private val createApolloStoreMethod: Method = apolloStoreKtClass.getMethod(
"ApolloStore",
NormalizedCacheFactory::class.java,
CacheKeyGenerator::class.java,
CacheResolver::class.java,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.apollographql.apollo3.benchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import com.apollographql.apollo3.api.json.jsonReader
import com.apollographql.apollo3.api.parseJsonResponse
import com.apollographql.apollo3.benchmark.Utils.dbName
import com.apollographql.apollo3.benchmark.Utils.operationBasedQuery
import com.apollographql.apollo3.benchmark.Utils.resource
import com.apollographql.apollo3.benchmark.test.R
import com.apollographql.apollo3.cache.normalized.ApolloStore
import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory
import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.Executors

class ApolloStoreTests {
@get:Rule
val benchmarkRule = BenchmarkRule()

@Test
fun concurrentReadWritesMemory() {
concurrentReadWrites(MemoryCacheFactory())
}

@Test
fun concurrentReadWritesSql() {
Utils.dbFile.delete()
val cacheFactory = SqlNormalizedCacheFactory(dbName)
concurrentReadWrites(cacheFactory)
}

@Test
fun concurrentReadWritesMemoryThenSql() {
Utils.dbFile.delete()
val cacheFactory = MemoryCacheFactory().chain(SqlNormalizedCacheFactory(dbName))
concurrentReadWrites(cacheFactory)
}

private fun concurrentReadWrites(cacheFactory: NormalizedCacheFactory) {
val apolloStore = createApolloStore(cacheFactory)
val query = operationBasedQuery
val data = query.parseJsonResponse(resource(R.raw.calendar_response_simple).jsonReader()).data!!
val threadPool = Executors.newFixedThreadPool(CONCURRENCY)
benchmarkRule.measureRepeated {
val futures = (1..CONCURRENCY).map {
threadPool.submit {
// Let each thread execute a few writes/reads
repeat(WORK_LOAD) {
apolloStore.writeOperation(query, data)
val data2 = apolloStore.readOperation(query)
Assert.assertEquals(data, data2)
}
}
}
// Wait for all threads to finish
futures.forEach { it.get() }
}
}

private fun createApolloStore(cacheFactory: NormalizedCacheFactory): ApolloStore {
return ApolloStore(cacheFactory)
}


companion object {
private const val CONCURRENCY = 10
private const val WORK_LOAD = 5
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.apollographql.apollo3.benchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.platform.app.InstrumentationRegistry
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.json.jsonReader
import com.apollographql.apollo3.api.parseJsonResponse
import com.apollographql.apollo3.benchmark.Utils.dbName
import com.apollographql.apollo3.benchmark.Utils.operationBasedQuery
import com.apollographql.apollo3.benchmark.Utils.resource
import com.apollographql.apollo3.benchmark.test.R
import com.apollographql.apollo3.cache.normalized.FetchPolicy
import com.apollographql.apollo3.cache.normalized.fetchPolicy
import com.apollographql.apollo3.cache.normalized.incubating.ApolloStore
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.api.CacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.FieldPolicyCacheResolver
import com.apollographql.apollo3.cache.normalized.incubating.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.incubating.api.NormalizedCacheFactory
import com.apollographql.apollo3.cache.normalized.incubating.api.TypePolicyCacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.incubating.sql.SqlNormalizedCacheFactory
import com.apollographql.apollo3.mockserver.MockRequestBase
import com.apollographql.apollo3.mockserver.MockResponse
import com.apollographql.apollo3.mockserver.MockServer
import com.apollographql.apollo3.mockserver.MockServerHandler
import com.apollographql.apollo3.testing.MapTestNetworkTransport
import com.apollographql.apollo3.testing.registerTestResponse
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import java.lang.reflect.Method

class CacheIncubatingIntegrationTests {
@get:Rule
val benchmarkRule = BenchmarkRule()

@Test
fun concurrentQueriesTestNetworkTransportMemory() {
concurrentQueries(MemoryCacheFactory(), withMockServer = false)
}

@Test
fun concurrentQueriesTestNetworkTransportSql() {
Utils.dbFile.delete()
val cacheFactory = SqlNormalizedCacheFactory(InstrumentationRegistry.getInstrumentation().context, dbName)
concurrentQueries(cacheFactory, withMockServer = false)
}

@Test
fun concurrentQueriesTestNetworkTransportMemoryThenSql() {
Utils.dbFile.delete()
val cacheFactory = MemoryCacheFactory().chain(SqlNormalizedCacheFactory(InstrumentationRegistry.getInstrumentation().context, dbName))
concurrentQueries(cacheFactory, withMockServer = false)
}


private fun concurrentQueries(cacheFactory: NormalizedCacheFactory, withMockServer: Boolean) {
val mockServer = MockServer.Builder()
.handler(
object : MockServerHandler {
private val mockResponse = MockResponse.Builder()
.statusCode(200)
.body(resource(R.raw.calendar_response_simple).readByteString())
.build()

override fun handle(request: MockRequestBase): MockResponse {
return mockResponse
}
}
)
.build()

val client = ApolloClient.Builder()
.let {
if (withMockServer) {
it.serverUrl(runBlocking { mockServer.url() })
} else {
it.networkTransport(MapTestNetworkTransport())
}
}
.store(createApolloStore(cacheFactory))
.build()
if (!withMockServer) {
client.registerTestResponse(operationBasedQuery, operationBasedQuery.parseJsonResponse(resource(R.raw.calendar_response_simple).jsonReader()).data!!)
}

benchmarkRule.measureRepeated {
runBlocking {
(1..CONCURRENCY).map {
launch {
// Let each job execute a few queries
repeat(WORK_LOAD) {
client.query(operationBasedQuery).fetchPolicy(FetchPolicy.NetworkOnly).execute().dataOrThrow()
client.query(operationBasedQuery).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow()
}
}
}
// Wait for all jobs to finish
.joinAll()
}
}
}

private fun createApolloStore(cacheFactory: NormalizedCacheFactory): ApolloStore {
return createApolloStoreMethod.invoke(
null,
cacheFactory,
TypePolicyCacheKeyGenerator,
FieldPolicyCacheResolver,
) as ApolloStore
}


companion object {
private const val CONCURRENCY = 10
private const val WORK_LOAD = 8

/**
* There doesn't seem to be a way to relocate Kotlin metadata and kotlin_module files so we rely on reflection to call top-level
* methods
* See https://discuss.kotlinlang.org/t/what-is-the-proper-way-to-repackage-shade-kotlin-dependencies/10869
*/
private val apolloStoreKtClass = Class.forName("com.apollographql.apollo3.cache.normalized.incubating.ApolloStoreKt")
private val createApolloStoreMethod: Method = apolloStoreKtClass.getMethod(
"ApolloStore",
NormalizedCacheFactory::class.java,
CacheKeyGenerator::class.java,
CacheResolver::class.java,
)

private val NormalizedCacheClass = Class.forName("com.apollographql.apollo3.cache.normalized.incubating.NormalizedCache")
private val storeMethod: Method = NormalizedCacheClass.getMethod(
"store",
ApolloClient.Builder::class.java,
ApolloStore::class.java,
Boolean::class.java,
)

private fun ApolloClient.Builder.store(store: ApolloStore): ApolloClient.Builder {
return storeMethod.invoke(
null,
this,
store,
false,
) as ApolloClient.Builder
}
}
}


0 comments on commit b72716d

Please sign in to comment.