-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add leak detection API and module (#2191)
This can be used to watch a reference when it is known to no longer be used, and to check whether or not it was collected by the Kotlin garbage collector or not.
- Loading branch information
1 parent
16dc069
commit d5defde
Showing
35 changed files
with
1,224 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import app.cash.redwood.buildsupport.ZiplineAppEmbedTask | ||
|
||
apply plugin: 'org.jetbrains.kotlin.multiplatform' | ||
apply plugin: 'app.cash.zipline' | ||
|
||
zipline { | ||
// This is a test-only contract. | ||
apiTracking = false | ||
} | ||
|
||
kotlin { | ||
js("guest") { | ||
nodejs() | ||
binaries.executable() | ||
} | ||
jvm("host") | ||
|
||
sourceSets { | ||
commonMain { | ||
dependencies { | ||
implementation libs.zipline | ||
} | ||
} | ||
commonTest { | ||
dependencies { | ||
implementation libs.kotlin.test | ||
} | ||
} | ||
guestMain { | ||
dependencies { | ||
implementation projects.redwoodLeakDetector | ||
implementation libs.assertk | ||
} | ||
} | ||
hostTest { | ||
dependencies { | ||
implementation libs.okio | ||
implementation libs.zipline.loader | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Compile JS tests into a Zipline application so we can test inside QuickJS VM in the JVM tests. | ||
def ziplineAppName = 'leaks' | ||
def ziplineCompile = tasks.named('compileDevelopmentExecutableKotlinGuestZipline') | ||
def ziplineEmbed = tasks.register('ziplineEmbed', ZiplineAppEmbedTask) { | ||
files.setFrom(ziplineCompile) | ||
appName = ziplineAppName | ||
outputDirectory = layout.buildDirectory.dir('generated/ziplineEmbed') | ||
} | ||
tasks.named('hostTest', Test) { | ||
// Explicit dependsOn and input because systemProperty doesn't accept providers. | ||
dependsOn(ziplineEmbed) | ||
inputs.dir(ziplineEmbed.get().outputDirectory) | ||
|
||
systemProperty('ziplineDir', ziplineEmbed.get().outputDirectory.get().asFile.absolutePath) | ||
systemProperty('ziplineAppName', ziplineAppName) | ||
} |
31 changes: 31 additions & 0 deletions
31
...line-test/src/commonMain/kotlin/app/cash/redwood/leaks/zipline/LeakDetectorTestService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/* | ||
* Copyright (C) 2024 Square, Inc. | ||
* | ||
* 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 app.cash.redwood.leaks.zipline | ||
|
||
import app.cash.zipline.ZiplineService | ||
|
||
interface LeakDetectorTestService : ZiplineService { | ||
fun leakDetectorDisabled() | ||
|
||
// TODO Once QuickJS supports WeakRef, enable regular tests: | ||
// suspend fun detectImmediateCollection() | ||
// suspend fun detectDelayedCollection() | ||
// suspend fun detectLeak() | ||
|
||
companion object { | ||
const val SERVICE_NAME = "leakDetectorTest" | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
...e-test/src/guestMain/kotlin/app/cash/redwood/leaks/zipline/LeakDetectorTestServiceImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* | ||
* Copyright (C) 2024 Square, Inc. | ||
* | ||
* 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 app.cash.redwood.leaks.zipline | ||
|
||
import app.cash.redwood.leaks.LeakDetector | ||
import app.cash.redwood.leaks.LeakListener | ||
import app.cash.redwood.leaks.zipline.LeakDetectorTestService.Companion.SERVICE_NAME | ||
import app.cash.zipline.Zipline | ||
import assertk.assertThat | ||
import assertk.assertions.isSameInstanceAs | ||
import kotlin.time.Duration | ||
import kotlin.time.Duration.Companion.seconds | ||
import kotlin.time.TimeSource | ||
|
||
class LeakDetectorTestServiceImpl : LeakDetectorTestService { | ||
override fun leakDetectorDisabled() { | ||
val leakDetector = LeakDetector.timeBased( | ||
listener = object : LeakListener { | ||
override fun onReferenceCollected(name: String) {} | ||
override fun onReferenceLeaked(name: String, alive: Duration) {} | ||
}, | ||
timeSource = TimeSource.Monotonic, | ||
leakThreshold = 2.seconds, | ||
) | ||
// QuickJS does not support WeakRef which is required for the leak detection to work correctly. | ||
// Once WeakRef is supported and this test starts failing, enable bridging of the real tests. | ||
assertThat(leakDetector).isSameInstanceAs(LeakDetector.None) | ||
} | ||
} | ||
|
||
fun main() { | ||
val zipline = Zipline.get() | ||
zipline.bind<LeakDetectorTestService>(SERVICE_NAME, LeakDetectorTestServiceImpl()) | ||
} |
79 changes: 79 additions & 0 deletions
79
...ipline-test/src/hostTest/kotlin/app/cash/redwood/leaks/zipline/LeakDetectorZiplineTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright (C) 2024 Square, Inc. | ||
* | ||
* 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 app.cash.redwood.leaks.zipline | ||
|
||
import app.cash.redwood.leaks.zipline.LeakDetectorTestService.Companion.SERVICE_NAME | ||
import app.cash.zipline.Zipline | ||
import app.cash.zipline.ZiplineManifest | ||
import app.cash.zipline.loader.FreshnessChecker | ||
import app.cash.zipline.loader.LoadResult.Failure | ||
import app.cash.zipline.loader.LoadResult.Success | ||
import app.cash.zipline.loader.ManifestVerifier.Companion.NO_SIGNATURE_CHECKS | ||
import app.cash.zipline.loader.ZiplineHttpClient | ||
import app.cash.zipline.loader.ZiplineLoader | ||
import kotlin.test.AfterTest | ||
import kotlin.test.BeforeTest | ||
import kotlin.test.Test | ||
import kotlinx.coroutines.Dispatchers.Unconfined | ||
import kotlinx.coroutines.runBlocking | ||
import okio.FileSystem.Companion.SYSTEM | ||
import okio.Path.Companion.toPath | ||
|
||
private val ziplineDir = System.getProperty("ziplineDir")!!.toPath() | ||
private val ziplineAppName = System.getProperty("ziplineAppName")!! | ||
|
||
class LeakDetectorZiplineTest { | ||
private val loader = ZiplineLoader( | ||
dispatcher = Unconfined, | ||
manifestVerifier = NO_SIGNATURE_CHECKS, | ||
httpClient = object : ZiplineHttpClient() { | ||
override suspend fun download( | ||
url: String, | ||
requestHeaders: List<Pair<String, String>>, | ||
) = throw UnsupportedOperationException() | ||
}, | ||
).withEmbedded(SYSTEM, ziplineDir) | ||
|
||
private lateinit var zipline: Zipline | ||
private lateinit var service: LeakDetectorTestService | ||
|
||
@BeforeTest fun before() = runBlocking { | ||
val result = loader.loadOnce( | ||
applicationName = ziplineAppName, | ||
freshnessChecker = object : FreshnessChecker { | ||
override fun isFresh(manifest: ZiplineManifest, freshAtEpochMs: Long) = true | ||
}, | ||
manifestUrl = "http://0.0.0.0:0", | ||
) | ||
|
||
when (result) { | ||
is Failure -> throw result.exception | ||
is Success -> { | ||
zipline = result.zipline | ||
} | ||
} | ||
|
||
service = zipline.take(SERVICE_NAME) | ||
} | ||
|
||
@AfterTest fun after() { | ||
zipline.close() | ||
} | ||
|
||
@Test fun leakDetectorDisabled() { | ||
service.leakDetectorDisabled() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
public abstract interface class app/cash/redwood/leaks/LeakDetector { | ||
public static final field Companion Lapp/cash/redwood/leaks/LeakDetector$Companion; | ||
public abstract fun checkLeaks (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
public abstract fun watchReference (Ljava/lang/Object;Ljava/lang/String;)V | ||
} | ||
|
||
public final class app/cash/redwood/leaks/LeakDetector$Companion { | ||
public final fun timeBased-SxA4cEA (Lapp/cash/redwood/leaks/LeakListener;Lkotlin/time/TimeSource;J)Lapp/cash/redwood/leaks/LeakDetector; | ||
} | ||
|
||
public final class app/cash/redwood/leaks/LeakDetector$None : app/cash/redwood/leaks/LeakDetector { | ||
public static final field INSTANCE Lapp/cash/redwood/leaks/LeakDetector$None; | ||
public fun checkLeaks (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
public fun watchReference (Ljava/lang/Object;Ljava/lang/String;)V | ||
} | ||
|
||
public abstract interface class app/cash/redwood/leaks/LeakListener { | ||
public abstract fun onReferenceCollected (Ljava/lang/String;)V | ||
public abstract fun onReferenceLeaked-HG0u8IE (Ljava/lang/String;J)V | ||
} | ||
|
||
public abstract interface annotation class app/cash/redwood/leaks/RedwoodLeakApi : java/lang/annotation/Annotation { | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Klib ABI Dump | ||
// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, wasmJs] | ||
// Rendering settings: | ||
// - Signature version: 2 | ||
// - Show manifest properties: true | ||
// - Show declarations: true | ||
|
||
// Library unique name: <app.cash.redwood:redwood-leak-detector> | ||
open annotation class app.cash.redwood.leaks/RedwoodLeakApi : kotlin/Annotation { // app.cash.redwood.leaks/RedwoodLeakApi|null[0] | ||
constructor <init>() // app.cash.redwood.leaks/RedwoodLeakApi.<init>|<init>(){}[0] | ||
} | ||
|
||
abstract interface app.cash.redwood.leaks/LeakDetector { // app.cash.redwood.leaks/LeakDetector|null[0] | ||
abstract fun watchReference(kotlin/Any, kotlin/String) // app.cash.redwood.leaks/LeakDetector.watchReference|watchReference(kotlin.Any;kotlin.String){}[0] | ||
abstract suspend fun checkLeaks() // app.cash.redwood.leaks/LeakDetector.checkLeaks|checkLeaks(){}[0] | ||
|
||
final object Companion { // app.cash.redwood.leaks/LeakDetector.Companion|null[0] | ||
final fun timeBased(app.cash.redwood.leaks/LeakListener, kotlin.time/TimeSource, kotlin.time/Duration): app.cash.redwood.leaks/LeakDetector // app.cash.redwood.leaks/LeakDetector.Companion.timeBased|timeBased(app.cash.redwood.leaks.LeakListener;kotlin.time.TimeSource;kotlin.time.Duration){}[0] | ||
} | ||
|
||
final object None : app.cash.redwood.leaks/LeakDetector { // app.cash.redwood.leaks/LeakDetector.None|null[0] | ||
final fun watchReference(kotlin/Any, kotlin/String) // app.cash.redwood.leaks/LeakDetector.None.watchReference|watchReference(kotlin.Any;kotlin.String){}[0] | ||
final suspend fun checkLeaks() // app.cash.redwood.leaks/LeakDetector.None.checkLeaks|checkLeaks(){}[0] | ||
} | ||
} | ||
|
||
abstract interface app.cash.redwood.leaks/LeakListener { // app.cash.redwood.leaks/LeakListener|null[0] | ||
abstract fun onReferenceCollected(kotlin/String) // app.cash.redwood.leaks/LeakListener.onReferenceCollected|onReferenceCollected(kotlin.String){}[0] | ||
abstract fun onReferenceLeaked(kotlin/String, kotlin.time/Duration) // app.cash.redwood.leaks/LeakListener.onReferenceLeaked|onReferenceLeaked(kotlin.String;kotlin.time.Duration){}[0] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import static app.cash.redwood.buildsupport.TargetGroup.Common | ||
|
||
redwoodBuild { | ||
targets(Common) | ||
publishing() | ||
} | ||
|
||
kotlin { | ||
js { | ||
nodejs { | ||
testTask { | ||
useMocha { | ||
// We use up to 10s of wall clock time to test leaks. | ||
timeout = '15s' | ||
// Required for access to V8 GC function. | ||
nodeJsArgs.add('--expose-gc') | ||
} | ||
} | ||
} | ||
} | ||
wasmJs { | ||
nodejs { | ||
testTask { | ||
useMocha { | ||
// We use up to 10s of wall clock time to test leaks. | ||
timeout = '15s' | ||
// Required for access to V8 GC function. | ||
nodeJsArgs.add('--expose-gc') | ||
} | ||
} | ||
} | ||
} | ||
|
||
sourceSets { | ||
commonMain { | ||
dependencies { | ||
implementation libs.kotlinx.coroutines.core | ||
} | ||
} | ||
commonTest { | ||
dependencies { | ||
implementation libs.kotlin.test | ||
implementation libs.kotlinx.coroutines.test | ||
implementation libs.assertk | ||
} | ||
} | ||
} | ||
} | ||
|
||
tasks.named('dokkaHtmlPartial') { | ||
// Don't document this module as it's entirely implementation detail. | ||
enabled = false | ||
} |
25 changes: 25 additions & 0 deletions
25
redwood-leak-detector/src/commonMain/kotlin/app/cash/redwood/leaks/ConcurrentMutableList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright (C) 2024 Square, Inc. | ||
* | ||
* 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 app.cash.redwood.leaks | ||
|
||
@Suppress("unused") // Used by extension functions. | ||
internal expect class ConcurrentMutableList<T> | ||
|
||
internal expect inline fun <T> concurrentMutableListOf(): ConcurrentMutableList<T> | ||
|
||
internal expect inline operator fun <T> ConcurrentMutableList<T>.plusAssign(element: T) | ||
|
||
internal expect inline fun <T> ConcurrentMutableList<T>.removeIf(predicate: (element: T) -> Boolean) |
Oops, something went wrong.