Архив Октябрь 2010

Оптимизация сборки мусора: интерфейс IDisposable

Garbage collectorМетодом проб (доброй половины выложенных в интернете способов оптимизации памяти) и ошибок, я пришел к выводу, что единственный способ гарантированно избежать проблем со сборкой мусора — завести интерфейс IDisposable (по аналогии с .NET). Вот краткое описание одноименного интерфейса из msdn:  «Основое назначение этого интерфейса заключается в высвобождении неуправляемых ресурсов». В нашем случае смысл другой — очистить  ссылки на другие объекты, которыми владеет данный объект. Для удобства использования добавился метод isDisposed(), который позволяет проверить был ли данный объект разрушен ранее.

package gc
{
    /**
     * Must be implemented in order to be properly disposed
     */
    public interface IDisposable
    {
        /**
         * Free references in this method
         */
        function dispose():void;

        /**
         * Check return value to prevent Object be disposed twice
         * @return true if Object was disposed, false in other case
         */
        function isDisposed():Boolean;
    }
}

Если в классе есть атрибуты содержащие ссылки на дисплей объекты, ссылки на методы других объектов, динамические объекты, массивы и коллекции(дисплейобъектов, сложных объектов, ссылок на методы) — необходимо добавить классу этот интерфейс и уже в конкретной имплементации очищать ссылки. Таким образом, завершив работу с объектом, проверяем isDisposed и очищаем его вызвав dispose(). Если класс владеет объектами реализующими интерфейс IDisposable, он может в свою очередь вызывать у них метод dispose().

На практике этот способ доказал свою работоспособность и эффективность (в комбинации с еще одной хитрой штукой, о которой я напишу позже).

Основная проблема заключается в том что флекс не оптимизирован для работы сборщика мусора, а иногда даже существенно тормозит этот процесс (яркий пример — основанные на ListBase компоненты), поэтому пришлось добавить имплементацию IDisposable расширив базовые классы, но об этом позже.

Реклама

, , ,

2 комментария

Профайлер памяти своими руками

Для флекса существует единственный полноценный профайлер — встроенный в Flex Builder. Для точной отладки он незаменим, однако в использовании несколько сложен и глючен, кроме того, для тяжелого проекта отладка начинает занимать очень много времени. Есть альтернативный профайлер — FlashPreloadProfiler. Какую-то полезную информацию он показывает, но подружиться с ним не удалось.

Именно для отладки тяжелого интранет приложения понадобилось придумать что-то простое, что позволило бы не теряя времени определять компоненты, которые вызывают утечки памяти и вести мониторинг приложения. И решение нашлось — flash.utils.Dictionary. Этот класс позволяет включить для ключей слабые ссылки (weak reference) через конструктор с параметром weakKeys=true:

var dict:Dictionary = new Dictionary(true);//weakKeys=true

Слабые ссылки не препятствуют сборке мусора, именно это свойство и можно использовать: подозрительные/проблемные объекты используем в качестве ключа:

dict[leaked_object] = UIDUtil.createUID();//Generates a UID (unique identifier); see mx.utils.UIDUtil

А полученный UID сохраняем в коллекцию:

var uids:ArrayCollection = new ArrayCollection();
uids.addItem(dict[leaked_object]);//save generated UID

В дальнейшем запустив таймер или используя setInterval мы пробегаем по ключам в dict (ключи со временем будут собираться сборщиком мусора) сравнивая значения по этим ключам (UID) с сохраненными в коллекции uids — получая возможность отслеживать работу сборщика мусора без профайлера:

        private function printDiff():void
        {
            var liveObjects:ArrayCollection = new ArrayCollection();//objects that were not collected
            for (var key:Object in references)
            {
                if (key != null)
                {
                    trace("Live object: ", uid, getQualifiedClassName(key));//print live object info
                    liveObjects.addItem(dict[key]);
                }
            }
            var removedObjects:ArrayCollection = new ArrayCollection();//objectes that were collected

            for each (var uid:String in uids)
            {
                if (!liveObjects.contains(uid))
                {
                    trace("Collected: ", uid); //print uid of collected object
                    removedObjects.addItem(uid);
                }
            }

            for each (var removedUID:String in removedObjects)
            {
                if (uids.contains(removedUID))
                {
                    uids.removeItemAt(uids.getItemIndex(removedUID));
                }
            }
            removedCodes.removeAll();//free memory
            liveObjects.removeAll();//free memory
        }

Необходимо также использовать хак (например из абзаца «Unsupported Way to Force GC» тут) для вызова сборщика мусора, иначе придется ждать некоторое время.

В таком виде уже можно по логам флешплеера установить объекты, которые могут вызывать утечки памяти (и уже точечно профайлить во Flex Builder), однако информация не очень удобна для восприятия. Для проекта я использовал более сложный вариант, написал визуальную часть с датагридом, автообновлением, сортировками, и еще парой тройкой удобных функций (выбор дисплейобъекта для мониторинга, добавление иерархии дисплейобъектов для монтиринга, копирование текущей иформации в буфер обмена). Исходники и рабочий пример я выложу в одном из следующих постов, так как надо причесать код :).

Окно профайлера

Окно профайлера

, , ,

3 комментария

Оптимизация drag and drop в Flex 3

Drag-and-dropОднажды в процессе разработки возникла необходимость отобразить список изображений с дополнительной информацией, кнопками и прочими панельками. Первым ходом стал собственный компонент основанный на гриде (mx.containers.Grid). Все вобщем-то работало, но drag-and-drop тормозил невероятно. После довольно длительного исследования выяснилось, что проблема находится в методе DragProxy#mouseMoveHandler:

/*having trouble with getObjectsUnderPoint.  Some things seem to get in list
 like cursors that shouldn't.  We roll our own for sandboxed apps and it works
 better for now.
 if (tlr)
   targetList = DisplayObjectContainer(tlr).
   getObjectsUnderPoint(stagePoint);
 else
 {*/
   targetList = [];
   DragProxy.getObjectsUnderPoint(DisplayObject(sandboxRoot), stagePoint, targetList);
/*}*/

Как видно из кода ребят из флекса неустроили результаты нативного метода getObjectsUnderPoint, ну и естественно чуваки написали свой — тормозной обход иерархии дисплейлиста. На первый раз мы забили и переделали компонент под mx.controls.TileList (в отличие от грида в листе использовалось меньше контейнеров).

Прошло время, функционал нарастал, компонент вновь стал неюзабельным, тормозило жутко даже нормальных компах. Пришлось разбираться. Проблем надо было решить 2:

  1. по условиям проекта патчить flex sdk было нельзя
  2. собственно сама оптимизация

Я начал со второй, почитав комент «having trouble with getObjectsUnderPoint«, я решил почему бы не попробовать использовать нативный может еще не все потеряно. И вот что получилось:

        private static const EMBED_CSS:String = "__embed_";//Classes created by mxmlc generator from styles

        public function getTarget(x:Number, y:Number):DisplayObject
        {
            var newTarget:DisplayObject;
            var point:Point = new Point(x, y);
            var targetList:Array = sandboxedRoot.getObjectsUnderPoint(point);
            newTarget = findTarget(targetList);

            return newTarget;
        }

        private function findTarget(targetList:Array):DisplayObject
        {
            var newTarget:DisplayObject = null;

            var length:uint = targetList.length;

            for (var i:uint = (length - 1); i--; i >= 0)
            {
                newTarget = targetList[i];

                if (newTarget is ProgrammaticSkin)
                {
                    newTarget = newTarget.parent;
                }
                else if (newTarget is DisplayObjectContainer && !DisplayObjectContainer(newTarget).mouseEnabled)
                {
                    newTarget = newTarget.parent;
                }
                else if (newTarget.toString().indexOf(EMBED_CSS) > 0)
                {
                    newTarget = newTarget.parent;
                }

                if ((newTarget != this) && !this.contains(newTarget))
                {
                    break;
                }
            }
            return newTarget;
        }

Этот код делает то, что нужно, но тормозит на порядок меньше, так как все же нативный. Дальше нужно было как-то прикрутить это все без патча sdk.

Первым желанием был гнусный хак: через dragImage.parent добраться до DragProxy, удалить листенеры, навесить свои и свой DragProxy. Решение это оказалось на практике глючным, плюc не все листенеры навешенные внутри этого класса можно было удалить. Пришлось вчитаться в код DragProxy.

Оказалось, что внутри используется только SystemManger компонента, который вызвал DragManager.acceptDrag. И тут уже созрел план: в методе dragStartHandler листа,  подменить его SystemManager на декорированный, получив контроль над вызовом всех его методов. В итоге пришлось сделать несколько декорторов:  для sytemManger, sanboxedRoot, childList. Далее, раз нельзя изменить код DragProxy — подсунуть через ChildList декорированного sandboxedRoot’a сразу нужный объект найденный быстрым кодом, уменьшив глубину рекурсии в DragProxy до 2х. С небольшими доработками (доработки связаны с ChildList и декоратором sandboxedRoot, фейковые значения некоторый свойств необходимы, чтобы остановить рекурсию в DragProxy) это сработало, но небольшие тормоза остались. И тут на помощь пришел Капитан Очевидность, зачем искать dropTarget, если выполняется операция MOVE? Ведь это будет сам лист! При операции MOVE достаточно проверить результат hitTestPoint для листа, и вернуть его же. Это уменьшит число ивентов которые диспатчатся всем компонентам по иерархии, плюс удастся избежать вызова медленного getObjectsUnderPoint и последующего перебора. Тормоза ушли совсем — победа.

Код можно скачать здесь. Использовать примерно так:

        override protected function dragStartHandler(event:DragEvent):void
        {
            if (event.isDefaultPrevented())
                return;

            var dragSource:DragSource = new DragSource();
            addDragData(dragSource);
            var dragImage:IUIComponent = dragImage;

            //restore decorated manager
            if (this.systemManager is SystemManagerDecorator)
            {
                this.systemManager = SystemManagerDecorator(this.systemManager).decorated;
            }

            var displayObjectContainer:DisplayObjectContainer = this;
            this.systemManager = new SystemManagerDecorator(displayObjectContainer, this.systemManager, dragImage);

            DragManager.doDrag(this, dragSource, event, dragImage, 0, 0, 0.5, dragMoveEnabled);
        }

        override protected function dragCompleteHandler(event:DragEvent):void
        {
            super.dragCompleteHandler(event);

            //restore original
            if (this.systemManager is SystemManagerDecorator)
            {
                this.systemManager = SystemManagerDecorator(this.systemManager).decorated;
            }
        }

В следующем посте простой профайлер для памяти, ужас какой простой и работающий)

, , , ,

2 комментария

Байндинг(Data Binding) и mxml

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

Результатом использования байндинга являются невидимые невооруженным взглядом переменные, которые генерирует компилятор mxmlc:
object.mx_internal::_bindings; //Array
object.mx_internal::_watchers; //Array
object.mx_internal::_bindingsByDestination; //Object
object.mx_internal::_bindingsBeginWithWord; //Object

К слову, массивы и динамические объекты (точнее ссылки, которые они хранят) представляют для сборщика мусора проблему, иными словами сборка мусора будет проблематична, так как механизма для отключения байндинга не предусмотрено.

Что же хранится в эти переменных? Свойство _bindings это массив, содержащий объекты типа mx.binding.Binding. Класс Binding имеет следующие проблемы:
binding.mx_internal::destFunc; //Function
binding.mx_internal::srcFunc; //Function
binding.mx_internal::document; //Could be reference to parent
binding.twoWayCounterpart; //Binding

Жирным хочется отметить, что атрибуты класса имеющие тип Function препятствуют сборке мусора, а значит необходимо в очищать их, если объект больше не используется. В случае Binding очистка проблемы не представляет: удаляем все элементы из массива и очищаем их.

Свойство _watchers это тоже массив, в нем хранятся объекты наследники класса mx.binding.Watcher.  А наследников этих несколько (подробно рассматривать не буду, так как тут мало что можно сделать, но все же стоит):

  • ArrayElementWatcher — написан так,  что очистить ссылки не возможно даже через mx_internal
  • PropertyWatcher — можно очистить вызвав PropertyWatcher(watcher).updateParent(null);
  • StaticPropertyWatcher — написан так,  что очистить ссылки не возможно даже через mx_internal
  • RepeaterComponentWatcher — написан так,  что очистить ссылки не возможно даже через mx_internal
  • RepeaterItemWatcher — написан так,  что очистить ссылки не возможно даже через mx_internal
  • FunctionReturnWatcher — написан так,  что очистить ссылки не возможно даже через mx_internal

Таким образом, если вы заметили, что ваши классы содержат один или несколько «красных» вотчеров, стоит подумать и воспользоваться BindingsUtil (возрвращающей ChangeWatcher, который имеет метод unwatch).

Свойства _bindingsByDestination и _bindingsBeginWithWord это динамические объекты, один содержат ссылки на объекты типа mx.binding.Binding (дублируя массив _bindings).  Их необходимо почистить используя оператор delete: «delete bindingsByDestination[propName];».

Следуя этим советам вы окажете посильную помощь сборщику мусора.

В следующем посте посмотрим как можно ускорить  drag and drop в 10 раз если используются сложные рендереры или просто много объектов в дисплейлисте.

, , , ,

2 комментария

Расширение для Firefox

Давно собирался, и наконец собрался. Хотелось сделать маленькую полезность. Расширение которое периодически загружает определенную страницу(с авторизацией логин/пасс), парсит и выводит в панель в статусбаре некоторую информацию. Посвящено оно удобному контролю аккаунта на adsl.by.

Скриншот

Скриншот расширения

Материалы по теме здесь: Обсуждние на mozilla-russia.org

скрины и установка здесь: Расширение для контроля аккаунта

пока что это глубокая бета-версия, тем не менее рабочая.

, ,

Оставить комментарий

Adobe Flex Compiler Shell

Не одни выходные потрачены на создание очередной хитрой штуки 🙂

Компилятор mxmlc удобен, если вы не пользуетесь Flex Builder‘ом, а например Intellij IDEA, в последней тоже есть поддержка mxml-компилятора, но этого не всегда достаточно. Однако скорость компиляции больших проектов оставляет желать лучшего. В тоже время Flex Builder компилирует шустрее. Все потому, что он использует fcsh.exe или Adobe Flex Compiler Shell, который является частью Flex SDK.

Работать с командной строкой неудобно, вот собственно для чего все  затевалось.

FCSH Server

Скриншот первой версии FCSH Server

Однако работа потребовала немного другого применения и все пришлось переделать. Проект состоял из множества swf модулей, которые естественно надо было часто пересобирать и для отладки. Причем не все, а лишь выбранные, поэтому приложение пришлось подружить с антом (Apache Ant).

Для компиляции я пользуюсь в основном панелью Ant Build в Intellij IDEA, можно конечно тоже самое делать и из консоли. В build.xml прописан ант-таск из jar-файла, который идет в комплекте с приложением, при запуске билда опрашивается порт (40000) . Если порт свободен значит запускается FCSH Server и обрабатывет команды анта, перенаправляя их fcsh.exe, если запущен — просто перенаправляет команды.

FCSH Server может конфигуриться через ini-файл (подробнее здесь), имеет свой лог компиляции, запоминает предыдущие команды, давая возможность перекомпилять через GUI-интерфейс.

FCSH Server

Скриншот финальной версии FCSH Server

Всех кого заинтересовал проет приглашаю скачать и попробовать самим: FCSH Server.

, , , ,

Оставить комментарий