spierce7
04/14/2022, 4:18 PMKarel Petránek
04/14/2022, 5:56 PMspierce7
04/14/2022, 6:00 PMIt can be tweaked manuallyCan you speak more to that?
Ola Adolfsson
04/14/2022, 7:37 PMKarel Petránek
04/14/2022, 7:42 PMspierce7
04/14/2022, 7:47 PMThe runtime type checking is actually crazy slow in the current implementation and release builds don’t seem to help with that muchhave you tried this with IR? It was my understanding that dev builds were focused on correctness, and release / prod builds were focused on speed / optimizations. It was my understanding that the removal of typechecking was one of the optimizations. When I tried out compose for web with the new canvas, dev builds ran at ~7FPS, while release builds were giving me a smooth 60 FPS.
ankushg
04/16/2022, 2:54 PMKarel Petránek
04/18/2022, 4:21 PMKarel Petránek
04/18/2022, 4:53 PMinternal external interface JSNativeArray<T> {
fun slice(): JSNativeArray<T>
fun push(element: T)
fun splice(from: Int, to: Int, element: T)
}
internal class JSArrayList<T> : AbstractMutableList<T> {
private val innerArray: Array<T>
constructor(array: Array<T>) {
innerArray = array.unsafeCast<JSNativeArray<T>>().slice().unsafeCast<Array<T>>()
}
constructor(array: Array<T>, noCopy: Boolean) {
innerArray = if (noCopy) {
array
} else {
array.unsafeCast<JSNativeArray<T>>().slice().unsafeCast<Array<T>>()
}
}
constructor() {
innerArray = emptyArray()
}
// Just for compatibility with ArrayList
constructor(capacity: Int) : this()
constructor(iterable: Iterable<T>) {
innerArray = if (iterable is JSArrayList<*>) {
val othArr = iterable.innerArray.unsafeCast<JSNativeArray<T>>()
othArr.slice().unsafeCast<Array<T>>()
} else {
val arr = js("[]").unsafeCast<JSNativeArray<T>>()
for (item in iterable) {
arr.push(item)
}
arr.unsafeCast<Array<T>>()
}
}
override val size: Int
get() = innerArray.size
override fun get(index: Int): T {
return innerArray[index]
}
override fun add(index: Int, element: T) {
val arr = innerArray.unsafeCast<JSNativeArray<T>>()
if (index == innerArray.size) {
arr.push(element)
} else {
arr.splice(index, 0, element)
}
}
override fun add(element: T): Boolean {
val arr = innerArray.unsafeCast<JSNativeArray<T>>()
arr.push(element)
return true
}
override fun addAll(elements: Collection<T>): Boolean {
if (elements.isEmpty()) {
return false
}
if (elements is JSArrayList<T>) {
val a = innerArray
val other = elements.innerArray
js("Array").prototype.push.apply(a, other)
return true
}
val arr = innerArray.unsafeCast<JSNativeArray<T>>()
for (element in elements) {
arr.push(element)
}
return true
}
override fun removeAt(index: Int): T {
val arr = innerArray
return js("arr.splice(index, 1)[0]").unsafeCast<T>()
}
override fun set(index: Int, element: T): T {
if (index < 0 || index >= innerArray.size) {
throw IndexOutOfBoundsException("Index $index is out of bounds of this list (size ${innerArray.size})")
}
innerArray[index] = element
return element
}
override fun iterator(): MutableIterator<T> {
return JSArrayIterator(innerArray)
}
}
@Suppress("UNUSED")
internal fun <T> optimizedListOf(elements: Array<T>): List<T> {
return JSArrayList(array = elements)
}
We then use an IR compiler plugin to replace ArrayList and listOf/mutableListOf calls with JSArrayList calls:
class JSOptimizer(
private val moduleName: String,
) :
IrGenerationExtension {
@OptIn(ObsoleteDescriptorBasedAPI::class)
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
val start = System.currentTimeMillis()
if (pluginContext.platform?.firstOrNull()?.platformName != "JS") {
return
}
if (pluginContext.moduleDescriptor.name.toString().endsWith("_test>")) {
return
}
val emptyArrayList = pluginContext.referenceConstructors(FqName("_js.JSArrayList")).first {
it.owner.valueParameters.isEmpty()
}
val arrayListWithCapacity = pluginContext.referenceConstructors(FqName("_js.JSArrayList")).first {
it.owner.valueParameters.size == 1 && it.owner.valueParameters[0].name.asString() == "capacity"
}
val toOptimizedList = pluginContext.referenceFunctions(FqName("_js.toOptimizedList")).first()
val optimizedListOf =
pluginContext.referenceFunctions(FqName("_js.optimizedListOf")).first()
val transformer = object : IrElementTransformerVoid() {
override fun visitConstructorCall(expression: IrConstructorCall): IrExpression {
val fqName = expression.type.classFqName?.asString()
val isArrayList = fqName == "kotlin.collections.ArrayList"
if (isArrayList && expression.valueArgumentsCount == 0) {
val result = IrConstructorCallImpl(-1, -1, expression.type, emptyArrayList, 1, 0, 0)
result.putTypeArgument(0, expression.getTypeArgument(0))
return result
}
if (isArrayList && expression.valueArgumentsCount == 1 && expression.getValueArgument(0)!!.type.isInt()) {
val result = IrConstructorCallImpl(-1, -1, expression.type, arrayListWithCapacity, 1, 0, 1)
result.putTypeArgument(0, expression.getTypeArgument(0))
result.putValueArgument(0, expression.getValueArgument(0))
return result
}
return super.visitConstructorCall(expression)
}
override fun visitCall(expression: IrCall): IrExpression {
val replacedExpression =
super.visitCall(expression) // Replace child expressions first
if (replacedExpression !is IrCall) {
return replacedExpression
}
val fn = replacedExpression.symbol.owner
val fqName = fn.fqNameWhenAvailable?.asString()
val isToList = fqName == "kotlin.collections.toList"
if (isToList && replacedExpression.typeArgumentsCount == 1 && replacedExpression.extensionReceiver?.type.isIterable()) {
return replaceCall(replacedExpression, toOptimizedList)
}
val isListOf = fqName == "kotlin.collections.listOf"
if (isListOf && replacedExpression.typeArgumentsCount == 1 && replacedExpression.valueArgumentsCount == 1 && fn.valueParameters.firstOrNull()?.name?.identifierOrNullIfSpecial == "elements") {
return replaceCall(replacedExpression, optimizedListOf)
}
return replacedExpression
}
}
moduleFragment.transformChildrenVoid(transformer)
val time = ((System.currentTimeMillis() - start).toDouble() / 100.0).roundToInt() / 10.0
println("Optimized JS for $moduleName in $time seconds")
}
private fun replaceCall(oldCall: IrCall, newFn: IrSimpleFunctionSymbol): IrCall {
val result = IrCallImpl(-1, -1, oldCall.type, newFn, oldCall.typeArgumentsCount, oldCall.valueArgumentsCount)
for (i in 0 until oldCall.typeArgumentsCount) {
result.putTypeArgument(i, oldCall.getTypeArgument(i))
}
for (i in 0 until oldCall.valueArgumentsCount) {
result.putValueArgument(i, oldCall.getValueArgument(i))
}
result.extensionReceiver = oldCall.extensionReceiver
result.dispatchReceiver = oldCall.dispatchReceiver
return result
}
}