JUNEAU-175 Execution statistics for REST methods.
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/testutils/TestUtils.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/testutils/TestUtils.java
index 9e480e8..0cc99d6 100644
--- a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/testutils/TestUtils.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/testutils/TestUtils.java
@@ -385,6 +385,18 @@
 		Assert.assertEquals(s, ws.toString(o));

 	}

 

+	public static final void assertObjectMatches(String s, Object o) {

+		assertObjectMatches(s, o, js2);

+	}

+

+	public static final void assertObjectMatches(String s, Object o, WriterSerializer ws) {

+		if ("xxx".equals(s))

+			System.err.println(ws.toString(o).replaceAll("\\\\", "\\\\\\\\")); // NOT DEBUG

+		String o2 =  ws.toString(o);

+		if (! StringUtils.getMatchPattern(s).matcher(o2).matches())

+			throw new ComparisonFailure(null, s, o2);

+	}

+

 	/**

 	 * Replaces all newlines with pipes, then compares the strings.

 	 */

diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/MethodInvokerTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/MethodInvokerTest.java
new file mode 100644
index 0000000..541f9d7
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/MethodInvokerTest.java
@@ -0,0 +1,98 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.junit.Assert.assertEquals;
+
+import java.lang.reflect.*;
+
+import org.junit.*;
+
+public class MethodInvokerTest {
+
+	public static class A {
+		public int foo() { return 0; }
+		public int bar() { throw new RuntimeException("bar"); }
+		public void baz(int x) { }
+	}
+
+	@Test
+	public void testBasic() throws Exception {
+		Method m = A.class.getMethod("foo");
+
+		MethodExecStats mes = new MethodExecStats(m);
+		MethodInvoker mi = new MethodInvoker(m, mes);
+
+		A a = new A();
+		mi.invoke(a);
+		mi.invoke(a);
+		mi.invoke(a);
+
+		assertObjectMatches("{method:'A.foo',runs:3,running:0,errors:0,avgTime:*,totalTime:*,exceptions:[]}", mes);
+	}
+
+	@Test
+	public void testException() throws Exception {
+		Method m = A.class.getMethod("bar");
+
+		MethodExecStats mes = new MethodExecStats(m);
+		MethodInvoker mi = new MethodInvoker(m, mes);
+
+		A a = new A();
+		try {
+			mi.invoke(a);
+		} catch (Exception e) {}
+		try {
+			mi.invoke(a);
+		} catch (Exception e) {}
+		try {
+			mi.invoke(a);
+		} catch (Exception e) {}
+
+		assertObjectMatches("{method:'A.bar',runs:3,running:0,errors:3,avgTime:0,totalTime:*,exceptions:[{exception:'RuntimeException',hash:'*',count:3}]}", mes);
+	}
+
+	@Test
+	public void testIllegalArgument() throws Exception {
+		Method m = A.class.getMethod("baz", int.class);
+
+		MethodExecStats mes = new MethodExecStats(m);
+		MethodInvoker mi = new MethodInvoker(m, mes);
+
+		A a = new A();
+		try {
+			mi.invoke(a, "x");
+		} catch (Exception e) {}
+		try {
+			mi.invoke(a);
+		} catch (Exception e) {}
+		try {
+			mi.invoke(a, 1, "x");
+		} catch (Exception e) {}
+
+		assertObjectMatches("{method:'A.baz',runs:3,running:0,errors:3,avgTime:0,totalTime:*,exceptions:[{exception:'IllegalArgumentException',hash:'*',count:3}]}", mes);
+	}
+
+	@Test
+	public void testOtherMethods() throws Exception {
+		Method m = A.class.getMethod("foo");
+		MethodExecStats mes = new MethodExecStats(m);
+		MethodInvoker mi = new MethodInvoker(m, mes);
+
+		assertEquals(m, mi.inner());
+		assertEquals("A", mi.getDeclaringClass().getSimpleName());
+		assertEquals("foo", mi.getName());
+	}
+
+}
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java
index 967e989..1ed653e 100644
--- a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java
@@ -13,9 +13,13 @@
 package org.apache.juneau.utils;
 
 import static org.junit.Assert.*;
+import static org.apache.juneau.testutils.TestUtils.*;
+
+import java.util.*;
 
 import org.junit.*;
 
+@SuppressWarnings("serial")
 public class StackTraceDatabaseTest {
 
 	@Test
@@ -26,31 +30,208 @@
 		t2.fillInStackTrace();
 
 		StackTraceDatabase db = new StackTraceDatabase();
-		StackTraceInfo t1a = db.getStackTraceInfo(t1, Integer.MAX_VALUE);
-		StackTraceInfo t1b = db.getStackTraceInfo(t1, Integer.MAX_VALUE);
-		StackTraceInfo t2a = db.getStackTraceInfo(t2, Integer.MAX_VALUE);
+		db.add(t1);
+		StackTraceInfo t1a = db.getStackTraceInfo(t1);
+		db.add(t1);
+		StackTraceInfo t1b = db.getStackTraceInfo(t1);
+		db.add(t2);
+		StackTraceInfo t2a = db.getStackTraceInfo(t2);
+
 		assertEquals(t1a.getHash(), t1b.getHash());
 		assertNotEquals(t1a.getHash(), t2a.getHash());
+
 		assertEquals(1, t1a.getCount());
 		assertEquals(2, t1b.getCount());
 		assertEquals(1, t2a.getCount());
 	}
 
 	@Test
+	public void testGetClonedStackTraceInfos() {
+		Throwable t1 = new Throwable();
+		t1.fillInStackTrace();
+		Throwable t2 = new Throwable();
+		t2.fillInStackTrace();
+
+		StackTraceDatabase db = new StackTraceDatabase();
+		db.add(t1);
+		db.add(t1);
+		db.add(t2);
+
+		List<StackTraceInfo> l = db.getClonedStackTraceInfos();
+		db.add(t1);
+
+		assertObjectMatches("[{exception:'Throwable',hash:'*',count:2},{exception:'Throwable',hash:'*',count:1}]", l);
+	}
+
+	@Test
 	public void testTimeout() {
 		Throwable t1 = new Throwable();
 		t1.fillInStackTrace();
 		Throwable t2 = new Throwable();
 		t2.fillInStackTrace();
 
-		StackTraceDatabase db = new StackTraceDatabase();
-		StackTraceInfo t1a = db.getStackTraceInfo(t1, -1);
-		StackTraceInfo t1b = db.getStackTraceInfo(t1, -1);
-		StackTraceInfo t2a = db.getStackTraceInfo(t2, -1);
+		StackTraceDatabase db = new StackTraceDatabase(-2, null);
+		db.add(t1);
+		StackTraceInfo t1a = db.getStackTraceInfo(t1);
+		db.add(t1);
+		StackTraceInfo t1b = db.getStackTraceInfo(t1);
+		db.add(t2);
+		StackTraceInfo t2a = db.getStackTraceInfo(t2);
+
 		assertEquals(t1a.getHash(), t1b.getHash());
 		assertNotEquals(t1a.getHash(), t2a.getHash());
+
+		assertEquals(0, t1a.getCount());
+		assertEquals(0, t1b.getCount());
+		assertEquals(0, t2a.getCount());
+	}
+
+	@Test
+	public void testReset() {
+		Throwable t1 = new Throwable();
+		t1.fillInStackTrace();
+
+		StackTraceDatabase db = new StackTraceDatabase();
+		db.add(t1);
+		StackTraceInfo t1a = db.getStackTraceInfo(t1);
 		assertEquals(1, t1a.getCount());
-		assertEquals(1, t1b.getCount());
-		assertEquals(1, t2a.getCount());
+
+		db.reset();
+		t1a = db.getStackTraceInfo(t1);
+		assertEquals(0, t1a.getCount());
+	}
+
+	@Test
+	public void testNullException() {
+		StackTraceDatabase db = new StackTraceDatabase();
+		db.add(null).add(null);
+		StackTraceInfo t1a = db.getStackTraceInfo(null);
+		assertEquals(2, t1a.getCount());
+	}
+
+	@Test
+	public void testSameStackTraces() {
+		StackTraceDatabase db = new StackTraceDatabase();
+
+		Throwable t1 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop", "baz", "Stop.class", 3),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		Throwable t2 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop", "baz", "Stop.class", 3),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		StackTraceInfo sti1 = db.getStackTraceInfo(t1);
+		StackTraceInfo sti2 = db.getStackTraceInfo(t2);
+		assertEquals(sti1.getHash(), sti2.getHash());
+	}
+
+	@Test
+	public void testSlightlyDifferentStackTraces() {
+		StackTraceDatabase db = new StackTraceDatabase();
+
+		Throwable t1 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop", "baz", "Stop.class", 3),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		Throwable t2 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop", "baz", "Stop.class", 3),
+					new StackTraceElement("Object", "baz", "Object.class", 7)
+				};
+			}
+		};
+		StackTraceInfo sti1 = db.getStackTraceInfo(t1);
+		StackTraceInfo sti2 = db.getStackTraceInfo(t2);
+		assertNotEquals(sti1.getHash(), sti2.getHash());
+	}
+
+	@Test
+	public void testStopClass() {
+		StackTraceDatabase db = new StackTraceDatabase(-1, StopClass.class);
+
+		Throwable t1 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement(StopClass.class.getName(), "baz", "Stop.class", 3),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		Throwable t2 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement(StopClass.class.getName(), "baz", "Stop.class", 4),
+					new StackTraceElement("Object", "baz", "Object.class", 7)
+				};
+			}
+		};
+		StackTraceInfo sti1 = db.getStackTraceInfo(t1);
+		StackTraceInfo sti2 = db.getStackTraceInfo(t2);
+		assertEquals(sti1.getHash(), sti2.getHash());
+	}
+
+	private static final class StopClass {}
+
+	@Test
+	public void testProxyElements() {
+		StackTraceDatabase db = new StackTraceDatabase();
+
+		Throwable t1 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop$1", "baz", "Stop.class", 5),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		Throwable t2 = new Throwable() {
+			@Override
+			public StackTraceElement[] getStackTrace() {
+				return new StackTraceElement[] {
+					new StackTraceElement("Foo", "bar", "Foo.class", 1),
+					new StackTraceElement("Foo", "baz", "Foo.class", 2),
+					new StackTraceElement("Stop$2", "baz", "Stop.class", 6),
+					new StackTraceElement("Object", "baz", "Object.class", 6)
+				};
+			}
+		};
+		StackTraceInfo sti1 = db.getStackTraceInfo(t1);
+		StackTraceInfo sti2 = db.getStackTraceInfo(t2);
+		assertEquals(sti1.getHash(), sti2.getHash());
 	}
 }
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/WeightedAverageTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/WeightedAverageTest.java
new file mode 100644
index 0000000..7c4e99b
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/WeightedAverageTest.java
@@ -0,0 +1,47 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+public class WeightedAverageTest {
+
+	@Test
+	public void testEmpty() {
+		WeightedAverage w = new WeightedAverage();
+		assertEquals(0f, w.getValue(), 0.01);
+	}
+
+	@Test
+	public void testBasic() {
+		WeightedAverage w = new WeightedAverage();
+		w.add(0,100).add(1,1).add(1,2).add(1,3).add(0,100);
+		assertEquals(2f, w.getValue(), 0.01);
+	}
+
+	@Test
+	public void testBasicWithNullValue() {
+		WeightedAverage w = new WeightedAverage();
+		w.add(1,1).add(1,null).add(1,3);
+		assertEquals(2f, w.getValue(), 0.01);
+	}
+
+	@Test
+	public void testDifferingWeights() {
+		WeightedAverage w = new WeightedAverage();
+		w.add(10,1).add(20,3);
+		assertEquals(2.33f, w.getValue(), 0.01);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index 5945ada..71cf6a1 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -404,7 +404,7 @@
 				// Check for missing properties.

 				for (String fp : fixedBeanProps)

 					if (! normalProps.containsKey(fp))

-						throw new BeanRuntimeException(c, "The property ''{0}'' was defined on the @Bean(bpi=X) annotation but was not found on the class definition.", fp);

+						throw new BeanRuntimeException(c, "The property ''{0}'' was defined on the @Bean(bpi=X) annotation of class ''{1}'' but was not found on the class definition.", fp, ci.getSimpleName());

 

 				// Mark constructor arg properties.

 				for (String fp : constructorArgs) {

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
index 5f5fdb0..f289ca2 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
@@ -45,7 +45,7 @@
  * 		properties={

  * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_title</jsf>, value=<js>"$L{title}"</js>),

  * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_description</jsf>, value=<js>"$L{description}"</js>),

- * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_navlinks</jsf>, value=<js>"{options:'?method=OPTIONS',doc:'doc'}"</js>)

+ * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_navlinks</jsf>, value=<js>"{options:'servlet:/?method=OPTIONS',doc:'doc'}"</js>)

  * 		}

  * 	)

  * 	<jk>public class</jk> AddressBookResource <jk>extends</jk> BasicRestServletJena {

@@ -60,7 +60,7 @@
  * 		description=<js>"$L{description}"</js>,

  * 		htmldoc=<ja>@HtmlDoc</ja>(

  * 			navlinks={

- * 				<js>"options: ?method=OPTIONS"</js>,

+ * 				<js>"options: servlet:/?method=OPTIONS"</js>,

  * 				<js>"doc: doc"</js>

  * 			}

  * 		)

@@ -333,7 +333,7 @@
 	 * 	<ja>@Rest</ja>(

 	 * 		properties={

 	 * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_navlinks</jsf>,

-	 * 				value=<js>"['options: ?method=OPTIONS', 'doc: doc']"</js>)

+	 * 				value=<js>"['options: servlet:/?method=OPTIONS', 'doc: doc']"</js>)

 	 * 		}

 	 * 	)

 	 * 	<jk>public class</jk> AddressBookResource <jk>extends</jk> BasicRestServletJena {

@@ -345,7 +345,7 @@
 	 * 	<ja>@Rest</ja>(

 	 * 		htmldoc=@HtmlDoc(

 	 * 			navlinks={

-	 * 				<js>"options: ?method=OPTIONS"</js>,

+	 * 				<js>"options: servlet:/?method=OPTIONS"</js>,

 	 * 				<js>"doc: doc"</js>

 	 * 			}

 	 * 		)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java
index 1d473b0..eea5aac 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java
@@ -222,7 +222,8 @@
 	 * <p>
 	 * 	<ja>@HtmlDocConfig</ja>(
 	 * 		navlinks={
-	 * 			<js>"options: ?method=OPTIONS"</js>,
+	 * 			<js>"options: servlet:/?method=OPTIONS"</js>,
+	 * 			<js>"stats: servlet:/stats"</js>,
 	 * 			<js>"doc: doc"</js>
 	 * 		}
 	 * 	)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java
index beee09a..81cb4e3 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java
@@ -294,7 +294,8 @@
 	 * <p class='bcode w800'>
 	 * 	<ja>@HtmlDocConfig</ja>(
 	 * 		navlinks={
-	 * 			<js>"options: ?method=OPTIONS"</js>,
+	 * 			<js>"options: servlet:/?method=OPTIONS"</js>,
+	 * 			<js>"stats: servlet:/stats"</js>,
 	 * 			<js>"doc: doc"</js>
 	 * 		}
 	 * 	)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/ExecutableInfo.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/ExecutableInfo.java
index 08d84f1..fb6d494 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/ExecutableInfo.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/reflect/ExecutableInfo.java
@@ -740,13 +740,9 @@
 	}
 
 	/**
-	 * Returns the simple name of the underlying class.
+	 * Returns the simple name of the underlying method.
 	 *
-	 * <p>
-	 * Returns either {@link Class#getSimpleName()} or {@link Type#getTypeName()} depending on whether
-	 * this is a class or type.
-	 *
-	 * @return The simple name of the underlying class;
+	 * @return The simple name of the underlying method;
 	 */
 	public final String getSimpleName() {
 		return isConstructor ? e.getDeclaringClass().getSimpleName() : e.getName();
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodExecStats.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodExecStats.java
new file mode 100644
index 0000000..831181e
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodExecStats.java
@@ -0,0 +1,160 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.concurrent.atomic.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.marshall.*;
+
+/**
+ * Basic timing information.
+ *
+ * Keeps track of number of starts/finishes on tasks and keeps an average run time.
+ */
+@Bean(bpi="method,runs,running,errors,avgTime,totalTime,exceptions")
+public class MethodExecStats implements Comparable<MethodExecStats> {
+	private String method;
+	private WeightedAverage avgTime = new WeightedAverage();
+
+	private AtomicInteger
+		starts = new AtomicInteger(),
+		finishes = new AtomicInteger(),
+		errors = new AtomicInteger();
+
+	private AtomicLong
+		totalTime = new AtomicLong();
+
+	private StackTraceDatabase stackTraceDb;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param method Arbitrary label.  Should be kept to less than 50 characters.
+	 * @param stackTraceStopClass Don't calculate stack traces when this class is encountered.
+	 */
+	public MethodExecStats(Method method, Class<?> stackTraceStopClass) {
+		this.method = method.getDeclaringClass().getSimpleName() + "." + method.getName();
+		this.stackTraceDb = new StackTraceDatabase(-1, stackTraceStopClass);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param method Arbitrary label.  Should be kept to less than 50 characters.
+	 */
+	public MethodExecStats(Method method) {
+		this(method, MethodInvoker.class);
+	}
+
+	/**
+	 * Call when task is started.
+	 */
+	public void started() {
+		starts.incrementAndGet();
+	}
+
+	/**
+	 * Call when task is finished.
+	 * @param time The execution time of the task.
+	 */
+	public void finished(long time) {
+		finishes.incrementAndGet();
+		totalTime.addAndGet(time);
+		avgTime.add(1, time);
+	}
+
+	/**
+	 * Call when an error occurs.
+	 * @param e The exception thrown.  Can be <jk>null</jk>.
+	 */
+	public void error(Throwable e) {
+		errors.incrementAndGet();
+		stackTraceDb.add(e);
+	}
+
+	/**
+	 * Returns the method name of these stats.
+	 *
+	 * @return The method name of these stats.
+	 */
+	public String getMethod() {
+		return method;
+	}
+
+	/**
+	 * Returns the number of times the {@link #started()} method was called.
+	 *
+	 * @return The number of times the {@link #started()} method was called.
+	 */
+	public int getRuns() {
+		return starts.get();
+	}
+
+	/**
+	 * Returns the number currently running method invocations.
+	 *
+	 * @return The number of currently running method invocations.
+	 */
+	public int getRunning() {
+		return starts.get() - finishes.get();
+	}
+
+	/**
+	 * Returns the number of times the {@link #error(Throwable)} method was called.
+	 *
+	 * @return The number of times the {@link #error(Throwable)} method was called.
+	 */
+	public int getErrors() {
+		return errors.get();
+	}
+
+	/**
+	 * Returns the average execution time.
+	 *
+	 * @return The average execution time in milliseconds.
+	 */
+	public int getAvgTime() {
+		return (int)avgTime.getValue();
+	}
+
+	/**
+	 * Returns the total execution time.
+	 *
+	 * @return The total execution time in milliseconds.
+	 */
+	public long getTotalTime() {
+		return totalTime.get();
+	}
+
+	/**
+	 * Returns information on all stack traces of all exceptions encountered.
+	 *
+	 * @return Information on all stack traces of all exceptions encountered.
+	 */
+	public List<StackTraceInfo> getExceptions() {
+		return stackTraceDb.getClonedStackTraceInfos();
+	}
+
+	@Override /* Object */
+	public String toString() {
+		return SimpleJson.DEFAULT.toString(this);
+	}
+
+	@Override /* Comparable */
+	public int compareTo(MethodExecStats o) {
+		return Long.compare(o.getTotalTime(), getTotalTime());
+	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodInvoker.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodInvoker.java
new file mode 100644
index 0000000..4290f6a
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/MethodInvoker.java
@@ -0,0 +1,87 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+import java.lang.reflect.*;
+
+/**
+ * A wrapper around a {@link Method#invoke(Object, Object...)} method that allows for basic instrumentation.
+ */
+public class MethodInvoker {
+	private final Method m;
+	private final MethodExecStats stats;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param m The method being wrapped.
+	 * @param stats The instrumentor.
+	 */
+	public MethodInvoker(Method m, MethodExecStats stats) {
+		this.m = m;
+		this.stats = stats;
+	}
+
+	/**
+	 * Returns the inner method.
+	 *
+	 * @return The inner method.
+	 */
+	public Method inner() {
+		return m;
+	}
+
+	/**
+	 * Invokes the underlying method.
+	 *
+	 * @param o  The object the underlying method is invoked from.
+	 * @param args  The arguments used for the method call.
+	 * @return  The result of dispatching the method represented by this object on {@code obj} with parameters {@code args}
+	 * @throws IllegalAccessException Thrown from underlying method.
+	 * @throws IllegalArgumentException Thrown from underlying method.
+	 * @throws InvocationTargetException Thrown from underlying method.
+	 */
+	public Object invoke(Object o, Object...args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+		long startTime = System.currentTimeMillis();
+		stats.started();
+		try {
+			return m.invoke(o, args);
+		} catch (IllegalAccessException|IllegalArgumentException e) {
+			stats.error(e);
+			throw e;
+		} catch (InvocationTargetException e) {
+			stats.error(e.getTargetException());
+			throw e;
+		} finally {
+			stats.finished(System.currentTimeMillis() - startTime);
+		}
+	}
+
+	/**
+	 * Convenience method for calling <c>inner().getDeclaringClass()</c>
+	 *
+	 * @return The declaring class of the method.
+	 */
+	public Class<?> getDeclaringClass() {
+		return m.getDeclaringClass();
+	}
+
+	/**
+	 * Convenience method for calling <c>inner().getName()</c>
+	 *
+	 * @return The name of the method.
+	 */
+	public String getName() {
+		return m.getName();
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java
index 0f8891c..2e54e22 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java
@@ -12,7 +12,11 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.utils;
 
+import static org.apache.juneau.utils.StackTraceUtils.*;
+
+import java.util.*;
 import java.util.concurrent.*;
+import java.util.stream.*;
 
 /**
  * An in-memory cache of stack traces.
@@ -24,56 +28,49 @@
 
 	private final ConcurrentHashMap<Integer,StackTraceInfo> db = new ConcurrentHashMap<>();
 	private final String stopClass;
+	private final long cacheTimeout;
 
 	/**
 	 * Constructor.
 	 */
 	public StackTraceDatabase() {
-		this.stopClass = null;
+		this(-1, null);
 	}
 
 	/**
 	 * Constructor.
 	 *
-	 * @param stopClass When this class is encountered in a stack trace, stop calculating the hash.
+	 * @param cacheTimeout
+	 * 	The amount of time in milliseconds to cache stack trace info in this database before discarding.
+	 * 	<br>If <c>-1</c>, never discard.
+	 * @param stopClass
+	 * 	When this class is encountered in a stack trace, stop calculating the hash.
+	 * 	<br>Can be <jk>null</jk>.
 	 */
-	public StackTraceDatabase(Class<?> stopClass) {
+	public StackTraceDatabase(long cacheTimeout, Class<?> stopClass) {
 		this.stopClass = stopClass == null ? "" : stopClass.getName();
+		this.cacheTimeout = cacheTimeout;
+	}
+
+	/**
+	 * Adds the specified throwable to this database.
+	 *
+	 * @param e The exception to add.
+	 * @return This object (for method chaining).
+	 */
+	public StackTraceDatabase add(Throwable e) {
+		find(e).increment();
+		return this;
 	}
 
 	/**
 	 * Retrieves the stack trace information for the specified exception.
 	 *
 	 * @param e The exception.
-	 * @param timeout The timeout in milliseconds to cache the hash for this stack trace.
-	 * @return The stack trace info, never <jk>null</jk>.
+	 * @return A clone of the stack trace info, never <jk>null</jk>.
 	 */
-	public StackTraceInfo getStackTraceInfo(Throwable e, int timeout) {
-		int hash = hash(e);
-		StackTraceInfo stc = db.get(hash);
-		if (stc != null && stc.timeout > System.currentTimeMillis()) {
-			stc.incrementAndClone();
-			return stc.clone();
-		}
-		synchronized (db) {
-			stc = new StackTraceInfo(timeout, hash);
-			db.put(hash, stc);
-			return stc.clone();
-		}
-	}
-
-	private int hash(Throwable t) {
-		int i = 0;
-		while (t != null) {
-			for (StackTraceElement e : t.getStackTrace()) {
-				if (e.getClassName().equals(stopClass))
-					break;
-				if (e.getClassName().indexOf('$') == -1)
-					i ^= e.hashCode();
-			}
-			t = t.getCause();
-		}
-		return i;
+	public StackTraceInfo getStackTraceInfo(Throwable e) {
+		return find(e).clone();
 	}
 
 	/**
@@ -82,4 +79,31 @@
 	public void reset() {
 		db.clear();
 	}
+
+	/**
+	 * Returns the list of all stack traces in this database.
+	 *
+	 * @return The list of all stack traces in this database, cloned and sorted by count descending.
+	 */
+	public List<StackTraceInfo> getClonedStackTraceInfos() {
+		return db.values().stream().map(x -> x.clone()).sorted().collect(Collectors.toList());
+	}
+
+	private StackTraceInfo find(Throwable e) {
+		int hash = hash(e, stopClass);
+		StackTraceInfo stc = db.get(hash);
+		long time = System.currentTimeMillis();
+
+		if (stc != null && stc.timeout > time) {
+			return stc;
+		}
+
+		String n = e == null ? null : e.getClass().getSimpleName();
+		long t = cacheTimeout == -1 ? Long.MAX_VALUE : time + cacheTimeout;
+		stc = new StackTraceInfo(n, t, hash);
+
+		db.put(hash, stc);
+
+		return db.get(hash);
+	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java
index 9d14436..6c42b27 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java
@@ -14,29 +14,37 @@
 
 import java.util.concurrent.atomic.*;
 
+import org.apache.juneau.annotation.*;
+
 /**
  * Represents an entry in {@link StackTraceDatabase}.
  */
-public class StackTraceInfo {
-	AtomicInteger count;
-	long timeout;
-	String hash;
+@Bean(bpi="exception,hash,count")
+public class StackTraceInfo implements Comparable<StackTraceInfo> {
+	final AtomicInteger count;
+	final long timeout;
+	final String hash;
+	final String exception;
 
-	StackTraceInfo(long timeout, int hash) {
-		this.count = new AtomicInteger(1);
-		this.timeout = System.currentTimeMillis() + timeout;
+	/**
+	 * Constructor.
+	 *
+	 * @param exception The simple name of the exception class.  Can be <jk>null</jk>.
+	 * @param timeout The time in UTC milliseconds at which point this stack trace info should be discarded from caches.
+	 * @param hash The hash id of this stack trace.
+	 */
+	public StackTraceInfo(String exception, long timeout, int hash) {
+		this.count = new AtomicInteger(0);
+		this.timeout = timeout;
 		this.hash = Integer.toHexString(hash);
+		this.exception = exception;
 	}
 
-	private StackTraceInfo(int count, long timeout, String hash) {
+	private StackTraceInfo(String exception, int count, long timeout, String hash) {
 		this.count = new AtomicInteger(count);
 		this.timeout = timeout;
 		this.hash = hash;
-	}
-
-	@Override
-	public StackTraceInfo clone() {
-		return new StackTraceInfo(count.intValue(), timeout, hash);
+		this.exception = exception;
 	}
 
 	/**
@@ -57,7 +65,32 @@
 		return hash;
 	}
 
-	StackTraceInfo incrementAndClone() {
-		return new StackTraceInfo(count.incrementAndGet(), timeout, hash);
+	/**
+	 * Returns the simple class name of the exception.
+	 *
+	 * @return The simple class name of the exception, or <jk>null</jk> if not specified.
+	 */
+	public String getException() {
+		return exception;
+	}
+
+	/**
+	 * Increments the occurrence count of this exception.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public StackTraceInfo increment() {
+		count.incrementAndGet();
+		return this;
+	}
+
+	@Override /* Comparable */
+	public int compareTo(StackTraceInfo o) {
+		return Integer.compare(o.getCount(), getCount());
+	}
+
+	@Override /* Object */
+	public StackTraceInfo clone() {
+		return new StackTraceInfo(exception, count.intValue(), timeout, hash);
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceUtils.java
new file mode 100644
index 0000000..62f25b2
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceUtils.java
@@ -0,0 +1,40 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+/**
+ * Stack trace utility methods.
+ */
+public class StackTraceUtils {
+
+	/**
+	 * Calculates a 16-bit hash for the specified throwable based on it's stack trace.
+	 *
+	 * @param t The throwable to calculate the stack trace on.
+	 * @param stopClass Optional stop class on which to stop calculation of a stack trace beyond when found.
+	 * @return A calculated hash.
+	 */
+	public static int hash(Throwable t, String stopClass) {
+		int i = 0;
+		while (t != null) {
+			for (StackTraceElement e : t.getStackTrace()) {
+				if (e.getClassName().equals(stopClass))
+					break;
+				if (e.getClassName().indexOf('$') == -1)
+					i ^= e.hashCode();
+			}
+			t = t.getCause();
+		}
+		return i;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/WeightedAverage.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/WeightedAverage.java
new file mode 100644
index 0000000..7355d01
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/WeightedAverage.java
@@ -0,0 +1,52 @@
+// ***************************************************************************************************************************
+// * 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.juneau.utils;
+
+/**
+ * A simple weighted average of numbers.
+ */
+public class WeightedAverage {
+	private Float value = 0f;
+	private int weight = 0;
+
+	/**
+	 * Add a number with a weight to this average.
+	 *
+	 * @param w The weight of the new value.
+	 * @param v The new value.
+	 * @return This object (for method chaining).
+	 */
+	public WeightedAverage add(int w, Number v) {
+		if (v != null) {
+			try {
+				float w1 = weight, w2 = w;
+				weight = Math.addExact(weight, w);
+				if (weight != 0) {
+					value = (value * (w1/weight)) + (v.floatValue() * (w2/weight));
+				}
+			} catch (ArithmeticException ae) {
+				throw new ArithmeticException("Weight overflow.");
+			}
+		}
+		return this;
+	}
+
+	/**
+	 * Returns the weighted average of all numbers.
+	 *
+	 * @return The weighted average of all numbers.
+	 */
+	public float getValue() {
+		return value;
+	}
+}
diff --git a/juneau-doc/docs/ReleaseNotes/8.1.3.html b/juneau-doc/docs/ReleaseNotes/8.1.3.html
index 0426a00..836424b 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.3.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.3.html
@@ -63,7 +63,8 @@
 			<li><ja>@RestMethod(attrs)</ja> --&gt; {@link oajr.annotation.RestMethod#reqAttrs() @RestMethod(reqAttrs)}
 			<li><ja>@RestMethod(defaultRequestHeaders)</ja> --&gt; {@link oajr.annotation.RestMethod#reqHeaders() @RestMethod(reqHeaders)}
 		</ul>
-		
+	<li>
+		New auto-generated REST method execution statistics: {@doc juneau-rest-server.ExecutionStatistics}
 </ul>
 
 <h5 class='topic w800'>juneau-rest-client</h5>
diff --git a/juneau-doc/docs/Topics/06.juneau-rest-server/30.ExecutionStatistics.html b/juneau-doc/docs/Topics/06.juneau-rest-server/30.ExecutionStatistics.html
new file mode 100644
index 0000000..4d47ad3
--- /dev/null
+++ b/juneau-doc/docs/Topics/06.juneau-rest-server/30.ExecutionStatistics.html
@@ -0,0 +1,45 @@
+<!--
+/***************************************************************************************************************************
+ * 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.
+ ***************************************************************************************************************************/
+ -->
+
+{new] REST method execution statistics
+
+<p>
+	<ja>@RestResource</ja> annotated classes get automated timing and error statistic information for all <ja>@RestMethod</ja>
+	and <ja>@RestHook</ja> annotated methods on the class.
+</p>
+<p>
+	The statistics are made available via the subpath <c>/stats</c> as shown below in the <c>HelloWorldResource</c> example:
+</p>
+<img class='bordered w900' src='doc-files/juneau-rest-server.ExecutionStatistics.1.png'>
+<p>
+	The default REST configuration provides a link to the stats in the navlinks section of the page:
+</p>
+<img class='bordered w900' src='doc-files/juneau-rest-server.ExecutionStatistics.2.png'>
+<p>
+	The exception hash shown is the same hash that is shown in the log file and provides a quick way of locating
+	the exception in the logs.
+</p>
+<p>
+	Programmatic access to the statistics are provided via the following methods:
+</p>
+<ul class='javatree'>
+	<li class='jc'>{@link oajr.RestContext}
+	<ul>
+		<li class='jm'>{@link oajr.RestContext#getStats() getStats()}
+		<li class='jm'>{@link oajr.RestContext#getMethodExecStats() getMethodExecStats()}
+		<li class='jm'>{@link oajr.RestContext#getMethodExecStatsReport() getMethodExecStatsReport()}
+	</ul>
+</ul>
+	
\ No newline at end of file
diff --git a/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExecutionStatistics.1.png b/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExecutionStatistics.1.png
new file mode 100644
index 0000000..7efbb1e
--- /dev/null
+++ b/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExecutionStatistics.1.png
Binary files differ
diff --git a/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExeuctionStatistics.2.png b/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExeuctionStatistics.2.png
new file mode 100644
index 0000000..3e195b1
--- /dev/null
+++ b/juneau-doc/docs/Topics/06.juneau-rest-server/doc-files/juneau-rest-server.ExeuctionStatistics.2.png
Binary files differ
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HelloWorldResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HelloWorldResource.java
index 82ad529..9d9658b 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HelloWorldResource.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HelloWorldResource.java
@@ -16,6 +16,7 @@
 

 import org.apache.juneau.dto.swagger.*;

 import org.apache.juneau.html.annotation.*;

+import org.apache.juneau.http.exception.*;

 import org.apache.juneau.rest.*;

 import org.apache.juneau.rest.annotation.*;

 

@@ -51,11 +52,26 @@
 		return "Hello world!";

 	}

 

-	@Override

+	/**

+	 * Make a request to /helloWorld/badRequest to trigger a 400 Bad Request.

+	 *

+	 * @throws BadRequest A bad request.

+	 */

+	@RestMethod

+	public void getBadRequest() throws BadRequest {

+		throw new BadRequest("example");

+	}

+

+	@Override /* BasicRestConfig */

 	public Swagger getOptions(RestRequest req) {

 		return req.getSwagger();

 	}

 

-	@Override

+	@Override /* BasicRestConfig */

 	public void error() {}

+

+	@Override /* BasicRestConfig */

+	public RestContextStats getStats(RestRequest req) {

+		return req.getContext().getStats();

+	}

 }

diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RequestEchoResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RequestEchoResource.java
index 2f290e1..fecced1 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RequestEchoResource.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RequestEchoResource.java
@@ -52,6 +52,7 @@
 	navlinks={

 		"up: request:/..",

 		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"$W{ContentTypeMenuItem}",

 		"$W{ThemeMenuItem}",

 		"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/$R{servletClassSimple}.java"

diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
index 59152f4..3e6231d 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
@@ -45,7 +45,8 @@
 		ThemeMenuItem.class

 	},

 	navlinks={

-		"options: ?method=OPTIONS",

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"$W{ContentTypeMenuItem}",

 		"$W{ThemeMenuItem}",

 		"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/$R{servletClassSimple}.java"

diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/AtomFeedResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/AtomFeedResource.java
index 5e0a80d..08fb8df 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/AtomFeedResource.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/AtomFeedResource.java
@@ -57,6 +57,7 @@
 	navlinks={

 		"up: request:/..",

 		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"$W{ContentTypeMenuItem}",

 		"$W{ThemeMenuItem}",

 		"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/dto/$R{servletClassSimple}.java"

diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/DtoExamples.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/DtoExamples.java
index d282a34..26b7c79 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/DtoExamples.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/DtoExamples.java
@@ -41,7 +41,8 @@
 	},

 	navlinks={

 		"up: request:/..",

-		"options: ?method=OPTIONS",

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"$W{ContentTypeMenuItem}",

 		"$W{ThemeMenuItem}",

 		"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/dto/$R{servletClassSimple}.java"

diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/JsonSchemaResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/JsonSchemaResource.java
index d1eb728..6bd236c 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/JsonSchemaResource.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/dto/JsonSchemaResource.java
@@ -50,6 +50,7 @@
 	navlinks={

 		"up: request:/..",

 		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"$W{ContentTypeMenuItem}",

 		"$W{ThemeMenuItem}",

 		"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/dto/$R{servletClassSimple}.java"

diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/ConfigResource.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/ConfigResource.java
index 246798c..00e95d9 100755
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/ConfigResource.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/ConfigResource.java
@@ -42,6 +42,7 @@
 	navlinks={

 		"up: request:/..",

 		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats",

 		"edit: servlet:/edit"

 	}

 )

diff --git a/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/resources/DebugResource.java b/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/resources/DebugResource.java
index 37021a3..9a260a3 100644
--- a/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/resources/DebugResource.java
+++ b/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/resources/DebugResource.java
@@ -36,7 +36,8 @@
 	navlinks={

 		"up: request:/..",

 		"jetty-thread-dump: servlet:/jetty/dump?method=POST",

-		"options: servlet:/?method=OPTIONS"

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats"

 	}

 )

 @SuppressWarnings("javadoc")

diff --git a/juneau-microservice/juneau-my-jetty-microservice/src/main/java/org/apache/juneau/microservice/jetty/template/RootResources.java b/juneau-microservice/juneau-my-jetty-microservice/src/main/java/org/apache/juneau/microservice/jetty/template/RootResources.java
index a8a5b32..bded6ec 100755
--- a/juneau-microservice/juneau-my-jetty-microservice/src/main/java/org/apache/juneau/microservice/jetty/template/RootResources.java
+++ b/juneau-microservice/juneau-my-jetty-microservice/src/main/java/org/apache/juneau/microservice/jetty/template/RootResources.java
@@ -41,7 +41,8 @@
 		ThemeMenuItem.class

 	},

 	navlinks={

-		"options: servlet:/?method=OPTIONS"

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats"

 	}

 )

 public class RootResources extends BasicRestServletGroup {

diff --git a/juneau-microservice/juneau-my-springboot-microservice/src/main/java/org/apache/juneau/microservice/springboot/template/RootResources.java b/juneau-microservice/juneau-my-springboot-microservice/src/main/java/org/apache/juneau/microservice/springboot/template/RootResources.java
index 4ee7677..56db9ad 100755
--- a/juneau-microservice/juneau-my-springboot-microservice/src/main/java/org/apache/juneau/microservice/springboot/template/RootResources.java
+++ b/juneau-microservice/juneau-my-springboot-microservice/src/main/java/org/apache/juneau/microservice/springboot/template/RootResources.java
@@ -39,7 +39,8 @@
 		ThemeMenuItem.class

 	},

 	navlinks={

-		"options: servlet:/?method=OPTIONS"

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats"

 	}

 )

 public class RootResources extends BasicRestServletGroup {

diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java
index 21e5d37..0f87f83 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java
@@ -21,6 +21,7 @@
 
 import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.utils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -74,8 +75,12 @@
 		return RestCallLoggerConfig.create().parent(config).build();
 	}
 
+	private BasicRestCallLogger logger(Logger l, StackTraceDatabase std) {
+		return new BasicRestCallLogger(l, std);
+	}
+
 	private BasicRestCallLogger logger(Logger l) {
-		return new BasicRestCallLogger(null, l);
+		return new BasicRestCallLogger(l, new StackTraceDatabase());
 	}
 
 	private RestCallLoggerRule.Builder rule() {
@@ -349,12 +354,11 @@
 				rule().codes("*").req(SHORT).res(SHORT).build()
 			)
 			.useStackTraceHashing()
-			.stackTraceHashingTimeout(100000)
 			.build();
 		RestCallLoggerConfig lcw = wrapped(lc);
 
 		TestLogger tc = new TestLogger();
-		BasicRestCallLogger cl = logger(tc).resetStackTraces();
+		BasicRestCallLogger cl = logger(tc, new StackTraceDatabase(100000, null)).resetStackTraces();
 		Exception e = new StringIndexOutOfBoundsException();
 		MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
 		MockServletResponse res = res(200);
@@ -380,27 +384,26 @@
 				rule().codes("*").req(SHORT).res(SHORT).build()
 			)
 			.useStackTraceHashing()
-			.stackTraceHashingTimeout(-1)
 			.build();
 		RestCallLoggerConfig lcw = wrapped(lc);
 
 		TestLogger tc = new TestLogger();
-		BasicRestCallLogger cl = logger(tc).resetStackTraces();
+		BasicRestCallLogger cl = logger(tc, new StackTraceDatabase(-2, null)).resetStackTraces();
 		Exception e = new StringIndexOutOfBoundsException();
 		MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
 		MockServletResponse res = res(200);
 
 		cl.log(lc, req, res);
-		tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+		tc.check(INFO, "[200,*.0] HTTP GET /foo", true);
 		cl.log(lc, req, res);
-		tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+		tc.check(INFO, "[200,*.0] HTTP GET /foo", true);
 
 		cl.resetStackTraces();
 
 		cl.log(lcw, req, res);
-		tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+		tc.check(INFO, "[200,*.0] HTTP GET /foo", true);
 		cl.log(lcw, req, res);
-		tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+		tc.check(INFO, "[200,*.0] HTTP GET /foo", true);
 	}
 
 	//------------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRest.java
index 842cfb3..f7f5c7f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRest.java
@@ -38,7 +38,8 @@
 	// Basic page navigation links.

 	navlinks={

 		"up: request:/..",

-		"options: servlet:/?method=OPTIONS"

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats"

 	}

 )

 public abstract class BasicRest implements BasicRestConfig {

@@ -82,6 +83,18 @@
 	@Override /* BasicRestConfig */

 	public void error() {}

 

+	/**

+	 * [GET /stats] - Timing statistics.

+	 *

+	 * <p>

+	 * Timing statistics for method invocations on this resource.

+	 *

+	 * @return A collection of timing statistics for each annotated method on this resource.

+	 */

+	@Override /* BasicRestConfig */

+	public RestContextStats getStats(RestRequest req) {

+		return req.getContext().getStats();

+	}

 

 	//-----------------------------------------------------------------------------------------------------------------

 	// Context methods.

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java
index b73828d..d7fa58b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java
@@ -38,10 +38,10 @@
  */
 public class BasicRestCallLogger implements RestCallLogger {
 
-	private final StackTraceDatabase stackTraceDb = new StackTraceDatabase(RestMethodContext.class);
-
+	private final String loggerName;
 	private final Logger logger;
 	private final RestContext context;
+	private final StackTraceDatabase stackTraceDb;
 
 	/**
 	 * Constructor.
@@ -50,18 +50,22 @@
 	 */
 	public BasicRestCallLogger(RestContext context) {
 		this.context = context;
+		this.loggerName = context.getResource().getClass().getName();
 		this.logger = Logger.getLogger(getLoggerName());
+		this.stackTraceDb = context.getStackTraceDb();
 	}
 
 	/**
 	 * Constructor.
 	 *
-	 * @param context The context of the resource object.
 	 * @param logger The logger to use for logging.
+	 * @param stackTraceDb The stack trace database for maintaining stack traces.
 	 */
-	public BasicRestCallLogger(RestContext context, Logger logger) {
-		this.context = context;
+	protected BasicRestCallLogger(Logger logger, StackTraceDatabase stackTraceDb) {
+		this.context = null;
+		this.loggerName = getClass().getName();
 		this.logger = logger;
+		this.stackTraceDb = stackTraceDb;
 	}
 
 	/**
@@ -76,7 +80,7 @@
 	 * @return The logger name.
 	 */
 	protected String getLoggerName() {
-		return context == null ? getClass().getName() : context.getResource().getClass().getName();
+		return loggerName;
 	}
 
 	/**
@@ -243,6 +247,7 @@
 	private StackTraceInfo getStackTraceInfo(RestCallLoggerConfig config, Throwable e) {
 		if (e == null || ! config.isUseStackTraceHashing())
 			return null;
-		return stackTraceDb.getStackTraceInfo(e, config.getStackTraceHashingTimeout());
+		stackTraceDb.add(e);
+		return stackTraceDb.getStackTraceInfo(e);
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestConfig.java
index fa613ee..6d0f9aa 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestConfig.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestConfig.java
@@ -104,7 +104,8 @@
 	// Basic page navigation links.
 	navlinks={
 		"up: request:/..",
-		"options: servlet:/?method=OPTIONS"
+		"options: servlet:/?method=OPTIONS",
+		"stats: servlet:/stats"
 	},
 
 	// Default stylesheet to use for the page.
@@ -123,6 +124,26 @@
 	// By default, table cell contents should not wrap.
 	nowrap="true"
 )
+@JsonSchemaConfig(
+	// Add descriptions to the following types when not specified:
+	addDescriptionsTo="bean,collection,array,map,enum",
+	// Add x-example to the following types:
+	addExamplesTo="bean,collection,array,map",
+	// Don't generate schema information on the Swagger bean itself or HTML beans.
+	ignoreTypes="Swagger,org.apache.juneau.dto.html5.*",
+	// Use $ref references for bean definitions to reduce duplication in Swagger.
+	useBeanDefs="true"
+)
+@BeanConfig(
+	// When parsing generated beans, ignore unknown properties that may only exist as getters and not setters.
+	ignoreUnknownBeanProperties="true",
+	// POJO swaps to apply to all serializers/parsers on this method.
+	pojoSwaps={
+		// Use the SwaggerUI swap when rendering Swagger beans.
+		// This is a per-media-type swap that only applies to text/html requests.
+		SwaggerUI.class
+	}
+)
 public interface BasicRestConfig {
 
 	/**
@@ -135,26 +156,6 @@
 		summary="Swagger documentation",
 		description="Swagger documentation for this resource."
 	)
-	@JsonSchemaConfig(
-		// Add descriptions to the following types when not specified:
-		addDescriptionsTo="bean,collection,array,map,enum",
-		// Add x-example to the following types:
-		addExamplesTo="bean,collection,array,map",
-		// Don't generate schema information on the Swagger bean itself or HTML beans.
-		ignoreTypes="Swagger,org.apache.juneau.dto.html5.*",
-		// Use $ref references for bean definitions to reduce duplication in Swagger.
-		useBeanDefs="true"
-	)
-	@BeanConfig(
-		// When parsing generated beans, ignore unknown properties that may only exist as getters and not setters.
-		ignoreUnknownBeanProperties="true",
-		// POJO swaps to apply to all serializers/parsers on this method.
-		pojoSwaps={
-			// Use the SwaggerUI swap when rendering Swagger beans.
-			// This is a per-media-type swap that only applies to text/html requests.
-			SwaggerUI.class
-		}
-	)
 	@HtmlDocConfig(
 		// Should override config annotations defined on class.
 		rank=10,
@@ -184,4 +185,30 @@
 		description="An error occurred during handling of the request."
 	)
 	public void error();
+
+	/**
+	 * [GET /stats] - Timing statistics.
+	 *
+	 * <p>
+	 * Timing statistics for method invocations on this resource.
+	 *
+	 * @param req The HTTP request.
+	 * @return A collection of timing statistics for each annotated method on this resource.
+	 */
+	@RestMethod(name=GET, path="/stats",
+		summary="Timing statistics",
+		description="Timing statistics for method invocations on this resource."
+	)
+	@HtmlDocConfig(
+		// Should override config annotations defined on class.
+		rank=10,
+		// Override the nav links for the swagger page.
+		navlinks={
+			"back: servlet:/",
+			"json: servlet:/stats?Accept=text/json&plainText=true"
+		},
+		// Never show aside contents of page inherited from class.
+		aside="NONE"
+	)
+	public RestContextStats getStats(RestRequest req);
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestInfoProvider.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestInfoProvider.java
index 5163601..66871cf 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestInfoProvider.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestInfoProvider.java
@@ -120,7 +120,7 @@
 			return swagger;
 
 		// Wasn't cached...need to create one.
-		swagger = new SwaggerGenerator(req).getSwagger();
+		swagger = new SwaggerGenerator(req.getContext(), req.getVarResolverSession(), req.getLocale()).getSwagger();
 
 		swaggers.get(locale).put(hashCode, swagger);
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestServlet.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestServlet.java
index ef90c95..af76bae 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestServlet.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestServlet.java
@@ -165,7 +165,8 @@
 	// Basic page navigation links.

 	navlinks={

 		"up: request:/..",

-		"options: servlet:/?method=OPTIONS"

+		"options: servlet:/?method=OPTIONS",

+		"stats: servlet:/stats"

 	}

 )

 public abstract class BasicRestServlet extends RestServlet implements BasicRestConfig {

@@ -196,4 +197,17 @@
 	 */

 	@Override /* BasicRestConfig */

 	public void error() {}

+

+	/**

+	 * [GET /stats] - Timing statistics.

+	 *

+	 * <p>

+	 * Timing statistics for method invocations on this resource.

+	 *

+	 * @return A collection of timing statistics for each annotated method on this resource.

+	 */

+	@Override /* BasicRestConfig */

+	public RestContextStats getStats(RestRequest req) {

+		return req.getContext().getStats();

+	}

 }

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index fa7b931..cbf42b1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -23,9 +23,11 @@
 import java.io.*;

 import java.lang.reflect.Method;

 import java.nio.charset.*;

+import java.time.*;

 import java.util.*;

 import java.util.concurrent.*;

 import java.util.concurrent.atomic.*;

+import java.util.stream.*;

 

 import javax.activation.*;

 import javax.servlet.*;

@@ -3447,6 +3449,7 @@
 	@SuppressWarnings("deprecation") private final RestLogger logger;

 	private final RestCallLogger callLogger;

 	private final RestCallLoggerConfig callLoggerConfig;

+	private final StackTraceDatabase stackTraceDb;

 	private final RestCallHandler callHandler;

 	private final RestInfoProvider infoProvider;

 	private final HttpException initException;

@@ -3454,9 +3457,11 @@
 	private final RestResourceResolver resourceResolver;

 	private final UriResolution uriResolution;

 	private final UriRelativity uriRelativity;

+	private final ConcurrentHashMap<String,MethodExecStats> methodExecStats = new ConcurrentHashMap<>();

+	private final Instant startTime;

 

 	// Lifecycle methods

-	private final Method[]

+	private final MethodInvoker[]

 		postInitMethods,

 		postInitChildFirstMethods,

 		preCallMethods,

@@ -3517,6 +3522,8 @@
 	RestContext(RestContextBuilder builder) throws Exception {

 		super(builder.getPropertyStore());

 

+		startTime = Instant.now();

+

 		HttpException _initException = null;

 

 		try {

@@ -3585,7 +3592,6 @@
 			staticFileResponseHeaders = getMapProperty(REST_staticFileResponseHeaders, Object.class);

 

 			logger = getInstanceProperty(REST_logger, resource, RestLogger.class, NoOpRestLogger.class, resourceResolver, this);

-			callLogger = getInstanceProperty(REST_callLogger, resource, RestCallLogger.class, BasicRestCallLogger.class, resourceResolver, this);

 

 			if (debug == Enablement.TRUE) {

 				this.callLoggerConfig = RestCallLoggerConfig.DEFAULT_DEBUG;

@@ -3599,6 +3605,10 @@
 					this.callLoggerConfig = RestCallLoggerConfig.DEFAULT;

 			}

 

+			this.stackTraceDb = new StackTraceDatabase(callLoggerConfig.getStackTraceHashingTimeout(), RestMethodContext.class);

+

+			callLogger = getInstanceProperty(REST_callLogger, resource, RestCallLogger.class, BasicRestCallLogger.class, resourceResolver, this);

+

 			properties = builder.properties;

 			serializers =

 				SerializerGroup

@@ -3852,13 +3862,13 @@
 			}

 

 			this.callMethods = unmodifiableMap(_javaRestMethods);

-			this.preCallMethods = _preCallMethods.values().toArray(new Method[_preCallMethods.size()]);

-			this.postCallMethods = _postCallMethods.values().toArray(new Method[_postCallMethods.size()]);

-			this.startCallMethods = _startCallMethods.values().toArray(new Method[_startCallMethods.size()]);

-			this.endCallMethods = _endCallMethods.values().toArray(new Method[_endCallMethods.size()]);

-			this.postInitMethods = _postInitMethods.values().toArray(new Method[_postInitMethods.size()]);

-			this.postInitChildFirstMethods = _postInitChildFirstMethods.values().toArray(new Method[_postInitChildFirstMethods.size()]);

-			this.destroyMethods = _destroyMethods.values().toArray(new Method[_destroyMethods.size()]);

+			this.preCallMethods = _preCallMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_preCallMethods.size()]);

+			this.postCallMethods = _postCallMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_postCallMethods.size()]);

+			this.startCallMethods = _startCallMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_startCallMethods.size()]);

+			this.endCallMethods = _endCallMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_endCallMethods.size()]);

+			this.postInitMethods = _postInitMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_postInitMethods.size()]);

+			this.postInitChildFirstMethods = _postInitChildFirstMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_postInitChildFirstMethods.size()]);

+			this.destroyMethods = _destroyMethods.values().stream().map(x->new MethodInvoker(x, getMethodExecStats(x))).collect(Collectors.toList()).toArray(new MethodInvoker[_destroyMethods.size()]);

 			this.preCallMethodParams = _preCallMethodParams.toArray(new RestMethodParam[_preCallMethodParams.size()][]);

 			this.postCallMethodParams = _postCallMethodParams.toArray(new RestMethodParam[_postCallMethodParams.size()][]);

 			this.startCallMethodParams = _startCallMethodParams.toArray(new Class[_startCallMethodParams.size()][]);

@@ -3949,6 +3959,22 @@
 	}

 

 	/**

+	 * Returns the time statistics gatherer for the specified method.

+	 *

+	 * @param m The method to get statistics for.

+	 * @return The cached time-stats object.

+	 */

+	protected MethodExecStats getMethodExecStats(Method m) {

+		String n = MethodInfo.of(m).getSimpleName();

+		MethodExecStats ts = methodExecStats.get(n);

+		if (ts == null) {

+			methodExecStats.putIfAbsent(n, new MethodExecStats(m));

+			ts = methodExecStats.get(n);

+		}

+		return ts;

+	}

+

+	/**

 	 * Returns the variable resolver for this servlet.

 	 *

 	 * <p>

@@ -3982,6 +4008,7 @@
 	 * 		navlinks={

 	 * 			<js>"up: $R{requestParentURI}"</js>,

 	 * 			<js>"options: servlet:/?method=OPTIONS"</js>,

+	 * 			<js>"stats: servlet:/stats"</js>,

 	 * 			<js>"editLevel: servlet:/editLevel?logger=$A{attribute.name, OFF}"</js>

 	 * 		}

 	 * 		header={

@@ -4940,6 +4967,52 @@
 	}

 

 	/**

+	 * Gives access to the internal stack trace database.

+	 *

+	 * @return The stack trace database.

+	 */

+	public StackTraceDatabase getStackTraceDb() {

+		return stackTraceDb;

+	}

+

+	/**

+	 * Returns timing information on all method executions on this class.

+	 *

+	 * <p>

+	 * Timing information is maintained for any <ja>@RestResource</ja>-annotated and hook methods.

+	 *

+	 * @return A list of timing statistics ordered by average execution time descending.

+	 */

+	public List<MethodExecStats> getMethodExecStats() {

+		return methodExecStats.values().stream().sorted().collect(Collectors.toList());

+	}

+

+	/**

+	 * Gives access to the internal stack trace database.

+	 *

+	 * @return The stack trace database.

+	 */

+	public RestContextStats getStats() {

+		return new RestContextStats(startTime, getMethodExecStats());

+	}

+

+	/**

+	 * Returns the timing information returned by {@link #getMethodExecStats()} in a readable format.

+	 *

+	 * @return A report of all method execution times ordered by .

+	 */

+	public String getMethodExecStatsReport() {

+		StringBuilder sb = new StringBuilder()

+			.append(" Method                         Runs      Running   Errors   Avg          Total     \n")

+			.append("------------------------------ --------- --------- -------- ------------ -----------\n");

+		getMethodExecStats()

+			.stream()

+			.sorted(Comparator.comparingDouble(MethodExecStats::getTotalTime).reversed())

+			.forEach(x -> sb.append(String.format("%30s %9d %9d %9d %10dms %10dms\n", x.getMethod(), x.getRuns(), x.getRunning(), x.getErrors(), x.getAvgTime(), x.getTotalTime())));

+		return sb.toString();

+	}

+

+	/**

 	 * Finds the {@link RestMethodParam} instances to handle resolving objects on the calls to the specified Java method.

 	 *

 	 * @param mi The Java method being called.

@@ -5017,7 +5090,7 @@
 			preOrPost(resource, postCallMethods[i], postCallMethodParams[i], req, res);

 	}

 

-	private static void preOrPost(Object resource, Method m, RestMethodParam[] mp, RestRequest req, RestResponse res) throws HttpException {

+	private static void preOrPost(Object resource, MethodInvoker m, RestMethodParam[] mp, RestRequest req, RestResponse res) throws HttpException {

 		if (m != null) {

 			Object[] args = new Object[mp.length];

 			for (int i = 0; i < mp.length; i++) {

@@ -5051,7 +5124,7 @@
 			startOrFinish(resource, endCallMethods[i], endCallMethodParams[i], call.getRequest(), call.getResponse());

 	}

 

-	private static void startOrFinish(Object resource, Method m, Class<?>[] p, HttpServletRequest req, HttpServletResponse res) throws HttpException, InternalServerError {

+	private static void startOrFinish(Object resource, MethodInvoker m, Class<?>[] p, HttpServletRequest req, HttpServletResponse res) throws HttpException, InternalServerError {

 		if (m != null) {

 			Object[] args = new Object[p.length];

 			for (int i = 0; i < p.length; i++) {

@@ -5096,7 +5169,7 @@
 		return this;

 	}

 

-	private void postInitOrDestroy(Object r, Method m, Class<?>[] p) {

+	private void postInitOrDestroy(Object r, MethodInvoker m, Class<?>[] p) {

 		if (m != null) {

 			Object[] args = new Object[p.length];

 			for (int i = 0; i < p.length; i++) {

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextStats.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextStats.java
new file mode 100644
index 0000000..1f0664c
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextStats.java
@@ -0,0 +1,63 @@
+// ***************************************************************************************************************************
+// * 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.juneau.rest;
+
+import java.time.*;
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.transforms.*;
+import org.apache.juneau.utils.*;
+
+/**
+ * A snapshot of execution statistics for REST resource classes.
+ */
+@Bean(bpi="startTime,upTime,methodStats")
+public class RestContextStats {
+	private final Instant startTime;
+	private final List<MethodExecStats> methodStats;
+
+	RestContextStats(Instant startTime, List<MethodExecStats> methodStats) {
+		this.startTime = startTime;
+		this.methodStats = methodStats;
+	}
+
+	/**
+	 * Returns the time this REST resource class was started.
+	 *
+	 * @return The time this REST resource class was started.
+	 */
+	@Swap(TemporalSwap.IsoInstant.class)
+	public Instant getStartTime() {
+		return startTime;
+	}
+
+	/**
+	 * Returns the time in milliseconds that this REST resource class has been running.
+	 *
+	 * @return The time in milliseconds that this REST resource class has been running.
+	 */
+	public String getUpTime() {
+		long s = Duration.between(startTime, Instant.now()).getSeconds();
+		return String.format("%dh:%02dm:%02ds", s / 3600, (s % 3600) / 60, (s % 60));
+	}
+
+	/**
+	 * Returns statistics on all method executions.
+	 *
+	 * @return Statistics on all method executions.
+	 */
+	public Collection<MethodExecStats> getMethodStats() {
+		return methodStats;
+	}
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
index f787979..fc20866 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
@@ -52,6 +52,7 @@
 import org.apache.juneau.rest.widget.*;
 import org.apache.juneau.serializer.*;
 import org.apache.juneau.svl.*;
+import org.apache.juneau.utils.*;
 
 /**
  * Represents a single Java servlet/resource method annotated with {@link RestMethod @RestMethod}.
@@ -552,6 +553,7 @@
 	private final Integer priority;
 	private final RestContext context;
 	final java.lang.reflect.Method method;
+	final MethodInvoker methodInvoker;
 	final MethodInfo mi;
 	final SerializerGroup serializers;
 	final ParserGroup parsers;
@@ -585,6 +587,7 @@
 
 		this.context = b.context;
 		this.method = b.method;
+		this.methodInvoker = new MethodInvoker(method, context.getMethodExecStats(method));
 		this.mi = MethodInfo.of(method);
 
 		// Need this to access methods in anonymous inner classes.
@@ -901,7 +904,7 @@
 
 			Object output;
 			try {
-				output = method.invoke(context.getResource(), args);
+				output = methodInvoker.invoke(context.getResource(), args);
 				if (res.getStatus() == 0)
 					res.setStatus(200);
 				if (! method.getReturnType().equals(Void.TYPE)) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerGenerator.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerGenerator.java
index 8011678..a24d9a6 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerGenerator.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerGenerator.java
@@ -61,7 +61,7 @@
  */
 final class SwaggerGenerator {
 
-	private final RestRequest req;
+//	private final RestRequest req;
 	private final VarResolverSession vr;
 	private final Locale locale;
 	private final RestContext context;
@@ -73,14 +73,16 @@
 
 	/**
 	 * Constructor.
-	 * @param req The HTTP request.
+	 *
+	 * @param context The REST class context.
+	 * @param vr The variable resolver.
+	 * @param locale The locale.
 	 */
-	public SwaggerGenerator(RestRequest req) {
-		this.req = req;
-		this.vr = req.getVarResolverSession();
-		this.locale = req.getLocale();
-		this.context = req.getContext();
-		this.js = req.getJsonSchemaGenerator().createSession();
+	public SwaggerGenerator(RestContext context, VarResolverSession vr, Locale locale) {
+		this.vr = vr;
+		this.locale = locale;
+		this.context = context;
+		this.js = context.getJsonSchemaGenerator().createSession();
 		this.c = context.getResource().getClass();
 		this.resource = context.getResource();
 		this.mb = context.getMessages();
@@ -273,10 +275,6 @@
 		// Iterate through all the @RestMethod methods.
 		for (RestMethodContext sm : context.getCallMethods().values()) {
 
-			// Skip it if user doesn't have access.
-			if (! sm.isRequestAllowed(req))
-				continue;
-
 			BeanSession bs = sm.createBeanSession();
 
 			Method m = sm.method;
@@ -862,13 +860,10 @@
 					SerializerSessionArgs args =
 						SerializerSessionArgs
 							.create()
-							.javaMethod(req.getJavaMethod())
-							.locale(req.getLocale())
+							.locale(locale)
 							.mediaType(mt)
-							.debug(req.isDebug() ? true : null)
-							.uriContext(req.getUriContext())
 							.useWhitespace(true)
-							.resolver(req.getVarResolverSession());
+						;
 					try {
 						String eVal = s2.createSession(args).serializeToString(example);
 						examples.put(s2.getPrimaryMediaType().toString(), eVal);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
index 8be7be4..37f5f7f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
@@ -34,7 +34,7 @@
  * 	<jc>// Defined via properties.</jc>
  * 	<ja>@RestResource</ja>(
  * 		properties={
- * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_navlinks</jsf>, value=<js>"{options:'?method=OPTIONS',doc:'doc'}"</js>)
+ * 			<ja>@Property</ja>(name=HtmlDocSerializer.<jsf>HTMLDOC_navlinks</jsf>, value=<js>"{options:'servlet:/?method=OPTIONS',stats:'servlet:/stats',doc:'doc'}"</js>)
  * 		}
  * 	)
  *
@@ -42,7 +42,8 @@
  * 	<ja>@RestResource</ja>(
  * 		htmldoc=<ja>@HtmlDoc</ja>(
  * 			navlinks={
- * 				<js>"options: ?method=OPTIONS"</js>,
+ * 				<js>"options: servlet:/?method=OPTIONS"</js>,
+ * 				<js>"stats: servlet:/stats"</js>,
  * 				<js>"doc: doc"</js>
  * 			}
  * 		)
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
index af2b561..5d48a47 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
@@ -472,6 +472,7 @@
 	 * 			navlinks={

 	 * 				<js>"up: request:/.."</js>,

 	 * 				<js>"options: servlet:/?method=OPTIONS"</js>,

+	 * 				<js>"stats: servlet:/stats"</js>,

 	 * 				<js>"source: $C{Source/gitHub}/org/apache/juneau/examples/rest/addressbook/AddressBookResource.java"</js>,

 	 * 			},

 	 * 			aside={