Skip to content

Commit f9f54e9

Browse files
java-team-github-botGoogle Java Core Libraries
authored andcommitted
Optimize StringUtil.javaScriptEscape() by replacing boxed Set lookups with binary search and fast-path range checks.
RELNOTES=n/a PiperOrigin-RevId: 918765667
1 parent cdd9215 commit f9f54e9

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.google.common.base;
2+
3+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
4+
5+
import java.io.IOException;
6+
import java.util.HashSet;
7+
import java.util.Set;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Fork;
11+
import org.openjdk.jmh.annotations.Mode;
12+
import org.openjdk.jmh.annotations.OutputTimeUnit;
13+
import org.openjdk.jmh.annotations.Param;
14+
import org.openjdk.jmh.annotations.Scope;
15+
import org.openjdk.jmh.annotations.Setup;
16+
import org.openjdk.jmh.annotations.State;
17+
18+
@BenchmarkMode(Mode.AverageTime)
19+
@OutputTimeUnit(NANOSECONDS)
20+
@State(Scope.Benchmark)
21+
@Fork(5)
22+
public class StringUtilJmhBenchmark {
23+
@Param({"100", "1000"})
24+
private int length;
25+
26+
@Param({"ASCII", "CJK"})
27+
private String type;
28+
29+
private String input;
30+
31+
@Setup
32+
public void setUp() {
33+
StringBuilder sb = new StringBuilder(length);
34+
for (int i = 0; i < length; i++) {
35+
if ("ASCII".equals(type)) {
36+
sb.append((char) ('a' + (i % 26)));
37+
} else {
38+
sb.append((char) (0x4e00 + (i % 1000))); // Chinese ideographs
39+
}
40+
}
41+
input = sb.toString();
42+
}
43+
44+
@Benchmark
45+
public String escapeUnicodeCurrent() {
46+
return StringUtil.javaScriptEscape(input);
47+
}
48+
49+
@Benchmark
50+
public String escapeUnicodeOriginal() {
51+
return originalJavaScriptEscape(input, false);
52+
}
53+
54+
@Benchmark
55+
public String escapeAsciiCurrent() {
56+
return StringUtil.javaScriptEscapeToAscii(input);
57+
}
58+
59+
@Benchmark
60+
public String escapeAsciiOriginal() {
61+
return originalJavaScriptEscape(input, true);
62+
}
63+
64+
private static String originalJavaScriptEscape(CharSequence s, boolean escapeToAscii) {
65+
StringBuilder sb = new StringBuilder(s.length() * 9 / 8);
66+
try {
67+
originalEscapeStringBody(s, escapeToAscii, StringUtil.JsEscapingMode.EMBEDDABLE_JS, sb);
68+
} catch (IOException ex) {
69+
throw new RuntimeException(ex);
70+
}
71+
return sb.toString();
72+
}
73+
74+
private static void originalEscapeStringBody(
75+
CharSequence plainText,
76+
boolean escapeToAscii,
77+
StringUtil.JsEscapingMode jsEscapingMode,
78+
Appendable out)
79+
throws IOException {
80+
int pos = 0;
81+
int len = plainText.length();
82+
for (int codePoint, charCount, i = 0; i < len; i += charCount) {
83+
codePoint = Character.codePointAt(plainText, i);
84+
charCount = Character.charCount(codePoint);
85+
86+
if (!originalShouldEscapeChar(codePoint, escapeToAscii, jsEscapingMode)) {
87+
continue;
88+
}
89+
90+
out.append(plainText, pos, i);
91+
pos = i + charCount;
92+
switch (codePoint) {
93+
case '\b':
94+
out.append("\\b");
95+
break;
96+
case '\t':
97+
out.append("\\t");
98+
break;
99+
case '\n':
100+
out.append("\\n");
101+
break;
102+
case '\f':
103+
out.append("\\f");
104+
break;
105+
case '\r':
106+
out.append("\\r");
107+
break;
108+
case '\\':
109+
out.append("\\\\");
110+
break;
111+
case '"':
112+
case '\'':
113+
if (jsEscapingMode == StringUtil.JsEscapingMode.JSON && codePoint == '\'') {
114+
out.append((char) codePoint);
115+
break;
116+
} else if (jsEscapingMode != StringUtil.JsEscapingMode.EMBEDDABLE_JS) {
117+
out.append('\\').append((char) codePoint);
118+
break;
119+
}
120+
// fall through
121+
default:
122+
if (codePoint >= 0x100 || jsEscapingMode == StringUtil.JsEscapingMode.JSON) {
123+
appendUnicode(codePoint, out);
124+
} else {
125+
appendHex((char) codePoint, out);
126+
}
127+
break;
128+
}
129+
}
130+
out.append(plainText, pos, len);
131+
}
132+
133+
private static boolean originalShouldEscapeChar(
134+
int codePoint, boolean escapeToAscii, StringUtil.JsEscapingMode jsEscapingMode) {
135+
if (escapeToAscii && (codePoint < 0x20 || codePoint > 0x7e)) {
136+
return true;
137+
}
138+
return ORIGINAL_JS_ESCAPE_CHARS.contains(codePoint);
139+
}
140+
141+
private static final char[] hexChars = "0123456789abcdef".toCharArray();
142+
143+
private static void appendHex(char ch, Appendable out) throws IOException {
144+
out.append("\\x").append(hexChars[(ch >>> 4) & 0xf]).append(hexChars[ch & 0xf]);
145+
}
146+
147+
private static void appendUnicode(int codePoint, Appendable out) throws IOException {
148+
if (Character.isSupplementaryCodePoint(codePoint)) {
149+
char[] surrogates = Character.toChars(codePoint);
150+
appendUnicode(surrogates[0], out);
151+
appendUnicode(surrogates[1], out);
152+
return;
153+
}
154+
out.append("\\u")
155+
.append(hexChars[(codePoint >>> 12) & 0xf])
156+
.append(hexChars[(codePoint >>> 8) & 0xf])
157+
.append(hexChars[(codePoint >>> 4) & 0xf])
158+
.append(hexChars[codePoint & 0xf]);
159+
}
160+
161+
private static final class BoxedCodePointSet {
162+
final boolean[] fastArray;
163+
final Set<Integer> elements;
164+
165+
BoxedCodePointSet(Set<Integer> codePoints) {
166+
this.elements = codePoints;
167+
fastArray = new boolean[0x100];
168+
for (int i = 0; i < fastArray.length; i++) {
169+
fastArray[i] = elements.contains(i);
170+
}
171+
}
172+
173+
boolean contains(int codePoint) {
174+
if (codePoint < fastArray.length) {
175+
return fastArray[codePoint];
176+
}
177+
return elements.contains(codePoint);
178+
}
179+
}
180+
181+
private static final BoxedCodePointSet ORIGINAL_JS_ESCAPE_CHARS;
182+
183+
static {
184+
Set<Integer> set = new HashSet<>();
185+
set.add(0xAD);
186+
for (int i = 0x600; i <= 0x603; i++) set.add(i);
187+
set.add(0x6DD);
188+
set.add(0x070F);
189+
for (int i = 0x17B4; i <= 0x17B5; i++) set.add(i);
190+
for (int i = 0x200B; i <= 0x200F; i++) set.add(i);
191+
for (int i = 0x202A; i <= 0x202E; i++) set.add(i);
192+
for (int i = 0x2028; i <= 0x2029; i++) set.add(i);
193+
for (int i = 0x2060; i <= 0x2064; i++) set.add(i);
194+
for (int i = 0x206A; i <= 0x206F; i++) set.add(i);
195+
set.add(0xFEFF);
196+
for (int i = 0xFFF9; i <= 0xFFFB; i++) set.add(i);
197+
for (int i = 0x1D173; i <= 0x1D17A; i++) set.add(i);
198+
set.add(0xE0001);
199+
for (int i = 0xE0020; i <= 0xE007F; i++) set.add(i);
200+
set.add(0x0000);
201+
set.add(0x000A);
202+
set.add(0x000D);
203+
set.add(0x0085);
204+
set.add((int) '\'');
205+
set.add((int) '\"');
206+
set.add((int) '&');
207+
set.add((int) '<');
208+
set.add((int) '>');
209+
set.add((int) '=');
210+
set.add((int) '\\');
211+
ORIGINAL_JS_ESCAPE_CHARS = new BoxedCodePointSet(set);
212+
}
213+
}

0 commit comments

Comments
 (0)