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

Large Memory in the browser #1089

Closed
Kamil-Och opened this issue Jun 26, 2023 · 17 comments
Closed

Large Memory in the browser #1089

Kamil-Och opened this issue Jun 26, 2023 · 17 comments
Assignees
Labels
bug Something isn't working
Milestone

Comments

@Kamil-Och
Copy link

Description

Hi,
I have an problem that when i inspect the browser the memory for the niceGui website is periodically rising from like 0.5 GB to 5 GB and its causing the website to crash. I don't really know what the problem is but I'm using ui.timer to get the data for the gui on 0.1 seconds timer.

@falkoschindler
Copy link
Contributor

Hi @Kamil-Och! Can you provide some more details or a code example reproducing the problem?

@falkoschindler falkoschindler added the question Further information is requested label Jun 26, 2023
@Kamil-Och
Copy link
Author

Kamil-Och commented Jun 27, 2023

Okay here is sample code based on my use case:

code
from nicegui import ui

class Robot:
     def __init__(self) -> None:
        self.current_robot_state = 1 
        self.goal_robot_state = 1 
        self.frames_sent = 0 
        self.loop_counter_overtime_error = 0 
        self.gripper_type = 0 
        self.payload = 0.0 
        self.test_var = 0

class Joint:
    def __init__(self,
                 goal_torque: float = 0.0,
                 goal_fsm: int = 0,
                 goal_position: float = 0.0,
                 goal_id_torque: float = 0.0,
                 goal_rl_torque: float = 0.0, 
                 goal_rl_pid: float = 0.0, 
                 goal_rl_friction: float = 0.0,
                 current_position: float = 0.0,
                 current_velocity: float = 0.0,
                 goal_velocity: float = 0.0,
                 current_torque: float = 0.0,
                 current_fsm: int = 0,
                 current_bearing_temperature: int = 0,
                 current_motor_temperature: int = 0,
                 current_warnings: int = 0,
                 current_errors: int = 0,
                 joint_registers: list [int] = [0]*256,
                 joint_rcv_counter: int = 0, 
                 joint_rcv_counter_error: int = 0): 
                
        self.goal_torque = goal_torque
        self.goal_fsm = goal_fsm
        self.goal_position = goal_position
        self.goal_id_torque = goal_id_torque
        self.goal_rl_torque = goal_rl_torque
        self.goal_rl_pid = goal_rl_pid
        self.goal_rl_friction = goal_rl_friction
        self.current_position = current_position
        self.current_velocity = current_velocity
        self.goal_velocity = goal_velocity
        self.current_torque = current_torque
        self.current_fsm = current_fsm
        self.current_bearing_temperature = current_bearing_temperature
        self.current_motor_temperature = current_motor_temperature
        self.current_warnings = current_warnings
        self.current_errors = current_errors
        self.joint_registers = joint_registers
        self.joint_rcv_counter = joint_rcv_counter
        self.joint_rcv_counter_error = joint_rcv_counter_error
            
def updateData():
    robot.current_robot_state += 1
    robot.frames_sent += 1
    robot.goal_robot_state += 1
    robot.gripper_type += 1
    robot.loop_counter_overtime_error += 1
    robot.test_var += 1
    robot.payload += 1

    for joint in joints:
        joint.goal_torque += 1
        joint.goal_fsm += 1
        joint.goal_position += 1
        joint.goal_id_torque += 1
        joint.goal_rl_torque += 1
        joint.goal_rl_pid += 1
        joint.goal_rl_friction += 1
        joint.current_position += 1
        joint.current_velocity += 1
        joint.goal_velocity += 1
        joint.current_torque += 1
        joint.current_fsm += 1
        joint.current_bearing_temperature += 1
        joint.current_motor_temperature += 1
        joint.current_warnings += 1
        joint.current_errors += 1
        joint.joint_rcv_counter += 1
        joint.joint_rcv_counter_error += 1

def reload_data_and_refresh():
    updateData()
    refreshUI()

def refreshUI():
    UI.refresh()

@ui.refreshable
def UI():
    ui.label("Robot")
    with ui.row():
        ui.label("robot.current_robot_state")
        ui.label(robot.current_robot_state)
        ui.label("robot.frames_sent")
        ui.label(robot.frames_sent)
        ui.label("robot.goal_robot_state")
        ui.label(robot.goal_robot_state)
        ui.label("robot.gripper_type")
        ui.label(robot.gripper_type)
        ui.label("robot.loop_counter_overtime_error")
        ui.label(robot.loop_counter_overtime_error)
        ui.label("robot.test_var")
        ui.label(robot.test_var)
        ui.label("robot.payload")
        ui.label(robot.payload)
    ui.label("Joints")
    with ui.row():
        for joint in joints:
            with ui.grid(columns=2):

                ui.label("joint.goal_torque")
                ui.label(joint.goal_torque)
                ui.label("joint.goal_fsm")
                ui.label(joint.goal_fsm)
                ui.label("joint.goal_position")
                ui.label(joint.goal_position)
                ui.label("joint.goal_id_torque")
                ui.label(joint.goal_id_torque)
                ui.label("joint.goal_rl_torque")
                ui.label(joint.goal_rl_torque)
                ui.label("joint.goal_rl_pid")
                ui.label(joint.goal_rl_pid)
                ui.label("joint.goal_rl_friction")
                ui.label(joint.goal_rl_friction)
                ui.label("joint.current_position")
                ui.label(joint.current_position)
                ui.label("joint.current_velocity")
                ui.label(joint.current_velocity)
                ui.label("joint.goal_velocity")
                ui.label(joint.goal_velocity)
                ui.label("joint.current_torque")
                ui.label(joint.current_torque)
                ui.label("joint.current_fsm")
                ui.label(joint.current_fsm)
                ui.label("joint.current_bearing_temperature")
                ui.label(joint.current_bearing_temperature)
                ui.label("joint.current_motor_temperature")
                ui.label(joint.current_motor_temperature)
                ui.label("joint.current_warnings")
                ui.label(joint.current_warnings)
                ui.label("joint.current_errors")
                ui.label(joint.current_errors)
                ui.label("joint.joint_rcv_counter")
                ui.label(joint.joint_rcv_counter)
                ui.label("joint.joint_rcv_counter_error")
                ui.label(joint.joint_rcv_counter_error)
            

if __name__ in {"__main__", "__mp_main__"}:
    if __name__ == "__mp_main__":
        robot = Robot()
        joints = [Joint() for _ in range(6)]

    ui.run(title='HMI', show=False)
    try:
        ui_timer = ui.timer(0.1, lambda: reload_data_and_refresh())

        UI()

    except KeyboardInterrupt: 
        
        print('KeyboardInterrupt')
        exit(0)

@falkoschindler
Copy link
Contributor

I can't reproduce the huge memory consumption you're describing. But apart from that you main block looks strange. Creating the UI should happen before calling ui.run():

if __name__ in {"__main__", "__mp_main__"}:
    if __name__ == "__mp_main__":
        robot = Robot()
        joints = [Joint() for _ in range(6)]

    UI()
    ui_timer = ui.timer(0.1, lambda: reload_data_and_refresh())

    ui.run(title='HMI', show=False)

Does the problem still occur?

@Kamil-Och
Copy link
Author

yea it doesn't seams to change anything. When I check with firefox process Manager in the span of little over 2 minutes memory for this code goes from 200 mb to 4 gb and then ether crash tab or freeze browser. Considering that you can't reproduce the error could it be the problem with my setup or browser?

@falkoschindler
Copy link
Contributor

Oh! Now I looked at the right place: The memory usage of "Google Chrome Helper (Renderer)" grows indeed about 1 GB per minute.

Here is a more compact reproduction:

import time
from nicegui import ui

@ui.refreshable
def labels():
    for _ in range(1000):
        ui.label(time.time())

labels()
ui_timer = ui.timer(0.1, labels.refresh)

ui.run()

Looks like ui.refreshable introduces a memory leak.

@falkoschindler
Copy link
Contributor

But without ui.refreshable the problem still occurs:

import time
from nicegui import ui

def render():
    container.clear()
    with container:
        for _ in range(1000):
            ui.label(time.time())

container = ui.column()
ui_timer = ui.timer(0.1, render)

ui.run()

@falkoschindler falkoschindler added bug Something isn't working and removed question Further information is requested labels Jun 27, 2023
@falkoschindler falkoschindler self-assigned this Jun 27, 2023
@falkoschindler falkoschindler added the help wanted Extra attention is needed label Jun 27, 2023
@Kamil-Och
Copy link
Author

Kamil-Och commented Jun 27, 2023

i just tried without the ui.refreshable and it works fine to be honest it shows me around 200 mb of memory

code
from nicegui import ui

class Robot:
     def __init__(self) -> None:
        self.current_robot_state = 1
        self.goal_robot_state = 1 
        self.frames_sent = 0 
        self.loop_counter_overtime_error = 0 
        self.gripper_type = 0 
        self.payload = 0.0 
        self.test_var = 0

class Joint:
    def __init__(self,
                 goal_torque: float = 0.0,
                 goal_fsm: int = 0,
                 goal_position: float = 0.0,
                 goal_id_torque: float = 0.0,
                 goal_rl_torque: float = 0.0,
                 goal_rl_pid: float = 0.0,
                 goal_rl_friction: float = 0.0,
                 current_position: float = 0.0,
                 current_velocity: float = 0.0,
                 goal_velocity: float = 0.0,
                 current_torque: float = 0.0,
                 current_fsm: int = 0,
                 current_bearing_temperature: int = 0,
                 current_motor_temperature: int = 0,
                 current_warnings: int = 0,
                 current_errors: int = 0,
                 joint_registers: list [int] = [0]*256,
                 joint_rcv_counter: int = 0, 
                 joint_rcv_counter_error: int = 0):
                
        self.goal_torque = goal_torque
        self.goal_fsm = goal_fsm
        self.goal_position = goal_position
        self.goal_id_torque = goal_id_torque
        self.goal_rl_torque = goal_rl_torque
        self.goal_rl_pid = goal_rl_pid
        self.goal_rl_friction = goal_rl_friction
        self.current_position = current_position
        self.current_velocity = current_velocity
        self.goal_velocity = goal_velocity
        self.current_torque = current_torque
        self.current_fsm = current_fsm
        self.current_bearing_temperature = current_bearing_temperature
        self.current_motor_temperature = current_motor_temperature
        self.current_warnings = current_warnings
        self.current_errors = current_errors
        self.joint_registers = joint_registers
        self.joint_rcv_counter = joint_rcv_counter
        self.joint_rcv_counter_error = joint_rcv_counter_error
            
def updateData():
    robot.current_robot_state += 1
    robot.frames_sent += 1
    robot.goal_robot_state += 1
    robot.gripper_type += 1
    robot.loop_counter_overtime_error += 1
    robot.test_var += 1
    robot.payload += 1

    for joint in joints:
        joint.goal_torque += 1
        joint.goal_fsm += 1
        joint.goal_position += 1
        joint.goal_id_torque += 1
        joint.goal_rl_torque += 1
        joint.goal_rl_pid += 1
        joint.goal_rl_friction += 1
        joint.current_position += 1
        joint.current_velocity += 1
        joint.goal_velocity += 1
        joint.current_torque += 1
        joint.current_fsm += 1
        joint.current_bearing_temperature += 1
        joint.current_motor_temperature += 1
        joint.current_warnings += 1
        joint.current_errors += 1
        joint.joint_rcv_counter += 1
        joint.joint_rcv_counter_error += 1

def reload_data_and_refresh():
    updateData()
    refreshUI()

def refreshUI():
    current_robot_state.set_text(current_robot_state)
    frame_sent.set_text(robot.frames_sent)
    goal_robot_state.set_text(robot.goal_robot_state)
    gripper_type.set_text(robot.gripper_type)
    loop_counter_overtime_error.set_text(robot.loop_counter_overtime_error)
    test_var.set_text(robot.test_var)
    robot_payload.set_text(robot.payload)
    i = 0
    for joint in joints:
        goal_torque[i].set_text(joint.goal_torque)
        goal_fsm[i].set_text(joint.goal_fsm)
        goal_position[i].set_text(joint.goal_position)
        goal_id_torque[i].set_text(joint.goal_id_torque)
        goal_rl_torque[i].set_text(joint.goal_rl_torque)
        goal_rl_pid[i].set_text(joint.goal_rl_pid)
        goal_rl_friction[i].set_text(joint.goal_rl_friction)
        current_position[i].set_text(joint.current_position)
        current_velocity[i].set_text(joint.current_velocity)
        goal_velocity[i].set_text(joint.goal_velocity)
        current_torque[i].set_text(joint.current_torque)
        current_fsm[i].set_text(joint.current_fsm)
        current_bearing_temperature[i].set_text(joint.current_bearing_temperature)
        current_motor_temperature[i].set_text(joint.current_motor_temperature)
        current_warning[i].set_text(joint.current_warnings)
        current_errors[i].set_text(joint.current_errors)
        joint_rcv_counter[i].set_text(joint.joint_rcv_counter)
        joint_rcv_counter_error[i].set_text(joint_rcv_counter_error)
        i += 1

if __name__ in {"__main__", "__mp_main__"}:
    robot = Robot()
    joints = [Joint() for _ in range(6)]

    i = 0
    goal_torque = []
    goal_fsm = []
    goal_position = []
    goal_id_torque = []
    goal_rl_torque = []
    goal_rl_pid = []
    goal_rl_friction = []
    current_position = []
    current_velocity = []
    goal_velocity = []
    current_torque = []
    current_fsm = []
    current_bearing_temperature = []
    current_motor_temperature = []
    current_warning = []
    current_errors = []
    joint_rcv_counter =[]
    joint_rcv_counter_error = []
    
    ui.label("Robot")
    with ui.row():
        ui.label("robot.current_robot_state")
        current_robot_state = ui.label(robot.current_robot_state)
        ui.label("robot.frames_sent")
        frame_sent = ui.label(robot.frames_sent)
        ui.label("robot.goal_robot_state")
        goal_robot_state = ui.label(robot.goal_robot_state)
        ui.label("robot.gripper_type")
        gripper_type = ui.label(robot.gripper_type)
        ui.label("robot.loop_counter_overtime_error")
        loop_counter_overtime_error = ui.label(robot.loop_counter_overtime_error)
        ui.label("robot.test_var")
        test_var = ui.label(robot.test_var)
        ui.label("robot.payload")
        robot_payload = ui.label(robot.payload)
    ui.label("Joints")
    with ui.row():
        for joint in joints:
            print(i)
            with ui.grid(columns=2):

                ui.label("joint.goal_torque")
                goal_torque.append(ui.label(joint.goal_torque))
                ui.label("joint.goal_fsm")
                goal_fsm.append(ui.label(joint.goal_fsm))
                ui.label("joint.goal_position")
                goal_position.append(ui.label(joint.goal_position))
                ui.label("joint.goal_id_torque")
                goal_id_torque.append(ui.label(joint.goal_id_torque))
                ui.label("joint.goal_rl_torque")
                goal_rl_torque.append(ui.label(joint.goal_rl_torque))
                ui.label("joint.goal_rl_pid")
                goal_rl_pid.append(ui.label(joint.goal_rl_pid))
                ui.label("joint.goal_rl_friction")
                goal_rl_friction.append(ui.label(joint.goal_rl_friction))
                ui.label("joint.current_position")
                current_position.append(ui.label(joint.current_position))
                ui.label("joint.current_velocity")
                current_velocity.append(ui.label(joint.current_velocity))
                ui.label("joint.goal_velocity")
                goal_velocity.append(ui.label(joint.goal_velocity))
                ui.label("joint.current_torque")
                current_torque.append(ui.label(joint.current_torque))
                ui.label("joint.current_fsm")
                current_fsm.append(ui.label(joint.current_fsm))
                ui.label("joint.current_bearing_temperature")
                current_bearing_temperature.append(ui.label(joint.current_bearing_temperature))
                ui.label("joint.current_motor_temperature")
                current_motor_temperature.append(ui.label(joint.current_motor_temperature))
                ui.label("joint.current_warnings")
                current_warning.append(ui.label(joint.current_warnings))
                ui.label("joint.current_errors")
                current_errors.append(ui.label(joint.current_errors))
                ui.label("joint.joint_rcv_counter")
                joint_rcv_counter.append(ui.label(joint.joint_rcv_counter))
                ui.label("joint.joint_rcv_counter_error")
                joint_rcv_counter_error.append(ui.label(joint.joint_rcv_counter_error))
            i += 1

    ui_timer = ui.timer(0.1, lambda: reload_data_and_refresh())
    
    ui.run(title='HMI', show=False)        

@falkoschindler
Copy link
Contributor

Yes, updating the existing elements is certainly the more efficient way to update the UI. Binding could also help to simplify the code.

But nonetheless, removing and re-creating elements should not leak any memory.

@falkoschindler
Copy link
Contributor

I found at least one memory leak:

window.socket.on("update", (msg) => Object.entries(msg).forEach(([id, el]) => this.elements[el.id] = el));

We only add elements to this.elements but never remove them. But how to do that? We either need to find out which elements lost their parents during the update, or we need to send explicit delete messages.

@falkoschindler
Copy link
Contributor

I pushed a fix for the leak from my previous comment #1089 (comment) on the "memory" branch:
https://github.com/zauberzeug/nicegui/compare/memory

For 1000 labels 10 times per second, the memory consumption still grows quite at bit. I assume something like the garbage collector having trouble to deal with so many objects.

But with reduced frequency to once per second the problem seems to be (almost?) gone. Even after many minutes the consumption oscillates around 500..600MB. I am, however, not 100% sure that there isn't any growth at all. Do we want to merge and close the issue for now, or should we keep investigating?

By the way: So far I didn't find a way to directly get number of Vue components or the memory profile of the Vue app. This would be a more definitive indicator of a memory leak.

@falkoschindler
Copy link
Contributor

No, we should not merge. Tests are red because moving elements from one container to another does not work anymore.
And it looks like the memory consumption is indeed still growing.

@CrystalWindSnake
Copy link
Contributor

CrystalWindSnake commented Jul 3, 2023

@falkoschindler
I believe that it is only possible to accurately determine whether a component should be released in the backend.Is it possible to consider using weak references and finalize in Python to solve this problem?

like this?

class Element:
    def __del__(self):
        print("should be release")
        # Notify the frontend that this component should be released.
        # self.push_del_message()

@falkoschindler
Copy link
Contributor

@CrystalWindSnake Yes, something like this might work. But I'm not sure if every element should notify the client about its removal independently, or if we better collect such element IDs as part of the update message.

@CrystalWindSnake
Copy link
Contributor

Collecting and scheduling seems to be a better approach. it will also be easy to use different schedulers and make different removal dependency strategies.

@falkoschindler falkoschindler added this to the 1.3.1 milestone Jul 12, 2023
@falkoschindler
Copy link
Contributor

New code for experimenting with the client-side element count:

def add():
    with card:
        ui.label('Some text')

async def count():
    ui.notify(await ui.run_javascript('return Object.keys(window.app.elements).length'))

card = ui.card()
ui.button('Add element', on_click=add)
ui.button('Remove element', on_click=card.clear)
ui.button('Count', on_click=count)

@falkoschindler
Copy link
Contributor

I think I fixed this issue in be30664:

  • Now we call delete() when clearing or removing an element. Somehow we used to call it only when clearing the client.
  • The delete() method adds the element ID to a new delete_queue in outbox.py.
  • The delete_queue is processed similarly to the update_queue. Therefore I refactored the code a little bit, especially using an _emit function to send messages to local and on-air clients.
  • For deleted elements we send an "update" message with None values. This way we don't have to introduce another message type that would require adjustments on the relay server.
  • In index.html these None elements are deleted from the app.

This works well with the example from #1089 (comment) and passes all pytests.

While experimenting with this stress test I noticed a huge performance bottleneck caused by the dependency loading function. It seems like calling the async function for every updated element (even without custom component or libraries) is much more expensive than checking for a component or library outside:
db18c94

With this change I can update 1000 elements almost 10 times per second on my machine. Of course, real-life scenarios should be much more conservative regarding performance and bandwidth.

@rodja Sorry for pushing to the main branch right before a big release. But I think we should include these changes.

@falkoschindler falkoschindler modified the milestones: 1.3.1, 1.3.0 Jul 13, 2023
@falkoschindler falkoschindler removed the help wanted Extra attention is needed label Jul 13, 2023
@rodja
Copy link
Member

rodja commented Jul 13, 2023

Looks good.

@rodja rodja closed this as completed Jul 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants