niivue / niivue

a WebGL2 based medical image viewer. Supports over 30 formats of volumes and meshes.
https://niivue.github.io/niivue/
BSD 2-Clause "Simplified" License
266 stars 66 forks source link

Niivue cleanup after useEffect unmount. #1009

Closed hari7696 closed 4 months ago

hari7696 commented 4 months ago

Hi

I am using niivue to display a 3d rendering, but the memory used by page is kept on increasing after every swtich to a new image. I believe I am not doing aproper unmount cleanup.

As a part of cleaning up, I am removing all volumes and updating the GLvolume, but looks like the earlier image arrays are still persisting in the memory. The images, I am fecthing are of a size of 100+ MB, so I clearly see the memory increase. I am no expert in WebGL and pretty new react. would apprecaite your inputs.

following is my unmount cleanup

return () => { 

        for (let i = 0; i < nv2.volumes.length; i++) {
          nv2.removeVolumeByIndex(i);
        }
        nv2.updateGLVolume();
      }

partial roguh code I am using.

 const [nv2,setNv2] = useState(new Niivue())
  useEffect(() => {

    console.log("TRIGGER: Image attachment")
    nv2.attachTo("gl2");
    setInitialRender(!initialRender);
    }, []);

 useEffect(()=> {             

    nv2_volumelist = [
                  {
                    url: `${process.env.REACT_APP_FLASK_API_URL}/data/Alzhemier/Normal/Boundary/${props.fileName}`, 
                    colormap: "gray",
                    opacity: 0.15,
                    visible: true,

                },
                    {
                        url: data.url + `?nocache=${new Date().getTime()}`,
                        colormap: "actc",
                        opacity: 0.9,
                        visible: true,
                        cal_min: data.cal_min,
                        cal_max: data.cal_max,
                        trustCalMinMax: true
                    }
                ];

nv2.loadVolumes(nv2_volumelist);
nv2.setSliceType(nv2.sliceTypeRender)

return () => { 

        for (let i = 0; i < nv1.volumes.length; i++) {
          nv2.removeVolumeByIndex(i);
        }
        nv2.updateGLVolume();
      }

}, [props.fileName]);
neurolabusc commented 4 months ago

All our live demos are pure html, which provide minimal recipes without regard to your preferred framework (Angular, React, Vue). I used this live demo and used Chrome's memory monitor and the MacOS activity monitor to track memory as I opened each of the buttons (chris_MRA, chris_PD, ...) on the top toolbar, each which loads a different volume with a different resolution. While some volumes demand more resources than others, I did not see a regular increase in resource demands.

Note that this live demo code simply loads a new set of volume(s), without worrying about removing pre-existing volumes.

nv1.loadVolumes(volumeList1);

pure_html

It is very hard to troubleshoot snippets of non-functional code, or when large frameworks are used. To create a minimal pure HTML project to showcase a problem, I would start a hot-reloadable instance of NiiVue:

git clone git@github.com:niivue/niivue.git
cd niivue
npm install
npm run dev

With this NiiVue instance loaded, edit the /niivue/src/index.html page to emulate the behavior that you find problematic.

hanayik commented 4 months ago

@hari7696 , in your clean up code, you are referencing nv1 in the for loop, but all other niivue instances in your code snippet are referenced as nv2. Is this a typo, or the source of your observed issue?

In addition, you may want to look into useContext and createContext with React. We use this in our NiiVue desktop app. Here is an example.

hanayik commented 4 months ago

@hari7696 , please reopen if you can provide a minimal React demo showing the bad behaviour.

hari7696 commented 4 months ago

Hi @hanayik @neurolabusc

Thanks for the inputs, here is the standalone code that simulates the issue I am facing.

I hosted the code here : https://devmetavision3d.rc.ufl.edu/ in "dummy" tab image

import React, { useState, useEffect } from 'react';
import { Niivue } from '@niivue/niivue';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';

function DummyRender() {
  const [fileName, setSelectedFile] = useState("");
  const [dropdownOpen, setDropdownOpen] = useState(false);
  const [isattached , setIsAttached] = useState(false);
  const nv1 = new Niivue();
  const nv2 = new Niivue();

  const files = ["5x_ADP.nii.gz","5x_AMP.nii.gz","5x_Aarachidonic.acid.nii.gz","5x_Alanine.nii.gz","5x_Ascorbic.Acid.nii.gz","5x_Aspartate.nii.gz","5x_Carnosine.nii.gz","5x_Citric.Acid.nii.gz","5x_Docosahexaenoic.acid.nii.gz","5x_Fructose.1.6.Bisphosphate.nii.gz","5x_G6P.nii.gz","5x_GMP.nii.gz","5x_GSH.nii.gz","5x_Glucose.nii.gz","5x_Glutamate.nii.gz","5x_Glutamine.nii.gz","5x_Glycerol.3.phosphate.nii.gz","5x_Glycerophosphorylethanolamine.nii.gz","5x_HEME.nii.gz","5x_Hypoxanthine.nii.gz","5x_IMP.nii.gz","5x_Inosine.nii.gz","5x_LPA..20.1..nii.gz","5x_LPA..20.4..nii.gz","5x_LPA..22.4..nii.gz","5x_LPA..22.6..nii.gz","5x_LPA.18.0..nii.gz","5x_LPA.18.1..nii.gz","5x_LPE..16.1..nii.gz"]

  useEffect( () => {
     nv1.attachTo('gl1');
     nv2.attachTo('gl2');
    setIsAttached(true);
  }, [fileName]);

  useEffect(() => {

    console.log("Main  func invoked")
    if (isattached) {
      console.log("volumes loading")
      const volumeList1 = [
        {
          url: `https://devmetavision3d.rc.ufl.edu/data/Alzhemier/Disease/Inverted/${fileName}`,
          opacity: 0.9,
          visible: true,
          colormap: 'blue2red',
        }
      ];
      nv1.loadVolumes(volumeList1);
      nv1.updateGLVolume();
      nv1.setSliceType(nv1.sliceTypeAxial);

      nv2.loadVolumes(volumeList1);
      nv2.updateGLVolume();
      nv2.setSliceType(nv2.sliceTypeRender);
    }

    return () => {
      for (let i = 0; i < nv1.volumes.length; i++) {
        nv1.removeVolumeByIndex(i);
      }

      for (let i = 0; i < nv2.volumes.length; i++) {
        nv2.removeVolumeByIndex(i);
      }
      setIsAttached(false);
      console.log("Volumes removed");
    };
  }, [fileName]);

  const toggleDropdown = () => setDropdownOpen(prevState => !prevState);

  return (
    <div className="container">
        <div >
            <Dropdown isOpen={dropdownOpen} toggle={toggleDropdown} style={{ textAlign: 'left', width: '300px', padding: '10px' }}>
                <DropdownToggle caret  style={{ width: '400px' }}>
                    {fileName || "Select a file"}
                </DropdownToggle>
                {/* The right prop is omitted or set to false for left alignment */}
                <DropdownMenu className="scrollable-menu">
                    {files.map((file, index) => (
                        <DropdownItem 
                            key={index}
                            onClick={() => setSelectedFile(file)}
                        >
                            {file}
                        </DropdownItem>
                    ))}
                </DropdownMenu>
            </Dropdown>
            </div>

      <div className="row"><div className="col"><hr style={{ border: 'none', height: '2px', backgroundColor: 'black' }} /></div></div>
      <div className="row">
        <div className="col-1" style={{ fontSize: '24px', fontWeight: 'bold', marginRight: '14px' }}>Wild Type Brain</div>
        <div id="demo1" className="col-5 p-2" style={{ height: '300px' }}>
          <canvas id="gl1" style={{ width: '500px', height: '400px' }}></canvas>
        </div>
        <div id="demo1" className="col-5 p-2" style={{ height: '300px' }}>
          <canvas id="gl2" style={{ width: '500px', height: '400px' }}></canvas>
        </div>
      </div>
    </div>
  );
}

export default DummyRender;

Behaviour observed.

  1. After every image switch, the the Memory usage [ Windows Task Manager] is kept on increasing. I am adding screen grabs taken between image switches.

image

  1. I monitored the page performance as well, I dont see any exponential increase. image
hanayik commented 4 months ago

@hari7696 , from what I can see, it looks like you have two useEffect statements that have the same dependency. In the past, this has caused issues in my other apps.

Also, your implementation seems to create a new Niivue instance on every re-render. I would suggest using useContext or useRef for the Niivue instances. I would also leave the Niivue instances attached to the canvas, and just use the Niivue removeVolume and loadVolumes methods to change the image you want rendered in the canvas.

hari7696 commented 4 months ago

Hi @hanayik

Thanks for the suggestion. Switching to useRef and leaving the instances attached solved my issue.