Skip to content

Commit c2c37e7

Browse files
committed
iptimized stack that implements a quickpath
1 parent e7ee841 commit c2c37e7

3 files changed

Lines changed: 232 additions & 35 deletions

File tree

src/main/java/org/htmlunit/cyberneko/HTMLTagBalancer.java

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public class HTMLTagBalancer
197197
// state
198198

199199
/** The element stack. */
200-
protected final InfoStack fElementStack = new InfoStack(20);
200+
protected final InfoStackWithOpenCounters fElementStack = new InfoStackWithOpenCounters(20);
201201

202202
/** The inline stack. */
203203
protected final InfoStack fFormattingStack = new InfoStack(10);
@@ -1359,8 +1359,14 @@ protected final void callEndElement(final QName element, final Augmentations aug
13591359
* @param element The element.
13601360
*/
13611361
protected final int getElementDepth(final HTMLElements.Element element) {
1362-
final boolean container = element.isContainer();
13631362
final short elementCode = element.code;
1363+
1364+
// fast path: this element type isn't open anywhere on the stack
1365+
if (!fElementStack.isOpen(elementCode)) {
1366+
return -1;
1367+
}
1368+
1369+
final boolean container = element.isContainer();
13641370
final boolean tableBodyOrHtml = (elementCode == HTMLElements.TABLE)
13651371
|| (elementCode == HTMLElements.BODY) || (elementCode == HTMLElements.HTML);
13661372
int depth = -1;
@@ -1475,20 +1481,20 @@ public Info(final HTMLElements.Element element, final QName qname) {
14751481
this(element, qname, null);
14761482
}
14771483

1478-
/**
1479-
* Creates an element information object.
1480-
* <p>
1481-
* <strong>Note:</strong>
1482-
* This constructor makes a deep copy of the qualified name and,
1483-
* for inline elements, of the attributes so that formatting
1484-
* elements can be re-opened with their original attributes.
1485-
*
1486-
* @param element The HTML element definition.
1487-
* @param qname The element's qualified name (will be deep-copied).
1488-
* @param attributes The element attributes to copy, or {@code null} if
1489-
* no attribute snapshot is needed.
1490-
*/
1491-
public Info(final HTMLElements.Element element, final QName qname,
1484+
/**
1485+
* Creates an element information object.
1486+
* <p>
1487+
* <strong>Note:</strong>
1488+
* This constructor makes a deep copy of the qualified name and,
1489+
* for inline elements, of the attributes so that formatting
1490+
* elements can be re-opened with their original attributes.
1491+
*
1492+
* @param element The HTML element definition.
1493+
* @param qname The element's qualified name (will be deep-copied).
1494+
* @param attributes The element attributes to copy, or {@code null} if
1495+
* no attribute snapshot is needed.
1496+
*/
1497+
public Info(final HTMLElements.Element element, final QName qname,
14921498
final XMLAttributes attributes) {
14931499
this.element = element;
14941500
this.qname = new QName(qname);
@@ -1545,6 +1551,58 @@ public void push(final Info info) {
15451551
data[length++] = info;
15461552
}
15471553

1554+
// Pops the top item off of the stack.
1555+
public Info pop() {
1556+
final Info info = data[--length];
1557+
data[length] = null;
1558+
return info;
1559+
}
1560+
1561+
// Simple representation to make debugging easier
1562+
@Override
1563+
public String toString() {
1564+
final StringBuilder sb = new StringBuilder("InfoStack(");
1565+
for (int i = length - 1; i >= 0; --i) {
1566+
sb.append(data[i]);
1567+
if (i != 0) {
1568+
sb.append(", ");
1569+
}
1570+
}
1571+
sb.append(")");
1572+
return sb.toString();
1573+
}
1574+
}
1575+
1576+
/** Unsynchronized stack of element information. */
1577+
public static class InfoStackWithOpenCounters {
1578+
1579+
/** The length of the stack. */
1580+
public int length;
1581+
1582+
/** The stack data. */
1583+
public Info[] data;
1584+
1585+
/** Count of currently-open elements per element code; index = HTMLElements code. */
1586+
private final int[] openCounters;
1587+
1588+
public InfoStackWithOpenCounters(final int initialSize) {
1589+
data = new Info[initialSize];
1590+
1591+
// hopefully noone will ever use more than 200 elements
1592+
openCounters = new int[200];
1593+
}
1594+
1595+
// Pushes element information onto the stack.
1596+
public void push(final Info info) {
1597+
if (length == data.length) {
1598+
final Info[] newarray = new Info[length + 10];
1599+
System.arraycopy(data, 0, newarray, 0, length);
1600+
data = newarray;
1601+
}
1602+
data[length++] = info;
1603+
openCounters[info.element.code]++;
1604+
}
1605+
15481606
// Peeks at the top of the stack.
15491607
public Info peek() {
15501608
return data[length - 1];
@@ -1554,19 +1612,30 @@ public Info peek() {
15541612
public Info pop() {
15551613
final Info info = data[--length];
15561614
data[length] = null;
1615+
openCounters[info.element.code]--;
15571616
return info;
15581617
}
15591618

1619+
/**
1620+
* Returns true if at least one element with this code is currently open.
1621+
*
1622+
* @return true if at least one element with this code is currently open.
1623+
*/
1624+
public boolean isOpen(final short elementCode) {
1625+
return openCounters[elementCode] > 0;
1626+
}
1627+
15601628
// Resets the stack and releases all Info references so they can be GC'd.
15611629
public void clear() {
15621630
Arrays.fill(data, null);
1631+
Arrays.fill(openCounters, 0);
15631632
length = 0;
15641633
}
15651634

15661635
// Simple representation to make debugging easier
15671636
@Override
15681637
public String toString() {
1569-
final StringBuilder sb = new StringBuilder("InfoStack(");
1638+
final StringBuilder sb = new StringBuilder("InfoStackWithOpenCounters(");
15701639
for (int i = length - 1; i >= 0; --i) {
15711640
sb.append(data[i]);
15721641
if (i != 0) {

src/test/java/org/htmlunit/cyberneko/InfoStackTest.java

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,13 @@ private static Info newInfo(final String name) {
3939
// ---- basic push / peek / pop ----
4040

4141
@Test
42-
public void pushPeekPop() {
42+
public void pushPop() {
4343
final InfoStack stack = new InfoStack(4);
4444
final Info info = newInfo("div");
4545

4646
stack.push(info);
4747

4848
assertEquals(1, stack.length);
49-
assertSame(info, stack.peek());
5049
assertSame(info, stack.pop());
5150
assertEquals(0, stack.length);
5251
}
@@ -86,22 +85,6 @@ public void pushBeyondInitialCapacityTriggersGrowth() {
8685
assertEquals(0, stack.length);
8786
}
8887

89-
// ---- clear ----
90-
91-
@Test
92-
public void clearResetsLengthAndNullsReferences() {
93-
final InfoStack stack = new InfoStack(4);
94-
stack.push(newInfo("div"));
95-
stack.push(newInfo("span"));
96-
97-
stack.clear();
98-
99-
assertEquals(0, stack.length);
100-
// verify references are nulled out to allow GC
101-
assertNull(stack.data[0]);
102-
assertNull(stack.data[1]);
103-
}
104-
10588
// ---- pop on empty (documents current behavior) ----
10689

10790
@Test
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright (c) 2017-2026 Ronald Brill
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.cyberneko;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertNotNull;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
20+
import static org.junit.jupiter.api.Assertions.assertSame;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
23+
import org.htmlunit.cyberneko.HTMLTagBalancer.Info;
24+
import org.htmlunit.cyberneko.HTMLTagBalancer.InfoStack;
25+
import org.htmlunit.cyberneko.HTMLTagBalancer.InfoStackWithOpenCounters;
26+
import org.htmlunit.cyberneko.xerces.xni.QName;
27+
import org.junit.jupiter.api.Test;
28+
29+
/**
30+
* Unit tests for {@link InfoStack}.
31+
*/
32+
public class InfoStackWithOpenCountersTest {
33+
34+
private static Info newInfo(final String name) {
35+
final HTMLElements elements = new HTMLElements();
36+
final QName qname = new QName(null, name, name, null);
37+
return new Info(elements.getElement(name), qname);
38+
}
39+
40+
// ---- basic push / peek / pop ----
41+
42+
@Test
43+
public void pushPeekPop() {
44+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
45+
final Info info = newInfo("div");
46+
47+
stack.push(info);
48+
49+
assertEquals(1, stack.length);
50+
assertSame(info, stack.peek());
51+
assertSame(info, stack.pop());
52+
assertEquals(0, stack.length);
53+
}
54+
55+
@Test
56+
public void lifoOrder() {
57+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
58+
final Info a = newInfo("div");
59+
final Info b = newInfo("span");
60+
final Info c = newInfo("p");
61+
62+
stack.push(a);
63+
stack.push(b);
64+
stack.push(c);
65+
66+
assertSame(c, stack.pop());
67+
assertSame(b, stack.pop());
68+
assertSame(a, stack.pop());
69+
}
70+
71+
// ---- growth beyond initial capacity ----
72+
73+
@Test
74+
public void pushBeyondInitialCapacityTriggersGrowth() {
75+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(2);
76+
77+
for (int i = 0; i < 50; i++) {
78+
stack.push(newInfo("div"));
79+
}
80+
81+
assertEquals(50, stack.length);
82+
83+
// verify we can pop all 50
84+
for (int i = 0; i < 50; i++) {
85+
assertNotNull(stack.pop());
86+
}
87+
assertEquals(0, stack.length);
88+
}
89+
90+
// ---- clear ----
91+
92+
@Test
93+
public void clearResetsLengthAndNullsReferences() {
94+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
95+
stack.push(newInfo("div"));
96+
stack.push(newInfo("span"));
97+
98+
stack.clear();
99+
100+
assertEquals(0, stack.length);
101+
// verify references are nulled out to allow GC
102+
assertNull(stack.data[0]);
103+
assertNull(stack.data[1]);
104+
}
105+
106+
// ---- pop on empty (documents current behavior) ----
107+
108+
@Test
109+
public void popOnEmptyThrowsArrayIndexOutOfBounds() {
110+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
111+
assertThrows(ArrayIndexOutOfBoundsException.class, stack::pop);
112+
}
113+
114+
@Test
115+
public void popNullsVacatedSlot() {
116+
// This test documents bug 1.1: after pop(), data[length] still
117+
// holds a reference. If the bug is fixed, change assertNotNull
118+
// to assertNull.
119+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
120+
final Info info = newInfo("div");
121+
stack.push(info);
122+
123+
stack.pop();
124+
125+
assertNull(stack.data[0]);
126+
}
127+
128+
// ---- toString ----
129+
130+
@Test
131+
public void toStringEmpty() {
132+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
133+
assertEquals("InfoStackWithOpenCounters()", stack.toString());
134+
}
135+
136+
@Test
137+
public void toStringNonEmpty() {
138+
final InfoStackWithOpenCounters stack = new InfoStackWithOpenCounters(4);
139+
stack.push(newInfo("div"));
140+
final String s = stack.toString();
141+
assertNotNull(s);
142+
// just verify it doesn't throw and contains the wrapper
143+
assertEquals("InfoStackWithOpenCounters(", s.substring(0, 26));
144+
}
145+
}

0 commit comments

Comments
 (0)