Skip to content

KotlinToJsBenchmark.kt

src/jvmMain/kotlin/afield/examples/KotlinToJsBenchmark.kt · 117 lines

package afield.examples

import afield.lang.remotejs.KotlinToJsCompiler
import afield.lang.remotejs.KotlinToJsCompiler.Profile
import java.nio.file.Files

FIELD_TEST id: remotejs.kotlin-to-js-benchmark runner: jvm-main entrypoint: afield.examples.KotlinToJsBenchmark tags: [remotejs, kotlinjs, benchmark] timeoutSec: 300 expect: exitCode: 0 stdoutContains: - "KotlinToJsBenchmark ok"

object KotlinToJsBenchmark

Measures warm, in-process compile latency for the two output profiles, to answer whether the whole-program DCE pass (PLAIN_DCE) adds meaningful per-edit latency versus per-module ES output that ships the stdlib once (ESM_SHARED). Prints median ms + output size for a trivial and a medium program. Not a pass/fail latency gate — just numbers.

object KotlinToJsBenchmark {

    private val TRIVIAL = """
        external val console: dynamic
        fun main() { console.log("hi") }
    """.trimIndent()

Exercises more of the stdlib (collections, sequences, math, strings) so DCE has real work.

    private val MEDIUM = """
        external val console: dynamic
        data class P(val x: Double, val y: Double) {
            operator fun plus(o: P) = P(x + o.x, y + o.y)
            operator fun times(k: Double) = P(x * k, y * k)
            fun len() = kotlin.math.sqrt(x * x + y * y)
        }
        fun ring(n: Int, r: Double): List<P> =
            (0 until n).map { i ->
                val a = 2.0 * kotlin.math.PI * i / n
                P(kotlin.math.cos(a) * r, kotlin.math.sin(a) * r)
            }
        fun main() {
            val pts = ring(64, 10.0)
            val centroid = pts.reduce { a, b -> a + b } * (1.0 / pts.size)
            val total = pts.asSequence().map { (it + centroid).len() }.sum()
            val labels = pts.mapIndexed { i, p -> "p" + i + "=" + p.x.toString().take(5) }
            console.log("centroid=" + centroid + " total=" + total + " " + labels.first())
        }
    """.trimIndent()

fun main(args: Array<String>)

body — 62 lines, from line 55
    @JvmStatic
    fun main(args: Array<String>) {
        val iters = 8

        // Prove both profiles produce working output first.
        for (p in Profile.entries) {
            val r = KotlinToJsCompiler.compile(MEDIUM, moduleName = "warm_${p.name.lowercase()}", profile = p)
            require(r.success && r.js != null) { "$p failed to compile MEDIUM:\n${r.rawLog.takeLast(1500)}" }
            require(r.mode == KotlinToJsCompiler.Mode.IN_PROCESS) { "$p fell back to ${r.mode}" }
        }

        println("profile        program   medianMs   minMs    outBytes   (warm, in-process, n=$iters)")
        for (program in listOf("trivial" to TRIVIAL, "medium" to MEDIUM)) {
            for (p in Profile.entries) {
                val times = ArrayList<Long>(iters)
                var bytes = 0
                repeat(iters) { k ->
                    val t0 = System.nanoTime()
                    val r = KotlinToJsCompiler.compile(
                        program.second, moduleName = "bench_${program.first}_${p.name.lowercase()}_$k", profile = p,
                    )
                    val dtMs = (System.nanoTime() - t0) / 1_000_000
                    require(r.success) { "compile failed mid-benchmark ($p/${program.first})" }
                    times.add(dtMs)
                    bytes = r.js!!.length
                }
                times.sort()
                println("%-14s %-9s %8d   %6d   %8d".format(p.name, program.first, times[times.size / 2], times.first(), bytes))
            }
        }

        // The latency lever: a persistent IC cache, re-editing ONE box (stable module name) so both
        // the stdlib cache and the box cache are reused. Iteration 0 populates; report warm median.
        // NOTE: IC and DCE are mutually exclusive — the IC path ships the full stdlib. So the
        // interesting combination is ESM_SHARED+IC: fast compile AND a tiny box module (stdlib is
        // shipped to the head once, separately), which is what emitSharedStdlibMjs() is for.
        for (p in listOf(Profile.PLAIN_DCE, Profile.ESM_SHARED)) {
            val cacheDir = Files.createTempDirectory("field-ktjs-iccache").toFile()
            try {
                val warm = ArrayList<Long>(iters)
                var firstMs = 0L
                var bytes = 0
                repeat(iters + 1) { k ->
                    val t0 = System.nanoTime()
                    val r = KotlinToJsCompiler.compile(
                        MEDIUM, moduleName = "bench_cached_box", profile = p, cacheDir = cacheDir,
                    )
                    val dtMs = (System.nanoTime() - t0) / 1_000_000
                    require(r.success) { "cached compile failed ($p): ${r.rawLog.takeLast(1200)}" }
                    if (k == 0) firstMs = dtMs else warm.add(dtMs)
                    bytes = r.js!!.length   // ESM_SHARED: this is the BOX module only (stdlib shipped once)
                }
                warm.sort()
                println("%-14s %-9s %8d   %6d   %8d   (cold populate=${firstMs}ms)".format("${p.name}+IC", "medium", warm[warm.size / 2], warm.first(), bytes))
            } finally {
                cacheDir.deleteRecursively()
            }
        }

        println("KotlinToJsBenchmark ok")
    }
}