Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions checkstyle_suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
<suppress checks="RegexpMultiline" files="HandlerBase.java"/>
<suppress checks="RegexpMultiline" files="LocatorTest.java"/>
<suppress checks="RegexpMultiline" files="SerializableTest.java"/>
<suppress checks="RegexpMultiline" files="PerformanceMetrics.java"/>
<suppress checks="RegexpMultiline" files="PerformanceTest.java"/>

</suppressions>
45 changes: 42 additions & 3 deletions src/main/java/org/htmlunit/cssparser/parser/AbstractCSSParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import org.htmlunit.cssparser.parser.javacc.CharStream;
import org.htmlunit.cssparser.parser.javacc.ParseException;
Expand All @@ -40,6 +41,15 @@ public abstract class AbstractCSSParser {
private InputSource source_;

private static final HashMap<String, String> PARSER_MESSAGES_ = new HashMap<>();
private static final ConcurrentHashMap<String, String> ERROR_MESSAGE_CACHE_ = new ConcurrentHashMap<>();
private static final int MAX_CACHE_SIZE = 100;

/**
* Thread-local StringBuilder cache to reduce allocations.
* Automatically cleared after use to prevent memory leaks.
*/
private static final ThreadLocal<StringBuilder> STRING_BUILDER_CACHE =
ThreadLocal.withInitial(() -> new StringBuilder(256));

static {
PARSER_MESSAGES_.put("invalidExpectingOne", "Invalid token \"{0}\". Was expecting: {1}.");
Expand Down Expand Up @@ -170,20 +180,49 @@ protected InputSource getInputSource() {
* @return the parser message
*/
protected String getParserMessage(final String key) {
final String cached = ERROR_MESSAGE_CACHE_.get(key);
if (cached != null) {
return cached;
}

final String msg = PARSER_MESSAGES_.get(key);
if (msg == null) {
return "[[" + key + "]]";
}

if (ERROR_MESSAGE_CACHE_.size() < MAX_CACHE_SIZE) {
ERROR_MESSAGE_CACHE_.put(key, msg);
}

return msg;
}

/**
* Gets a cached StringBuilder, cleared and ready to use.
* @return A cleared StringBuilder instance
*/
protected StringBuilder getCachedStringBuilder() {
final StringBuilder sb = STRING_BUILDER_CACHE.get();
sb.setLength(0);
return sb;
}

/**
* Returns a cached StringBuilder after extracting its content.
* @param sb The StringBuilder to extract from
* @return The string content
*/
protected String returnCachedStringBuilder(final StringBuilder sb) {
return sb.toString();
}

/**
* Returns a new locator for the given token.
* @param t the token to generate the locator for
* @return a new locator
*/
protected Locator createLocator(final Token t) {
return new Locator(getInputSource().getURI(),
return LocatorPool.acquire(getInputSource().getURI(),
t == null ? 0 : t.beginLine,
t == null ? 0 : t.beginColumn);
}
Expand All @@ -194,7 +233,7 @@ protected Locator createLocator(final Token t) {
* @return a new string with the escaped values
*/
protected String addEscapes(final String str) {
final StringBuilder sb = new StringBuilder();
final StringBuilder sb = getCachedStringBuilder();
char ch;
for (int i = 0; i < str.length(); i++) {
ch = str.charAt(i);
Expand Down Expand Up @@ -236,7 +275,7 @@ protected String addEscapes(final String str) {
continue;
}
}
return sb.toString();
return returnCachedStringBuilder(sb);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/htmlunit/cssparser/parser/Locator.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ public void setLineNumber(final int line) {
lineNumber_ = line;
}

/**
* Clears all fields (for object pooling).
*/
public void clear() {
uri_ = null;
lineNumber_ = 0;
columnNumber_ = 0;
}

/** {@inheritDoc} */
@Override
public boolean equals(final Object obj) {
Expand Down
83 changes: 83 additions & 0 deletions src/main/java/org/htmlunit/cssparser/parser/LocatorPool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2019-2024 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.cssparser.parser;

import java.util.ArrayDeque;
import java.util.Deque;

/**
* Simple object pool for Locator instances to reduce allocations.
* Uses ThreadLocal to avoid synchronization overhead.
*
* @author Ronald Brill
*/
public final class LocatorPool {

private static final int MAX_POOL_SIZE = 32;

private static final ThreadLocal<Deque<Locator>> POOL =
ThreadLocal.withInitial(() -> new ArrayDeque<>(MAX_POOL_SIZE));

private LocatorPool() {
}

/**
* Acquires a Locator from the pool or creates a new one.
*
* @param uri The URI
* @param line The line number
* @param column The column number
* @return A Locator instance
*/
public static Locator acquire(final String uri, final int line, final int column) {
final Deque<Locator> pool = POOL.get();
Locator locator = pool.poll();

if (locator == null) {
locator = new Locator(uri, line, column);
}
else {
locator.setUri(uri);
locator.setLineNumber(line);
locator.setColumnNumber(column);
}

return locator;
}

/**
* Returns a Locator to the pool for reuse.
* Note: This method is provided for completeness but typically
* Locator objects are not explicitly released in the parser.
*
* @param locator The locator to return
*/
public static void release(final Locator locator) {
if (locator != null) {
final Deque<Locator> pool = POOL.get();
if (pool.size() < MAX_POOL_SIZE) {
locator.clear();
pool.offer(locator);
}
}
}

/**
* Clears the pool (useful for testing).
*/
public static void clear() {
POOL.get().clear();
}
}
121 changes: 121 additions & 0 deletions src/main/java/org/htmlunit/cssparser/parser/PerformanceMetrics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (c) 2019-2024 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.cssparser.parser;

/**
* Optional performance metrics for the CSS parser.
* Enable with -Dhtmlunit.cssparser.metrics=true
*
* @author Ronald Brill
*/
public class PerformanceMetrics {

private static final boolean ENABLED =
Boolean.getBoolean("htmlunit.cssparser.metrics");

private long parseTimeMs_;
private int tokenCount_;
private int ruleCount_;
private int propertyCount_;

/**
* Creates a new PerformanceMetrics instance if enabled.
*
* @return a new instance or null if disabled
*/
public static PerformanceMetrics start() {
return ENABLED ? new PerformanceMetrics() : null;
}

/**
* Records the parse time.
*
* @param ms the time in milliseconds
*/
public void recordParseTime(final long ms) {
if (ENABLED) {
parseTimeMs_ = ms;
}
}

/**
* Increments the token count.
*/
public void incrementTokens() {
if (ENABLED) {
tokenCount_++;
}
}

/**
* Increments the rule count.
*/
public void incrementRules() {
if (ENABLED) {
ruleCount_++;
}
}

/**
* Increments the property count.
*/
public void incrementProperties() {
if (ENABLED) {
propertyCount_++;
}
}

/**
* Prints the metrics report to System.out.
*/
public void report() {
if (ENABLED) {
System.out.println("=== CSS Parser Performance Metrics ===");
System.out.println("Parse time: " + parseTimeMs_ + "ms");
System.out.println("Tokens: " + tokenCount_);
System.out.println("Rules: " + ruleCount_);
System.out.println("Properties: " + propertyCount_);
System.out.println("=====================================");
}
}

/**
* @return the parse time in milliseconds
*/
public long getParseTimeMs() {
return parseTimeMs_;
}

/**
* @return the token count
*/
public int getTokenCount() {
return tokenCount_;
}

/**
* @return the rule count
*/
public int getRuleCount() {
return ruleCount_;
}

/**
* @return the property count
*/
public int getPropertyCount() {
return propertyCount_;
}
}
57 changes: 57 additions & 0 deletions src/main/java/org/htmlunit/cssparser/util/ParserUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ public static String trimBy(final StringBuilder s, final int left, final int rig
return s.substring(left, s.length() - right);
}

/**
* Remove the given number of chars from start and end.
* Optimized version with bounds checking for String input.
*
* @param s the String
* @param left no of chars to be removed from start
* @param right no of chars to be removed from end
* @return the trimmed string
*/
public static String trimBy(final String s, final int left, final int right) {
if (s == null) {
return null;
}

final int length = s.length();

if (left < 0 || right < 0 || left + right >= length) {
return s;
}

if (left == 0 && right == 0) {
return s;
}

return s.substring(left, length - right);
}

/**
* Helper that removes the leading "url(", the trailing ")"
* and surrounding quotes from the given string builder.
Expand All @@ -101,4 +128,34 @@ public static String trimUrl(final StringBuilder s) {
return s1;
}

/**
* Compare CharSequence without creating String objects.
* Case-insensitive comparison.
*
* @param cs1 the first CharSequence
* @param cs2 the second CharSequence
* @return true if the CharSequences are equal ignoring case
*/
public static boolean equalsIgnoreCase(final CharSequence cs1, final CharSequence cs2) {
if (cs1 == cs2) {
return true;
}
if (cs1 == null || cs2 == null) {
return false;
}
if (cs1.length() != cs2.length()) {
return false;
}

for (int i = 0; i < cs1.length(); i++) {
final char c1 = cs1.charAt(i);
final char c2 = cs2.charAt(i);
if (c1 != c2 && Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
return false;
}
}

return true;
}

}
Loading