Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MemoryLeak #595

Open
betschwa opened this issue Feb 20, 2024 · 7 comments · May be fixed by #611
Open

MemoryLeak #595

betschwa opened this issue Feb 20, 2024 · 7 comments · May be fixed by #611
Assignees

Comments

@betschwa
Copy link
Contributor

The library probably contains a memory leak. If KVar is updated regularly, the server consumes more and more memory until the OutOfMemoryError occurs.

I have created a sample project to reproduce this behavior.

https://github.com/betschwa/KWebMemoryProblemTest

Here is the job for updating a KVar:

val webServerMemoryLoad = KVar(initialValue = MemoryLoad())
webServerMemoryLoadJob = scope.launch {
    val runtime = Runtime.getRuntime()

    while (isActive) {
        val load = MemoryLoad(max = runtime.maxMemory(),
                              free = runtime.freeMemory(),
                              total = runtime.totalMemory())

        webServerMemoryLoad.value = load

        delay(duration = 1.seconds)
    }
}

Here is the display:

div(attributes = fomantic.field) {
    label().addText(value = "Used [MB]")
    div(attributes = fomantic.ui.input) {
        render(webServerMemoryLoad) { memoryLoad ->
            input(type = InputType.text) { element ->
                element.setReadOnly(true)
            }.value.value = memoryLoad.usedText
        }
    }
}
div(attributes = fomantic.field) {
    label().addText(value = "Free [MB]")
    div(attributes = fomantic.ui.input) {
        render(webServerMemoryLoad) { memoryLoad ->
            input(type = InputType.text) { element ->
                element.setReadOnly(true)
            }.value.value = memoryLoad.freeText
        }
    }
}
div(attributes = fomantic.field) {
    label().addText(value = "Total [MB]")
    div(attributes = fomantic.ui.input) {
        render(webServerMemoryLoad) { memoryLoad ->
            input(type = InputType.text) { element ->
                element.setReadOnly(true)
            }.value.value = memoryLoad.totalText
        }
    }
}
div(attributes = fomantic.field) {
    label().addText(value = "Max. [MB]")
    div(attributes = fomantic.ui.input) {
        render(webServerMemoryLoad) { memoryLoad ->
            input(type = InputType.text) { element ->
                element.setReadOnly(true)
            }.value.value = memoryLoad.maxText
        }
    }
}
Copy link

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@github-actions github-actions bot added the stale label Mar 22, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Mar 27, 2024
@betschwa
Copy link
Contributor Author

betschwa commented Apr 4, 2024

This still a problem.

@chucky2002
Copy link

+1
please do not ignore this problem. i like kweb but the implementation is to komplex for me to find and fix the source of the bug

@chucky2002
Copy link

chucky2002 commented Apr 8, 2024

it seems there is a Problem for input-elements.

I reduced @betschwa example, to only print out the inputfield. The memory usage is printed on command-line.
If i print the label on kvar-change, there is no memory leak, but if i create a new input field for every change of random value, the memory usages increases.

to make it more visilbe, i use a List of 1000 Input-Fields.


package org.example

import com.ibm.icu.text.DecimalFormat
import kotlinx.coroutines.*
import kweb.*
import kweb.plugins.fomanticUI.fomantic
import kweb.plugins.fomanticUI.fomanticUIPlugin
import kweb.state.KVar
import kweb.state.render
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class MemoryTestSever(private val port: Int = 8080) : AutoCloseable {

    private val scope = CoroutineScope(context = Dispatchers.IO)
    private lateinit var server: Kweb
    private lateinit var webServerMemoryLoadJob: Job
    private lateinit var randomDataJob: Job

    suspend fun start() {
        val runtime = Runtime.getRuntime()

        var counter = 0
        val randomDataList = (0..1000).map { KVar(initialValue = "") }
        randomDataJob = scope.launch {
            while (isActive) {
                randomDataList.forEach { randomData ->
                    randomData.value = Random.nextDouble(
                        from = -Double.MAX_VALUE,
                        until = Double.MAX_VALUE
                    ).toString()
                }
                if (counter % 10 == 0)
                    MemoryLoad(
                        max = runtime.maxMemory(),
                        free = runtime.freeMemory(),
                        total = runtime.totalMemory()
                    ).printMemoryLoad()

                System.gc()
                delay(duration = 100.milliseconds)
            }
        }

        server = Kweb(
            port = port,
            debug = false,
            plugins = listOf(fomanticUIPlugin)
        ) {
            doc.body {
                route {
                    path(template = "/") {
                        div(attributes = fomantic.field) {
                            label().addText(value = "Random Double")
                            randomDataList.forEach { randomData ->
                                div {
                                    val memoryLeak = true
                                    render(randomData) { randomData: String ->

                                        if(memoryLeak) {
                                            /* With memory Leak */
                                            input(type = InputType.text) { element ->
                                                element.setReadOnly(true)
                                            }
                                        } else {
                                            /* without memory leak */
                                            label().text(randomData)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    override fun close() {
        webServerMemoryLoadJob.cancel()
        randomDataJob.cancel()
        scope.cancel()
        server.close()
    }
}


data class MemoryLoad(
    val max: Long = 0L,
    val free: Long = 0L,
    val total: Long = 0L
) {
    companion object {
        val format = DecimalFormat("0.00")
        private val MEGA = (1024 * 1024).toLong()
    }

    val used: Long by lazy { total - free }

    fun printMemoryLoad() {
        println(
            "Memory Load: ${format.format(max / MEGA)}MB max, ${format.format(free / MEGA)}MB free, ${
                format.format(
                    total / MEGA
                )
            }MB total, ${format.format(used / MEGA)}MB used"
        )
    }
}


fun main() = runBlocking {
    MemoryTestSever().use { server ->
        server.start()
        delay(duration = Int.MAX_VALUE.seconds)
    }
}

@sanity sanity self-assigned this May 5, 2024
@sanity sanity reopened this May 5, 2024
@sanity
Copy link
Member

sanity commented May 5, 2024

I appreciate the effort to diagnose this problem. I'll try to investigate but unfortunately I've had limited spare time to devote to Kweb lately. I'll look into it today.

@sanity sanity removed the stale label May 5, 2024
@sanity sanity linked a pull request May 5, 2024 that will close this issue
@sanity
Copy link
Member

sanity commented May 5, 2024

Spent a while staring at code. I think @chucky2002's code is helpful but is putting the browser under such load that it maybe be triggering other problems.

I think the root of the issue may be Kweb's listener mechanism, specifically the way that adding a listener (eg. to a KVal) returns a handle which must be used to remove the listener later - which is very error prone.

I think the solution may be to replace this with a more robust mechanism where listeners have a hierarchy of "owners" - which can be deleted en-masse when no-longer needed.

I'll continue to work on this as time allows. I've also stopped the rather obnoxious auto-closing of "stale" issues.

@chucky2002
Copy link

chucky2002 commented May 13, 2024

Currently, this bug is causing a significant problem for me.
I need to update a value on the page ten times per second. The value originates from a scale that is connected to the computer. I write the value to a Kvar and map the Kvar to a label.
If I refresh the page, a new label is connected to the Kvar, but the old label remains connected to the Kvar. Therefore, the value is written to both the Kvar and the old label. To illustrate the issue, I have constructed an extreme example where the memory consumption escalates rapidly.

package de.mpit.kweb

import io.github.bonigarcia.wdm.WebDriverManager
import kotlinx.coroutines.*
import kweb.Kweb
import kweb.button
import kweb.label
import kweb.state.KVar
import org.junit.AfterClass
import org.junit.BeforeClass
import org.openqa.selenium.Dimension
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver

class WebserverLoadTest {

    companion object {
        lateinit var driver: WebDriver

        @JvmStatic
        @BeforeClass
        fun setup() {

            // Setup Firefox using WebDriverManager
            WebDriverManager.firefoxdriver().setup()
            driver =  FirefoxDriver()
        }


        @AfterClass
        @JvmStatic
        fun teardown() {
            driver.quit()
        }
    }

    val randomNumber = KVar(0.0)


    @org.junit.Test
    fun test1() {
        runBlocking {
            Kweb(port = 16097, debug = true) {
                doc.body {
                    /* Button to stop the test */
                    button {
                        it.on.click {
                            this@runBlocking.cancel()
                        }
                    }.text("close")

                    /* add the output of 100 random numbers */
                    repeat(100) {
                        label().text(randomNumber.map { it.toString() })
                    }
                }
            }


            /* open 5 tabs */
            driver.manage().window().size = Dimension(800, 200)
            driver.get("http://localhost:16097")
            repeat(4) {
                driver.switchTo().newWindow(org.openqa.selenium.WindowType.TAB)
                driver.get("http://localhost:16097")
            }


            /* refresh the page every second */
            launch {
                while (isActive) {
                    driver.windowHandles.forEach {
                        driver.switchTo().window(it)
                        driver.navigate().refresh()
                    }
                    delay(1000)
                }
            }

            /* update the Random Number every 100 milliseconds and print memory usage */
            launch {
                while (isActive) {
                    randomNumber.value = Math.random()
                    printMemory()
                    delay(100)
                }
            }
        }
    }


    /**
     * Print the memory usage
     */
    private fun printMemory() {
        val runtime = Runtime.getRuntime()
        val totalMemoryMB = runtime.totalMemory() / (1024 * 1024)
        val freeMemoryMB = runtime.freeMemory() / (1024 * 1024)
        val usedMemoryMB = totalMemoryMB - freeMemoryMB

        println("------------------------------------------------------------")
        println(
            String.format(
                "|%20s|%20s|%20s|",
                "Total Memory (MB)",
                "Free Memory (MB)",
                "Used Memory (MB)"
            )
        )
        println(String.format("|%20d|%20d|%20d|", totalMemoryMB, freeMemoryMB, usedMemoryMB))
        println("------------------------------------------------------------")
    }
}


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants