Skip to content

Commit

Permalink
Implement State change tracking feature (#55)
Browse files Browse the repository at this point in the history
* Runtime logic and IR improvements

* Add state change tracking

Resolves #39

* Fix tests
  • Loading branch information
jisungbin committed Dec 1, 2023
1 parent a2d591d commit db33af7
Show file tree
Hide file tree
Showing 25 changed files with 735 additions and 314 deletions.
4 changes: 2 additions & 2 deletions compiler-integration-test/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Designed and developed by Duckie Team 2023.
~ Designed and developed by Ji Sungbin 2023.
~
~ Licensed under the MIT.
~ Please see full license: https://github.com/duckie-team/quack-quack-android/blob/main/LICENSE
~ Please see full license: https://github.com/jisungbin/ComposeInvestigator/blob/main/LICENSE
-->

<manifest xmlns:android="http:https://schemas.android.com/apk/res/android">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,25 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun InvalidationProcessedParameterChangedRoot() {
fun InvalidationProcessedRoot_StateDelegateReference() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count = 1 }) {}
InvalidationProcessedParameterChangedChild(count)
InvalidationProcessedChild_StateDelegateReference(count)
}

@Composable
private fun InvalidationProcessedParameterChangedChild(count: Int) {
private fun InvalidationProcessedChild_StateDelegateReference(count: Int) {
Text(text = "$count")
}

@Composable
fun InvalidationProcessedRoot_StateDirectReference() {
val count = remember { mutableIntStateOf(0) }
Button(onClick = { count.intValue = 1 }) {}
InvalidationProcessedChild_StateDirectReference(count.intValue)
}

@Composable
private fun InvalidationProcessedChild_StateDirectReference(count: Int) {
Text(text = "$count")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

package land.sungbin.composeinvestigator.compiler.test.source.logger

import land.sungbin.composeinvestigator.runtime.AffectedComposable
import land.sungbin.composeinvestigator.runtime.ComposableInvalidationLogger
import land.sungbin.composeinvestigator.runtime.ComposableInvalidationType
import land.sungbin.composeinvestigator.runtime.affect.AffectedComposable

val invalidationLog = mutableMapOf<AffectedComposable, MutableList<ComposableInvalidationType>>()

fun clearInvalidationLog() {
invalidationLog.clear()
}

fun findInvalidationLog(composableName: String): List<ComposableInvalidationType> =
invalidationLog.filterKeys { composable -> composable.name == composableName }.values.flatten()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentRecomposeScope
import land.sungbin.composeinvestigator.runtime.AffectedComposable
import land.sungbin.composeinvestigator.runtime.ComposableInvalidationType
import land.sungbin.composeinvestigator.runtime.affect.AffectedComposable
import land.sungbin.composeinvestigator.runtime.currentComposableInvalidationTracker

val invalidationListensViaManualRegister = mutableMapOf<AffectedComposable, MutableList<ComposableInvalidationType>>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
@file:Suppress("TestFunctionName", "ComposableNaming", "unused")

package land.sungbin.composeinvestigator.compiler.test.source

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@Composable
fun InvalidationProcessedRoot_StateDelegateReference() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count = 1 }) {}
InvalidationProcessedChild(count)
}

@Composable
fun InvalidationProcessedRoot_StateDirectReference() {
val count = remember { mutableIntStateOf(0) }
Button(onClick = { count.intValue = 1 }) {}
InvalidationProcessedChild(count.intValue)
}

@Composable
private fun InvalidationProcessedChild(count: Int) {
Text(text = "$count")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Please see full license: https://github.com/jisungbin/ComposeInvestigator/blob/main/LICENSE
*/

package land.sungbin.composeinvestigator.compiler.test.tracker
package land.sungbin.composeinvestigator.compiler.test.stability

import androidx.compose.compiler.plugins.kotlin.analysis.Stability
import io.kotest.core.spec.style.FunSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.kotest.matchers.collections.shouldBeSameSizeAs
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import land.sungbin.composeinvestigator.compiler.test.source.logger.InvalidationProcessedParameterChangedRoot
import land.sungbin.composeinvestigator.compiler.test.source.logger.InvalidationProcessedRoot_StateDelegateReference
import land.sungbin.composeinvestigator.compiler.test.source.logger.InvalidationProcessedRoot_StateDirectReference
import land.sungbin.composeinvestigator.compiler.test.source.logger.InvalidationSkippedRoot
import land.sungbin.composeinvestigator.compiler.test.source.logger.InvalidationSkippedRoot_CustomName
import land.sungbin.composeinvestigator.compiler.test.source.logger.findInvalidationLog
import land.sungbin.composeinvestigator.runtime.ChangedFieldPair
import land.sungbin.composeinvestigator.runtime.ComposableInvalidationType
import land.sungbin.composeinvestigator.runtime.DeclarationStability
import land.sungbin.composeinvestigator.runtime.InvalidationReason
import land.sungbin.composeinvestigator.runtime.ParameterInfo
import land.sungbin.composeinvestigator.runtime.affect.AffectedField
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -32,6 +34,9 @@ class InvalidationLoggerTest {
@get:Rule
val compose = createAndroidComposeRule<ComponentActivity>()

@get:Rule
val loggerTest = InvalidationLoggerTestRule()

@Test
fun invalidation_skipped() {
compose.setContent { InvalidationSkippedRoot() }
Expand Down Expand Up @@ -73,36 +78,114 @@ class InvalidationLoggerTest {
}

@Test
fun invalidation_processed_unknown_parameter_changed() {
compose.setContent { InvalidationProcessedParameterChangedRoot() }
fun invalidation_processed_state_delegate() {
compose.setContent { InvalidationProcessedRoot_StateDelegateReference() }
compose.onNode(hasClickAction()).performClick()

compose.runOnIdle {
val rootLogs = findInvalidationLog("InvalidationProcessedParameterChangedRoot")
val childLogs = findInvalidationLog("InvalidationProcessedParameterChangedChild")
val rootLogs = findInvalidationLog("InvalidationProcessedRoot_StateDelegateReference")
val childLogs = findInvalidationLog("InvalidationProcessedChild_StateDelegateReference")

rootLogs shouldHaveSize 2
rootLogs shouldBeSameSizeAs childLogs

rootLogs[0] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Initial)
rootLogs[1] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Unknown(params = emptyList()))
rootLogs[1] shouldBe ComposableInvalidationType.Processed(
InvalidationReason.FieldChanged(
changed = listOf(
ChangedFieldPair(
old = AffectedField.StateProperty(
name = "count",
valueString = "0",
valueHashCode = 0,
),
new = AffectedField.StateProperty(
name = "count",
valueString = "1",
valueHashCode = 1,
),
),
),
),
)

childLogs[0] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Initial)
childLogs[1] shouldBe ComposableInvalidationType.Processed(
InvalidationReason.FieldChanged(
changed = listOf(
ChangedFieldPair(
old = AffectedField.ValueParameter(
name = "count",
valueString = "0",
valueHashCode = 0,
stability = DeclarationStability.Stable,
),
new = AffectedField.ValueParameter(
name = "count",
valueString = "1",
valueHashCode = 1,
stability = DeclarationStability.Stable,
),
),
),
),
)
}
}

@Test
fun invalidation_processed_state_direct() {
compose.setContent { InvalidationProcessedRoot_StateDirectReference() }
compose.onNode(hasClickAction()).performClick()

compose.runOnIdle {
val rootLogs = findInvalidationLog("InvalidationProcessedRoot_StateDirectReference")
val childLogs = findInvalidationLog("InvalidationProcessedChild_StateDirectReference")

rootLogs shouldHaveSize 2
rootLogs shouldBeSameSizeAs childLogs

rootLogs[0] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Initial)
rootLogs[1] shouldBe ComposableInvalidationType.Processed(
InvalidationReason.FieldChanged(
changed = listOf(
ChangedFieldPair(
old = AffectedField.StateProperty(
name = "count",
valueString = "0",
valueHashCode = 0,
),
new = AffectedField.StateProperty(
name = "count",
valueString = "1",
valueHashCode = 1,
),
),
),
),
)

childLogs[0] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Initial)
childLogs[1] shouldBe ComposableInvalidationType.Processed(InvalidationReason.ParameterChanged(
changedParams = listOf(
ParameterInfo(
name = "count",
valueString = "0",
valueHashCode = 0,
stability = DeclarationStability.Stable,
) to ParameterInfo(
name = "count",
valueString = "1",
valueHashCode = 1,
stability = DeclarationStability.Stable,
)
childLogs[1] shouldBe ComposableInvalidationType.Processed(
InvalidationReason.FieldChanged(
changed = listOf(
ChangedFieldPair(
old = AffectedField.ValueParameter(
name = "count",
valueString = "0",
valueHashCode = 0,
stability = DeclarationStability.Stable,
),
new = AffectedField.ValueParameter(
name = "count",
valueString = "1",
valueHashCode = 1,
stability = DeclarationStability.Stable,
),
),
),
),
))
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Designed and developed by Ji Sungbin 2023.
*
* Licensed under the MIT.
* Please see full license: https://github.com/jisungbin/ComposeInvestigator/blob/main/LICENSE
*/

package land.sungbin.composeinvestigator.compiler.test.tracker.logger

import land.sungbin.composeinvestigator.compiler.test.source.logger.clearInvalidationLog
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class InvalidationLoggerTestRule : TestWatcher() {
override fun finished(description: Description?) {
clearInvalidationLog()
super.finished(description)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.kotest.matchers.collections.shouldBeSameSizeAs
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import land.sungbin.composeinvestigator.compiler.test.source.table.callback.RegisterListener_InvalidationSkippedRoot
import land.sungbin.composeinvestigator.compiler.test.source.table.callback.findInvalidationListensViaManualRegister
import land.sungbin.composeinvestigator.runtime.ComposableInvalidationType
Expand All @@ -37,12 +37,17 @@ class InvalidationCallbackTest {
val rootListens = findInvalidationListensViaManualRegister("RegisterListener_InvalidationSkippedRoot")
val childListens = findInvalidationListensViaManualRegister("RegisterListener_InvalidationSkippedChild")

rootListens shouldHaveSize 1
rootListens shouldHaveSize 2
rootListens shouldBeSameSizeAs childListens

// The initial composition is not callbacked because the listener is registered after the initial composition (the first run of the composable).
rootListens[0] shouldBe ComposableInvalidationType.Processed(InvalidationReason.Unknown(params = emptyList()))
childListens[0] shouldBe ComposableInvalidationType.Skipped
rootListens shouldContainExactly listOf(
ComposableInvalidationType.Processed(InvalidationReason.Initial),
ComposableInvalidationType.Processed(InvalidationReason.Unknown()),
)
childListens shouldContainExactly listOf(
ComposableInvalidationType.Processed(InvalidationReason.Initial),
ComposableInvalidationType.Skipped,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:Suppress("NOTHING_TO_INLINE")
@file:Suppress("NOTHING_TO_INLINE", "unused")

package land.sungbin.composeinvestigator.compiler.test.utils

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ package land.sungbin.composeinvestigator.compiler

import androidx.compose.compiler.plugins.kotlin.analysis.StabilityInferencer
import land.sungbin.composeinvestigator.compiler.internal.tracker.InvalidationTrackableTransformer
import land.sungbin.composeinvestigator.compiler.internal.tracker.affect.IrAffectedField
import land.sungbin.composeinvestigator.compiler.internal.tracker.affect.IrAffectedComposable
import land.sungbin.composeinvestigator.compiler.internal.tracker.key.DurableFunctionKeyTransformer
import land.sungbin.composeinvestigator.compiler.internal.tracker.logger.InvalidationLogger
import land.sungbin.composeinvestigator.compiler.internal.tracker.logger.InvalidationLoggerVisitor
import land.sungbin.composeinvestigator.compiler.internal.tracker.logger.IrInvalidationLogger
import land.sungbin.composeinvestigator.compiler.util.VerboseLogger
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
Expand All @@ -21,20 +23,26 @@ import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
internal class InvalidationTrackingExtension(private val logger: VerboseLogger) : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
try {
InvalidationLogger.init(pluginContext)
IrInvalidationLogger.init(pluginContext)
IrAffectedField.init(pluginContext)
IrAffectedComposable.init(pluginContext)

moduleFragment.transformChildrenVoid(InvalidationLoggerVisitor(pluginContext, logger))

if (InvalidationLogger.getCurrentLoggerSymbolOrNull() == null) {
InvalidationLogger.useDefaultLogger(pluginContext)
if (IrInvalidationLogger.getCurrentLoggerSymbolOrNull() == null) {
IrInvalidationLogger.useDefaultLogger(pluginContext)
}
} finally {
moduleFragment.transformChildrenVoid(DurableFunctionKeyTransformer(pluginContext))
moduleFragment.transformChildrenVoid(
InvalidationTrackableTransformer(
context = pluginContext,
logger = logger,
// TODO: Supports externalStableTypeMatchers (StabilityInferencer)
stabilityInferencer = StabilityInferencer(moduleFragment.descriptor, emptySet()),
stabilityInferencer = StabilityInferencer(
currentModule = moduleFragment.descriptor,
// TODO: support this field
externalStableTypeMatchers = emptySet(),
),
),
)
}
Expand Down
Loading

0 comments on commit db33af7

Please sign in to comment.