• Техногрет
  • О производительности при работе с временными деревьями

    HTML и CSSXSLTJavaScriptИзображенияСофтEtc
    Александр Самиляк

    14 июля 2011


    Задача.

    Разобрать по косточкам теорию и практику использования временных деревьев в XSLT.

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

    • libxslt 1.1.26;
    • Xalan-Java 2.7.1;
    • Saxon-Java 9.1.0.8;
    • MSXML 4.0.

    Входящие данные

    1. Каждый тест имел какую-либо цель. Эта цель достигалась двумя способами — без использования временных деревьев и с их использованием.
    2. Числовым результатом теста являлось время его выполнения. Использовались показания самих трансформаторов, которые после преобразования выводили потраченное время.
    3. Каждый тест прогонялся по 5 раз, итоговым результатом являлось среднее из этих 5 прогонов. Чем меньше результат (время трансформации), тем лучше.
    4. На вход трансформаторам подавался XML объемом 50 КБ, состоящий примерно из 500 элементов.
    5. Транзисторная аппаратура — MacBook Pro # Intel Core i7 2.66 GHz, 8 GB RAM.
    6. MSXML работала в виртуальной Windows внутри VMware Fusion.
    7. Все трансформации запускались из командной строки.

    Тест № 1 — создание временного дерева из входящего XML

    В первом, слегка синтетическом, тесте входящий XML копируется без каких-либо изменений.

    Сначала просто:

     <xsl:template match="/">
      <xsl:copy-of select="node()" />
    </xsl:template>
    

    А затем сложно — с превращением во временное дерево:

     <xsl:template match="/">
      <xsl:variable name="input">
        <doc>
          <xsl:copy-of select="node()" />
        </doc>
      </xsl:variable>
      
      <xsl:copy-of select="exsl:node-set($input)/doc/node()" />
    </xsl:template>
    

    И вот графики. Каждому трансформатору на графике соответствуют два столбика: левый — замер без использования временного дерева, правый — с использованием. Каждый столбик (каждый замер) состоит из 3 частей: нижняя — парсинг XML, средняя — парсинг XSL, верхняя — трансформация. Исключением из этого правила является Xalan — он, в отличие от своих коллег, предоставляет в логе только полное время преобразования, поэтому его столбик состоит лишь из одной части. Для нас самой интересной частью является трансформация, поэтому числами на графике помечена только она.

    Может показаться, что Java-трансформаторы сливают по полной. Однако не будем забывать, что трансформации запускались из командной строки, поэтому значительное время тратилось на старт Java-машины и подтягивание необходимых jar-ов. И самое главное: наша задача заключается не в сравнении трансформаторов, а в замере влияния временных деревьев на каждый из них. И влияние это налицо — превращение входящего XML на 500 элементов во временное дерево заняло у всех трансформаторов больше времени, чем простое копирование этого XML. Пара дополнительных тестов показала (здесь я их не привожу), что время трансформации растет пропорционально объему XML, превращаемого во временное дерево.

    Замечу, что libxslt и MSXML на этом и последующих графиках не соответствуют принятому масштабу, потому что иначе на фоне Java-процессоров они превращаются в один пиксель и на них ничего не видно. При этом числовые значения на выносках, разумеется, сохранены такими, какими они были получены при замерах.


    Тест № 2 — вывод XML-атрибутов

    Тест основан на задаче вывода атрибута с предварительной проверкой, не пуст ли он. Эта задача обсуждалась в применении № 2.

    Без временных деревьев:

     <xsl:template match="/">
      <div>
        <xsl:attribute name="id">megadiv</xsl:attribute>
        <xsl:attribute name="class">slogan</xsl:attribute>
      
        <xsl:text>Дизайн спасет мир</xsl:text>
      </div>
    </xsl:template>
    

    На временных деревьях:

     <xsl:template match="/">
      <div>
        <xsl:call-template name="attributes">
          <xsl:with-param name="set">
            <id>megadiv</id>
            <class>slogan</class>
          </xsl:with-param>
        </xsl:call-template>
      
        <xsl:text>Дизайн спасет мир</xsl:text>
      </div>
    </xsl:template>
      
    <xsl:template name="attributes">
      <xsl:param name="set" />
      
      <xsl:for-each select="exsl:node-set($set)/*">
        <xsl:if test="normalize-space(.)">
          <xsl:attribute name="{name()}">
            <xsl:value-of select="normalize-space(.)" />
          </xsl:attribute>
        </xsl:if>
      </xsl:for-each>
    </xsl:template>
    

    Чтобы увидеть хоть какую-то разницу, этот код был выполнен не единожды, а 100 раз в цикле.

    Видно, что на libxslt и MSXML использование временных деревьев никак не повлияло (или это влияние ничтожно малó и незаметно). А вот Java-трансформаторы явно замедлились.

    У Saxon хочу обратить наше внимание на то, что во втором столбике время парсинга XML тоже выросло. Это довольно неожиданно, так как XML не менялся вообще. Также выросло и время парсинга XSL, что связано с более сложным XSL-кодом.


    Тест № 3 — статические данные в коде шаблона

    Этот тест навеян применением № 1. Там мы выводили контрол размножения полей, состоящий из четырех кнопок.

    Левый столбик представлен кодом, который хардкорно выводит эти кнопки одну за другой:

     <xsl:template match="/">
      <xsl:call-template name="repeat_control" />
    </xsl:template>
      
    <xsl:template name="repeat_control">
      <div class="repeat_control">
        <input type="button" class="append">
          <xsl:attribute name="value">
            <xsl:choose>
              <xsl:when test="//label[@for = 'append_button']">
                <xsl:value-of select="//label[@for = 'append_button'][1]" />
              </xsl:when>
              <xsl:otherwise>&#43;<!-- плюс --></xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
        </input>
        <input type="button" class="remove">
          <xsl:attribute name="value">
            <xsl:choose>
              <xsl:when test="//label[@for = 'remove_button']">
                <xsl:value-of select="//label[@for = 'remove_button'][1]" />
              </xsl:when>
              <xsl:otherwise>&#8722;<!-- минус --></xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
        </input>
        <input type="button" class="up">
          <xsl:attribute name="value">
            <xsl:choose>
              <xsl:when test="//label[@for = 'up_button']">
                <xsl:value-of select="//label[@for = 'up_button'][1]" />
              </xsl:when>
              <xsl:otherwise>&#8593;<!-- вверх --></xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
        </input>
        <input type="button" class="down">
          <xsl:attribute name="value">
            <xsl:choose>
              <xsl:when test="//label[@for = 'down_button']">
                <xsl:value-of select="//label[@for = 'down_button'][1]" />
              </xsl:when>
              <xsl:otherwise>&#8595;<!-- вниз --></xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
        </input>
      </div>
    </xsl:template>
    

    В правом столбике происходит динамический обход временного дерева, хранящего 4 элемента <button>:

     <xsl:template match="/">
      <xsl:call-template name="repeat_control" />
    </xsl:template>
      
      
    <xsl:variable name="repeat_control_buttons">
      <button name="append" label="&#43;" />
      <button name="remove" label="&#8722;" />
      <button name="up" label="&#8593;" />
      <button name="down" label="&#8595;" />
    </xsl:variable>
    <xsl:variable
      name="repeat_control_buttons_set"
      select="exsl:node-set($repeat_control_buttons)"
    />
      
    <xsl:variable name="input_root" select="/" />
      
    <xsl:template name="repeat_control">
      <div class="repeat_control">
        <xsl:for-each select="$repeat_control_buttons_set/button">
          <input type="button" class="{@name}">
            <xsl:attribute name="value">
              <xsl:variable
                name="custom_label"
                select="$input_root//label[@for = concat(current()/@name, '_button')][1]"
              />
              <xsl:choose>
                <!-- Если есть пользовательский лейбл, выводим его -->
                <xsl:when test="$custom_label">
                  <xsl:value-of select="$custom_label" />
                </xsl:when>
                <!-- Если нет – выводим лейбл по умолчанию -->
                <xsl:otherwise>
                  <xsl:value-of select="@label" />
                </xsl:otherwise>
              </xsl:choose>
            </xsl:attribute>
          </input>
        </xsl:for-each>
      </div>
    </xsl:template>
    

    Тест № 4 — оборачивание произвольного контента

    Нам нужно обернуть некий заранее неизвестный контент определенной разметкой, но только если этот контент не пустой (см. применение № 2).

    В первом варианте делаем это по старинке:

     <xsl:template match="/">
      <xsl:variable name="content">
        <xsl:call-template name="content" />
      </xsl:variable>
      
      <xsl:if test="normalize-space($content)">
        <div class="Product">
          <h2>Продукт</h2>
          <xsl:copy-of select="$content" />
        </div>
      </xsl:if>
    </xsl:template>
      
    <xsl:template name="content">
      <xsl:copy-of select="/descendant::Product[1]" />
    </xsl:template>
    

    Во втором варианте задействуем наши вспомогательные шаблоны, использующие временное дерево:

     <xsl:template match="/">
      <xsl:call-template name="wrap">
        <xsl:with-param name="content">
          <xsl:call-template name="content" />
        </xsl:with-param>
        <xsl:with-param name="wrap">
          <div class="Product">
            <h2>Продукт</h2>
            <put_content_here />
          </div>
        </xsl:with-param>
      </xsl:call-template>
    </xsl:template>
      
    <xsl:template name="content">
      <xsl:copy-of select="/descendant::Product[1]" />
    </xsl:template>
      
      
    <xsl:template name="wrap">
      <xsl:param name="content" />
      <xsl:param name="wrap" />
      
      <!-- Что-то выводим, только если контент не пуст -->
      <xsl:if test="normalize-space($content)">
        <xsl:apply-templates select="exsl:node-set($wrap)/node()" mode="wrap_copy">
          <xsl:with-param name="content" select="$content" />
        </xsl:apply-templates>
      </xsl:if>
    </xsl:template>
      
    <!-- Полностью копируем разметку обертки -->
    <xsl:template match="*" mode="wrap_copy">
      <xsl:param name="content" />
      
      <xsl:element name="{name()}">
        <xsl:copy-of select="@*" />
        <xsl:apply-templates mode="wrap_copy">
          <xsl:with-param name="content" select="$content" />
        </xsl:apply-templates>
      </xsl:element>
    </xsl:template>
      
    <!-- Но элемент <put_content_here /> подменяем на контент из параметра $content -->
    <xsl:template match="put_content_here" mode="wrap_copy">
      <xsl:param name="content" />
      <xsl:copy-of select="$content" />
    </xsl:template>
    

    В match="/" продукт был выведен 100 раз. Результаты замеров:


    Тест № 5 — подстройка под готовый шаблон

    В последнем тесте мы хотим вывести файл для скачивания, используя для этого типовой шаблон.

    Сначала делаем это очевидным способом (см. применение № 4):

     <xsl:template match="/">
      <xsl:apply-templates select="/descendant::file[1]" mode="html" />
    </xsl:template>
      
    <xsl:template match="file[@src and (string(.) or @name)]" mode="html">
      <span class="file {@ext}">
        <a href="{@src}" target="_blank">
          <i />
      
          <xsl:choose>
            <xsl:when test="string(.)">
              <xsl:value-of select="." />
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@name" />
            </xsl:otherwise>
          </xsl:choose>
        </a>
        <nobr>
          <xsl:value-of select="concat(@ext, ', ', @size)"/>
        </nobr>
      </span>
    </xsl:template>
    

    Во втором случае подразумеваем, что формат входящего XML изменился и мы подстраиваемся под уже написанный нами шаблон:

     <xsl:template match="/">
      <xsl:variable name="input_file" select="/descendant::file[1]" />
      <xsl:variable name="my_file">
        <file
          src="{$input_file/@src}"
          ext="{$input_file/@ext}"
          name="{$input_file/@name}"
          size="{$input_file/@size}"
        />
      </xsl:variable>
      
      <xsl:apply-templates select="exsl:node-set($my_file)/file" />
    </xsl:template>
      
      
    <xsl:template match="file[@src and (string(.) or @name)]" mode="html">
      <span class="file {@ext}">
        <a href="{@src}" target="_blank">
          <i />
          <xsl:choose>
            <xsl:when test="string(.)">
              <xsl:value-of select="." />
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@name" />
            </xsl:otherwise>
          </xsl:choose>
        </a>
        <nobr>
          <xsl:value-of select="concat(@ext, ', ', @size)"/>
        </nobr>
      </span>
    </xsl:template>
    

    Наблюдаем картину, аналогичную предыдущему тесту, — libxslt и MSXML все нипочем, а «Java-товарищи» опять слегка просели по времени, однако вызвано это в большей степени удлинением парсинга XSL.


    Очевидный вывод: временные деревья замедляют XSL-трансформацию. Однако для libxslt и MSXML это замедление настолько малó, что в наших тестах не превышает и 2 миллисекунд. Java-трансформаторы определенно более чувствительны, хотя их замедление тоже не является критичным, кроме аномального поведения Xalan в тесте № 3.

    Бóльшая часть этих тестов моделирует реальные жизненные задачи, поэтому ситуация, когда использование временного дерева станет фактором сильного снижения производительности сайта, на мой взгляд, маловероятна.


    В следующей, последней, части мы обсудим тонкости временных деревьев.

    1
    2
    3
    4
    5
    6
    7
    8
    9