From 9fa13fd07f75986a9d8b73c4693e168de5f6a007 Mon Sep 17 00:00:00 2001 From: Isaac Levy Date: Mon, 20 Aug 2018 12:22:07 -0400 Subject: [PATCH] Optimize escape functions - Use java.lang.StringBuilder instead of scala wrapper - Rewrite Html.buildString to use bulk copy methods when possible. --- .../main/scala/play/twirl/api/Content.scala | 13 ++++---- .../main/scala/play/twirl/api/Formats.scala | 33 ++++++++++++------- .../twirl/api/utils/StringEscapeUtils.scala | 6 ++-- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/api/shared/src/main/scala/play/twirl/api/Content.scala b/api/shared/src/main/scala/play/twirl/api/Content.scala index 1c263a5a..50fe7472 100644 --- a/api/shared/src/main/scala/play/twirl/api/Content.scala +++ b/api/shared/src/main/scala/play/twirl/api/Content.scala @@ -4,6 +4,7 @@ package play.twirl.api import scala.collection.immutable +import java.lang.{StringBuilder => jStringBuilder} /** * Generic type representing content to be sent over an HTTP response. @@ -33,13 +34,13 @@ trait Content { * @tparam A self-type */ abstract class BufferedContent[A <: BufferedContent[A]](protected val elements: immutable.Seq[A], protected val text: String) extends Appendable[A] with Content { this: A => - protected def buildString(builder: StringBuilder): Unit = { + protected def buildString(sb: jStringBuilder): Unit = { if (!elements.isEmpty) { elements.foreach { e => - e.buildString(builder) + e.buildString(sb) } } else { - builder.append(text) + sb.append(text) } } @@ -48,9 +49,9 @@ abstract class BufferedContent[A <: BufferedContent[A]](protected val elements: * to avoid unneeded memory allocation. */ private lazy val builtBody = { - val builder = new StringBuilder() - buildString(builder) - builder.toString + val sb = new jStringBuilder() + buildString(sb) + sb.toString } override def toString = builtBody diff --git a/api/shared/src/main/scala/play/twirl/api/Formats.scala b/api/shared/src/main/scala/play/twirl/api/Formats.scala index ec4dbce3..f1cf13e7 100644 --- a/api/shared/src/main/scala/play/twirl/api/Formats.scala +++ b/api/shared/src/main/scala/play/twirl/api/Formats.scala @@ -5,6 +5,7 @@ package play.twirl.api import play.twirl.api.utils.StringEscapeUtils import scala.collection.immutable +import java.lang.{StringBuilder => jStringBuilder} object MimeTypes { val TEXT = "text/plain" @@ -36,28 +37,38 @@ class Html private[api] (elements: immutable.Seq[Html], text: String, escape: Bo * of Strings, if it doesn't, performance actually goes down (measured 10%), due to the fact that the JVM can't * optimise the invocation of buildString as well because there are two different possible implementations. */ - override protected def buildString(builder: StringBuilder): Unit = { - if (elements.nonEmpty) { + override protected def buildString(sb: jStringBuilder): Unit = { + if (!elements.isEmpty) { elements.foreach { e => - e.buildString(builder) + e.buildString(sb) } } else if (escape) { // Using our own algorithm here because commons lang escaping wasn't designed for protecting against XSS, and there // don't seem to be any other good generic escaping tools out there. + val len = text.length + var copyIdx = 0 var i = 0 - while (i < text.length) { + while (i < len) { text.charAt(i) match { - case '<' => builder.append("<") - case '>' => builder.append(">") - case '"' => builder.append(""") - case '\'' => builder.append("'") - case '&' => builder.append("&") - case c => builder += c + case '<' | '>' | '"' | '\'' | '&' => { + sb.append(text, copyIdx, i) + text.charAt(i) match { + case '<' => sb.append("<") + case '>' => sb.append(">") + case '"' => sb.append(""") + case '\'' => sb.append("'") + case '&' => sb.append("&") + } + copyIdx = i + 1 + } + case _ => () } i += 1 } + if (copyIdx == 0) sb.append(text) + else sb.append(text, copyIdx, len) } else { - builder.append(text) + sb.append(text) } } diff --git a/api/shared/src/main/scala/play/twirl/api/utils/StringEscapeUtils.scala b/api/shared/src/main/scala/play/twirl/api/utils/StringEscapeUtils.scala index db6a5371..2f70fff9 100644 --- a/api/shared/src/main/scala/play/twirl/api/utils/StringEscapeUtils.scala +++ b/api/shared/src/main/scala/play/twirl/api/utils/StringEscapeUtils.scala @@ -3,11 +3,13 @@ */ package play.twirl.api.utils +import java.lang.{StringBuilder => jStringBuilder} + object StringEscapeUtils { def escapeEcmaScript(input: String): String = { - val s = new StringBuilder() val len = input.length + val s = new jStringBuilder(len) var pos = 0 while (pos < len) { input.charAt(pos) match { @@ -36,8 +38,8 @@ object StringEscapeUtils { def escapeXml11(input: String): String = { // Implemented per XML spec: // http://www.w3.org/International/questions/qa-controls - val s = new StringBuilder() val len = input.length + val s = new jStringBuilder(len) var pos = 0 while (pos < len) {