diff options
author | Yigit Boyar <yboyar@google.com> | 2018-10-24 10:58:39 +0100 |
---|---|---|
committer | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-10-29 23:49:49 +0000 |
commit | dc87cb65521a02cebce8c73e5d9b8dc5d4811c1e (patch) | |
tree | 711caee5d99c6125f4a6fbdac6851fb253022c5c | |
parent | c063c66bd12a6f5a818761ed39cb5831683547b0 (diff) | |
download | data-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
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 |