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

Memory Leak Issue with ui.refreshable in NiceGUI Leading to Potential risk of Service #2502

Closed
duckgun13476 opened this issue Feb 2, 2024 · 5 comments

Comments

@duckgun13476
Copy link

Description

I've encountered a potential memory leak issue when using the ui.refreshable decorator in NiceGUI, which could lead to a Denial of Service (DoS) if exploited. The issue arises when running a specific code snippet that utilizes multiple ui.refreshable functions to update UI elements. In normal use, the variables update as expected across multiple connections. However, I observed a significant increase in virtual memory (approximately 20MB) when opening around 40 browser tabs, which seemed normal at first.

The problem becomes apparent when these tabs are closed; the memory allocated does not seem to be released. This behavior suggests that the ui.refreshable decorator, along with the prune() function, does not effectively remove the created labels, leading to persistent memory consumption.

My concern is that if this behavior is exploited by creating and closing a large number of connections repeatedly, it could potentially crash the web service due to uncontrolled memory consumption.

Here is a simplified version of the code that demonstrates the issue:

from nicegui import ui, app
from nicegui import ui, app
from dashboard.bin.log_write import log
import os
import psutil
user_number = 0
@ui.refreshable
def xxxx(title):
    ui.label(title)
@ui.refreshable
def xxxx1(title):
    ui.label(title)
@ui.refreshable
def xxxx2(title):
    ui.label(title)
@ui.refreshable
def xxxx3(title):
    ui.label(title)
@ui.refreshable
def xxxx4(title):
    ui.label(title)
@ui.refreshable
def xxxx5(title):
    ui.label(title)
@ui.refreshable
def xxxx6(title):
    ui.label(title)
@ui.refreshable
def xxxx7(title):
    ui.label(title)
@ui.refreshable
def xxxx8(title):
    ui.label(title)
@ui.refreshable
def xxxx9(title):
    ui.label(title)
@ui.refreshable
def xxxx10(title):
    ui.label(title)
@ui.refreshable
def xxxx11(title):
    ui.label(title)
def xxxx_delete():
    pass
@ui.page("/1")
def home():
    xxxx("123")
    xxxx1("123")
    xxxx2("123")
    xxxx3("123")
    xxxx4("123")
    xxxx5("123")
    xxxx6("123")
    xxxx7("123")
    xxxx8("123")
    xxxx9("123")
    xxxx10("123")
    xxxx11("123")
    ui.button("delete", on_click=xxxx_delete)
def update():
    global k
    k += 1
    xxxx.refresh(f"new{k}")
    xxxx1.refresh(f"new{k * 2}")
    xxxx2.refresh(f"new{k * 2}")
    xxxx3.refresh(f"new{k * 2}")
    xxxx4.refresh(f"new{k * 2}")
    xxxx5.refresh(f"new{k * 2}")
    xxxx6.refresh(f"new{k * 2}")
    xxxx7.refresh(f"new{k * 2}")
    xxxx8.refresh(f"new{k * 2}")
    xxxx9.refresh(f"new{k * 2}")
    xxxx10.refresh(f"new{k * 2}")
    xxxx11.refresh(f"new{k * 2}")
    pid = os.getpid()
    process = psutil.Process(pid)
    memory_info = process.memory_info()
    # print computer physical virtual memory
    log.info(f"P: {memory_info.rss / 1024 ** 2:.2f} MB V: {memory_info.vms / 1024 ** 2:.2f} MB")
@ui.page("/")
def jump():
    ui.open("/1")
def timer00001():
    ui.timer(3, update)
def user_connect():
    global user_number
    user_number += 1
    log.info(f"usercount:{user_number} user connect")
app.on_connect(user_connect)
def user_disconnect():
    global user_number
    user_number -= 1
    log.info(f"usercount:{user_number} user disconnect")
    xxxx.prune()
    xxxx1.prune()
    xxxx2.prune()
    xxxx3.prune()
    xxxx4.prune()
    xxxx5.prune()
    xxxx6.prune()
    xxxx7.prune()
    xxxx8.prune()
    xxxx9.prune()
    xxxx10.prune()
    xxxx11.prune()
app.on_disconnect(user_disconnect)
app.on_startup(timer00001)
ui.run(port=2891)

Could this be a bug in the NiceGUI framework, or is there a recommended approach to ensure that memory is properly freed when using ui.refreshable decorators and dynamically updating UI elements?
Or should I only use a single refreshable decorator?

@falkoschindler
Copy link
Contributor

Hi @duckgun13476,

The problem becomes apparent when these tabs are closed; the memory allocated does not seem to be released.

After objects have been deleted and the garbage collector has run, Python won't release allocated memory back to the operating system. Instead it will keep it for later. Therefore it isn't sufficient to look at the memory footprint of the Python footprint to spot memory leaks.

@rodja
Copy link
Member

rodja commented Feb 2, 2024

My concern is that if this behavior is exploited by creating and closing a large number of connections repeatedly, it could potentially crash the web service due to uncontrolled memory consumption.

Have a look at discussion #1727 on how track "rough" ips which open lots of connections.

@duckgun13476
Copy link
Author

Thank you for your response. It seems I have already found the issue. The problem lies in the Python interpreter itself not reclaiming the memory used by ng, which in turn causes the displayed data to not reflect ng's memory usage. This means that the ng server is actually reclaiming this memory, so when I access it with different IDs, the memory does not increase. This also proves that it is indeed reclaiming this data, so there is no memory leak issue. Thank you very much!

@duckgun13476
Copy link
Author

By the way, may I ask if ng has any relevant methods or functions for detecting its own runtime memory?

@duckgun13476
Copy link
Author

Solution:

gc.collect()
log.info(f"P: {memory_info.rss / 1024 ** 2:.2f} MB V: {memory_info.vms / 1024 ** 2:.2f} MB")

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

No branches or pull requests

3 participants