Skip to content

Commit ea0a076

Browse files
sugmanuekstich
authored andcommitted
Improve deferred rendering performance
1 parent 903df65 commit ea0a076

3 files changed

Lines changed: 68 additions & 34 deletions

File tree

smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.function.Consumer;
2121
import java.util.function.Function;
2222
import java.util.function.Supplier;
23-
import java.util.regex.Pattern;
2423

2524
/**
2625
* Helper class for generating code.
@@ -609,7 +608,6 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
609608
'_',
610609
'`'};
611610

612-
private static final Pattern LINES = Pattern.compile("\\r?\\n");
613611
private static final Map<Character, BiFunction<Object, String, String>> DEFAULT_FORMATTERS = MapUtils.of(
614612
'L',
615613
(s, i) -> formatLiteral(s),
@@ -621,7 +619,6 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
621619
private boolean trailingNewline = true;
622620
private int trimBlankLines = -1;
623621
private boolean enableStackTraceComments;
624-
private final List<String> deferredPlaceholders = new ArrayList<>();
625622
private final List<Supplier<String>> deferredSuppliers = new ArrayList<>();
626623
private int deferredCounter = 0;
627624

@@ -788,47 +785,66 @@ public char getExpressionStart() {
788785
public String toString() {
789786
String result = currentState.toString();
790787

791-
// Resolve any deferred values.
792-
if (!deferredPlaceholders.isEmpty()) {
793-
// Find all sentinel positions, then resolve in a single linear pass.
794-
List<int[]> positions = new ArrayList<>();
795-
for (int i = 0; i < deferredPlaceholders.size(); i++) {
796-
String key = deferredPlaceholders.get(i);
797-
int fromIndex = 0;
798-
while ((fromIndex = result.indexOf(key, fromIndex)) != -1) {
799-
positions.add(new int[] {fromIndex, i, key.length()});
800-
fromIndex += key.length();
788+
// Resolve any deferred values with a single linear scan.
789+
if (deferredCounter > 0) {
790+
StringBuilder resolved = new StringBuilder(result.length());
791+
int pos = 0;
792+
int fromIndex = 0;
793+
int len = result.length();
794+
while (fromIndex < len) {
795+
if (result.charAt(fromIndex) == '\0' && fromIndex + 1 < len && result.charAt(fromIndex + 1) == '\0') {
796+
int idStart = fromIndex + 2;
797+
int end = result.indexOf("\0\0", idStart);
798+
if (end != -1) {
799+
int id = 0;
800+
for (int k = idStart; k < end; k++) {
801+
id = id * 10 + (result.charAt(k) - '0');
802+
}
803+
int keyLen = end + 2 - fromIndex;
804+
resolved.append(result, pos, fromIndex);
805+
resolved.append(deferredSuppliers.get(id).get());
806+
fromIndex += keyLen;
807+
pos = fromIndex;
808+
continue;
809+
}
801810
}
811+
fromIndex++;
802812
}
803-
if (!positions.isEmpty()) {
804-
positions.sort((a, b) -> Integer.compare(a[0], b[0]));
805-
StringBuilder resolved = new StringBuilder(result.length());
806-
int pos = 0;
807-
for (int[] entry : positions) {
808-
resolved.append(result, pos, entry[0]);
809-
resolved.append(deferredSuppliers.get(entry[1]).get());
810-
pos = entry[0] + entry[2];
811-
}
812-
resolved.append(result, pos, result.length());
813+
if (pos > 0) {
814+
resolved.append(result, pos, len);
813815
result = resolved.toString();
814816
}
815817
}
816818

817819
// Trim excessive blank lines.
818820
if (trimBlankLines > -1) {
819821
StringBuilder builder = new StringBuilder(result.length());
820-
String[] lines = LINES.split(result);
821822
int blankCount = 0;
822-
823-
for (String line : lines) {
824-
if (!StringUtils.isBlank(line)) {
825-
builder.append(line).append(currentState.newline);
823+
int start = 0;
824+
int len = result.length();
825+
int lastNonBlankEnd = 0;
826+
827+
while (start < len) {
828+
int newlineIdx = result.indexOf('\n', start);
829+
int lineEnd = (newlineIdx == -1) ? len : newlineIdx;
830+
// Handle \r\n
831+
int lineContentEnd = (lineEnd > start && result.charAt(lineEnd - 1) == '\r')
832+
? lineEnd - 1
833+
: lineEnd;
834+
835+
boolean blank = StringUtils.isBlank(result, start, lineContentEnd);
836+
if (!blank) {
837+
builder.append(result, start, lineContentEnd).append(currentState.newline);
826838
blankCount = 0;
839+
lastNonBlankEnd = builder.length();
827840
} else if (blankCount++ < trimBlankLines) {
828-
builder.append(line).append(currentState.newline);
841+
builder.append(result, start, lineContentEnd).append(currentState.newline);
829842
}
843+
844+
start = (newlineIdx == -1) ? len : newlineIdx + 1;
830845
}
831846

847+
builder.setLength(lastNonBlankEnd);
832848
result = builder.toString();
833849
}
834850

@@ -1804,7 +1820,6 @@ public final String format(Object content, Object... args) {
18041820
*/
18051821
protected final String defer(Supplier<String> supplier) {
18061822
String id = "\u0000\u0000" + (deferredCounter++) + "\u0000\u0000";
1807-
deferredPlaceholders.add(id);
18081823
deferredSuppliers.add(supplier);
18091824
return id;
18101825
}

smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import java.util.Optional;
1616
import java.util.function.Function;
1717
import java.util.function.Supplier;
18-
import java.util.regex.Pattern;
1918
import java.util.stream.Collectors;
2019

2120
@SmithyInternalApi
@@ -321,8 +320,6 @@ public boolean hasNext() {
321320
}
322321

323322
private static final class Parser {
324-
private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z]+[a-zA-Z0-9_.#$]*$");
325-
326323
private final String template;
327324
private final SimpleParser parser;
328325
private final char expressionStart;
@@ -704,9 +701,14 @@ private Function<AbstractCodeWriter<?>, Object> parsePositionalArgumentGetter()
704701
}
705702

706703
private void ensureNameIsValid(String name) {
707-
if (!NAME_PATTERN.matcher(name).matches()) {
704+
if (name.isEmpty() || name.charAt(0) < 'a' || name.charAt(0) > 'z') {
708705
throw error(String.format("Invalid format expression name `%s`", name));
709706
}
707+
for (int i = 1; i < name.length(); i++) {
708+
if (!isNameCharacter(name.charAt(i))) {
709+
throw error(String.format("Invalid format expression name `%s`", name));
710+
}
711+
}
710712
}
711713

712714
private boolean isNameCharacter(int c) {

smithy-utils/src/main/java/software/amazon/smithy/utils/StringUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ public static boolean isBlank(final CharSequence cs) {
162162
return true;
163163
}
164164

165+
/**
166+
* Checks if a region of a CharSequence contains only whitespace.
167+
*
168+
* @param cs the CharSequence to check, must not be null
169+
* @param start the start index (inclusive)
170+
* @param end the end index (exclusive)
171+
* @return {@code true} if the region is empty or contains only whitespace
172+
*/
173+
public static boolean isBlank(final CharSequence cs, int start, int end) {
174+
for (int i = start; i < end; i++) {
175+
if (!Character.isWhitespace(cs.charAt(i))) {
176+
return false;
177+
}
178+
}
179+
return true;
180+
}
181+
165182
/**
166183
* <p>Checks if a CharSequence is not empty (""), not null and not whitespace only.</p>
167184
*

0 commit comments

Comments
 (0)