
Ajax в действии
.pdfГлава 8. Производительность приложения 319
Минимизация "числа точек"
3 JavaScript, как и во многих других языках, можно обращаться к переменным, используя сложную иерархию объектов. При этом родительский объект отделяется от дочернего точкой. Например:
myGrandFather.clock.hands.minute
Это выражение представляет собой ссылку на минутную стрелку дедушкиных часов. Предположим, что мы хотим иметь ссылки на все три стрелки. Для этого нам надо написать следующие выражения:
var hourHand=myGrandFather.clock.hands.hour;
var minuteHand=myGrandFather.clock.hands.minute; var secondHand=myGrandFather.clock.hands.second;
Каждый раз, когда интерпретатор встречает имя, следующее после точки, он обращается к дочернему объекту. В предыдущем примере интерпретатору пришлось выполнить поиск дочернего объекта девять раз, причем некоторые из операций поиска дублировали друг друга. Перепишем пример следующим образом:
var hands=myGrandFather.clock.hands; var hourHand=hands.hour;
var minuteHand=hands.minute; var secondHand=hands.second;
Теперь поиск дочернего объекта осуществляется всего пять раз, а интерпретатор избавлен от лишней работы. В компилируемых языках, например в Java или С#, компилятор выполняет оптимизацию автоматически. Нам неизвестно, существует ли версия JavaScriptинтерпретатор а, реализующая подобную возможность, и, если есть, в каком браузере она используется, поэтому позаботимся об оптимизации сами. Воспользуемся для этой цели библиотекойсеку ндомер ом.
Рассмотрим в качестве примера программу, которая вычисляет силу гравитационного взаимодействия двух тел, например Земли и Луны. С каждым телом связаны некоторые физические свойства, например, масса, положение, скорость и ускорение. Эти параметры используются при вычислении гравитационного взаимодействия. Поскольку мы обсуждаем обращение к объектам посредством имен, разделенных точками, оформим свойства в виде графа.
var earth={ physics:{ mass:10,
pos:{ x:250,y:250 }, vel:{ x:0, y:0 }, ace:I x:0, y:0 }
}
};
Объект верхнего уровня, physics, здесь лишний; мы включили его лишь Для увеличения количества точек при обращении к свойству.
Выполнение приложения происходит в два этапа. Сначала вычисляется состояние в фиксированные моменты времени — расстояние, сила притяжения, ускорение и другие характеристики, о которых многие из нас не вспо-
<script type='text/javascript1 src='x/x_core.js'x/script> <script type='text/javascript' src='extras-array.j s'></script> <script type='text/javascript1 src^'styling.js'X/script> <script type=t text /javascript' src='objviewer. js'></scro_pt> <script type='text/javascript1 src='stopwatch.js'X/script> <script type='text/javascript' src^'eventRouter.js'X/script> <script type='text/javascript'>
// Инициализация данных о планетах var moon={
physics:{
mass:1,
pos:{ x:120,y:80 }, vel:[ x:-24, y:420 },
ace:{ x:0, у:О }
1
};
var earth={ physics:{ mags:10,
pos:{ x:250,y:250 }, vel:{ x:Q, y:0 },
ace:{ x:0, у:О }
1
};
var gravF=100000;
function showOrbit(count,isFast){
//О Выбор типа вычислений var data=(isFast) ?
fastData(count) : slowData(count);
var watch=stopwatch.getWatch("render",true);
//© Отображение орбиты
var canvas=document.getElementBy!d('canvas' ) ; var dx=data.max.x-data.min.x;
var dy=data.max.y-data.min.y; var sx=(dx==0) ? 1 : 500/dx; var sy=(dy==0) ? 1 : 500/dy; var offx=data.min.x*sx;
var offy=data.min.y*sy;
for (var i=0;i<data-path.length;i+=10){ var datum=data.path[i];
var dpm=datum.moon; var dpe=datum.earth;
var moonDiv=document.createElement("div"); moonDiv.className='cursor1; raoonDiv.style.position='absolute' ; moonDiv.style.left=parselnt((dpm.x*sx)-offx)+"px"; moonDiv.style.top=parselnt((dpm.x*sx)-offy)+"px" ; canvas.appendChiId(moonDiv);
var earthDiv=document.createElement("div"); earthDiv.className='cursor' ; earthDiv.style.position='absolute' ; earthDiv.style.left=parselnt((dpe,x*sx)-offx)+"px"; earthDiv.style.top=parselnt((dpe.x*sx)-offy)+"px";
canvas.appendChiId(earthDiv);
}
Часть III. Создание профессиональных Ajax-приложений
watch.stop();
}
//© Использование максимального количества операций
//поиска дочерних объектов
function slowData(count){
var watch=stopwatch.getWatch("slow orbit",true); var data={
min:{x:0,y:0},
max:{x:0,y:0},
path:П
};
}
watch.stop(); return data;
}
//О Использование минимального количества операций
//поиска дочерних объектов
function fastData(count){
var watch=stopwatch.getWatch{"fast orbit",true); var data={
min:{x:0,у:0},
max:{x:0,y:0},
path:[]
};
}
watch.stop{); return data;
}
function go{isFast){
var count=parselnt(document.getElementBy!d{"count").value); if (count==NaN){
alert("please enter a valid number"); }else{
showOrbit(count,isFast);
}
}
</script>
</head>
<body>
<div>
<a href=ljavascript:stopwatch.report("profiler")'>profile</a> <input id='count1 value='640'/>
<a href='javascript:go(true)'>fast loop</a> <a href=ljavascript:go(false)'>slow loop</a> </div>
<div id='top'>
<div class='mouseraat' id='canvas'> </div>
<div class='profiler ob^ViewBorder' id='profiler'X/div> </div>
</body>
</html>
Глава 8. Производительность приложения 323 •Таблица 8.3. Профилирование результатов для различных вариантов обращения
к переменным |
|
длТоритм |
Время выполнения (в миллисекундах) |
Исходный алгоритм вычислений |
94 |
Оптимизированный алгоритм вычислений |
57 |
Воспроизведение (среднее значение) |
702 |
Структура страницы уже знакома вам. Функции slowData() © и fastData() О содержат два варианта кода, реализующего этап вычислений. Они используются на первом этапе вычислений при генерации структуры данных О. Мы не приводим здесь полный код вычислений так как его запись заняла бы много места. Различие между вариантами алгоритма ясны из приведенных выше фрагментов; кроме того, вы можете скопировать полный код примера. С каждой функцией, выполняющей вычисления, связан объект stopwatch, выполняющий ее профилирование. Обращение к функциям осуществляется в теле showOrbit (). На основании вычисленных данных эта функция отображает траектории ©. Ее профилирование осуществляется с помощью третьего секундомера.
Элементы интерфейса в данном случае те же, что и в предыдущих двух примерах. Посредством гипертекстовых ссылок вызывается "быстрый" и "медленный" варианты вычислений, а параметр вводится посредством поля редактирования. В данном случае параметром является число точек, для которых выполняется имитация. При активизации третьей ссылки отображаются результаты профилирования. В табл. 8.3 отображены данные, полученные для 640 точек.
И снова мы видим, что в результате оптимизации достигнуто значительное увеличение скорости — сэкономлено более трети времени, требовавшегося ранее для вычислений. Мы можем сделать вывод, что первоначальная догадка о том, что количество точек в сложных именах влияет на скорость вычислений, верна.
Но если мы рассмотрим все этапы работы, то увидим, что оптимизированная программа выполняется за 760 миллисекунд, а неоптимизированная — за 792 миллисекунды, т.е. выигрыш в результате оптимизации ближе к 5, чем к 40%. В данном случае узким местом приложения является не функция, выполняющая вычисления, а подсистема отображения. Таким образом, сосредоточив внимание на оптимизации процесса вычислений, мы приняли неправильное решение.
Этот пример демонстрирует еще один важный аспект профилирования. Одно дело — знать, что некоторый фрагмент кода можно оптимизировать, и совсем другое — представлять себе результаты, которые можно получить от оптимизации. Мы видим, что операции со структурой DOM занимают приблизительно в восемь раз больше времени, чем подготовка данных, но это справедливо лишь для данного конкретного примера. Возможно, что в других
324 Часть III. Создание профессиональных Ajax-припожений
ситуациях дело обстоит так же, но выяснить это можно, лишь выполнив измерения, причем желательно сделать их для разных платформ и браузеров.
На этом мы заканчиваем разговор о профилировании и скорости выполнения программ. Рассмотренные примеры дают полное представление о том. какую пользу можно извлечь, правильно выполняя профилирование при работе над проектом Ajax. Предположим, что благодаря профилированию вы до бились приемлемой скорости выполнения кода. Однако производительность зависит не только от скорости работы, но и от объема памяти, используемой приложением. Этому вопросу и будет посвящен следующий раздел.
8.3. Использование памяти JavaScript-кодом
Этот раздел посвящен вопросам управления памятью в Ajax-программах. Некоторые из рассмотренных здесь решений применимы для любой системы и любого языка программирования, другие специфичны для Ajaxприложений и даже для конкретных Web-браузеров.
Выполняющееся приложение запрашивает у операционной системы определенный объем памяти. В идеальном случае программа должна получить столько памяти, сколько необходимо для эффективного решения поставленной задачи, и вернуть неиспользуемую память системе. Если приложение написано непрофессионально, оно может потреблять неоправданно много памяти либо не возвращать память системе по окончании работы.
По мере перехода от простых Web-страниц к богатым клиентам Ajax качество управления памятью все больше влияет на время отклика программы
инадежность ее работы. Применение стандартных программных решений помогает создать код, простой для восприятия и сопровождения, а также выявить причины утечки памяти и устранить их. (Утечкой памяти мы будем называть ситуацию, при которой программа запрашивает память у системы
ине возвращает ее после завершения работы.)
Начнем с рассмотрения общих вопросов управления памятью.
8.3.1, Борьба с утечкой памяти
В любой программе может иметь место утечка памяти. Вопросы выделения памяти и ее освобождения требуют пристального внимания разработчиков, применяющих языки, в которых не предусмотрено автоматическое управление памятью, например язык С. В JavaScript имеется подсистема "сборки мусора", которая автоматически следит за выделенной памятью и освобождает ее, если она не используется. Это позволяет избежать многих проблем, типичных для языков, в которых отсутствуют подобные средства, однако было бы ошибкой считать, что при наличии подсистемы управления утечка памяти не может произойти.
Процедура "сборки мусора" пытается выяснить, можно ли удалить неиспользуемую переменную, при этом она учитывает возможность доступа к ней по сети или посредством ссылок, содержащихся в других переменных. Если получить доступ к переменной невозможно, она помечается как разрешен-
ная к удалению, и на следующем шаге занимаемая ею память освобождается (момент времени, когда это произойдет, предсказать невозможно). Забывая удалить ссылки на переменную после того, как работа с ней окончена, мы создаем условия для утечки памяти, которая может иметь место даже в языках с управлением памятью.
Рассмотрим простой пример. Определим объектную модель, описывающую домашних животных и их хозяев. Начнем с хозяев; для их описания используем объект Person.
function Person(name){ this.name=name; this.pets=new Array();}
Хозяину может принадлежать один или несколько домашних любимцев. Когда человек приобретает животное, он сообщает о том, что у того теперь есть конкретный хозяин.
Person.prototype.addPet=function(pet){
this.pets[pet.name]=pet; if (pet.assignOwner){
pet.assignOwner(this);}
}
Аналогично, когда человек удаляет животное из списка своих любимцев, он сообщает животному о том, что оно больше никому не принадлежит.
this.removePet{petName)=function{ var orphan=this.pets[petName];
t h i s . pets[petNarrLe]=null,•
i £ ( o r p h a n . u n a s s i g n O w n e r ) {
o r p h a n . u n a s s i g n O w n e r ( t h i s ) ; )
}
Влюбой момент времени хозяин знает, какие животные ему принадлежат,
иможет управлять списком своих любимцев посредством методов addPet ()
иremovePet (). Человек информирует животное о принадлежности хозяину или ее отсутствии, считая, что животное автоматически начнет считать человека своим хозяином (подобное поведение можно предполагать по умолчанию, а необходимую проверку выполнять в процессе работы программы).
Определим два типа домашних животных: кошку и собаку. Эти животные различаются по своему отношению к хозяину, кошка не обращает внимания на тот факт, кому она принадлежит, а собака привязывается к человеку на всю жизнь. (Мы приносим извинения всему животному миру за столь вольное обобщение.)
Таким образом, определение кошки может выглядеть как показано ниже:
function Cat(name){this.name=name;} Cat.prototype.assignOwner=function(person){} Cat.prototype.unassignOwner=function(person){}
Кошка не интересуется фактом своей принадлежности кому-либо, поэтому тело соответствующих методов пусто.
Опишем теперь собаку. Она помнит, кому она принадлежит, и после того, как человек откажется от нее, по-прежнему считает его своим хозяином (многие собаки ведут себя именно так).
о*о часть III. Создание профессиональных Ajax-приложений
f u n c t i o n Dog(name){this.name=name;}
D o g . p r o t o t y p e . a s s i g n O w n e r = f u n c t i o n ( p e r s o n ) { t h i s . o w n e r = p e r s o n ; }
D o g . p r o t o t y p e . u n a s s i g n O w n e r = f u n c t i o n ( p e r s o n ) { t h i s . o w n e r = p e r s o n ; }
Объекты Cat и Dog представляют собой реализации класса Pet, но поведение их нельзя назвать корректным. Оно соответствует букве соглашения о принадлежности, но не его "духу". В Java или С# мы можем явным образом определить интерфейс Pet, но это не спасет классы, реализующие его, от неправильного поведения. При работе над реальными проектами специалисты, занимающиеся объектными моделями, тратят много времени, исправляя некорректное поведение реализаций интерфейсов.
Поработаем немного над моделью. Создадим три объекта.
1.jim — Person
2.whiskers — Cat
3.fido — Dog
Впервую очередь создадим экземпляр Person (этап 1). var jim=new Person("jim");
Далее сделаем человека владельцем кошки (этап 2). Экземпляр whiskers создается в процессе вызова метода addPet (), поэтому ссылка на объект Cat существует лишь до завершения метода. Однако объект jim также создает ссылку на whiskers, а она доступна в течение всего времени существования объекта jim.
jim.addPet(new Cat("whiskers"));
Пусть теперь jim владеет также собакой (этап 3). В отличие от whiskers, мы объявим для этого объекта глобальную переменную.
var fido=new Dog("fido"); j im.addPet(fido);
Однажды jim решает избавиться от кошки (этап 4). jim.removepet("whiskers") ;
Затем он решает, что собака ему тоже не нужна (этап 5). Наверное, он собирается переехать в другой город.
j im.removePet("fido");
Мы также больше не интересуемся этим человеком и удаляем ссылку на него (этап 6).
jim=null,-
И наконец, мы удаляем ссылку на fido (этап 7). fido=null;
По окончании этапа 6 мы считаем, что избавились от объекта jim, поскольку присвоили соответствующей переменной значение null. На самом деле ссылка на него существует в fido и доступна посредством выражения fido. owner.
Глава 8 Производительность приложения 327
Система "сборки мусора" не затронет этот объект, и он останется незамеченным в области памяти, называемой "куча" (heap), интерпретатора JavaScript. Т1йшь на этапе 7, когда переменной fido присваивается значение null, объект -i^m становится недоступным, и память может быть освобождена.
В нашем простом сценарии проблема не является серьезной, к тому же о Н а быстро решается, но данный пример иллюстрирует, как решения, не зависящие друг от друга, на первый взгляд, влияют на процесс "сборки мусора'. Объект fido не обязательно будет удален сразу же после jim, и если мы предус м°т Ри м в н е м возможность хранить информацию о нескольких предыдущих владельцах, то в памяти может остаться целый набор объектов Person, ведущих призрачное существование, но занимающих реальные ресурсы. Если бы мы объявили fido при вызове метода, а ссылку на объект Cat поместили в глобальную переменную, то такое явление не возникло бы. Для того чтобы оценить, насколько серьезную проблему может создать подобное поведение объекта fido, нам надо ответить на следующие вопросы.
1. Какой объем памяти он может занимать с учетом ссылок на объекты, которые в других обстоятельствах были бы удалены? Мы знаем, что в данном примере fido может ссылаться только на один объект Person. Но даже в этом случае не исключено, что объект Person содержит ссылки на 500 объектов Cat, поэтому объем расходуемой памяти может быть существенным.
2. В течение какого времени память будет оставаться занятой? В рассмотренном примере ответом на этот вопрос будет "не очень долго". Однако впоследствии между удалением jim и fido могут появиться команды, выполнение которых будет занимать длительное время. Более того, язык JavaScript позволяет создавать программы, управляемые событиями, и удаление j im и fido может происходить при обработке различных событий. В этом случае, чтобы предсказать, в течение какого времени будет занята дополнительная память, пришлось бы анализировать типичные сценарии работы пользователя.
Ни на один из приведенных выше вопросов непросто ответить. Самое лучшее, что можно сделать, — это задавать их себе в процессе создания и модификации кода и выяснять в процессе тестирования, правильны ли наши предположения.
Мы рассмотрели основные принципы управления памятью. Однако существуют проблемы, специфические для приложений Ajax. Обсудим их в следующем разделе.
8.3.2. Особенности управления памятью в приложениях Ajax
Мы рассмотрели понятия, лежащие в основе управления памятью, для многих языков программирования. Отслеживать объем используемой памяти и доступность переменных важно при работе над любым приложением, однако есть также вопросы, специфические для Ajax. Ajax-программы pa-