summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYigit Boyar <yboyar@google.com>2018-10-24 10:58:39 +0100
committerTreeHugger Robot <treehugger-gerrit@google.com>2018-10-29 23:49:49 +0000
commitdc87cb65521a02cebce8c73e5d9b8dc5d4811c1e (patch)
tree711caee5d99c6125f4a6fbdac6851fb253022c5c
parentc063c66bd12a6f5a818761ed39cb5831683547b0 (diff)
downloaddata-binding-dc87cb65521a02cebce8c73e5d9b8dc5d4811c1e.tar.gz
Cache found classes
We spend a lot of time re-finding same classes and then querying their properties. This CL introduces a new class finder cache that holds onto already found classes keyed by its name and available imports. The hit rate for TestApp is quite crazy (97%) and brings down no-cache compilation from 3 sec to ~ 2.5 sec. Bug: 117808327 Test: ClassFinderCacheTest Change-Id: I1981627588fe411b1970df99c722a91fe4b657b2
-rw-r--r--BUILD.bazel1
-rw-r--r--compiler/build.gradle1
-rw-r--r--compiler/db-compiler.iml1
-rw-r--r--compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java2
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/ClassFinderCache.kt69
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/ImportBag.kt16
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/InjectedClass.kt3
-rw-r--r--compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt8
-rw-r--r--compiler/src/test/java/android/databinding/tool/reflection/ClassFinderCacheTest.kt144
9 files changed, 238 insertions, 7 deletions
diff --git a/BUILD.bazel b/BUILD.bazel
index 7571dd2c..556f8888 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -219,6 +219,7 @@ iml_module(
"//tools/idea/.idea/libraries:Guava",
"//tools/idea/.idea/libraries:gson",
"//tools/idea/.idea/libraries:JUnit4[test]",
+ "//tools/idea/.idea/libraries:mockito[test]",
"//prebuilts/tools/common/m2/repository/com/squareup/javapoet/1.8.0:jar",
"//tools/idea/.idea/libraries:KotlinJavaRuntime",
"//tools/idea/.idea/libraries:jetbrains-annotations-java5",
diff --git a/compiler/build.gradle b/compiler/build.gradle
index b9febc82..9a1d9007 100644
--- a/compiler/build.gradle
+++ b/compiler/build.gradle
@@ -40,6 +40,7 @@ dependencies {
compile libs.antlr4
compile libs.juniversalchardet
testCompile libs.junit
+ testCompile libs.mockito_core
}
uploadArchives {
diff --git a/compiler/db-compiler.iml b/compiler/db-compiler.iml
index 9f4f10ec..8e8ad149 100644
--- a/compiler/db-compiler.iml
+++ b/compiler/db-compiler.iml
@@ -55,6 +55,7 @@
<orderEntry type="library" name="Guava" level="project" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
+ <orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="module-library">
<library name="javapoet" type="repository">
<properties maven-id="com.squareup:javapoet:1.8.0" />
diff --git a/compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java b/compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java
index 7d476840..45cece6c 100644
--- a/compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java
+++ b/compiler/src/main/java/android/databinding/annotationprocessor/ProcessDataBinding.java
@@ -16,9 +16,9 @@
package android.databinding.annotationprocessor;
+import android.databinding.tool.CompilerArguments;
import android.databinding.tool.CompilerChef;
import android.databinding.tool.Context;
-import android.databinding.tool.CompilerArguments;
import android.databinding.tool.processing.Scope;
import android.databinding.tool.processing.ScopedException;
import android.databinding.tool.store.GenClassInfoLog;
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ClassFinderCache.kt b/compiler/src/main/java/android/databinding/tool/reflection/ClassFinderCache.kt
new file mode 100644
index 00000000..24c191b1
--- /dev/null
+++ b/compiler/src/main/java/android/databinding/tool/reflection/ClassFinderCache.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.reflection
+
+import android.databinding.tool.util.L
+
+/**
+ * A cache object that can index classes based on when it is found and its imports.
+ */
+class ClassFinderCache(
+ private val doFind : ((className : String, imports : ImportBag?) -> ModelClass?)
+) {
+ private val cache = mutableMapOf<CacheKey, ModelClass>()
+ private val importCache = mutableMapOf<ImportBag, ImmutableImportBag>()
+ private var hit = 0
+ private var miss = 0
+ private var missForNull = 0
+ fun find(className : String, imports: ImportBag?) : ModelClass? {
+ val immutableImports = if(imports == null) {
+ null
+ } else {
+ importCache.getOrPut(imports) {
+ imports.toImmutable()
+ }
+ }
+ val key = CacheKey(className = className, imports = immutableImports)
+ val existing = cache[key]
+ if (existing == null) {
+ miss ++
+ val found = doFind(className, imports)
+ if (found == null) {
+ missForNull ++
+ } else {
+ cache[key] = found
+ return found
+ }
+ return found
+ } else {
+ hit++
+ return existing
+ }
+ }
+
+ fun logStats() {
+ val ratio = (miss * 1f) / (miss + hit)
+ val nonNullMiss = miss - missForNull
+ val nonNullRatio = (nonNullMiss * 1f) / (nonNullMiss + hit)
+ L.w("class finder cache: miss: $miss, hit: $hit, ratio : $ratio, ratio w/o nulls: $nonNullRatio")
+ }
+
+ private data class CacheKey(
+ val className: String,
+ val imports: ImmutableImportBag?
+ )
+} \ No newline at end of file
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ImportBag.kt b/compiler/src/main/java/android/databinding/tool/reflection/ImportBag.kt
index 8b6fae1a..30fefb9a 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/ImportBag.kt
+++ b/compiler/src/main/java/android/databinding/tool/reflection/ImportBag.kt
@@ -21,8 +21,12 @@ class MutableImportBag : ImportBag() {
}
}
-private class ImmutableImportBag : ImportBag() {
-
+class ImmutableImportBag(imports : Map<String, String>? = null) : ImportBag() {
+ init {
+ if (imports != null) {
+ this.imports.putAll(imports)
+ }
+ }
}
/**
@@ -56,6 +60,14 @@ sealed class ImportBag {
return imports.hashCode()
}
+ fun toImmutable() : ImmutableImportBag {
+ return if (this is ImmutableImportBag) {
+ this
+ } else {
+ ImmutableImportBag(imports)
+ }
+ }
+
companion object {
@JvmField
val EMPTY : ImportBag = ImmutableImportBag()
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/InjectedClass.kt b/compiler/src/main/java/android/databinding/tool/reflection/InjectedClass.kt
index d3103c3e..a7d94846 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/InjectedClass.kt
+++ b/compiler/src/main/java/android/databinding/tool/reflection/InjectedClass.kt
@@ -102,8 +102,7 @@ class InjectedClass(private val mClassName: String, private val mSuperClass: Str
val setName = "set" + capName!!
val getName = "get$capName"
addMethod(InjectedMethod(this, false, getName, imports, type))
- addMethod(InjectedMethod(this, false, setName, imports,
- "void", type))
+ addMethod(InjectedMethod(this, false, setName, imports, "void", type))
}
fun addField(name: String, type: String) {
diff --git a/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt b/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt
index 4d0786ae..86022293 100644
--- a/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt
+++ b/compiler/src/main/java/android/databinding/tool/reflection/ModelAnalyzer.kt
@@ -118,14 +118,18 @@ abstract class ModelAnalyzer protected constructor(@JvmField val libTypes: LibTy
fun getDefaultValue(className: String) = DEFAULT_VALUES[className] ?: "null"
- fun findClass(className: String, imports: ImportBag?): ModelClass? {
- return if (mInjectedClasses.containsKey(className)) {
+ val classFinderCache = ClassFinderCache { className, imports ->
+ if (mInjectedClasses.containsKey(className)) {
mInjectedClasses[className]
} else {
findClassInternal(className, imports)
}
}
+ fun findClass(className: String, imports: ImportBag?): ModelClass? {
+ return classFinderCache.find(className, imports)
+ }
+
abstract fun findClassInternal(className: String, importBag: ImportBag?): ModelClass
abstract fun findClass(classType: Class<*>): ModelClass
diff --git a/compiler/src/test/java/android/databinding/tool/reflection/ClassFinderCacheTest.kt b/compiler/src/test/java/android/databinding/tool/reflection/ClassFinderCacheTest.kt
new file mode 100644
index 00000000..b6b4df62
--- /dev/null
+++ b/compiler/src/test/java/android/databinding/tool/reflection/ClassFinderCacheTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.reflection
+
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.nullValue
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito
+
+@RunWith(JUnit4::class)
+class ClassFinderCacheTest {
+ private val loggingFinder = LoggingFinder()
+ @Test
+ fun simple_fail() {
+ val cache = ClassFinderCache(loggingFinder::doFind)
+ val result = cache.find("foo", null)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", null)
+ )
+ ))
+ assertThat(result, nullValue())
+ }
+
+ @Test
+ fun simple_succeed() {
+ val cache = ClassFinderCache(loggingFinder::doFind)
+ val fake = loggingFinder.addClassFor("foo")
+ val result = cache.find("foo", null)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", null)
+ )
+ ))
+ assertThat(result, `is`(fake))
+ cache.find("foo", null)
+ // should return from cache
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", null)
+ )
+ ))
+ }
+
+ @Test
+ fun import_different() {
+ val cache = ClassFinderCache(loggingFinder::doFind)
+ val fake = loggingFinder.addClassFor("foo")
+ val result = cache.find("foo", null)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", null)
+ )
+ ))
+ assertThat(result, `is`(fake))
+ val result2 = cache.find("foo", ImportBag.EMPTY)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", null),
+ DoFindCall("foo", ImportBag.EMPTY.toImmutable())
+ )
+ ))
+ assertThat(result2, `is`(fake))
+ }
+
+ @Test
+ fun import_mutated() {
+ val cache = ClassFinderCache(loggingFinder::doFind)
+ val fake = loggingFinder.addClassFor("foo")
+ val imports = MutableImportBag().also {
+ it.put("baz", "foo.baz")
+ }
+ val imports1 = imports.toImmutable()
+ val result = cache.find("foo", imports)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", imports1)
+ )
+ ))
+ assertThat(result, `is`(fake))
+
+ imports.put("bazar", "foo.bazar")
+ val imports2 = imports.toImmutable()
+ val result2 = cache.find("foo", imports2)
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", imports1),
+ DoFindCall("foo", imports2)
+ )
+ ))
+ assertThat(result2, `is`(fake))
+
+ val result3 = cache.find("foo", MutableImportBag().also {
+ it.put("baz", "foo.baz")
+ })
+ // should come from cache
+ assertThat(loggingFinder.calls, `is`(
+ listOf(
+ DoFindCall("foo", imports1),
+ DoFindCall("foo", imports2)
+ )
+ ))
+ assertThat(result3, `is`(fake))
+
+
+ }
+
+ class LoggingFinder {
+ val calls = arrayListOf<DoFindCall>()
+ private val classes = mutableMapOf<String, ModelClass>()
+ fun doFind(className: String, imports: ImportBag?): ModelClass? {
+ calls.add(DoFindCall(
+ className = className,
+ imports = imports?.toImmutable()
+ ))
+ return classes[className]
+ }
+
+ fun addClassFor(className: String): ModelClass {
+ val mock = Mockito.mock(ModelClass::class.java)
+ classes[className] = mock
+ return mock
+ }
+ }
+
+ data class DoFindCall(val className: String, val imports: ImmutableImportBag?)
+} \ No newline at end of file