Merge "Fix kdoc issue in Serializer.kt" into androidx-main
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
index 7eda4fc..538cba8 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoConfig.kt
@@ -108,8 +108,44 @@
             )
         }
     }
+
+    /**
+     * Only used by group-internal tests
+     *
+     * Most minimal config possible
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    class MinimalTest(
+        private val appTagPackages: List<String>
+    ) : PerfettoConfig(isTextProto = false) {
+        @RequiresApi(23)
+        override fun writeTo(file: File) {
+            file.writeBytes(
+                configOf(
+                    listOf(
+                        minimalAtraceDataSource(atraceApps = appTagPackages)
+                    )
+                ).validateAndEncode()
+            )
+        }
+    }
 }
 
+private fun minimalAtraceDataSource(
+    atraceApps: List<String>
+) = TraceConfig.DataSource(
+    config = DataSourceConfig(
+        name = "linux.ftrace",
+        target_buffer = 0,
+        ftrace_config = FtraceConfig(
+            ftrace_events = emptyList(),
+            atrace_categories = emptyList(),
+            atrace_apps = atraceApps,
+            compact_sched = null
+        )
+    )
+)
+
 private fun ftraceDataSource(
     atraceApps: List<String>
 ) = TraceConfig.DataSource(
@@ -315,6 +351,27 @@
     return sources
 }
 
+private fun configOf(
+    dataSources: List<TraceConfig.DataSource>
+) = TraceConfig(
+    buffers = listOf(
+        BufferConfig(size_kb = 32768, FillPolicy.RING_BUFFER),
+        BufferConfig(size_kb = 4096, FillPolicy.RING_BUFFER)
+    ),
+    data_sources = dataSources,
+    // periodically dump to file, so we don't overrun our ring buffer
+    // buffers are expected to be big enough for 5 seconds, so conservatively set 2.5 dump
+    write_into_file = true,
+    file_write_period_ms = 2500,
+
+    // multiple of file_write_period_ms, enables trace processor to work in batches
+    flush_period_ms = 5000,
+
+    // reduce timeout to reduce trace capture overhead when devices have data source issues
+    // See b/323601788 and b/307649002.
+    data_source_stop_timeout_ms = 2500,
+)
+
 /**
  * Config for perfetto.
  *
@@ -346,24 +403,7 @@
             config = stackSamplingConfig
         )
     }
-    return TraceConfig(
-        buffers = listOf(
-            BufferConfig(size_kb = 32768, FillPolicy.RING_BUFFER),
-            BufferConfig(size_kb = 4096, FillPolicy.RING_BUFFER)
-        ),
-        data_sources = dataSources,
-        // periodically dump to file, so we don't overrun our ring buffer
-        // buffers are expected to be big enough for 5 seconds, so conservatively set 2.5 dump
-        write_into_file = true,
-        file_write_period_ms = 2500,
-
-        // multiple of file_write_period_ms, enables trace processor to work in batches
-        flush_period_ms = 5000,
-
-        // reduce timeout to reduce trace capture overhead when devices have data source issues
-        // See b/323601788 and b/307649002.
-        data_source_stop_timeout_ms = 2500,
-    )
+    return configOf(dataSources)
 }
 
 @RequiresApi(21) // needed for shell access
diff --git a/benchmark/benchmark-macro-junit4/api/current.txt b/benchmark/benchmark-macro-junit4/api/current.txt
index d4c3495..e06bf57 100644
--- a/benchmark/benchmark-macro-junit4/api/current.txt
+++ b/benchmark/benchmark-macro-junit4/api/current.txt
@@ -19,6 +19,10 @@
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
   }
 
diff --git a/benchmark/benchmark-macro-junit4/api/restricted_current.txt b/benchmark/benchmark-macro-junit4/api/restricted_current.txt
index d4c3495..e06bf57 100644
--- a/benchmark/benchmark-macro-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-macro-junit4/api/restricted_current.txt
@@ -19,6 +19,10 @@
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, @IntRange(from=1L) int iterations, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, optional androidx.benchmark.macro.CompilationMode compilationMode, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, optional androidx.benchmark.macro.StartupMode? startupMode, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, optional androidx.benchmark.macro.CompilationMode compilationMode, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
+    method @SuppressCompatibility @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, androidx.benchmark.perfetto.PerfettoConfig perfettoConfig, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
     method public void measureRepeated(String packageName, java.util.List<? extends androidx.benchmark.macro.Metric> metrics, @IntRange(from=1L) int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> measureBlock);
   }
 
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index c99550b..f2f45e0 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -59,6 +59,17 @@
     // DexMaker has it"s own MockMaker
 }
 
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        // Enable using experimental APIs from within same version group
+        freeCompilerArgs += [
+                "-opt-in=androidx.benchmark.macro.ExperimentalMetricApi",
+                "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
+                "-opt-in=androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi"
+        ]
+    }
+}
+
 androidx {
     name = "Benchmark - Macrobenchmark JUnit4"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
index d8a01b7..6ca8df5 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
@@ -24,6 +24,8 @@
 import androidx.benchmark.macro.Metric
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.macrobenchmarkWithStartupMode
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoConfig
 import androidx.test.rule.GrantPermissionRule
 import org.junit.Assume.assumeTrue
 import org.junit.rules.RuleChain
@@ -94,7 +96,7 @@
      * @param measureBlock The block performing app actions to benchmark each iteration.
      */
     @JvmOverloads
-    public fun measureRepeated(
+    fun measureRepeated(
         packageName: String,
         metrics: List<Metric>,
         compilationMode: CompilationMode = CompilationMode.DEFAULT,
@@ -113,6 +115,78 @@
             compilationMode = compilationMode,
             iterations = iterations,
             startupMode = startupMode,
+            perfettoConfig = null,
+            setupBlock = setupBlock,
+            measureBlock = measureBlock
+        )
+    }
+
+    /**
+     * Measure behavior of the specified [packageName] given a set of [metrics], with a custom
+     * [PerfettoConfig].
+     *
+     * This performs a macrobenchmark with the below control flow:
+     * ```
+     *     resetAppCompilation()
+     *     compile(compilationMode)
+     *     repeat(iterations) {
+     *         setupBlock()
+     *         captureTraceAndMetrics {
+     *             measureBlock()
+     *         }
+     *     }
+     * ```
+     *
+     * Note that a custom [PerfettoConfig]s may result in built-in [Metric]s not working.
+     *
+     * You can see the PerfettoConfig used by a trace (as a text proto) by opening the trace in
+     * [ui.perfetto.dev](http://ui.perfetto.dev), and selecting `Info and Stats` view on the left
+     * panel. You can also generate a custom text proto config by selecting `Record new trace` on
+     * the same panel, selecting recording options, and then clicking `Recording command` to access
+     * the generated text proto.
+     *
+     * @param packageName ApplicationId / Application manifest package name of the app for
+     *   which profiles are generated.
+     * @param metrics List of metrics to measure.
+     * @param compilationMode Mode of compilation used before capturing measurement, such as
+     * [CompilationMode.Partial], defaults to [CompilationMode.DEFAULT].
+     * @param startupMode Optional mode to force app launches performed with
+     * [MacrobenchmarkScope.startActivityAndWait] (and similar variants) to be of the assigned
+     * type. For example, `COLD` launches kill the process before the measureBlock, to ensure
+     * startups will go through full process creation. Generally, leave as null for non-startup
+     * benchmarks.
+     * @param iterations Number of times the [measureBlock] will be run during measurement. Note
+     * that total iteration count may not match, due to warmup iterations needed for the
+     * [compilationMode].
+     * @param perfettoConfig Configuration for Perfetto trace capture during each iteration. Note
+     * that insufficient or invalid configs may result in built-in [Metric]s not working.
+     * @param setupBlock The block performing app actions each iteration, prior to the
+     * [measureBlock]. For example, navigating to a UI where scrolling will be measured.
+     * @param measureBlock The block performing app actions to benchmark each iteration.
+     */
+    @ExperimentalPerfettoCaptureApi
+    @JvmOverloads
+    fun measureRepeated(
+        packageName: String,
+        metrics: List<Metric>,
+        @IntRange(from = 1)
+        iterations: Int,
+        perfettoConfig: PerfettoConfig,
+        compilationMode: CompilationMode = CompilationMode.DEFAULT,
+        startupMode: StartupMode? = null,
+        setupBlock: MacrobenchmarkScope.() -> Unit = {},
+        measureBlock: MacrobenchmarkScope.() -> Unit,
+    ) {
+        macrobenchmarkWithStartupMode(
+            uniqueName = currentDescription.toUniqueName(),
+            className = currentDescription.className,
+            testName = currentDescription.methodName,
+            packageName = packageName,
+            metrics = metrics,
+            compilationMode = compilationMode,
+            iterations = iterations,
+            perfettoConfig = perfettoConfig,
+            startupMode = startupMode,
             setupBlock = setupBlock,
             measureBlock = measureBlock
         )
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index 09a6c7d..50b1662 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -16,8 +16,10 @@
 
 package androidx.benchmark.macro
 
+import android.annotation.SuppressLint
 import androidx.annotation.RequiresApi
 import androidx.benchmark.DeviceInfo
+import androidx.benchmark.perfetto.PerfettoConfig
 import androidx.benchmark.perfetto.PerfettoHelper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -55,6 +57,7 @@
                 compilationMode = CompilationMode.Ignore(),
                 iterations = 1,
                 startupMode = null,
+                perfettoConfig = null,
                 setupBlock = {},
                 measureBlock = {}
             )
@@ -74,6 +77,7 @@
                 compilationMode = CompilationMode.Ignore(),
                 iterations = 0, // invalid
                 startupMode = null,
+                perfettoConfig = null,
                 setupBlock = {},
                 measureBlock = {}
             )
@@ -101,6 +105,7 @@
             compilationMode = CompilationMode.DEFAULT,
             iterations = 2,
             startupMode = startupMode,
+            perfettoConfig = null,
             setupBlock = {
                 opOrder += Block.Setup
                 setupIterations += iteration
@@ -157,6 +162,49 @@
     @Test
     fun callbackBehavior_hot() = validateCallbackBehavior(StartupMode.HOT)
 
+    @SuppressLint("BanThreadSleep") // need non-zero duration to assert sum, regardless of clock
+    private fun validateSlicesCustomConfig(includeMacroAppTag: Boolean) {
+        val atraceApps = if (includeMacroAppTag) {
+            listOf(Packages.TEST)
+        } else {
+            emptyList()
+        }
+        val measurements = macrobenchmarkWithStartupMode(
+            uniqueName = "MacrobenchmarkTest#validateSlicesCustomConfig",
+            className = "MacrobenchmarkTest",
+            testName = "validateCallbackBehavior",
+            packageName = Packages.TARGET,
+            // disable targetPackageOnly filter, since this process emits the event
+            metrics = listOf(TraceSectionMetric(TRACE_LABEL, targetPackageOnly = false)),
+            compilationMode = CompilationMode.DEFAULT,
+            iterations = 3,
+            startupMode = null,
+            perfettoConfig = PerfettoConfig.MinimalTest(atraceApps),
+            setupBlock = { },
+            measureBlock = {
+                trace(TRACE_LABEL) {
+                    Thread.sleep(2)
+                }
+            }
+        ).metrics[TRACE_LABEL + "SumMs"]!!.runs
+
+        assertEquals(3, measurements.size)
+
+        if (includeMacroAppTag) {
+            assertTrue(measurements.all { it > 0.0 })
+        } else {
+            assertEquals(listOf(0.0, 0.0, 0.0), measurements)
+        }
+    }
+
+    @LargeTest
+    @Test
+    fun customConfig_thisProcess() = validateSlicesCustomConfig(includeMacroAppTag = true)
+
+    @LargeTest
+    @Test
+    fun customConfig_noProcess() = validateSlicesCustomConfig(includeMacroAppTag = false)
+
     companion object {
         const val TRACE_LABEL = "MacrobencharkTestTraceLabel"
     }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 8203ef7..92eecb5 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -35,6 +35,7 @@
 import androidx.benchmark.conditionalError
 import androidx.benchmark.inMemoryTrace
 import androidx.benchmark.json.BenchmarkData
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig
 import androidx.benchmark.perfetto.PerfettoCapture.PerfettoSdkConfig.InitialProcessState
 import androidx.benchmark.perfetto.PerfettoCaptureWrapper
@@ -190,6 +191,7 @@
  *
  * This function is a building block for public testing APIs
  */
+@ExperimentalPerfettoCaptureApi
 private fun macrobenchmark(
     uniqueName: String,
     className: String,
@@ -200,10 +202,11 @@
     iterations: Int,
     launchWithClearTask: Boolean,
     startupModeMetricHint: StartupMode?,
+    perfettoConfig: PerfettoConfig?,
     perfettoSdkConfig: PerfettoSdkConfig?,
     setupBlock: MacrobenchmarkScope.() -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
-) {
+): BenchmarkData.TestResult {
     require(iterations > 0) {
         "Require iterations > 0 (iterations = $iterations)"
     }
@@ -276,7 +279,7 @@
                 val fileLabel = "${uniqueName}_iter$iterString"
                 val tracePath = perfettoCollector.record(
                     fileLabel = fileLabel,
-                    config = PerfettoConfig.Benchmark(
+                    config = perfettoConfig ?: PerfettoConfig.Benchmark(
                         /**
                          * Prior to API 24, every package name was joined into a single setprop
                          * which can overflow, and disable *ALL* app level tracing.
@@ -405,18 +408,18 @@
         } + methodTracingResultFiles).map {
             BenchmarkData.TestResult.ProfilerOutput(it)
         }
-        ResultWriter.appendTestResult(
-            BenchmarkData.TestResult(
-                className = className,
-                name = testName,
-                totalRunTimeNs = System.nanoTime() - startTime,
-                metrics = measurements.singleMetrics + measurements.sampledMetrics,
-                repeatIterations = iterations,
-                thermalThrottleSleepSeconds = 0,
-                warmupIterations = warmupIterations,
-                profilerOutputs = mergedProfilerOutputs
-            )
+        val testResult = BenchmarkData.TestResult(
+            className = className,
+            name = testName,
+            totalRunTimeNs = System.nanoTime() - startTime,
+            metrics = measurements.singleMetrics + measurements.sampledMetrics,
+            repeatIterations = iterations,
+            thermalThrottleSleepSeconds = 0,
+            warmupIterations = warmupIterations,
+            profilerOutputs = mergedProfilerOutputs
         )
+        ResultWriter.appendTestResult(testResult)
+        return testResult
     } finally {
         scope.killProcess()
     }
@@ -426,6 +429,7 @@
  * Run a macrobenchmark with the specified StartupMode
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@ExperimentalPerfettoCaptureApi
 fun macrobenchmarkWithStartupMode(
     uniqueName: String,
     className: String,
@@ -434,10 +438,11 @@
     metrics: List<Metric>,
     compilationMode: CompilationMode,
     iterations: Int,
+    perfettoConfig: PerfettoConfig?,
     startupMode: StartupMode?,
     setupBlock: MacrobenchmarkScope.() -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
-) {
+): BenchmarkData.TestResult {
     val perfettoSdkConfig =
         if (Arguments.perfettoSdkTracingEnable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             PerfettoSdkConfig(
@@ -449,7 +454,7 @@
                 }
             )
         } else null
-    macrobenchmark(
+    return macrobenchmark(
         uniqueName = uniqueName,
         className = className,
         testName = testName,
@@ -458,6 +463,7 @@
         compilationMode = compilationMode,
         iterations = iterations,
         startupModeMetricHint = startupMode,
+        perfettoConfig = perfettoConfig,
         perfettoSdkConfig = perfettoSdkConfig,
         setupBlock = {
             if (startupMode == StartupMode.COLD) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index dbbc781..51df347 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -340,6 +340,6 @@
     SourceOption(ComposePluginId, "sourceInformation"),
     MetricsOption(ComposePluginId, "metricsDestination"),
     ReportsOption(ComposePluginId, "reportsDestination"),
-    StrongSkippingOption(ComposePluginId, "experimentalStrongSkipping"),
+    StrongSkippingOption(ComposePluginId, "strongSkipping"),
     NonSkippingGroupOption(ComposePluginId, "nonSkippingGroupOptimization")
 }
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt
index 74e4d20..e9118d6 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/RenderInTransitionOverlayNodeElement.kt
@@ -66,9 +66,9 @@
     override fun equals(other: Any?): Boolean {
         if (other is RenderInTransitionOverlayNodeElement) {
             return sharedTransitionScope == other.sharedTransitionScope &&
-                renderInOverlay == other.renderInOverlay &&
+                renderInOverlay === other.renderInOverlay &&
                 zIndexInOverlay == other.zIndexInOverlay &&
-                clipInOverlay == other.clipInOverlay
+                clipInOverlay === other.clipInOverlay
         }
         return false
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index 4be9688..c445253 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -166,9 +166,16 @@
             allowMultipleOccurrences = false
         )
         val STRONG_SKIPPING_OPTION = CliOption(
+            "strongSkipping",
+            "<true|false>",
+            "Enable strong skipping mode",
+            required = false,
+            allowMultipleOccurrences = false
+        )
+        val EXPERIMENTAL_STRONG_SKIPPING_OPTION = CliOption(
             "experimentalStrongSkipping",
             "<true|false>",
-            "Enable experimental strong skipping mode",
+            "Deprecated - Use strongSkipping instead",
             required = false,
             allowMultipleOccurrences = false
         )
@@ -200,6 +207,7 @@
         NON_SKIPPING_GROUP_OPTIMIZATION_ENABLED_OPTION,
         SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION,
         DECOYS_ENABLED_OPTION,
+        EXPERIMENTAL_STRONG_SKIPPING_OPTION,
         STRONG_SKIPPING_OPTION,
         STABLE_CONFIG_PATH_OPTION,
         TRACE_MARKERS_OPTION,
@@ -250,6 +258,18 @@
             ComposeConfiguration.DECOYS_ENABLED_KEY,
             value == "true"
         )
+        EXPERIMENTAL_STRONG_SKIPPING_OPTION -> {
+            val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
+            msgCollector?.report(
+                CompilerMessageSeverity.WARNING,
+                "${EXPERIMENTAL_STRONG_SKIPPING_OPTION.optionName} is deprecated." +
+                    " Use ${STRONG_SKIPPING_OPTION.optionName} instead."
+            )
+            configuration.put(
+                ComposeConfiguration.STRONG_SKIPPING_ENABLED_KEY,
+                value == "true"
+            )
+        }
         STRONG_SKIPPING_OPTION -> configuration.put(
             ComposeConfiguration.STRONG_SKIPPING_ENABLED_KEY,
             value == "true"
@@ -473,6 +493,7 @@
             )
 
             val useK2 = configuration.languageVersionSettings.languageVersion.usesK2
+
             val strongSkippingEnabled = configuration.get(
                 ComposeConfiguration.STRONG_SKIPPING_ENABLED_KEY,
                 false
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
index 4fa34b4..3fc7a65 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
@@ -217,7 +217,7 @@
         if (this === other) return true
         val otherModifier = other as? OffsetPxElement ?: return false
 
-        return offset == otherModifier.offset &&
+        return offset === otherModifier.offset &&
             rtlAware == otherModifier.rtlAware
     }
 
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index e011e59..7467caf 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -559,7 +559,7 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         val otherModifier = other as? WithAlignmentLineBlockElement ?: return false
-        return block == otherModifier.block
+        return block === otherModifier.block
     }
 
     override fun hashCode(): Int = block.hashCode()
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
index 2a93e7a1..e35dd4b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
@@ -249,7 +249,7 @@
             return false
         }
 
-        return other.block == block
+        return other.block === block
     }
 
     override fun hashCode(): Int = block.hashCode()
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
index 9c83dd3..4d2cc98 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
@@ -161,7 +161,7 @@
         if (other !is DerivedWidthModifier) {
             return false
         }
-        return insets == other.insets && widthCalc == other.widthCalc
+        return insets == other.insets && widthCalc === other.widthCalc
     }
 
     override fun hashCode(): Int = 31 * insets.hashCode() + widthCalc.hashCode()
@@ -206,7 +206,7 @@
         if (other !is DerivedHeightModifier) {
             return false
         }
-        return insets == other.insets && heightCalc == other.heightCalc
+        return insets == other.insets && heightCalc === other.heightCalc
     }
 
     override fun hashCode(): Int = 31 * insets.hashCode() + heightCalc.hashCode()
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.android.kt
index 59e1459..186bdd2 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Magnifier.android.kt
@@ -199,15 +199,15 @@
         if (this === other) return true
         if (other !is MagnifierElement) return false
 
-        if (sourceCenter != other.sourceCenter) return false
-        if (magnifierCenter != other.magnifierCenter) return false
+        if (sourceCenter !== other.sourceCenter) return false
+        if (magnifierCenter !== other.magnifierCenter) return false
         if (zoom != other.zoom) return false
         if (useTextDefault != other.useTextDefault) return false
         if (size != other.size) return false
         if (cornerRadius != other.cornerRadius) return false
         if (elevation != other.elevation) return false
         if (clippingEnabled != other.clippingEnabled) return false
-        if (onSizeChanged != other.onSizeChanged) return false
+        if (onSizeChanged !== other.onSizeChanged) return false
         if (platformMagnifierFactory != other.platformMagnifierFactory) return false
 
         return true
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PreferKeepClear.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PreferKeepClear.android.kt
index 346ba634b..e6bd917 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PreferKeepClear.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/PreferKeepClear.android.kt
@@ -84,7 +84,7 @@
 
     override fun equals(other: Any?): Boolean {
         if (other !is PreferKeepClearNode) return false
-        return clearRect == other.rect
+        return clearRect === other.rect
     }
 
     override fun InspectorInfo.inspectableProperties() {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.android.kt
index a9b7f5d..77699fa 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/SystemGestureExclusion.android.kt
@@ -84,7 +84,7 @@
 
     override fun equals(other: Any?): Boolean {
         if (other !is ExcludeFromSystemGestureElement) return false
-        return exclusion == other.exclusion
+        return exclusion === other.exclusion
     }
 
     override fun InspectorInfo.inspectableProperties() {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDelegator.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDelegator.android.kt
index b4a18c5..4545e0a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDelegator.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDelegator.android.kt
@@ -65,7 +65,7 @@
     override fun hashCode() = 31 * callback.hashCode()
 
     override fun equals(other: Any?) =
-        (this === other) or ((other is HandwritingDelegatorElement) && callback == other.callback)
+        (this === other) or ((other is HandwritingDelegatorElement) && callback === other.callback)
 
     override fun InspectorInfo.inspectableProperties() {
         name = "handwritingDelegator"
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 3981ac6..2cea5ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -514,7 +514,7 @@
         if (enabled != other.enabled) return false
         if (onClickLabel != other.onClickLabel) return false
         if (role != other.role) return false
-        if (onClick != other.onClick) return false
+        if (onClick !== other.onClick) return false
 
         return true
     }
@@ -592,10 +592,10 @@
         if (enabled != other.enabled) return false
         if (onClickLabel != other.onClickLabel) return false
         if (role != other.role) return false
-        if (onClick != other.onClick) return false
+        if (onClick !== other.onClick) return false
         if (onLongClickLabel != other.onLongClickLabel) return false
-        if (onLongClick != other.onLongClick) return false
-        if (onDoubleClick != other.onDoubleClick) return false
+        if (onLongClick !== other.onLongClick) return false
+        if (onDoubleClick !== other.onDoubleClick) return false
 
         return true
     }
@@ -826,7 +826,7 @@
             resetPointerInputHandling = true
         }
 
-        if (this.onLongClick != onLongClick) {
+        if (this.onLongClick !== onLongClick) {
             this.onLongClick = onLongClick
             invalidateSemantics()
         }
@@ -1225,9 +1225,9 @@
         if (enabled != other.enabled) return false
         if (role != other.role) return false
         if (onLongClickLabel != other.onLongClickLabel) return false
-        if (onLongClick != other.onLongClick) return false
+        if (onLongClick !== other.onLongClick) return false
         if (onClickLabel != other.onClickLabel) return false
-        if (onClick != other.onClick) return false
+        if (onClick !== other.onClick) return false
 
         return true
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
index 01f5f64..df7af80 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
@@ -55,7 +55,7 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         val otherModifier = other as? FocusedBoundsObserverElement ?: return false
-        return onPositioned == otherModifier.onPositioned
+        return onPositioned === otherModifier.onPositioned
     }
 
     override fun InspectorInfo.inspectableProperties() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
index 2a082d1..2a14342 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/draganddrop/DragAndDropTarget.kt
@@ -79,7 +79,7 @@
         if (other !is DropTargetElement) return false
         if (target != other.target) return false
 
-        return shouldStartDragAndDrop == other.shouldStartDragAndDrop
+        return shouldStartDragAndDrop === other.shouldStartDragAndDrop
     }
 
     override fun hashCode(): Int {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
index 1e4abd9..297c21c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt
@@ -116,7 +116,7 @@
         other as TransformableElement
 
         if (state != other.state) return false
-        if (canPan != other.canPan) return false
+        if (canPan !== other.canPan) return false
         if (lockRotationOnZoomPan != other.lockRotationOnZoomPan) return false
         if (enabled != other.enabled) return false
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
index 18a65d3..1920744 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -89,7 +89,7 @@
         if (this === other) return true
         if (other !is LazyLayoutSemanticsModifier) return false
 
-        if (itemProviderLambda != other.itemProviderLambda) return false
+        if (itemProviderLambda !== other.itemProviderLambda) return false
         if (state != other.state) return false
         if (orientation != other.orientation) return false
         if (userScrollEnabled != other.userScrollEnabled) return false
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
index caa756a..70d94a9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
@@ -203,7 +203,7 @@
         if (indicationNodeFactory != other.indicationNodeFactory) return false
         if (enabled != other.enabled) return false
         if (role != other.role) return false
-        if (onClick != other.onClick) return false
+        if (onClick !== other.onClick) return false
 
         return true
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index b75e3c1..bcaa2ed 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -197,7 +197,7 @@
         if (indicationNodeFactory != other.indicationNodeFactory) return false
         if (enabled != other.enabled) return false
         if (role != other.role) return false
-        if (onValueChange != other.onValueChange) return false
+        if (onValueChange !== other.onValueChange) return false
 
         return true
     }
@@ -433,7 +433,7 @@
         if (indicationNodeFactory != other.indicationNodeFactory) return false
         if (enabled != other.enabled) return false
         if (role != other.role) return false
-        if (onClick != other.onClick) return false
+        if (onClick !== other.onClick) return false
 
         return true
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/GenericShape.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/GenericShape.kt
index e3a65a3..79ca230 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/GenericShape.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/GenericShape.kt
@@ -46,7 +46,7 @@
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
-        return (other as? GenericShape)?.builder == builder
+        return (other as? GenericShape)?.builder === builder
     }
 
     override fun hashCode(): Int {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
index d5112f2..428c7b8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
@@ -74,12 +74,12 @@
         if (this === other) return true
         if (other !is KeyboardActions) return false
 
-        return onDone == other.onDone &&
-            onGo == other.onGo &&
-            onNext == other.onNext &&
-            onPrevious == other.onPrevious &&
-            onSearch == other.onSearch &&
-            onSend == other.onSend
+        return onDone === other.onDone &&
+            onGo === other.onGo &&
+            onNext === other.onNext &&
+            onPrevious === other.onPrevious &&
+            onSearch === other.onSearch &&
+            onSend === other.onSend
     }
 
     override fun hashCode(): Int {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
index fd739d6..7a1cd33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringElement.kt
@@ -93,14 +93,14 @@
 
         // these are equally unlikely to change
         if (fontFamilyResolver != other.fontFamilyResolver) return false
-        if (onTextLayout != other.onTextLayout) return false
+        if (onTextLayout !== other.onTextLayout) return false
         if (overflow != other.overflow) return false
         if (softWrap != other.softWrap) return false
         if (maxLines != other.maxLines) return false
         if (minLines != other.minLines) return false
 
         // these never change, but check anyway for correctness
-        if (onPlaceholderLayout != other.onPlaceholderLayout) return false
+        if (onPlaceholderLayout !== other.onPlaceholderLayout) return false
         if (selectionController != other.selectionController) return false
 
         return true
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
index f8853ad..6525c51 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringElement.kt
@@ -102,15 +102,15 @@
 
         // these are equally unlikely to change
         if (fontFamilyResolver != other.fontFamilyResolver) return false
-        if (onTextLayout != other.onTextLayout) return false
-        if (onShowTranslation != other.onShowTranslation) return false
+        if (onTextLayout !== other.onTextLayout) return false
+        if (onShowTranslation !== other.onShowTranslation) return false
         if (overflow != other.overflow) return false
         if (softWrap != other.softWrap) return false
         if (maxLines != other.maxLines) return false
         if (minLines != other.minLines) return false
 
         // these never change, but check anyway for correctness
-        if (onPlaceholderLayout != other.onPlaceholderLayout) return false
+        if (onPlaceholderLayout !== other.onPlaceholderLayout) return false
         if (selectionController != other.selectionController) return false
 
         return true
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index aa99015..9c3e8a6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -209,12 +209,12 @@
     ): Boolean {
         var changed = false
 
-        if (this.onTextLayout != onTextLayout) {
+        if (this.onTextLayout !== onTextLayout) {
             this.onTextLayout = onTextLayout
             changed = true
         }
 
-        if (this.onPlaceholderLayout != onPlaceholderLayout) {
+        if (this.onPlaceholderLayout !== onPlaceholderLayout) {
             this.onPlaceholderLayout = onPlaceholderLayout
             changed = true
         }
@@ -224,7 +224,7 @@
             changed = true
         }
 
-        if (this.onShowTranslation != onShowTranslation) {
+        if (this.onShowTranslation !== onShowTranslation) {
             this.onShowTranslation = onShowTranslation
             changed = true
         }
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 43f6253..36ecaff 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -37,6 +37,7 @@
             CommonModuleIncompatibilityDetector.REFERENCE_ISSUE,
             CommonModuleIncompatibilityDetector.EXTENDS_LAMBDA_ISSUE,
             PrimitiveInCollectionDetector.ISSUE,
+            LambdaStructuralEqualityDetector.ISSUE
         )
     }
     override val vendor = Vendor(
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/LambdaStructuralEqualityDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/LambdaStructuralEqualityDetector.kt
new file mode 100644
index 0000000..6d7c0c0
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/LambdaStructuralEqualityDetector.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 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 androidx.compose.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import java.util.EnumSet
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.uast.UBinaryExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UastBinaryOperator
+
+/**
+ * Lint [Detector] to ensure that lambda instances are compared referentially, instead of
+ * structurally.
+ *
+ * This is needed as function references (::lambda) do not consider their capture scope in their
+ * equals implementation. This means that structural equality can return true, even if the lambdas
+ * are different references with a different capture scope. Instead, lambdas should be compared
+ * referentially (=== or !==) to avoid this issue.
+ */
+class LambdaStructuralEqualityDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableUastTypes() = listOf(
+        UBinaryExpression::class.java,
+        UCallExpression::class.java
+    )
+
+    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+        override fun visitBinaryExpression(node: UBinaryExpression) {
+            val op = node.operator
+            if (op == UastBinaryOperator.EQUALS ||
+                op == UastBinaryOperator.NOT_EQUALS) {
+                val left = node.leftOperand.sourcePsi as? KtExpression ?: return
+                val right = node.rightOperand.sourcePsi as? KtExpression ?: return
+                if (left.isFunctionType() && right.isFunctionType()) {
+                    val replacement = if (op == UastBinaryOperator.EQUALS) "===" else "!=="
+                    context.report(
+                        ISSUE,
+                        node.operatorIdentifier,
+                        context.getNameLocation(node.operatorIdentifier ?: node),
+                        BriefDescription,
+                        LintFix.create()
+                            .replace()
+                            .name("Change to $replacement")
+                            .text(op.text)
+                            .with(replacement)
+                            .autoFix()
+                            .build()
+                    )
+                }
+            }
+        }
+
+        override fun visitCallExpression(node: UCallExpression) {
+            if (node.methodName == "equals") {
+                val left = node.receiver?.sourcePsi as? KtExpression ?: return
+                val right = node.valueArguments.firstOrNull()?.sourcePsi as? KtExpression ?: return
+                if (left.isFunctionType() && right.isFunctionType()) {
+                    context.report(
+                        ISSUE,
+                        node,
+                        context.getNameLocation(node),
+                        BriefDescription
+                    )
+                }
+            }
+        }
+    }
+
+    private fun KtExpression.isFunctionType(): Boolean = analyze(this) {
+        getKtType()?.isFunctionType == true
+    }
+
+    companion object {
+        private const val BriefDescription = "Checking lambdas for structural equality, instead " +
+            "of checking for referential equality"
+        private const val Explanation =
+            "Checking structural equality on lambdas can lead to issues, as function references " +
+                "(::lambda) do not consider their capture scope in their equals implementation. " +
+                "This means that structural equality can return true, even if the lambdas are " +
+                "different references with a different capture scope. Instead, lambdas should be" +
+                "compared referentially (=== or !==) to avoid this issue."
+
+        val ISSUE = Issue.create(
+            "LambdaStructuralEquality",
+            BriefDescription,
+            Explanation,
+            Category.CORRECTNESS, 5, Severity.ERROR,
+            Implementation(
+                LambdaStructuralEqualityDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/LambdaStructuralEqualityDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/LambdaStructuralEqualityDetectorTest.kt
new file mode 100644
index 0000000..0206fd9
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/LambdaStructuralEqualityDetectorTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class LambdaStructuralEqualityDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = LambdaStructuralEqualityDetector()
+
+    override fun getIssues(): MutableList<Issue> = mutableListOf(
+        LambdaStructuralEqualityDetector.ISSUE
+    )
+
+    @Test
+    fun noErrors() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                val lambda1 = { 1 }
+                val lambda2 = { 2 }
+
+                fun test() {
+                    lambda1 === lambda2
+                    lambda1 !== lambda2
+                }
+            """
+            )
+        )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun errors() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                val lambda1 = { 1 }
+                val lambda2 = { 2 }
+                val lambda3: (() -> Unit)? = null
+
+                fun test() {
+                    lambda1 == lambda2
+                    lambda1 != lambda2
+                    lambda1.equals(lambda2)
+                    lambda3?.equals(lambda2)
+                }
+            """
+            )
+        )
+            .run()
+            .expect(
+                """
+src/test/test.kt:9: Error: Checking lambdas for structural equality, instead of checking for referential equality [LambdaStructuralEquality]
+                    lambda1 == lambda2
+                            ~~
+src/test/test.kt:10: Error: Checking lambdas for structural equality, instead of checking for referential equality [LambdaStructuralEquality]
+                    lambda1 != lambda2
+                            ~~
+src/test/test.kt:11: Error: Checking lambdas for structural equality, instead of checking for referential equality [LambdaStructuralEquality]
+                    lambda1.equals(lambda2)
+                            ~~~~~~
+src/test/test.kt:12: Error: Checking lambdas for structural equality, instead of checking for referential equality [LambdaStructuralEquality]
+                    lambda3?.equals(lambda2)
+                             ~~~~~~
+4 errors, 0 warnings
+            """
+            )
+            .expectFixDiffs(
+                """
+Autofix for src/test/test.kt line 9: Change to ===:
+@@ -9 +9
+-                     lambda1 == lambda2
++                     lambda1 === lambda2
+Autofix for src/test/test.kt line 10: Change to !==:
+@@ -10 +10
+-                     lambda1 != lambda2
++                     lambda1 !== lambda2
+                """
+            )
+    }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
index d90d9b3..307d512 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
@@ -838,7 +838,7 @@
         if (other !is DraggableAnchorsElement<*>) return false
 
         if (state != other.state) return false
-        if (anchors != other.anchors) return false
+        if (anchors !== other.anchors) return false
         if (orientation != other.orientation) return false
 
         return true
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index cf714ea..7941e5a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -823,7 +823,7 @@
         if (other !is DraggableAnchorsElement<*>) return false
 
         if (state != other.state) return false
-        if (anchors != other.anchors) return false
+        if (anchors !== other.anchors) return false
         if (orientation != other.orientation) return false
 
         return true
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
index 5cf1202..0efe678 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt
@@ -155,7 +155,7 @@
                     previous
                 else null
             is ComputedValueHolder ->
-                if (value.compute == previous.compute)
+                if (value.compute === previous.compute)
                     previous
                 else null
             else -> null
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 6e6bb2b..28e609f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -1794,7 +1794,7 @@
 ): ((Any) -> Unit)? {
     @Suppress("NAME_SHADOWING")
     val parentObserver = if (mergeReadObserver) parentObserver else null
-    return if (readObserver != null && parentObserver != null && readObserver != parentObserver) {
+    return if (readObserver != null && parentObserver != null && readObserver !== parentObserver) {
         { state: Any ->
             readObserver(state)
             parentObserver(state)
@@ -1806,7 +1806,7 @@
     writeObserver: ((Any) -> Unit)?,
     parentObserver: ((Any) -> Unit)?
 ): ((Any) -> Unit)? =
-    if (writeObserver != null && parentObserver != null && writeObserver != parentObserver) {
+    if (writeObserver != null && parentObserver != null && writeObserver !== parentObserver) {
         { state: Any ->
             writeObserver(state)
             parentObserver(state)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
index d1db594b..13d2354 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
@@ -90,7 +90,7 @@
 
         if (autofillTypes != other.autofillTypes) return false
         if (boundingBox != other.boundingBox) return false
-        if (onFill != other.onFill) return false
+        if (onFill !== other.onFill) return false
 
         return true
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
index d6ee7f8..065c55e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier.kt
@@ -54,7 +54,7 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is OnGloballyPositionedElement) return false
-        return onGloballyPositioned == other.onGloballyPositioned
+        return onGloballyPositioned === other.onGloballyPositioned
     }
 
     override fun hashCode(): Int {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
index 91fe684..a6b2e22 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
@@ -59,7 +59,7 @@
         if (this === other) return true
         if (other !is OnSizeChangedModifier) return false
 
-        return onSizeChanged == other.onSizeChanged
+        return onSizeChanged === other.onSizeChanged
     }
 
     override fun hashCode(): Int {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalConsumer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalConsumer.kt
index 6d291ff..e7bb4b1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalConsumer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalConsumer.kt
@@ -67,7 +67,7 @@
     }
 
     override fun equals(other: Any?): Boolean {
-        return other is ModifierLocalConsumerImpl && other.consumer == consumer
+        return other is ModifierLocalConsumerImpl && other.consumer === consumer
     }
 
     override fun hashCode() = consumer.hashCode()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 50dffd3..0d60bb9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -620,7 +620,7 @@
         if (other !is CustomAccessibilityAction) return false
 
         if (label != other.label) return false
-        if (action != other.action) return false
+        if (action !== other.action) return false
 
         return true
     }
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index 04f1c6d..32ac6ba 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -957,7 +957,7 @@
         override fun hashCode() = constrainBlock.hashCode()
 
         override fun equals(other: Any?) =
-            constrainBlock == (other as? ConstrainAsModifier)?.constrainBlock
+            constrainBlock === (other as? ConstrainAsModifier)?.constrainBlock
     }
 }
 
@@ -1064,7 +1064,7 @@
     override val layoutId: Any = ref.id
 
     override fun equals(other: Any?) = other is ConstraintLayoutParentData &&
-        ref.id == other.ref.id && constrain == other.constrain
+        ref.id == other.ref.id && constrain === other.constrain
 
     override fun hashCode() = ref.id.hashCode() * 31 + constrain.hashCode()
 }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt
index 7584d68..29de39c 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/ScalingLazyColumnMeasure.kt
@@ -236,7 +236,7 @@
         if (minTransitionArea != other.minTransitionArea) return false
         if (maxTransitionArea != other.maxTransitionArea) return false
         if (scaleInterpolator != other.scaleInterpolator) return false
-        if (viewportVerticalOffsetResolver != other.viewportVerticalOffsetResolver) return false
+        if (viewportVerticalOffsetResolver !== other.viewportVerticalOffsetResolver) return false
 
         return true
     }
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index b3abd6e..bf11025 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -240,7 +240,7 @@
         if (minTransitionArea != other.minTransitionArea) return false
         if (maxTransitionArea != other.maxTransitionArea) return false
         if (scaleInterpolator != other.scaleInterpolator) return false
-        if (viewportVerticalOffsetResolver != other.viewportVerticalOffsetResolver) return false
+        if (viewportVerticalOffsetResolver !== other.viewportVerticalOffsetResolver) return false
 
         return true
     }