Skip to content

Commit 2753a12

Browse files
authored
Implement snapshot changes analysis by looking up the slot table (#147)
1 parent 20724ec commit 2753a12

File tree

18 files changed

+734
-72
lines changed

18 files changed

+734
-72
lines changed

app/src/main/kotlin/com/skydoves/myapplication/ExampleRecompositionTracking.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import com.skydoves.myapplication.models.UnstableUser
4646
* ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
4747
* 4. Run your app and check Logcat for "Recomposition" tag
4848
*/
49+
@TraceRecomposition(traceStates = true)
4950
@Composable
5051
fun RecompositionTrackingExample() {
5152
var counter by remember { mutableIntStateOf(0) }

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
5151

5252
# Maven publishing
5353
GROUP=com.github.skydoves
54-
VERSION_NAME=0.7.2
54+
VERSION_NAME=0.7.3-SNAPSHOT
5555

5656
POM_URL=https://github.com/skydoves/compose-stability-analyzer/
5757
POM_SCM_URL=https://github.com/skydoves/compose-stability-analyzer/

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ androidGradlePlugin = "8.13.1"
1313
androidxActivity = "1.11.0"
1414
androidxComposeBom = "2025.12.00"
1515
jetbrains-compose = "1.9.3"
16-
compose-stability-analyzer = "0.7.0"
16+
compose-stability-analyzer = "0.7.2"
1717
runtimeAnnotation = "1.9.0"
1818
spotless = "6.21.0"
1919
shadow = "8.1.1"

stability-compiler/api/stability-compiler.api

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ public final class com/skydoves/compose/stability/compiler/StabilityReport$Compa
156156
public final class com/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder {
157157
public fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;)V
158158
public final fun initializeSymbols ()Z
159-
public final fun injectTrackingCode (Lorg/jetbrains/kotlin/ir/declarations/IrFunction;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;)Z
159+
public final fun injectTrackingCode (Lorg/jetbrains/kotlin/ir/declarations/IrFunction;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;Ljava/util/List;)Z
160+
public static synthetic fun injectTrackingCode$default (Lcom/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder;Lorg/jetbrains/kotlin/ir/declarations/IrFunction;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;Ljava/util/List;ILjava/lang/Object;)Z
160161
}
161162

162163
public final class com/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder$ParameterStabilityData {
@@ -176,6 +177,26 @@ public final class com/skydoves/compose/stability/compiler/lower/RecompositionIr
176177
public fun toString ()Ljava/lang/String;
177178
}
178179

180+
public final class com/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder$StateVariableData {
181+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/kotlin/ir/declarations/IrVariable;ZLorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;)V
182+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/kotlin/ir/declarations/IrVariable;ZLorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
183+
public final fun component1 ()Ljava/lang/String;
184+
public final fun component2 ()Ljava/lang/String;
185+
public final fun component3 ()Lorg/jetbrains/kotlin/ir/declarations/IrVariable;
186+
public final fun component4 ()Z
187+
public final fun component5 ()Lorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;
188+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/kotlin/ir/declarations/IrVariable;ZLorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;)Lcom/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder$StateVariableData;
189+
public static synthetic fun copy$default (Lcom/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder$StateVariableData;Ljava/lang/String;Ljava/lang/String;Lorg/jetbrains/kotlin/ir/declarations/IrVariable;ZLorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;ILjava/lang/Object;)Lcom/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder$StateVariableData;
190+
public fun equals (Ljava/lang/Object;)Z
191+
public final fun getGetter ()Lorg/jetbrains/kotlin/ir/declarations/IrSimpleFunction;
192+
public final fun getName ()Ljava/lang/String;
193+
public final fun getTypeString ()Ljava/lang/String;
194+
public final fun getVariable ()Lorg/jetbrains/kotlin/ir/declarations/IrVariable;
195+
public fun hashCode ()I
196+
public final fun isDelegated ()Z
197+
public fun toString ()Ljava/lang/String;
198+
}
199+
179200
public final class com/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer : org/jetbrains/kotlin/backend/common/IrElementTransformerVoidWithContext {
180201
public static final field Companion Lcom/skydoves/compose/stability/compiler/lower/StabilityAnalyzerTransformer$Companion;
181202
public fun <init> (Lorg/jetbrains/kotlin/backend/common/extensions/IrPluginContext;Lcom/skydoves/compose/stability/compiler/StabilityInfoCollector;Ljava/util/List;)V

stability-compiler/src/main/kotlin/com/skydoves/compose/stability/compiler/lower/RecompositionIrBuilder.kt

Lines changed: 172 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@ import org.jetbrains.kotlin.ir.builders.irGet
2727
import org.jetbrains.kotlin.ir.builders.irInt
2828
import org.jetbrains.kotlin.ir.builders.irString
2929
import org.jetbrains.kotlin.ir.declarations.IrFunction
30+
import org.jetbrains.kotlin.ir.declarations.IrLocalDelegatedProperty
31+
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
3032
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
3133
import org.jetbrains.kotlin.ir.declarations.IrVariable
34+
import org.jetbrains.kotlin.ir.expressions.IrBlock
3235
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
3336
import org.jetbrains.kotlin.ir.expressions.IrExpression
37+
import org.jetbrains.kotlin.ir.expressions.IrWhen
3438
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
3539
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
3640
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
@@ -64,6 +68,7 @@ public class RecompositionIrBuilder(
6468
private var trackerClassSymbol: IrClassSymbol? = null
6569
private var rememberTrackerFunctionSymbol: IrSimpleFunctionSymbol? = null
6670
private var trackParameterFunctionSymbol: IrSimpleFunctionSymbol? = null
71+
private var trackStateFunctionSymbol: IrSimpleFunctionSymbol? = null
6772
private var logIfThresholdMetFunctionSymbol: IrSimpleFunctionSymbol? = null
6873

6974
/**
@@ -104,6 +109,12 @@ public class RecompositionIrBuilder(
104109
return false
105110
}
106111

112+
trackStateFunctionSymbol = trackerClass?.functions?.firstOrNull {
113+
it.name.asString() == "trackState"
114+
}?.symbol
115+
116+
// trackState is optional - don't fail if missing (backward compat)
117+
107118
logIfThresholdMetFunctionSymbol = trackerClass?.functions?.firstOrNull {
108119
it.name.asString() == "logIfThresholdMet"
109120
}?.symbol
@@ -136,52 +147,154 @@ public class RecompositionIrBuilder(
136147
tag: String,
137148
threshold: Int,
138149
parameterStabilities: List<ParameterStabilityData>,
150+
stateVariables: List<StateVariableData> = emptyList(),
139151
): Boolean {
140152
try {
141153
val body = function.body as? IrBlockBody ?: return false
142154

143155
val builder = DeclarationIrBuilder(context, function.symbol)
144156

145-
// Create new body with tracking code prepended
146-
val newStatements = mutableListOf<org.jetbrains.kotlin.ir.IrStatement>()
147-
148-
// 1. Create tracker variable: val _tracker = remember { ... }
157+
// 1. Create tracker variable: val _tracker = rememberRecompositionTracker(...)
149158
val trackerVariable = createTrackerVariable(
150159
builder = builder,
151160
function = function,
152161
functionName = functionName,
153162
tag = tag,
154163
threshold = threshold,
155164
)
156-
newStatements.add(trackerVariable)
157165

158-
// 2. Add trackParameter calls for each parameter
159-
parameterStabilities.forEach { paramData ->
160-
val trackParameterCall = createTrackParameterCall(
166+
// 2. Create trackParameter calls for each parameter
167+
val trackParameterCalls = parameterStabilities.map { paramData ->
168+
createTrackParameterCall(
161169
builder = builder,
162170
trackerVariable = trackerVariable,
163171
paramData = paramData,
164172
)
165-
newStatements.add(trackParameterCall)
166173
}
167174

168-
// 3. Add logIfThresholdMet call
175+
// 3. Create logIfThresholdMet call
169176
val logCall = createLogIfThresholdMetCall(builder, trackerVariable)
170-
newStatements.add(logCall)
171177

172-
// 4. Add original body statements
173-
newStatements.addAll(body.statements)
178+
if (stateVariables.isEmpty()) {
179+
// Standard mode: prepend tracker + params + log, then body
180+
val newStatements = mutableListOf<org.jetbrains.kotlin.ir.IrStatement>()
181+
newStatements.add(trackerVariable)
182+
newStatements.addAll(trackParameterCalls)
183+
newStatements.add(logCall)
184+
newStatements.addAll(body.statements)
185+
186+
body.statements.clear()
187+
body.statements.addAll(newStatements)
188+
} else {
189+
// State tracking mode: interleave trackState calls after
190+
// state variable declarations (may be nested in IrBlocks
191+
// due to Compose compiler lowering), and move
192+
// logIfThresholdMet to the end
193+
val stateVarSet = stateVariables.associateBy {
194+
it.variable
195+
}
196+
197+
injectTrackStateRecursive(
198+
body.statements,
199+
builder,
200+
trackerVariable,
201+
stateVarSet,
202+
)
174203

175-
// Replace body with new statements
176-
body.statements.clear()
177-
body.statements.addAll(newStatements)
204+
val newStatements =
205+
mutableListOf<org.jetbrains.kotlin.ir.IrStatement>()
206+
newStatements.add(trackerVariable)
207+
newStatements.addAll(trackParameterCalls)
208+
newStatements.addAll(body.statements)
209+
newStatements.add(logCall)
210+
211+
body.statements.clear()
212+
body.statements.addAll(newStatements)
213+
}
178214

179215
return true
180216
} catch (e: Exception) {
181217
return false
182218
}
183219
}
184220

221+
/**
222+
* Recursively walks IR statements and inserts trackState calls
223+
* after detected state variable declarations. Handles IrBlock
224+
* nesting from Compose compiler lowering.
225+
*/
226+
private fun injectTrackStateRecursive(
227+
statements: MutableList<org.jetbrains.kotlin.ir.IrStatement>,
228+
builder: IrBuilderWithScope,
229+
trackerVariable: IrVariable,
230+
stateVarSet: Map<IrVariable, StateVariableData>,
231+
) {
232+
var i = 0
233+
while (i < statements.size) {
234+
val stmt = statements[i]
235+
236+
// Recurse into blocks
237+
if (stmt is IrBlock) {
238+
injectTrackStateRecursive(
239+
stmt.statements,
240+
builder,
241+
trackerVariable,
242+
stateVarSet,
243+
)
244+
}
245+
246+
// Recurse into when branches
247+
if (stmt is IrWhen) {
248+
for (branch in stmt.branches) {
249+
val branchResult = branch.result
250+
if (branchResult is IrBlock) {
251+
injectTrackStateRecursive(
252+
branchResult.statements,
253+
builder,
254+
trackerVariable,
255+
stateVarSet,
256+
)
257+
}
258+
}
259+
}
260+
261+
// Check if this is a detected state variable (IrVariable)
262+
val variable = stmt as? IrVariable
263+
if (variable != null && variable in stateVarSet) {
264+
val stateData = stateVarSet[variable]!!
265+
val trackStateCall = createTrackStateCall(
266+
builder,
267+
trackerVariable,
268+
stateData,
269+
)
270+
if (trackStateCall != null) {
271+
statements.add(i + 1, trackStateCall)
272+
i++
273+
}
274+
}
275+
276+
// Check IrLocalDelegatedProperty (delegate is our key)
277+
val delegatedProp = stmt as? IrLocalDelegatedProperty
278+
if (delegatedProp != null) {
279+
val delegate = delegatedProp.delegate
280+
if (delegate in stateVarSet) {
281+
val stateData = stateVarSet[delegate]!!
282+
val trackStateCall = createTrackStateCall(
283+
builder,
284+
trackerVariable,
285+
stateData,
286+
)
287+
if (trackStateCall != null) {
288+
statements.add(i + 1, trackStateCall)
289+
i++
290+
}
291+
}
292+
}
293+
294+
i++
295+
}
296+
}
297+
185298
/**
186299
* Creates an IrVariable for the tracker.
187300
*/
@@ -275,6 +388,38 @@ public class RecompositionIrBuilder(
275388
return call
276389
}
277390

391+
/**
392+
* Creates IR call to `tracker.trackState(name, type, value)`.
393+
*
394+
* For delegated state variables (var x by ...), reads the variable directly (already unwrapped).
395+
* For non-delegated state variables (val s = mutableStateOf()),
396+
* reads s.value via property getter.
397+
*/
398+
private fun createTrackStateCall(
399+
builder: IrBuilderWithScope,
400+
trackerVariable: IrVariable,
401+
stateData: StateVariableData,
402+
): IrExpression? {
403+
val trackStateSymbol = trackStateFunctionSymbol ?: return null
404+
405+
val call = builder.irCall(trackStateSymbol)
406+
call.arguments[0] = builder.irGet(trackerVariable) // dispatch receiver
407+
call.arguments[1] = builder.irString(stateData.name) // name: String
408+
call.arguments[2] = builder.irString(stateData.typeString) // type: String
409+
410+
if (stateData.getter != null) {
411+
// Use the getter from IrLocalDelegatedProperty to read
412+
// the unwrapped value (calls getValue on the delegate)
413+
val getterCall = builder.irCall(stateData.getter.symbol)
414+
call.arguments[3] = getterCall
415+
} else {
416+
// Fallback: read the variable directly
417+
call.arguments[3] = builder.irGet(stateData.variable)
418+
}
419+
420+
return call
421+
}
422+
278423
/**
279424
* Data class holding parameter information for tracking.
280425
*/
@@ -284,4 +429,15 @@ public class RecompositionIrBuilder(
284429
val parameter: IrValueParameter,
285430
val stability: ParameterStability,
286431
)
432+
433+
/**
434+
* Data class holding state variable information for tracking.
435+
*/
436+
public data class StateVariableData(
437+
val name: String,
438+
val typeString: String,
439+
val variable: IrVariable,
440+
val isDelegated: Boolean,
441+
val getter: IrSimpleFunction? = null,
442+
)
287443
}

0 commit comments

Comments
 (0)