Pull to refresh

JavaScript: оператор delete создает утечку!?

Reading time4 min
Views18K
Здравствуй хабрнарод, хочу вам поведать об истории коварной утечки, и о великом недопонимании.
На самом деле все очень просто, вот такая, казалось бы, обычная строчка кода, в определенных условиях может вызвать утечку:
delete testedObject[ i ].obj;

Но, повторюсь только в определенных условиях. Еще одно но, пока точно неизвестно это браузерный баг или особенность JS.
Гугл, ничего не сказал мне по этому поводу, Копания в спецификации ECMAScript, тоже ничего не дало, ибо ее трудно понимать в трезвом состоянии. Собственно это и стало поводом написания данной статьи.


Предистория


Итак, я начал заниматься проектом X, для одной фирмы Y. Им нужна была система управления фирмой, сотрудниками и т.д., все в одном пакете. Все это дело должно было работать через браузер. Было решено писать это чудо на JS. Специфика приложения такова, что за вкладка могла быть открыта целый день, и активно использоваться. Поэтому мы боролись за каждый килобайт памяти и каждую миллисекунду процессорного времени. Фреймворки, не очень справлялись с этой задачей, и пришлось писать свои велосипеды, тут то и начались чудеса!

Суть


После очередной порции законченного куска приложения началось тестирование, и о боже приложение потихоньку кушает память и не хочет останавливаться. Причем во всех браузерах поголовно. Для Лисы самый фатальный исход, закрытие вкладки не помогает, и память остается съеденной. Сначала я грешил на свой велосипед, но спустя некоторое время я докопался до истины и понял что все дело в операторе delete.

Поначалу я «обрадовался» что нашел очередной браузеры баг. Но потом мучился сомнениями, не может ведь быть такое, что все браузеры поголовно текут. Да и в крупных Фреймворках оператор delete очень активно используется.

Поэтому я начал компрометировать утечку в JQuery, мучился пару часов, но утечки так и не было. В чем же дело, заглянул в код JQuery и не обнаружил там никакой магии. Ну и ладно подумал я, создал новый файлик, сделал прототип той функции, где есть утечка и начал с ней заниматься непристойностями.

Собственно код прототипа, с утечкой
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script type="text/javascript">

    var count = 100000;
    var testedObject;

    function getObject() {
        // делаем объект потяжелее
         return {
            first:   { val : 'tratata', item : { val : 'tratata' } },
            second:  { val : 'tratata', item : { val : 'tratata' } },
            third:   { val : 'tratata', item : { val : 'tratata' } },
            fourth:  { val : 'tratata', item : { val : 'tratata' } },
            fifth:   { val : 'tratata', item : { val : 'tratata' } },
            sixth:   { val : 'tratata', item : { val : 'tratata' } },
            seventh: { val : 'tratata', item : { val : 'tratata' } },
            eighth:  { val : 'tratata', item : { val : 'tratata' } },
            ninth:   { val : 'tratata', item : { val : 'tratata' } }
        };
    }

    //Заполняем массив объектами
    function fillArray() {
        testedObject = {};
        for( var i = 0; i < count; i++ ) {
            testedObject[ i ] = {
                obj : getObject()
            };
        }
    }

    //Удаляем созданные объекты
    function deleteObjects() {
        for( var i = 0; i < count; i++ ) {
            delete testedObject[ i ].obj;
        }
    }
    </script>
</head>
<body>
<div>
    <button onclick="fillArray()" >Заполнить массив</button>
    <button onclick="deleteObjects()" >Удалить объекты</button>
</div>
</body>
</html>



Скрин утечки:


На скрине хорошо видно, что в момент удаления объектов память почему то сильно отжирается, но потом освобождается до прежнего состояния. Но все же память, отведенная для объектов которые были удалены осталась не очищенной.

При этом оператор delete, ведет себя вполне нормально, не ругается и не кричит, объекты как бы удаляются и как бы все нормально, но память, занимаемая ими, не освобождается.

Долой утечку


Некоторое время спустя я понял, что все достаточно просто, и утечки можно избежать, но придется немного перестроить код.
Память очищалась, если сделать вот так:
delete testedObject[ i ];


Вариант без утечки
<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script type="text/javascript">

    var count = 100000;
    var testedObject;
    
    function getObject() {
        // делаем объект потяжелее
         return {
            first:   { val : 'tratata', item : { val : 'tratata' } },
            second:  { val : 'tratata', item : { val : 'tratata' } },
            third:   { val : 'tratata', item : { val : 'tratata' } },
            fourth:  { val : 'tratata', item : { val : 'tratata' } },
            fifth:   { val : 'tratata', item : { val : 'tratata' } },
            sixth:   { val : 'tratata', item : { val : 'tratata' } },
            seventh: { val : 'tratata', item : { val : 'tratata' } },
            eighth:  { val : 'tratata', item : { val : 'tratata' } },
            ninth:   { val : 'tratata', item : { val : 'tratata' } }
        };
    }

    function fillArray() {
        testedObject = {
            obj : {}
        };
        for( var i = 0; i < count; i++ ) {
            testedObject.obj[ i ] = getObject();
        }
    }

    function deleteObjects() {
        for( var i = 0; i < count; i++ ) {
            delete testedObject.obj[ i ];
        }
    }
    </script>
</head>
<body>
<div>
    <button onclick="fillArray()" >Заполнить массив</button>
    <button onclick="deleteObjects()" >Удалить объекты</button>
</div>
</body>
</html>



Изменения конечно не столь сложные, и сильно не повлияли на архитектуру моего велосипеда, но было очень неожиданно и досадно, что вариант первый создавал утечку.

Сразу оговорюсь что, причина утечки не скоупы, не контексты, не индексы, не имена параметров — я это все проверял.

Заключение


Итак, я вроде устранил утечку, и все хорошо, жизнь удалась. Но меня все же мучают много вопросов:
Чем кординально отличается первый вариант функции от второго?
И особенность ли это или браузерный баг?
Если особенность, то почему ни в одном мане по JS эта особенность не описана?
Если браузерный баг, то почему столько лет спустя он до сих пор есть?
Может ктото уже знает про это, но молчит?

Надеюсь, моя статья кому то поможет! И думаю, стоит проверить свои велосипеды на подобные утечки!

PS: Спасибо всем за внимание!!!

UPDATE 1:

Написал об этом пока что в багктрекер Мозилы, посмотрим что они скажут.

UPDATE 2:

Луч в конце тунеля



Итак, ситуация прояснилась, спасибо за это огромное mraleph и seriyPS, в их комментариях, все подробно описано. #comment_5107501, #comment_5107585, #comment_5107692, так же Mavim подробно описывает мой конкретный случай.

На все вопросы получены четкие ответы, это не магия JavaScript, не утечка и не браузерный баг, это особенность реализации оператора delete. И скорее это можно назвать избыточное использование памяти, и я как раз наступил на такую мину. Но теперь врага мы знаем в лицо и без проблем на от него избавимся.

Всем спасибо!!!
Tags:
Hubs:
+28
Comments48

Articles

Change theme settings