Переполнение стека вызовов JavaScript, SetTimeout и снижение производительности AJAX

от автора

Проблема

Некоторое время назад в работе над клиентской (javascript) частью движка josi возникла, кстати, достаточно часто встречающаяся проблема переполнения стека:
Uncaught RangeError: Maximum call stack size exceeded (google chrome)
В статье рассматривается решение без использования setTimout или setInterval.

Суть

Причина такого поведения известна и понятна, и в той или иной форме всегда вызвана следующим. Классическая(прямая) рекурсия порождает цепочку последовательных вызовов, что соответственно ведет к наполнению стека вызовов, однако, стек вызовов браузера достаточно мал, в chrome на момент тестирования это 500 вызовов, в safari, если не ошибаюсь, тоже. В любом случае- это предельное значение, а значит его можно превысить и получить exception. Естественно, столь долгое выполнение кода не желательно в принципе, и этого стоит избегать. И все же лично мне не хочеться полагаться на удачу, не смотря на то, что ситуация в которой пришлось столкнуться с проблемой на продакшен возникнуть не должна, я потратил время на изучение данного вопроса.

Решение

Классическим решением (имею ввиду подавляющее количество статей предлагающих его) является использование косвенной рекурсии посредством: setTimeout либо setInterval.

В качестве примера приведу простенькую рекурсивную функция, единственное назначение которой рано или поздно вернуть Вам предел размера стека вместе с exeption о превышении этого предела…

function f(args)  {      var self=this;      var k=args.k;       //вызываем себя же      try      {          f({k:k+1});      }      catch(ex)      {           alert(k);      } } 

та же бесполезная функция, но теперь теоретически бесконечная, разве что k переполнится

function f(args)  {      var self=this;      var k=args.k;            //косвенно вызываем себя же, через посредника setTimeout      setTimeout(function(){ f({k:k+1}) }, 0); } 

Текущая функция сразу завершается за счет использования для рекурсии посредника setTimout, а следующий вызов выполняется по событию.
Отрицательной стороной такого подхода является его крайне низкая производительность, несмотря на то, что мы указываем нулевую задержку. Вызвана функция будет в зависимости от браузера в среднем не раньше чем через 10 мс. Но ведь мы боремся с превышением стека вызовов, а значит наша функция вызывается сотни раз, что означает потерю в производительности ~1 с на каждые 100 вызовов. Детальное тестирование нашел тут.
Самое простое, что пришло в голову — организовать симбиоз из попеременного использования прямого и косвенного вызовов, чтобы при достижении некоторого значения счетчика прерывать стек косвенным вызовом. Отчасти такое решение сейчас и используется. Но здесь тоже все не так просто, особенно если рекурсия представлена петлей из нескольких функций.
Вот простенький пример отражающий суть такого решения:

var max_call_i=300; function f(args)  {      var self=this;      var k=args.k;      var call_i=args.call_i      //alert(k);       if (call_i>=max_call_i)      {                  //косвенно вызываем себя же, через посредника setTimeout             setTimeout(function(){ f({k:k+1, call_i:call_i+1}) }, 0);      }      else      {            //напрямую вызываем себя же            f({k:k+1, call_i:call_i+1});      } } 

В моем коде проблема возникла в шаблонизаторе, который как раз незадолго до этого был переписан согласно новой парадигме. Не хотелось отказываться от принятой архитектуры. В тоже время реальное падение производительности составило 20-30% — что было просто чудовищно. Предложенное выше решение тоже не идеал: сохранялось падение производительности на 5-7%. Это меня не устроило: много гуглил, и напал на то, что нужно.
А это тест от туда же, из которого видно что, предложенный подход, в сравнении с setTimeout 0, гораздо более производительный, что на практике дало не более 3% падения производительности в моем случае…

Данное решение основано на связке window.postMessage и element.addEventListener, для меня достаточно кроссбраузерно (ie8+).

Я переработал функцию из приведенной выше статьи в AMD модуль. Возможно, кому-то это будет полезным…

define([], function ()  {          	return        { 		 		args:							//аргументы 		{ 			indirect_call: 			{ 				f_arr:[], 				msg_name:"indirect_call-message", 				handler_f:null, 			} 		}, 		 		/*** работа с событиями ***/  		f_indirect_call:function(f) 		{ 			var self=this; 			 			//если это первый вызов то создаем обработчик события и привязываем к window 			if (t_uti.f_is_empty(self.args.indirect_call.handler_f)) 			{ 				//создаем обработчик события 				self.args.indirect_call.handler_f=function(event)  				{ 					if (event.source == window && event.data == self.args.indirect_call.msg_name)  					{ 						event.stopPropagation(); 						if (self.args.indirect_call.f_arr.length> 0)  						{ 							var f = self.args.indirect_call.f_arr.shift(); 							f(); 						} 					} 				} 				 				window.addEventListener("message", self.args.indirect_call.handler_f, true); 			} 			 			self.args.indirect_call.f_arr.push(f); 			window.postMessage(self.args.indirect_call.msg_name, "*"); 		},       }; });  

ссылка на оригинал статьи http://habrahabr.ru/post/167033/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *