This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git


The following commit(s) were added to refs/heads/3 by this push:
     new 3d3dcac6 Forward ported from 2.3-gae: ConcatenatedSequence performance 
improvements
3d3dcac6 is described below

commit 3d3dcac6b355a5a4ef39726a03a229a4112c82a2
Author: ddekany <[email protected]>
AuthorDate: Fri Dec 15 23:15:11 2023 +0100

    Forward ported from 2.3-gae: ConcatenatedSequence performance improvements
---
 .../freemarker/core/ConcatenatedSequenceTest.java  | 318 +++++++++++++++++++++
 .../apache/freemarker/core/ASTExpAddOrConcat.java  | 229 +++++++++++++--
 2 files changed, 528 insertions(+), 19 deletions(-)

diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConcatenatedSequenceTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConcatenatedSequenceTest.java
new file mode 100644
index 00000000..0e71e4bc
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConcatenatedSequenceTest.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.freemarker.core;
+
+import org.apache.freemarker.core.ASTExpAddOrConcat.ConcatenatedSequence;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.impl.DefaultListAdapter;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleString;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.*;
+
+public class ConcatenatedSequenceTest {
+    interface SeqFactory {
+        TemplateSequenceModel create(String... items);
+        boolean isUnrepeatable();
+    }
+
+    @Test
+    public void testForNativeSequences() throws TemplateException {
+        testWithSegmentFactory(new SeqFactory() {
+            @Override
+            public TemplateSequenceModel create(String... items) {
+                return new NativeSequence(Arrays.stream(items).map(it -> it == 
null ? null : (TemplateModel) new SimpleString(it)).toList());
+            }
+
+            @Override
+            public boolean isUnrepeatable() {
+                return false;
+            }
+        });
+    }
+
+    @Test
+    public void testForListAdapter() throws TemplateException {
+        DefaultObjectWrapper objectWrapper = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        testWithSegmentFactory(new SeqFactory() {
+            @Override
+            public TemplateSequenceModel create(String... items) {
+                return DefaultListAdapter.adapt(Arrays.asList(items), 
objectWrapper);
+            }
+
+            @Override
+            public boolean isUnrepeatable() {
+                return false;
+            }
+        });
+    }
+
+    public void testWithSegmentFactory(SeqFactory segmentFactory) throws 
TemplateException {
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                new ConcatenatedSequence(segmentFactory.create(), 
segmentFactory.create()));
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(segmentFactory.create(), 
segmentFactory.create("b")),
+                "b");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(segmentFactory.create("a"), 
segmentFactory.create()),
+                "a");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(segmentFactory.create("a"), 
segmentFactory.create("b")),
+                "a", "b");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                new ConcatenatedSequence(
+                        new ConcatenatedSequence(
+                                segmentFactory.create(),
+                                segmentFactory.create()),
+                        new ConcatenatedSequence(
+                                segmentFactory.create(),
+                                segmentFactory.create())
+                )
+        );
+
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a", "b"),
+                                        segmentFactory.create()),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create())
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create("a", "b")),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create())
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create()),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a", "b"),
+                                        segmentFactory.create())
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create()),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create("a", "b"))
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a"),
+                                        segmentFactory.create("b")),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create())
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create("a")),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("b"),
+                                        segmentFactory.create())
+                        ),
+                "a", "b"
+        );
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(),
+                                        segmentFactory.create()),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a"),
+                                        segmentFactory.create("b"))
+                        ),
+                "a", "b"
+        );
+
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        new ConcatenatedSequence(
+                                                segmentFactory.create("a"),
+                                                segmentFactory.create("b")),
+                                        segmentFactory.create("c")),
+                                segmentFactory.create("d")),
+                "a", "b", "c", "d");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a"),
+                                        segmentFactory.create("b")),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("c"),
+                                        segmentFactory.create("d"))
+                        ),
+                "a", "b", "c", "d");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                segmentFactory.create("a"),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("b"),
+                                        new ConcatenatedSequence(
+                                                segmentFactory.create("c"),
+                                                segmentFactory.create("d")
+                                        )
+                                )
+                        ),
+                "a", "b", "c", "d");
+
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a", "b"),
+                                        segmentFactory.create("c", "d")),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("e", "f"),
+                                        segmentFactory.create("g", "h"))
+                        ),
+                "a", "b", "c", "d", "e", "f", "g", "h");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                segmentFactory.create("a", "b", "c"),
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("d", "e"),
+                                        segmentFactory.create("f", "g", "h"))
+                        ),
+                "a", "b", "c", "d", "e", "f", "g", "h");
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create("a", "b"),
+                                        segmentFactory.create("c", "d")),
+                                segmentFactory.create("e", "f", "g", "h")
+                        ),
+                "a", "b", "c", "d", "e", "f", "g", "h");
+
+        if (!segmentFactory.isUnrepeatable()) {
+            // Test when the same segment seq instance is for multiple times 
in a concatenated seq.
+            assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                    {
+                        TemplateSequenceModel ab = segmentFactory.create("a", 
"b");
+                        ConcatenatedSequence abab = new 
ConcatenatedSequence(ab, ab);
+                        return new ConcatenatedSequence(abab, abab);
+                    },
+                    "a", "b", "a", "b", "a", "b", "a", "b");
+        }
+
+        assertConcatenationResult(segmentFactory.isUnrepeatable(), () ->
+                        new ConcatenatedSequence(
+                                new ConcatenatedSequence(
+                                        segmentFactory.create(null, "a"),
+                                        segmentFactory.create("b", null)),
+                                segmentFactory.create((String) null)
+                        ),
+                null, "a", "b", null, null);
+    }
+
+    private void assertConcatenationResult(
+            boolean repeatable,
+            Supplier<ConcatenatedSequence> seqSupplier,
+            String... expectedItems)
+            throws TemplateException {
+        ConcatenatedSequence seq = seqSupplier.get();
+
+        {
+            List<String> actualItems = new ArrayList<>();
+            for (TemplateModelIterator iter = seq.iterator(); iter.hasNext(); 
) {
+                actualItems.add(asNullableString((TemplateStringModel) 
iter.next()));
+            }
+            assertEquals(Arrays.asList(expectedItems), actualItems);
+        }
+
+        if (repeatable) {
+            seq = seqSupplier.get();
+        }
+
+        {
+            List<String> actualItems = new ArrayList<>();
+            for (TemplateModelIterator iter = seq.iterator(); iter.hasNext(); 
) {
+                assertTrue(iter.hasNext());
+                actualItems.add(asNullableString((TemplateStringModel) 
iter.next()));
+            }
+            assertEquals(Arrays.asList(expectedItems), actualItems);
+        }
+
+        if (repeatable) {
+            seq = seqSupplier.get();
+        }
+
+        {
+            List<String> actualItems = new ArrayList<>();
+            int size = seq.getCollectionSize();
+            for (int i = 0; i < size; i++) {
+                actualItems.add(asNullableString((TemplateStringModel) 
seq.get(i)));
+            }
+            assertEquals(Arrays.asList(expectedItems), actualItems);
+            assertNull(seq.get(-1));
+            assertNull(seq.get(size));
+            assertNull(seq.get(size + 1));
+        }
+
+        if (repeatable) {
+            seq = seqSupplier.get();
+        }
+
+        assertEquals(expectedItems.length, seq.getCollectionSize());
+
+        if (repeatable) {
+            seq = seqSupplier.get();
+        }
+
+        assertEquals(expectedItems.length == 0, seq.isEmptyCollection());
+    }
+
+    private String asNullableString(TemplateStringModel model) throws 
TemplateException {
+        return model != null ? model.getAsString() : null;
+    }
+
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
index d222aa57..bb345a3e 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java
@@ -19,22 +19,13 @@
 
 package org.apache.freemarker.core;
 
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
 import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
-import org.apache.freemarker.core.model.TemplateCollectionModel;
-import org.apache.freemarker.core.model.TemplateHashModel;
-import org.apache.freemarker.core.model.TemplateHashModelEx;
-import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
-import org.apache.freemarker.core.model.TemplateModel;
-import org.apache.freemarker.core.model.TemplateModelIterator;
-import org.apache.freemarker.core.model.TemplateNumberModel;
-import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.*;
 import org.apache.freemarker.core.model.impl.SimpleNumber;
 import org.apache.freemarker.core.model.impl.SimpleString;
 
+import java.util.*;
+
 /**
  * AST expression node: binary {@code +} operator. Note that this is treated 
separately from the other 4 arithmetic
  * operators, since it's overloaded to mean concatenation of string-s, 
sequences and hash-es too.
@@ -178,7 +169,7 @@ final class ASTExpAddOrConcat extends ASTExpression {
         return ParameterRole.forBinaryOperatorOperand(idx);
     }
 
-    private static final class ConcatenatedSequence implements 
TemplateSequenceModel {
+    static final class ConcatenatedSequence implements TemplateSequenceModel {
         private final TemplateSequenceModel left;
         private final TemplateSequenceModel right;
 
@@ -190,23 +181,223 @@ final class ASTExpAddOrConcat extends ASTExpression {
         @Override
         public int getCollectionSize()
         throws TemplateException {
-            return left.getCollectionSize() + right.getCollectionSize();
+            int totalSize = 0;
+
+            ConcatenatedSequence[] concSeqsWithRightPending = new 
ConcatenatedSequence[2];
+            int concSeqsWithRightPendingLength = 0;
+            ConcatenatedSequence concSeqInFocus = this;
+
+            while (true) {
+                TemplateSequenceModel left;
+                while ((left = concSeqInFocus.left) instanceof 
ConcatenatedSequence) {
+                    if (concSeqsWithRightPendingLength == 
concSeqsWithRightPending.length) {
+                        concSeqsWithRightPending = 
Arrays.copyOf(concSeqsWithRightPending, concSeqsWithRightPendingLength * 2);
+                    }
+                    concSeqsWithRightPending[concSeqsWithRightPendingLength++] 
= concSeqInFocus;
+                    concSeqInFocus = (ConcatenatedSequence) left;
+                }
+                totalSize += left.getCollectionSize();
+
+                while (true) {
+                    TemplateSequenceModel right = concSeqInFocus.right;
+                    if (right instanceof ConcatenatedSequence) {
+                        concSeqInFocus = (ConcatenatedSequence) right;
+                        break; // To jump at the left-descending loop
+                    }
+                    totalSize += right.getCollectionSize();
+
+                    if (concSeqsWithRightPendingLength == 0) {
+                        return totalSize;
+                    }
+
+                    concSeqsWithRightPendingLength--;
+                    concSeqInFocus = 
concSeqsWithRightPending[concSeqsWithRightPendingLength];
+                }
+            }
         }
 
         @Override
         public boolean isEmptyCollection() throws TemplateException {
-            return left.isEmptyCollection() && right.isEmptyCollection();
+            ConcatenatedSequence[] concSeqsWithRightPending = new 
ConcatenatedSequence[2];
+            int concSeqsWithRightPendingLength = 0;
+            ConcatenatedSequence concSeqInFocus = this;
+
+            while (true) {
+                TemplateSequenceModel left;
+                while ((left = concSeqInFocus.left) instanceof 
ConcatenatedSequence) {
+                    if (concSeqsWithRightPendingLength == 
concSeqsWithRightPending.length) {
+                        concSeqsWithRightPending = 
Arrays.copyOf(concSeqsWithRightPending, concSeqsWithRightPendingLength * 2);
+                    }
+                    concSeqsWithRightPending[concSeqsWithRightPendingLength++] 
= concSeqInFocus;
+                    concSeqInFocus = (ConcatenatedSequence) left;
+                }
+                if (!left.isEmptyCollection()) {
+                    return false;
+                }
+
+                while (true) {
+                    TemplateSequenceModel right = concSeqInFocus.right;
+                    if (right instanceof ConcatenatedSequence) {
+                        concSeqInFocus = (ConcatenatedSequence) right;
+                        break; // To jump at the left-descending loop
+                    }
+                    if (!right.isEmptyCollection()) {
+                        return false;
+                    }
+
+                    if (concSeqsWithRightPendingLength == 0) {
+                        return true;
+                    }
+
+                    concSeqsWithRightPendingLength--;
+                    concSeqInFocus = 
concSeqsWithRightPending[concSeqsWithRightPendingLength];
+                }
+            }
         }
 
         @Override
-        public TemplateModel get(int i) throws TemplateException {
-            int leftSize = left.getCollectionSize();
-            return i < leftSize ? left.get(i) : right.get(i - leftSize);
+        public TemplateModel get(int index) throws TemplateException {
+            if (index < 0) {
+                return null;
+            }
+
+            int totalSize = 0;
+
+            ConcatenatedSequence[] concSeqsWithRightPending = new 
ConcatenatedSequence[2];
+            int concSeqsWithRightPendingLength = 0;
+            ConcatenatedSequence concSeqInFocus = this;
+
+            while (true) {
+                TemplateSequenceModel left;
+                while ((left = concSeqInFocus.left) instanceof 
ConcatenatedSequence) {
+                    if (concSeqsWithRightPendingLength == 
concSeqsWithRightPending.length) {
+                        concSeqsWithRightPending = 
Arrays.copyOf(concSeqsWithRightPending, concSeqsWithRightPendingLength * 2);
+                    }
+                    concSeqsWithRightPending[concSeqsWithRightPendingLength++] 
= concSeqInFocus;
+                    concSeqInFocus = (ConcatenatedSequence) left;
+                }
+                {
+                    int segmentSize = left.getCollectionSize();
+                    totalSize += segmentSize;
+                    if (totalSize > index) {
+                        return left.get(index - (totalSize - segmentSize));
+                    }
+                }
+
+                while (true) {
+                    TemplateSequenceModel right = concSeqInFocus.right;
+                    if (right instanceof ConcatenatedSequence) {
+                        concSeqInFocus = (ConcatenatedSequence) right;
+                        break; // To jump at the left-descending loop
+                    }
+                    {
+                        int segmentSize = right.getCollectionSize();
+                        totalSize += segmentSize;
+                        if (totalSize > index) {
+                            return right.get(index - (totalSize - 
segmentSize));
+                        }
+                    }
+
+                    if (concSeqsWithRightPendingLength == 0) {
+                        return null;
+                    }
+
+                    concSeqsWithRightPendingLength--;
+                    concSeqInFocus = 
concSeqsWithRightPending[concSeqsWithRightPendingLength];
+                }
+            }
         }
 
         @Override
         public TemplateModelIterator iterator() throws TemplateException {
-            return new ConcatenatedTemplateModelIterator(left.iterator(), 
right.iterator());
+            return new ConcatenatedSequenceIterator(this);
+        }
+    }
+
+    private static class ConcatenatedSequenceIterator implements 
TemplateModelIterator {
+        /** The path from the root down to the parent of the current segment 
({@link #currentSegmentIterator}) */
+        private final List<ConcatenatedSequence> concSeqsWithRightPending = 
new ArrayList<>();
+        private ConcatenatedSequence concSeqWithLeftDescentPending;
+
+        private TemplateModelIterator currentSegmentIterator;
+
+        private boolean hasPrefetchedResult;
+        private TemplateModel prefetchedNext;
+        private boolean prefetchedHasNext;
+
+        public ConcatenatedSequenceIterator(ConcatenatedSequence concatSeq) 
throws TemplateException {
+            // Descent down to the first nested sequence, and memorize the 
path down there
+            concSeqWithLeftDescentPending = concatSeq;
+        }
+
+        @Override
+        public TemplateModel next() throws TemplateException {
+            ensureHasPrefetchedResult();
+
+            if (!prefetchedHasNext) {
+                throw new TemplateException("The collection has no more 
elements.");
+            }
+
+            TemplateModel result = prefetchedNext;
+            hasPrefetchedResult = false; // To consume prefetched element
+            prefetchedNext = null; // To not prevent GC
+            return result;
+        }
+
+        @Override
+        public boolean hasNext() throws TemplateException {
+            ensureHasPrefetchedResult();
+            return prefetchedHasNext;
+        }
+
+        private void ensureHasPrefetchedResult() throws TemplateException {
+            if (hasPrefetchedResult) {
+                return;
+            }
+
+            while (true) {
+                // Try to fetch the next value from the current segment:
+                if (currentSegmentIterator != null) {
+                    boolean hasNext = currentSegmentIterator.hasNext();
+                    if (hasNext) {
+                        prefetchedNext = currentSegmentIterator.next();
+                        prefetchedHasNext = true;
+                        hasPrefetchedResult = true;
+                        return;
+                    } else {
+                        currentSegmentIterator = null;
+                        // Falls through
+                    }
+                } else if (concSeqWithLeftDescentPending != null) { // Nothing 
to fetch from, has to descend left first
+                    ConcatenatedSequence leftDescentCurrentConcSeq = 
concSeqWithLeftDescentPending;
+                    concSeqWithLeftDescentPending = null;
+                    concSeqsWithRightPending.add(leftDescentCurrentConcSeq);
+
+                    TemplateSequenceModel leftSeq;
+                    while ((leftSeq = leftDescentCurrentConcSeq.left) 
instanceof ConcatenatedSequence) {
+                        leftDescentCurrentConcSeq = (ConcatenatedSequence) 
leftSeq;
+                        
concSeqsWithRightPending.add(leftDescentCurrentConcSeq);
+                    }
+                    this.currentSegmentIterator = leftSeq.iterator();
+                    continue; // Jump to fetching from current segment
+                }
+
+                // If we reach this, then the current segment was fully 
consumed, so we have to switch to the next segment.
+
+                if (concSeqsWithRightPending.isEmpty()) {
+                    prefetchedNext = null;
+                    prefetchedHasNext = false;
+                    hasPrefetchedResult = true;
+                    return;
+                }
+
+                TemplateSequenceModel right = 
concSeqsWithRightPending.remove(concSeqsWithRightPending.size() - 1).right;
+                if (right instanceof ConcatenatedSequence) {
+                    concSeqWithLeftDescentPending = (ConcatenatedSequence) 
right;
+                } else {
+                    this.currentSegmentIterator = right.iterator();
+                }
+            }
         }
     }
 

Reply via email to