blob: 06ceb0d7fe23d606f12908445341484e8c4ddae7 [file] [log] [blame]
/*
* Copyright 2021 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.compiler.plugins.kotlin
import androidx.compose.compiler.plugins.kotlin.analysis.Stability
import androidx.compose.compiler.plugins.kotlin.analysis.knownStable
import androidx.compose.compiler.plugins.kotlin.analysis.knownUnstable
import androidx.compose.compiler.plugins.kotlin.lower.ComposableFunctionBodyTransformer
import androidx.compose.compiler.plugins.kotlin.lower.IrSourcePrinterVisitor
import androidx.compose.compiler.plugins.kotlin.lower.isUnitOrNullableUnit
import java.io.File
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrField
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtParameter
@JvmDefaultWithCompatibility
interface FunctionMetrics {
val isEmpty: Boolean get() = false
val packageName: FqName
val name: String
val composable: Boolean
val skippable: Boolean
val restartable: Boolean
val readonly: Boolean
val inline: Boolean
val isLambda: Boolean
val hasDefaults: Boolean
val defaultsGroup: Boolean
val groups: Int
val calls: Int
val scheme: String?
fun recordGroup()
fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
)
fun recordParameter(
declaration: IrValueParameter,
type: IrType,
stability: Stability,
default: IrExpression?,
defaultStatic: Boolean,
used: Boolean,
)
fun recordFunction(
composable: Boolean,
restartable: Boolean,
skippable: Boolean,
isLambda: Boolean,
inline: Boolean,
hasDefaults: Boolean,
readonly: Boolean
)
fun recordScheme(
scheme: String
)
fun print(out: Appendable, src: IrSourcePrinterVisitor)
}
@JvmDefaultWithCompatibility
interface ModuleMetrics {
val isEmpty get() = false
fun recordFunction(
function: FunctionMetrics,
)
fun recordClass(
declaration: IrClass,
marked: Boolean,
stability: Stability,
)
fun recordLambda(
composable: Boolean,
memoized: Boolean,
singleton: Boolean,
)
fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
)
fun log(message: String)
fun Appendable.appendModuleJson()
fun Appendable.appendComposablesCsv()
fun Appendable.appendComposablesTxt()
fun Appendable.appendClassesTxt()
fun saveMetricsTo(directory: String)
fun saveReportsTo(directory: String)
fun makeFunctionMetrics(function: IrFunction): FunctionMetrics
}
object EmptyModuleMetrics : ModuleMetrics {
override val isEmpty: Boolean get() = true
override fun recordFunction(function: FunctionMetrics) {}
override fun recordClass(declaration: IrClass, marked: Boolean, stability: Stability) {}
override fun recordLambda(composable: Boolean, memoized: Boolean, singleton: Boolean) {}
override fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
) {}
override fun log(message: String) {
println(message)
}
override fun Appendable.appendModuleJson() {}
override fun Appendable.appendComposablesCsv() {}
override fun Appendable.appendComposablesTxt() {}
override fun Appendable.appendClassesTxt() {}
override fun saveMetricsTo(directory: String) {}
override fun saveReportsTo(directory: String) {}
override fun makeFunctionMetrics(function: IrFunction): FunctionMetrics = EmptyFunctionMetrics
}
object EmptyFunctionMetrics : FunctionMetrics {
private fun emptyMetricsAccessed(): Nothing = error("Empty metrics accessed")
override val isEmpty: Boolean get() = true
override val packageName: FqName
get() = emptyMetricsAccessed()
override val name: String
get() = emptyMetricsAccessed()
override val composable: Boolean
get() = emptyMetricsAccessed()
override val skippable: Boolean
get() = emptyMetricsAccessed()
override val restartable: Boolean
get() = emptyMetricsAccessed()
override val readonly: Boolean
get() = emptyMetricsAccessed()
override val inline: Boolean
get() = emptyMetricsAccessed()
override val isLambda: Boolean
get() = emptyMetricsAccessed()
override val hasDefaults: Boolean
get() = emptyMetricsAccessed()
override val defaultsGroup: Boolean
get() = emptyMetricsAccessed()
override val groups: Int
get() = emptyMetricsAccessed()
override val calls: Int
get() = emptyMetricsAccessed()
override val scheme: String
get() = emptyMetricsAccessed()
override fun recordGroup() {}
override fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
) {}
override fun recordParameter(
declaration: IrValueParameter,
type: IrType,
stability: Stability,
default: IrExpression?,
defaultStatic: Boolean,
used: Boolean
) {}
override fun recordFunction(
composable: Boolean,
restartable: Boolean,
skippable: Boolean,
isLambda: Boolean,
inline: Boolean,
hasDefaults: Boolean,
readonly: Boolean
) {}
override fun recordScheme(scheme: String) {}
override fun print(out: Appendable, src: IrSourcePrinterVisitor) {}
}
class ModuleMetricsImpl(
var name: String,
val stabilityOf: (IrType) -> Stability
) : ModuleMetrics {
private var skippableComposables = 0
private var restartableComposables = 0
private var readonlyComposables = 0
private var totalComposables = 0
private var restartGroups = 0
private var totalGroups = 0
private var staticArguments = 0
private var certainArguments = 0
private var knownStableArguments = 0
private var knownUnstableArguments = 0
private var unknownStableArguments = 0
private var totalArguments = 0
private var markedStableClasses = 0
private var inferredStableClasses = 0
private var inferredUnstableClasses = 0
private var inferredUncertainClasses = 0
private var effectivelyStableClasses = 0
private var totalClasses = 0
private var memoizedLambdas = 0
private var singletonLambdas = 0
private var singletonComposableLambdas = 0
private var composableLambdas = 0
private var totalLambdas = 0
private val composables = mutableListOf<FunctionMetrics>()
private val classes = mutableListOf<ClassMetrics>()
private val logMessages = mutableListOf<String>()
private inner class ClassMetrics(
val declaration: IrClass,
val marked: Boolean,
val stability: Stability
) {
private fun Stability.simpleHumanReadable() = when {
knownStable() -> "stable"
knownUnstable() -> "unstable"
else -> "runtime"
}
fun print(out: Appendable, src: IrSourcePrinterVisitor) = with(out) {
append("${stability.simpleHumanReadable()} ")
append("class ")
append(declaration.name.asString())
appendLine(" {")
for (decl in declaration.declarations) {
val isVar = when (decl) {
is IrProperty -> decl.isVar
is IrField -> true
else -> false
}
val field = when (decl) {
is IrProperty -> decl.backingField ?: continue
is IrField -> decl
else -> continue
}
if (field.name == KtxNameConventions.STABILITY_FLAG) continue
append(" ")
val fieldStability = stabilityOf(field.type)
append("${fieldStability.simpleHumanReadable()} ")
append(if (isVar) "var " else "val ")
append(field.name.asString())
append(": ")
append(src.printType(field.type))
appendLine()
}
if (!marked) {
appendLine(" <runtime stability> = $stability")
}
appendLine("}")
}
}
override fun recordFunction(function: FunctionMetrics) {
if (!function.composable) return
totalComposables++
if (!function.isLambda) composables.add(function)
if (function.readonly) readonlyComposables++
if (function.skippable) skippableComposables++
if (function.restartable) {
restartableComposables++
restartGroups++
}
totalGroups += function.groups
}
override fun recordClass(
declaration: IrClass,
marked: Boolean,
stability: Stability
) {
classes.add(
ClassMetrics(
declaration,
marked,
stability
)
)
totalClasses++
when {
marked -> {
markedStableClasses++
effectivelyStableClasses++
}
stability.knownStable() -> {
inferredStableClasses++
effectivelyStableClasses++
}
stability.knownUnstable() -> {
inferredUnstableClasses++
}
else -> {
inferredUncertainClasses++
}
}
}
override fun recordLambda(
composable: Boolean,
memoized: Boolean,
singleton: Boolean,
) {
totalLambdas++
if (composable) composableLambdas++
if (memoized) memoizedLambdas++
if (composable && singleton) singletonComposableLambdas++
if (!composable && singleton) singletonLambdas++
}
override fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
) {
for (arg in paramMeta) {
totalArguments++
if (arg.isCertain) certainArguments++
if (arg.isStatic) staticArguments++
when {
arg.stability.knownStable() -> knownStableArguments++
arg.stability.knownUnstable() -> knownUnstableArguments++
else -> unknownStableArguments++
}
}
}
override fun log(message: String) {
logMessages.add(message)
}
override fun Appendable.appendModuleJson() = appendJson {
entry("skippableComposables", skippableComposables)
entry("restartableComposables", restartableComposables)
entry("readonlyComposables", readonlyComposables)
entry("totalComposables", totalComposables)
entry("restartGroups", restartGroups)
entry("totalGroups", totalGroups)
entry("staticArguments", staticArguments)
entry("certainArguments", certainArguments)
entry("knownStableArguments", knownStableArguments)
entry("knownUnstableArguments", knownUnstableArguments)
entry("unknownStableArguments", unknownStableArguments)
entry("totalArguments", totalArguments)
entry("markedStableClasses", markedStableClasses)
entry("inferredStableClasses", inferredStableClasses)
entry("inferredUnstableClasses", inferredUnstableClasses)
entry("inferredUncertainClasses", inferredUncertainClasses)
entry("effectivelyStableClasses", effectivelyStableClasses)
entry("totalClasses", totalClasses)
entry("memoizedLambdas", memoizedLambdas)
entry("singletonLambdas", singletonLambdas)
entry("singletonComposableLambdas", singletonComposableLambdas)
entry("composableLambdas", composableLambdas)
entry("totalLambdas", totalLambdas)
}
override fun Appendable.appendComposablesCsv() = appendCsv {
row {
col("package")
col("name")
col("composable")
col("skippable")
col("restartable")
col("readonly")
col("inline")
col("isLambda")
col("hasDefaults")
col("defaultsGroup")
col("groups")
col("calls")
}
for (fn in composables) {
row {
col(fn.packageName.asString())
col(fn.name)
col(fn.composable)
col(fn.skippable)
col(fn.restartable)
col(fn.readonly)
col(fn.inline)
col(fn.isLambda)
col(fn.hasDefaults)
col(fn.defaultsGroup)
col(fn.groups)
col(fn.calls)
}
}
}
override fun Appendable.appendComposablesTxt() {
val src = IrSourcePrinterVisitor(this)
for (fn in composables) {
fn.print(this, src)
}
}
override fun Appendable.appendClassesTxt() {
val src = IrSourcePrinterVisitor(this)
for (declaration in classes) {
declaration.print(this, src)
}
}
override fun saveMetricsTo(directory: String) {
val dir = File(directory)
val prefix = name
.replace('.', '_')
.replace("<", "")
.replace(">", "")
File(dir, "$prefix-module.json").write {
appendModuleJson()
}
}
override fun saveReportsTo(directory: String) {
val dir = File(directory)
val prefix = name
.replace('.', '_')
.replace("<", "")
.replace(">", "")
File(dir, "$prefix-composables.csv").write {
appendComposablesCsv()
}
File(dir, "$prefix-composables.txt").write {
appendComposablesTxt()
}
if (logMessages.isNotEmpty()) {
File(dir, "$prefix-composables.log").write {
for (line in logMessages) appendLine(line)
}
}
File(dir, "$prefix-classes.txt").write {
appendClassesTxt()
}
}
override fun makeFunctionMetrics(function: IrFunction): FunctionMetrics =
FunctionMetricsImpl(function)
}
class FunctionMetricsImpl(
val function: IrFunction
) : FunctionMetrics {
override var packageName: FqName = function.fqNameForIrSerialization
override var name: String = function.name.asString()
override var composable: Boolean = false
override var skippable: Boolean = false
override var restartable: Boolean = false
override var readonly: Boolean = false
override var inline: Boolean = false
override var isLambda: Boolean = false
override var hasDefaults: Boolean = false
override var defaultsGroup: Boolean = false
override var groups: Int = 0
override var calls: Int = 0
override var scheme: String? = null
private class Param(
val declaration: IrValueParameter,
val type: IrType,
val stability: Stability,
val default: IrExpression?,
val defaultStatic: Boolean,
val used: Boolean
) {
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun print(out: Appendable, src: IrSourcePrinterVisitor) = with(out) {
if (!used) append("unused ")
when {
stability.knownStable() -> append("stable ")
stability.knownUnstable() -> append("unstable ")
}
append(declaration.name.asString())
append(": ")
append(src.printType(type))
if (default != null) {
append(" = ")
if (defaultStatic) append("@static ")
else append("@dynamic ")
val psi = declaration.symbol.descriptor.findPsi() as? KtParameter
val str = psi?.defaultValue?.text
if (str != null) {
append(str)
} else {
default.accept(src, null)
}
}
}
}
private val parameters = mutableListOf<Param>()
override fun recordGroup() {
groups++
}
override fun recordComposableCall(
expression: IrCall,
paramMeta: List<ComposableFunctionBodyTransformer.CallArgumentMeta>
) {
calls++
}
override fun recordFunction(
composable: Boolean,
restartable: Boolean,
skippable: Boolean,
isLambda: Boolean,
inline: Boolean,
hasDefaults: Boolean,
readonly: Boolean
) {
this.composable = composable
this.restartable = restartable
this.skippable = skippable
this.isLambda = isLambda
this.inline = inline
this.hasDefaults = hasDefaults
this.readonly = readonly
}
override fun recordParameter(
declaration: IrValueParameter,
type: IrType,
stability: Stability,
default: IrExpression?,
defaultStatic: Boolean,
used: Boolean,
) {
parameters.add(
Param(
declaration,
type,
stability,
default,
defaultStatic,
used,
)
)
}
override fun recordScheme(scheme: String) {
this.scheme = scheme
}
override fun print(out: Appendable, src: IrSourcePrinterVisitor): Unit = with(out) {
if (restartable) append("restartable ")
if (skippable) append("skippable ")
if (readonly) append("readonly ")
if (inline) append("inline ")
scheme?.let { append("scheme(\"$it\") ") }
append("fun ")
append(name)
if (parameters.isEmpty()) {
appendLine("()")
} else {
appendLine("(")
for (param in parameters) {
append(" ")
param.print(out, src)
appendLine()
}
append(")")
if (!function.returnType.isUnitOrNullableUnit()) {
append(": ")
append(src.printType(function.returnType))
}
appendLine()
}
}
}