Are there any benchmark of Kotlin js? i.e. I’d lov...
# javascript
s
Are there any benchmark of Kotlin js? i.e. I’d love to know how Kotlin stacks up against GWT or just standard written JS code.
k
I don’t think there is. Be prepared for a very low performance. It can be tweaked manually (we do it for our app) but it’s quite involved (we wrote our own compiler plugin for our specific use case). At the moment the Kotlin team is focused on correctness, not performance. That said, I see a bright future, there is nothing in theory that prevents Kotlin from performing on par with native JS.
s
So dev builds perform terribly compared to release builds. I know that.
It can be tweaked manually
Can you speak more to that?
o
I’m also interested 🙂
k
Basically we do two things - we have our own implementation of List that just wraps the JS array and Map that wraps the native JS Map and we have a compiler plugin that replaces existing hash maps with these implementations. Then we also strip out expensive isInterface and isObject checks in the resulting JS using regex expressions. The runtime type checking is actually crazy slow in the current implementation and release builds don’t seem to help with that much. At the end we also use webpack to optimize the JS using its Terser plugin.
👀 1
s
The runtime type checking is actually crazy slow in the current implementation and release builds don’t seem to help with that much
have 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.
a
@Karel Petránek I'd love to see how you all replace Lists and Maps — seems like a huge perf win, and something that many other folks could benefit from @jw and I were chatting about that elsewhere too (but were saddened when we realized we couldn't use ES6 collections)
2
k
We are still stuck at 1.5.30 because 1.6 has several blocker bugs for us and 1.5.30 does not seem to optimize out the checks (@spierce7). If 1.6 is better, there’s hope we can throw away our hacky workarounds 🙂
@ankushg This is for example how we optimize ArrayList usage under JS:
Copy code
internal 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:
Copy code
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
    }
}
❤️ 3