Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache lock changes #5608

Merged
merged 4 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
}
Comment on lines +30 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are those uploaded to datadog automagically using the run-benchmarks script ? I would say so but I can't remember.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so from what I read in the script 😅.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want that? I guess yes? Additional question is: do we want to track it in the dashboard? I don't think it's going to appear in the dashboard without manual datadog configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say yes (and also yes for DataDog) Doesn't hurt to track it :) I'll have a look at the DD conf.


@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)
}
Comment on lines +59 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add an integration test, that tests end to end using apolloClient.query() and the default dispatcher? (should be Dispatchers.Default IIRC). Are the results similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean still in a microbenchmark or something else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so? Same thing but closer to the real scenario of executing a query. Probably using MockWebServer or so. I know it adds more variance but it's also what users are actually doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in c8cee0f benches that execute queries in parallel. The results are a bit questionable:

Memory Sql Memory then sql
Before 44,201,994 1,047,540,153 41,335,067
After 40,916,743 1,079,325,820 38,458,753
Improvement 7.4% -3% 6.9%

}
}
// 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
}
}
}


Loading
Loading