Skip to content

Commit c4f9c7a

Browse files
committed
Optimize escape functions
- Use java.lang.StringBuilder instead of scala wrapper - Rewrite Html.buildString to use bulk copy methods when possible.
1 parent af0de40 commit c4f9c7a

3 files changed

Lines changed: 33 additions & 19 deletions

File tree

api/shared/src/main/scala/play/twirl/api/Content.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package play.twirl.api
55

66
import scala.collection.immutable
7+
import java.lang.{StringBuilder => jsStringBuilder}
78

89
/**
910
* Generic type representing content to be sent over an HTTP response.
@@ -33,13 +34,13 @@ trait Content {
3334
* @tparam A self-type
3435
*/
3536
abstract class BufferedContent[A <: BufferedContent[A]](protected val elements: immutable.Seq[A], protected val text: String) extends Appendable[A] with Content { this: A =>
36-
protected def buildString(builder: StringBuilder): Unit = {
37+
protected def buildString(sb: jsStringBuilder): Unit = {
3738
if (!elements.isEmpty) {
3839
elements.foreach { e =>
39-
e.buildString(builder)
40+
e.buildString(sb)
4041
}
4142
} else {
42-
builder.append(text)
43+
sb.append(text)
4344
}
4445
}
4546

@@ -48,9 +49,9 @@ abstract class BufferedContent[A <: BufferedContent[A]](protected val elements:
4849
* to avoid unneeded memory allocation.
4950
*/
5051
private lazy val builtBody = {
51-
val builder = new StringBuilder()
52-
buildString(builder)
53-
builder.toString
52+
val sb = new jsStringBuilder()
53+
buildString(sb)
54+
sb.toString
5455
}
5556

5657
override def toString = builtBody

api/shared/src/main/scala/play/twirl/api/Formats.scala

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package play.twirl.api
55

66
import play.twirl.api.utils.StringEscapeUtils
77
import scala.collection.immutable
8+
import java.lang.{StringBuilder => jsStringBuilder}
89

910
object MimeTypes {
1011
val TEXT = "text/plain"
@@ -36,28 +37,38 @@ class Html private[api] (elements: immutable.Seq[Html], text: String, escape: Bo
3637
* of Strings, if it doesn't, performance actually goes down (measured 10%), due to the fact that the JVM can't
3738
* optimise the invocation of buildString as well because there are two different possible implementations.
3839
*/
39-
override protected def buildString(builder: StringBuilder): Unit = {
40-
if (elements.nonEmpty) {
40+
override protected def buildString(sb: jsStringBuilder): Unit = {
41+
if (!elements.isEmpty) {
4142
elements.foreach { e =>
42-
e.buildString(builder)
43+
e.buildString(sb)
4344
}
4445
} else if (escape) {
4546
// Using our own algorithm here because commons lang escaping wasn't designed for protecting against XSS, and there
4647
// don't seem to be any other good generic escaping tools out there.
48+
val len = text.length
49+
var copyIdx = 0
4750
var i = 0
48-
while (i < text.length) {
51+
while (i < len) {
4952
text.charAt(i) match {
50-
case '<' => builder.append("&lt;")
51-
case '>' => builder.append("&gt;")
52-
case '"' => builder.append("&quot;")
53-
case '\'' => builder.append("&#x27;")
54-
case '&' => builder.append("&amp;")
55-
case c => builder += c
53+
case '<' | '>' | '"' | '\'' | '&' => {
54+
sb.append(text, copyIdx, i)
55+
text.charAt(i) match {
56+
case '<' => sb.append("&lt;")
57+
case '>' => sb.append("&gt;")
58+
case '"' => sb.append("&quot;")
59+
case '\'' => sb.append("&#x27;")
60+
case '&' => sb.append("&amp;")
61+
}
62+
copyIdx = i + 1
63+
}
64+
case _ => ()
5665
}
5766
i += 1
5867
}
68+
if (copyIdx == 0) sb.append(text)
69+
else sb.append(text, copyIdx, len)
5970
} else {
60-
builder.append(text)
71+
sb.append(text)
6172
}
6273
}
6374

api/shared/src/main/scala/play/twirl/api/utils/StringEscapeUtils.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
*/
44
package play.twirl.api.utils
55

6+
import java.lang.{StringBuilder => jsStringBuilder}
7+
68
object StringEscapeUtils {
79

810
def escapeEcmaScript(input: String): String = {
9-
val s = new StringBuilder()
1011
val len = input.length
12+
val s = new jsStringBuilder(len)
1113
var pos = 0
1214
while (pos < len) {
1315
input.charAt(pos) match {
@@ -36,8 +38,8 @@ object StringEscapeUtils {
3638
def escapeXml11(input: String): String = {
3739
// Implemented per XML spec:
3840
// http://www.w3.org/International/questions/qa-controls
39-
val s = new StringBuilder()
4041
val len = input.length
42+
val s = new jsStringBuilder(len)
4143
var pos = 0
4244

4345
while (pos < len) {

0 commit comments

Comments
 (0)