-
Notifications
You must be signed in to change notification settings - Fork 657
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
Cache lock changes #5608
Changes from all commits
c3007d2
d91bd3e
f87e47a
c8cee0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+59
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean still in a microbenchmark or something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
// 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 | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 😅.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.