I need help in resolving a memory leak found using JavaScript. What the program does is auto scroll the videos and when it reaches the center (supposed to be a grid but for the purposes of this problem, it is not visibily shown but can be noticed immediately) or scrolls off the screen, it will chop the video into two separate videos and put one chopped video on one side of the area and the other chopped video on the other side of the area.

Here is the JavaScript code for the program:

window.addEventListener("DOMContentLoaded", function()
{
    var patternNumbers = new RegExp("[0-9]+", "g");

    var videoCoordinates = {};

    for (var i = 1; i <= 4; i++)
    {
        var videoElement = document.createElement("video");
        var videoSource = document.createElement("source");

        var videoId = "video-" + i;

        videoCoordinates[videoId] = {};
        videoCoordinates[videoId]["latitude"] = (Math.random() * 180 - 90).toFixed(8);
        videoCoordinates[videoId]["longitude"] = (Math.random() * 360 - 180).toFixed(8);

        videoElement.id = videoId;
        videoElement.preload = "auto";
        videoElement.setAttribute("webkit-playsinline", "");

        if (((i - 1) % 4) < 2)
            videoElement.style.left = ((i - 1) % 4) * 50 / 4 + "%";
        else
            videoElement.style.left = 50 + ((i - 1) % 4) * 50 / 4 + "%";

        videoElement.style.top = Math.floor((i - 1) / 4) * 100 / Math.ceil(25 / 4) + "%";

        videoElement.style.width = "2px";
        videoElement.style.height = "1px";

        videoSource.src = "test" + (((i - 1) % 5) + 1) + ".mp4";
        videoSource.type = "video/mp4";

        document.body.appendChild(videoElement);
        videoElement.appendChild(videoSource);

        videoElement.onclick = function()
        {
            var videoElement = this;

            hiddenFunction(videoElement);;
        }

        videoElement.addEventListener("loadedmetadata", function()
        {
            var videoElement = this;

            videoElement.style.width = Math.min(50 / 4, ((50 / 4) / 100 * window.innerHeight * videoElement.videoWidth / videoElement.videoHeight) / window.innerWidth * 100) + "%";
            videoElement.style.height = (Math.min(50 / 4, ((50 / 4) / 100 * window.innerHeight * videoElement.videoWidth / videoElement.videoHeight) / window.innerWidth * 100) / 100 * window.innerWidth * videoElement.videoHeight / videoElement.videoWidth) / window.innerHeight * 100 + "%";

            videoElement.removeEventListener("loadedmetadata", arguments.callee, false);
        }, false);

        videoSource = null;
        videoElement = null;
    }

    function videoCopyAdd(videoElement)
    {
        var videoId = videoElement.id;
        var videoCopy = videoElement.cloneNode(true);

        videoElement.id = videoId + "-1";
        videoCopy.id = videoId + "-2";

        if (videoElement.nextSibling)
            document.body.insertBefore(videoCopy, videoElement.nextSibling);
        else
            document.body.appendChild(videoCopy);

        if (0.25 < videoElement.getBoundingClientRect().right / window.innerWidth && videoElement.getBoundingClientRect().left / window.innerWidth < 0.75)
        {
            videoCopy.style.left = 50 + parseFloat(videoElement.style.left) + "%";

            videoElement.style.clip = "rect(0px, " + (0.25 * window.innerWidth - videoElement.getBoundingClientRect().left) + "px, " + videoElement.offsetHeight + "px, 0px)";
            videoCopy.style.clip = "rect(0px, " + videoCopy.offsetWidth + "px, " + videoCopy.offsetHeight + "px, " + (0.75 * window.innerWidth - videoCopy.getBoundingClientRect().left) + "px)";
        }
        else if (videoElement.getBoundingClientRect().right / window.innerWidth > 1)
        {
            videoCopy.style.left = parseFloat(videoElement.style.left) - 100 + "%";

            videoElement.style.clip = "rect(0px, " + (window.innerWidth - videoElement.getBoundingClientRect().left) + "px, " + videoElement.offsetHeight + "px, 0px)";
            videoCopy.style.clip = "rect(0px, " + videoCopy.offsetWidth + "px, " + videoCopy.offsetHeight + "px, " + (0 - videoCopy.getBoundingClientRect().left) + "px)";
        }

        videoCopy.onclick = function()
        {
            var videoElement = this;

            hiddenFunction(videoElement);;
        }

        videoCopy = null;
    }

    function videoCopyMove(videoElement)
    {
        var videoId = "video-" + videoElement.id.match(patternNumbers)[0];
        var videoCopy = document.getElementById(videoId + "-2");

        videoCopy.style.left = parseFloat(videoCopy.style.left) + 2 + "%";

        if (0.25 < videoElement.getBoundingClientRect().right / window.innerWidth && videoElement.getBoundingClientRect().left / window.innerWidth < 0.75)
        {
            videoElement.style.clip = "rect(0px, " + (0.25 * window.innerWidth - videoElement.getBoundingClientRect().left) + "px, " + videoElement.offsetHeight + "px, 0px)";
            videoCopy.style.clip = "rect(0px, " + videoCopy.offsetWidth + "px, " + videoCopy.offsetHeight + "px, " + (0.75 * window.innerWidth - videoCopy.getBoundingClientRect().left) + "px)";
        }
        else if (videoElement.getBoundingClientRect().right / window.innerWidth > 1)
        {
            videoElement.style.clip = "rect(0px, " + (window.innerWidth - videoElement.getBoundingClientRect().left) + "px, " + videoElement.offsetHeight + "px, 0px)";
            videoCopy.style.clip = "rect(0px, " + videoCopy.offsetWidth + "px, " + videoCopy.offsetHeight + "px, " + (0 - videoCopy.getBoundingClientRect().left) + "px)";
        }

        if ((0.25 < videoElement.getBoundingClientRect().right / window.innerWidth && videoElement.getBoundingClientRect().left / window.innerWidth < 0.75 && videoCopy.getBoundingClientRect().left / window.innerWidth >= 0.75) || (videoElement.getBoundingClientRect().right / window.innerWidth > 1 && videoCopy.getBoundingClientRect().left / window.innerWidth >= 0))
        {
            videoElement.onclick = null;

            for (var i = 0; i < videoElement.childNodes.length; i++)
            {
                videoElement.removeChild(videoElement.childNodes[i]);
            }

            videoElement.parentNode.removeChild(videoElement);

            videoElement = null;

            videoCopy.id = videoId;

            document.getElementById(videoId).style.clip = "";
        }

        videoCopy = null;
    }

    function videoScroll()
    {
        var videoNonGridElements = document.getElementsByTagName("video");

        for (var i = 0; i < videoNonGridElements.length; i++)
        {
            if (!videoNonGridElements[i].style.zIndex && videoNonGridElements[i].id.match(patternNumbers)[1] != 2)
            {
                var videoElement = videoNonGridElements[i];
                var videoId = "video-" + videoElement.id.match(patternNumbers)[0];

                videoElement.style.left = parseFloat(videoElement.style.left) + 2 + "%";

                if (0.25 < videoElement.getBoundingClientRect().right / window.innerWidth && videoElement.getBoundingClientRect().left / window.innerWidth < 0.75 || videoElement.getBoundingClientRect().right / window.innerWidth > 1)
                {
                    if (!document.getElementById(videoId + "-2"))
                        videoCopyAdd(videoElement);
                    else
                        videoCopyMove(videoElement);
                }

                videoElement = null;
            }
        }

        videoNonGridElements = null;

        videoAnimateScroll = setTimeout(function() { videoScroll(); }, 3000);
    }

    var videoAnimateScroll = setTimeout(function() { videoScroll(); }, 3000);
}, false);

I can give a demonstration but since this is a temporary project that won't be publicly available, I will not provide the link since it'll be broken in the future in case other people have similar problems. You can private message me for the link and I'll provide it. The JavaScript code here is exactly the same as the one in the demonstration so the solution can be archived here in the forums.

The memory leak appears when the video is chopped the first time, which is executed using the videoCopyAdd function. Now, of course, when the video is chopped, it creates another video element to be added in the DOM. That part I do understand the sudden increae in memory. However, when the chopped video has fully been shown, it removes one part of the chopped video from the DOM and the other video stays in the DOM. The problem here is that when the removal of the video from the DOM is done, the memory is not released. I suspect that it is either the videoCopyAdd function or the videoCopyMove function, or maybe both, that a variable is referencing a video node that has been removed from the DOM.

I have tested it on an old Chrome version (I believe version 31) and the memory leak is there. The newest Chrome version (36.0.1985.125) that I have tested it on has some problem with the CSS clip property. Nevertheless, the code still works, despite the appearance of the videos, and the memory leak still exists. Even so, Chrome mainly, not always, leaks the memory after a few minutes and is able to release the memory within the first few minutes. With FireFox, it's an entirely different story. The memory leaks indefinitely and does not release any memory at all, even on page refresh. I've used the timeline memory function in Chrome and I can confirm that the memory keeps increasing and the section where it shows a memory leak shows an increase amount of nodes, even if no new nodes have been added or nodes have been removed from the DOM. I tried isolating the problem with the heap snapshots but I haven't seen anything strange in the comparison of before and after a memory leak.

Any help would be greatly appreciated as I've been working on this issue for way too long now and I am very much out of ideas on what to do.

Recommended Answers

All 7 Replies

If you believe it is a memory leak problem, you need to look at how you remove nodes/elements from the DOM. From what I see so far, you remove nodes/elements by using removeChild() which is correct; however, you should also remove its function attribute before you remove the node/element as well. Below is a sample script.

function purgeHTMLElement(elem) {
  var a = elem.attributes;
  var i;
  if (a) {
    var n;
    for (i=a.length-1; i>=0; i--) {  // attempt to remove all functions
      n = a[i].name;
      if (typeof(elem[n])==="function") { elem[n] = null; }
    }
  }
  a = elem.childNodes;
  if (a) {
    for (i=a.length-1; i>=0; i--) { purgeHTMLElement(elem.childNodes[i]); }
  }
  elem.parentNode.removeChild(elem)  // remove the element from DOM
}  // purgeHTMLElement(HTMLelement)

Also, please explain how videoCopyAdd() and videoMove() works in detail. And what does hiddenFunction() do?

Member Avatar for stbuchok

Try using the profiler that comes with the browser. This will help you narrow down exactly what is happening and where.

@Taywin: Thanks for the suggestion, I will work with the sample script and see if I can come up with anything. This was something I did not know at all and all the searching I have done did not mention any of this. hiddenFunction() is a function that is another part of the program that is irrelevant to this memory leak problem, as it has no code where a memory leak can be created, therefore I have no need to show it. videoCopyAdd() adds a duplicate video element to the DOM when the original video moves outside of the window while scrolling or into the grid, the middle 50% of the screen. videoCopyMove() scrolls the duplicate video and clips (hides) both videos with respect to how much should be shown as part of it will be one side of the window/grid and another part of it will be on the other side. I also stumbled onto this page while searching that there may be a memory leak with the browser itself related to the creation and removal of video elements: http://code.google.com/p/chromium/issues/detail?id=255456

@stbuchok: I have tried using the profilier on Google Chrome using heap snapshots but there was no information that depicted there was a memory leak, even though the timeline showed signs of memory leak as well as the task manager memory.

Interesting... What I am thinking about work around is that you do not remove a video when it is loaded on the page but rather hide it. You could still keep creating/moving video object and allow it to be displayed when meet criteria. Move the video object out of display and hide it when needed. This way, you are not shuffling video on the display which could cause the memory leak. Hope this help.

I have tested the code out and it does not seem there would be any changes. The video element has 1 function attribute associated to it and the children, which would be the source element, has 0 function attributes. The function attribute is already nulled on line 117 in my original post.

I may do a workaround using a div tag then using innerHTML to display the video element. It seems that it won't leak memory that way, based on the information provided in the Google Code forums from the link I posted previously.

Worst case scenario, I will use your suggestion on doubling the video elements on page load and hiding half of it but this may not work due to the fact that 32-bit browsers can only use 4GB of memory, albeit some memory is not used by the operating system so more like 3 to 3.5 GB. Based on the videos I have (1080p), Chrome uses about 50 MB per video while FireFox uses about 250MB per video.

If you want to try another way, force the garbage collector (GC) to do its job every time you remove a video? You could find how to do it on Firefox here, and on Chrome here.

There are 2 articles on analyze the leak -- IBM and Microsoft. You may need to pay attention on the Cross-page leak in the Microsoft article. However, in the article, I am not sure that their implementation/explanation of removing parent nodes first and then its child nodes is correct in general. Who knows, IE may have its internal memory management differs from other browsers...

I found out what was causing the memory leak. Apparently, when the video tag is removed, the source/location file of the video is still kept in memory. I found this solution on a different website but essentially the solution is the addition of the following 2 lines prior to removing the video from the DOM:

videoElement.src = "";
videoElement.load();

This will set the video to have no source file destination. When the video loads the empty source file, there is nothing to load into the video tag so I assume garbage collection will kick in immediately as there is no need to keep it in memory.

Thanks for the help everyone. This was more trouble than I thought it was going to be but I guess this happened since HTML5 is still new and browsers haven't done all the work for us yet as developers. I was also on vacation recently thus why I haven't replied in a while.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.