[Bug 316988] Removing duplicates and putting into document order takes O(n^2)
Increased feature and plugin versions for 3.2.3
diff --git a/tests/org.eclipse.wst.xml.xpath2.processor.tests/src/org/eclipse/wst/xml/xpath2/processor/test/TestPerformance.java b/tests/org.eclipse.wst.xml.xpath2.processor.tests/src/org/eclipse/wst/xml/xpath2/processor/test/TestPerformance.java
new file mode 100644
index 0000000..154e8c2
--- /dev/null
+++ b/tests/org.eclipse.wst.xml.xpath2.processor.tests/src/org/eclipse/wst/xml/xpath2/processor/test/TestPerformance.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Jesper S Moller and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ * 
+ * Contributors:
+ *  Jesper S Moller - initial API and implementation 
+ *******************************************************************************/
+package org.eclipse.wst.xml.xpath2.processor.test;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.eclipse.wst.xml.xpath2.processor.DefaultEvaluator;
+import org.eclipse.wst.xml.xpath2.processor.DynamicContext;
+import org.eclipse.wst.xml.xpath2.processor.DynamicError;
+import org.eclipse.wst.xml.xpath2.processor.Evaluator;
+import org.eclipse.wst.xml.xpath2.processor.ast.XPath;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public class TestPerformance extends AbstractPsychoPathTest {
+
+	private static final String COUNT = "count";
+	private static final double FUDGE_FACTOR = 1.8;
+	private static boolean CHATTY = true;
+	
+	private final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+	{
+		factory.setNamespaceAware(true);
+	}
+
+	interface DocBuilder {
+		Document createDocument(int complexity) throws ParserConfigurationException;
+	}
+
+	public void testLinearPerformance() throws Exception {
+		checkPerformance("count(for $elem in //* return 'hej')", 1, 1000, 15, new DocBuilder() {
+			
+			public Document createDocument(int complexity) throws ParserConfigurationException {
+				Document document = factory.newDocumentBuilder().newDocument();
+
+				Element root = document.createElementNS("urn:x-root","xr:Root");
+				int count = 1;
+				document.appendChild(root);
+				for (int i = 0; i < complexity; ++i) {
+					Element child = root.getOwnerDocument().createElementNS("urn:x-child-" + (i%5+1), "c"+(i%5+1) + ":UpperChild" + (i+1));
+					root.appendChild(child);
+					count += addChildren(child, 3) + 1;
+				}
+				document.setUserData(COUNT, count, null);
+				return document;
+			}
+		});
+	}
+
+	public void testDeepPerformance() throws Exception {
+		checkPerformance("count(for $elem in //* return 'hej')", 0, 1000, 15, new DocBuilder() {
+			
+			public Document createDocument(int complexity) throws ParserConfigurationException {
+
+				factory.setNamespaceAware(true);
+				Document document = factory.newDocumentBuilder().newDocument();
+				
+				Element root = document.createElementNS("urn:x-root","xr:Root");
+				document.appendChild(root);
+				int count = 1;
+				Element current = root;
+				for (int i = 0; i < complexity; ++i) {
+					Element child = root.getOwnerDocument().createElementNS("urn:x-child-" + (i%5+1), "c"+(i%5+1) + ":UpperChild" + (i+1));
+					current.appendChild(child);
+					count += addChildren(child, 2) + 1;
+					current = child;
+				}
+
+				document.setUserData(COUNT, count, null);
+				return document;
+			}
+		});
+	}
+	
+	public void testExplosivePerformance() throws Exception {
+		checkPerformance("count(for $elem in //* return 'hej')", 2, 11, 1, new DocBuilder() {
+			
+			public Document createDocument(int complexity) throws ParserConfigurationException {
+
+				factory.setNamespaceAware(true);
+				Document document = factory.newDocumentBuilder().newDocument();
+				
+				Element root = document.createElementNS("urn:x-root","xr:Root");
+				document.appendChild(root);
+				int count = addChildren(root, complexity);
+
+				document.setUserData(COUNT, count+1, null);
+				return document;
+			}
+		});
+	}
+	
+	public void checkPerformance(String xpath, int low, int high, int step, DocBuilder builder) throws Exception {
+		DynamicContext dc = setupDynamicContext(null);
+
+		XPath path = compileXPath(dc, xpath);
+		Document smallDoc = builder.createDocument(low);
+		int referenceNodeCount = (Integer)smallDoc.getUserData(COUNT);
+		long time = timeEvaluation(dc, path, smallDoc, referenceNodeCount);
+		StringBuilder sb = new StringBuilder();
+		
+		log(sb, "First iteration " + time + " µs for " + referenceNodeCount + " nodes");
+		long timePerComplexity = (long)(FUDGE_FACTOR  * time / referenceNodeCount);
+		log(sb, "Expected bound of " + timePerComplexity + " µs per unit");
+
+		int violations = 0;
+		for (int i = low + 1; i <= high; i+=step) {
+			Document compareDoc = builder.createDocument(i);
+			int nodeCount = (Integer)compareDoc.getUserData(COUNT);
+			if (CHATTY) {
+				System.out.println("Complexity " + i + " (going to " + high + "): Trying " + nodeCount + " nodes");
+			}
+			long measured = timeEvaluation(dc, path, compareDoc, nodeCount);
+			
+			long actualTime = measured / nodeCount;
+			if (CHATTY) {
+				System.out.println("At " + nodeCount + " units, time per unit " + actualTime + " µs");
+			}
+			if (actualTime > timePerComplexity) {
+				log(sb, "At " + nodeCount + " units, the time per unit " + actualTime + " µs was too high, indicating non-linear complexity");
+				if (++violations >= 3) {
+					fail(sb.toString());
+				}
+			}
+		}
+	}
+	
+	private void log(StringBuilder sb, String string) {
+		if (CHATTY) {
+			System.out.println(string);
+		}
+		sb.append(string).append("\n");
+		
+	}
+
+	private long timeEvaluation(DynamicContext dc, XPath path, Document bigDoc, long expected)
+			throws DynamicError {
+		long before = System.nanoTime();
+		Evaluator eval = new DefaultEvaluator(dc, bigDoc);
+		String count = eval.evaluate(path).first().string_value();
+		assertEquals(""+expected, count);
+		long after = System.nanoTime();
+		return (after-before) / 1000;
+	}
+	
+	/**
+	 * Constructs a tree of equal depth and number of children
+	 * 
+	 * @param parent Element to add children to
+	 * @param depth Desired depth
+	 * @return number of nodes created
+	 */
+	private int addChildren(Element parent, int depth) {
+		int count = 0;
+		for  (int i = 0; i < depth*2; ++i) {
+			count++;
+			Element child = parent.getOwnerDocument().createElementNS("urn:x-child-" + (i+1), "c"+(i+1) + ":Child" + (i+1));
+			parent.appendChild(child);
+			count += addChildren(child, depth-2);
+		}
+		return count;
+	}
+	
+	public static void main(String[] args) throws Exception {
+		TestPerformance tp = new TestPerformance();
+		tp.setUp();
+		try {
+			tp.testLinearPerformance();
+//			tp.testDeepPerformance();
+		} catch (Exception e) {
+			// Boom
+		}
+		tp.tearDown();
+	}
+}