// KIND: STANDALONE_NO_TR
// DISABLE_NATIVE: gcType=NOOP
// DISABLE_NATIVE: gcScheduler=AGGRESSIVE
// DISABLE_NATIVE: gcScheduler=MANUAL
// The test checks GC, we need to allocate everything on the heap.
// FREE_COMPILER_ARGS: -opt-in=kotlin.native.internal.InternalForKotlinNative -Xdisable-phases=EscapeAnalysis
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class, kotlin.native.runtime.NativeRuntimeApi::class, kotlin.native.concurrent.ObsoleteWorkersApi::class)

import kotlin.concurrent.AtomicInt
import kotlin.concurrent.Volatile
import kotlin.native.concurrent.*
import kotlin.native.identityHashCode
import kotlin.native.internal.MemoryUsageInfo
import kotlin.native.ref.createCleaner
import kotlin.random.Random

// Copying what's done in kotlinx.benchmark
// TODO: Could we benefit, if this was in stdlib, and the compiler just new about it?
object Blackhole {
    @Volatile
    var i0: Int = Random.nextInt()
    var i1 = i0 + 1

    fun consume(value: Any?) {
        consume(value.identityHashCode())
    }

    fun consume(i: Int) {
        if ((i0 == i) && (i1 == i)) {
            i0 = i
        }
    }
}

class ArrayOfBytes(bytes: Int) {
    val data = ByteArray(bytes)
    init {
        // Write into every OS page.
        for (i in 0 until data.size step 4096) {
            data[i] = 42
        }
        Blackhole.consume(data)
    }
}

class ArrayOfBytesWithFinalizer(bytes: Int) {
    val impl = ArrayOfBytes(bytes)
    val cleaner = createCleaner(impl) {
        Blackhole.consume(it)
    }
}

fun allocateGarbage() {
    // Total amount of objects here:
    // - 1 big object with finalizer
    // - 9 big objects
    // - 2490 small objects with finalizers
    // - 97500 small objects without finalizers
    // And total size is ~50MiB
    for (i in 0..100_000) {
        val obj: Any = when {
            i == 50_000 -> ArrayOfBytesWithFinalizer(1_000_000) // ~1MiB
            i % 10_000 == 0 -> ArrayOfBytes(1_000_000) // ~1MiB
            i % 40 == 0 -> ArrayOfBytesWithFinalizer(((i / 100) % 10) * 80) // ~1-100 pointers
            else -> ArrayOfBytes(((i / 100) % 10) * 80) // ~1-100 pointers
        }
        Blackhole.consume(obj)
    }
}

class PeakRSSChecker(private val rssDiffLimitBytes: Long) {
    // On Linux, the child process might immediately commit the same amount of memory as the parent.
    // So, measure difference between peak RSS measurements.
    private val initialBytes = MemoryUsageInfo.peakResidentSetSizeBytes.also {
        check(it != 0L) { "Error trying to obtain peak RSS. Check if current platform is supported" }
    }

    fun check(): Long {
        val diffBytes = MemoryUsageInfo.peakResidentSetSizeBytes - initialBytes
        check(diffBytes <= rssDiffLimitBytes) { "Increased peak RSS by $diffBytes bytes which is more than $rssDiffLimitBytes" }
        return diffBytes
    }
}

fun main() {
    // allocateGarbage allocates ~50MiB. Make total amount per mutator ~5GiB.
    val count = 100
    // Total amount overall is ~20GiB
    val threadCount = 4
    val progressReportsCount = 10
    // Setting the initial boundary to ~50MiB. The scheduler will adapt this value
    // dynamically with no upper limit.
    kotlin.native.runtime.GC.targetHeapBytes = 50_000_000
    kotlin.native.runtime.GC.minHeapBytes = 50_000_000
    // Limit memory usage at ~200MiB. 4 times the initial boundary yet still
    // way less than total expected allocated amount.
    val peakRSSChecker = PeakRSSChecker(200_000_000L)

    val workers = Array(threadCount) { Worker.start() }
    val globalCount = AtomicInt(0)
    val finalGlobalCount = count * workers.size
    workers.forEach {
        it.executeAfter(0L) {
            for (i in 0 until count) {
                allocateGarbage()
                peakRSSChecker.check()
                globalCount.getAndAdd(1)
            }
        }
    }

    val reportStep = finalGlobalCount / progressReportsCount
    var lastReportCount = -reportStep
    while (true) {
        val diffPeakRss = peakRSSChecker.check()
        val currentCount = globalCount.value
        if (currentCount >= finalGlobalCount) {
            break
        }
        if (lastReportCount + reportStep <= currentCount) {
            println("Allocating iteration $currentCount of $finalGlobalCount with peak RSS increase: $diffPeakRss bytes")
            lastReportCount = currentCount
        }
    }

    workers.forEach {
        it.requestTermination().result
    }
    peakRSSChecker.check()
}
