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

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 5ba7c4b4a1 Finalize TODO-105 container timing workflow
5ba7c4b4a1 is described below

commit 5ba7c4b4a121cbf36cd68e134544c014477f7964
Author: James Bognar <[email protected]>
AuthorDate: Tue May 26 09:39:52 2026 -0400

    Finalize TODO-105 container timing workflow
---
 juneau-utest/pom.xml                               |  20 ++
 .../microservice/Microservice_Builder_Test.java    |   1 +
 .../microservice/Microservice_Inject_Test.java     |   1 +
 .../Microservice_Listener_Fanout_Test.java         |   1 +
 .../Microservice_OverridingBeanStore_Test.java     |   1 +
 .../Microservice_PushPopOverlay_Test.java          |   1 +
 .../jetty/JettyConfiguration_Test.java             |   1 +
 .../RestPathsRuntimeOverride_JettyMount_Test.java  |   1 +
 .../juneau/microservice/jetty/Rest_Paths_Test.java |   1 +
 ...asicVersionResource_JettyMicroservice_Test.java |   1 +
 .../BasicVersionResource_Springboot_Test.java      |   1 +
 .../docs/BasicApiDocs_JettyMicroservice_Test.java  |   1 +
 ...iDocs_Springboot_MultiOpenApiProvider_Test.java |   1 +
 .../rest/docs/BasicApiDocs_Springboot_Test.java    |   1 +
 .../BasicEchoResource_JettyMicroservice_Test.java  |   1 +
 .../ops/BasicEchoResource_Springboot_Test.java     |   1 +
 .../RestPathsRuntimeOverride_Springboot_Test.java  |   1 +
 .../rest/springboot/SpringBeanStore_Test.java      |   1 +
 ...tPropertySource_SpringbootIntegration_Test.java |   1 +
 .../SpringEnvironmentPropertySource_Test.java      |   1 +
 ...StaticFilesResource_JettyMicroservice_Test.java |   1 +
 ...StaticFilesResource_SpringbootMetaInf_Test.java |   1 +
 .../BasicStaticFilesResource_Springboot_Test.java  |   1 +
 .../testing/annotations/JettyMicroserviceTest.java |  30 ++
 .../juneau/testing/annotations/SpringbootTest.java |  30 ++
 scripts/check-container-tags.py                    |  86 ++++++
 scripts/push-timings.py                            | 109 +++++++
 scripts/push.py                                    |  49 ++-
 scripts/surefire-rollup.py                         | 166 ++++++++++
 scripts/test.py                                    | 315 ++++++++++++-------
 todo/FINISHED-105-container-tag-perf-tracking.md   |  54 ++++
 todo/FINISHED.md                                   |   2 +
 todo/TODO-105-container-tag-perf-tracking.md       | 340 +++++++++++++++++++++
 todo/TODO.md                                       |   9 +-
 34 files changed, 1113 insertions(+), 119 deletions(-)

diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index a65e85c2d6..9ed5cbcfc5 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -331,6 +331,26 @@
                                                
<java.locale.providers>JRE,COMPAT,SPI,CLDR</java.locale.providers>
                                        </systemPropertyVariables>
                                </configuration>
+                               <executions>
+                                       <execution>
+                                               <id>default-test</id>
+                                               <configuration>
+                                                       
<excludedGroups>container</excludedGroups>
+                                                       
<reportsDirectory>${project.build.directory}/surefire-reports/core</reportsDirectory>
+                                               </configuration>
+                                       </execution>
+                                       <execution>
+                                               <id>container-test</id>
+                                               <phase>test</phase>
+                                               <goals>
+                                                       <goal>test</goal>
+                                               </goals>
+                                               <configuration>
+                                                       
<groups>container</groups>
+                                                       
<reportsDirectory>${project.build.directory}/surefire-reports/container</reportsDirectory>
+                                               </configuration>
+                                       </execution>
+                               </executions>
                        </plugin>
                        <plugin>
                                <groupId>org.jacoco</groupId>
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Builder_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Builder_Test.java
index 077921b224..c16960cdd9 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Builder_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Builder_Test.java
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.*;
 /**
  * Tests for {@link Microservice.Builder} methods.
  */
[email protected]
 class Microservice_Builder_Test extends TestBase {
 
        @Test void a01_create() {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Inject_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Inject_Test.java
index 14e8569555..0cacfa0f21 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Inject_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Inject_Test.java
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.*;
  * that explicit builder calls always win, and that the bean store is closed on
  * {@link Microservice#stop()} so <c>@PreDestroy</c> hooks fire.
  */
[email protected]
 class Microservice_Inject_Test extends TestBase {
 
        
//-----------------------------------------------------------------------------------------------------------------
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Listener_Fanout_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Listener_Fanout_Test.java
index 04d8b9ce18..8be2894cd0 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Listener_Fanout_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_Listener_Fanout_Test.java
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.*;
  * for each lifecycle event ({@link Microservice#start()}, {@link 
Microservice#stop()}, and config-change
  * notifications). {@code onStart} runs in registration order; {@code onStop} 
runs in the reverse order.
  */
[email protected]
 class Microservice_Listener_Fanout_Test extends TestBase {
 
        /**
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_OverridingBeanStore_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_OverridingBeanStore_Test.java
index 7e79e9c54f..523ec940e2 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_OverridingBeanStore_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_OverridingBeanStore_Test.java
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.*;
  * resolve before locally-registered beans during the microservice's 
startup-time injection
  * (Phase 4 of TODO-35).
  */
[email protected]
 class Microservice_OverridingBeanStore_Test extends TestBase {
 
        interface ExternalApi {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_PushPopOverlay_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_PushPopOverlay_Test.java
index 0645bf2eae..350e7ccb16 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_PushPopOverlay_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/Microservice_PushPopOverlay_Test.java
@@ -41,6 +41,7 @@ import org.junit.jupiter.api.*;
  */
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 @TestMethodOrder(MethodOrderer.MethodName.class)
[email protected]
 class Microservice_PushPopOverlay_Test extends TestBase {
 
        interface ExternalApi {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/JettyConfiguration_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/JettyConfiguration_Test.java
index d15b8c0cbd..75d8bb5b6c 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/JettyConfiguration_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/JettyConfiguration_Test.java
@@ -39,6 +39,7 @@ import jakarta.servlet.http.*;
  * that auto-discovered <c>@Rest</c> servlets are mounted at 
<c>@Rest(path=...)</c>, and that duplicate
  * mount paths fail fast.
  */
[email protected]
 class JettyConfiguration_Test extends TestBase {
 
        private static Microservice create(Class<?>... configurations) throws 
Exception {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/RestPathsRuntimeOverride_JettyMount_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/RestPathsRuntimeOverride_JettyMount_Test.java
index 150b5e83b4..ef2e890f68 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/RestPathsRuntimeOverride_JettyMount_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/RestPathsRuntimeOverride_JettyMount_Test.java
@@ -53,6 +53,7 @@ import jakarta.servlet.*;
  *
  * @since 9.5.0
  */
[email protected]
 class RestPathsRuntimeOverride_JettyMount_Test extends TestBase {
 
        private static Microservice create(Class<?>... configurations) throws 
Exception {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/Rest_Paths_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/Rest_Paths_Test.java
index bddb41e286..04f8a7a8c4 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/Rest_Paths_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/jetty/Rest_Paths_Test.java
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.*;
 
 import jakarta.servlet.*;
 
[email protected]
 class Rest_Paths_Test extends TestBase {
 
        private static Microservice create(Class<?>... configurations) throws 
Exception {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
index 1cb050315c..7721db0b06 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_JettyMicroservice_Test.java
@@ -57,6 +57,7 @@ import jakarta.servlet.*;
  *
  * @since 9.5.0
  */
[email protected]
 class BasicVersionResource_JettyMicroservice_Test extends TestBase {
 
        @Rest(mixins=BasicVersionResource.class)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
index 5cbf1b6d7e..6c8ba80b44 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/convention/BasicVersionResource_Springboot_Test.java
@@ -58,6 +58,7 @@ import org.springframework.test.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SpringBootTest(classes = BasicVersionResource_Springboot_Test.TestApp.class,
        webEnvironment = WebEnvironment.RANDOM_PORT)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_JettyMicroservice_Test.java
index d1b080f977..075156bb78 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_JettyMicroservice_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_JettyMicroservice_Test.java
@@ -64,6 +64,7 @@ import jakarta.servlet.*;
  *
  * @since 9.5.0
  */
[email protected]
 class BasicApiDocs_JettyMicroservice_Test extends TestBase {
 
        /** Test host: vanilla {@link BasicRestServlet} subclass; inherits the 
four-mixin pack. */
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_MultiOpenApiProvider_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_MultiOpenApiProvider_Test.java
index 662d80672f..28f7deb62a 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_MultiOpenApiProvider_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_MultiOpenApiProvider_Test.java
@@ -68,6 +68,7 @@ import org.springframework.context.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 class BasicApiDocs_Springboot_MultiOpenApiProvider_Test {
 
        /** Spring Boot app config with two {@link OpenApiProvider} beans, one 
{@code @Primary}. */
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_Test.java
index 748b963905..0e13ab00e8 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/docs/BasicApiDocs_Springboot_Test.java
@@ -71,6 +71,7 @@ import org.springframework.test.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SpringBootTest(classes = BasicApiDocs_Springboot_Test.TestApp.class,
        webEnvironment = WebEnvironment.RANDOM_PORT)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java
index ec5212d2f9..f106120c43 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java
@@ -54,6 +54,7 @@ import jakarta.servlet.*;
  *
  * @since 9.5.0
  */
[email protected]
 class BasicEchoResource_JettyMicroservice_Test extends TestBase {
 
        @Rest(mixins=BasicEchoResource.class, debug=@Debug("always"))
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java
index 919cef2a4c..65ef6a7138 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java
@@ -59,6 +59,7 @@ import org.springframework.test.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SpringBootTest(classes = BasicEchoResource_Springboot_Test.TestApp.class,
        webEnvironment = WebEnvironment.RANDOM_PORT)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/RestPathsRuntimeOverride_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/RestPathsRuntimeOverride_Springboot_Test.java
index e6e51ba76a..d394fb86a4 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/RestPathsRuntimeOverride_Springboot_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/RestPathsRuntimeOverride_Springboot_Test.java
@@ -58,6 +58,7 @@ import org.springframework.context.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SuppressWarnings({
        "unchecked"  // ApplicationContext.getBeanProvider(Class) is 
unchecked-bound in the Spring API.
 })
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringBeanStore_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringBeanStore_Test.java
index 5dd8212afa..9acaed17e4 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringBeanStore_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringBeanStore_Test.java
@@ -30,6 +30,7 @@ import org.springframework.context.*;
 /**
  * Tests for {@link SpringBeanStore}.
  */
[email protected]
 @SuppressWarnings({
        "unchecked" // getBean(String, Class) invocation in test
 })
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_SpringbootIntegration_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_SpringbootIntegration_Test.java
index 65592c5e65..49d622341d 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_SpringbootIntegration_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_SpringbootIntegration_Test.java
@@ -54,6 +54,7 @@ import org.springframework.test.context.*;
  * minimal {@code @SpringBootConfiguration} class, no web environment, dirty 
context after the
  * class so subsequent tests don't see the bridge.
  */
[email protected]
 @SpringBootTest(classes = 
SpringEnvironmentPropertySource_SpringbootIntegration_Test.TestApp.class,
        webEnvironment = SpringBootTest.WebEnvironment.NONE)
 @TestPropertySource(properties = {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_Test.java
index d9b12eb5a0..ce00bfbc35 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/springboot/SpringEnvironmentPropertySource_Test.java
@@ -49,6 +49,7 @@ import org.springframework.mock.env.*;
  * context which now installs the bridge automatically via {@link 
SpringBeanStore}'s constructor.
  * This test keeps the focus on the bridge itself.
  */
[email protected]
 @SuppressWarnings({
        "java:S2094" // Test fixture / data class, no methods required.
 })
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
index 457112bad0..21149081a3 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java
@@ -61,6 +61,7 @@ import jakarta.servlet.*;
  *
  * @since 9.5.0
  */
[email protected]
 class BasicStaticFilesResource_JettyMicroservice_Test extends TestBase {
 
        /** Test host: vanilla {@link BasicRestServlet} subclass with the 
static-files mixin attached. */
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
index cc2e672ef2..566de728e5 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java
@@ -61,6 +61,7 @@ import org.springframework.test.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SpringBootTest(classes = 
BasicStaticFilesResource_SpringbootMetaInf_Test.TestApp.class,
        webEnvironment = WebEnvironment.RANDOM_PORT)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
index 4774378ae1..edf0055907 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java
@@ -62,6 +62,7 @@ import org.springframework.test.annotation.*;
  *
  * @since 9.5.0
  */
[email protected]
 @SpringBootTest(classes = 
BasicStaticFilesResource_Springboot_Test.TestApp.class,
        webEnvironment = WebEnvironment.RANDOM_PORT)
 @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/JettyMicroserviceTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/JettyMicroserviceTest.java
new file mode 100644
index 0000000000..a9dd6b690e
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/JettyMicroserviceTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.testing.annotations;
+
+import java.lang.annotation.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Marker for container-backed Jetty microservice tests.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Tag("container")
+@Tag("jetty")
+public @interface JettyMicroserviceTest {}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/SpringbootTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/SpringbootTest.java
new file mode 100644
index 0000000000..0ff3f8bc07
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/testing/annotations/SpringbootTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.testing.annotations;
+
+import java.lang.annotation.*;
+
+import org.junit.jupiter.api.*;
+
+/**
+ * Marker for container-backed Spring Boot tests.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Tag("container")
+@Tag("springboot")
+public @interface SpringbootTest {}
diff --git a/scripts/check-container-tags.py b/scripts/check-container-tags.py
new file mode 100644
index 0000000000..ff3428d373
--- /dev/null
+++ b/scripts/check-container-tags.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# 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.
+#
+"""
+Guardrail for container-backed tests.
+
+Fails when a container-booting test class is missing @SpringbootTest or
+@JettyMicroserviceTest.
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+SPRING_HINTS = (
+    r"@SpringBootTest\b",
+    r"@AutoConfigureMockMvc\b",
+    r"@ExtendWith\s*\(\s*SpringExtension\.class",
+    r"SpringApplication\.run\s*\(",
+)
+
+JETTY_HINTS = (
+    r"JettyMicroservice\.(?:create|builder)\s*\(",
+    r"Microservice\.(?:create|builder)\s*\(",
+    r"new\s+org\.eclipse\.jetty\..*?Server\s*\(",
+)
+
+TAG_MARKERS = (
+    "@SpringbootTest",
+    "@JettyMicroserviceTest",
+    "@org.apache.juneau.testing.annotations.SpringbootTest",
+    "@org.apache.juneau.testing.annotations.JettyMicroserviceTest",
+)
+
+
+def should_require_tag(content: str) -> bool:
+    return any(re.search(p, content) for p in (*SPRING_HINTS, *JETTY_HINTS))
+
+
+def has_container_tag(content: str) -> bool:
+    return any(marker in content for marker in TAG_MARKERS)
+
+
+def scan(repo_root: Path) -> list[Path]:
+    test_root = repo_root / "juneau-utest" / "src" / "test" / "java"
+    offenders: list[Path] = []
+    for path in sorted(test_root.rglob("*Test.java")):
+        content = path.read_text(encoding="utf-8")
+        if should_require_tag(content) and not has_container_tag(content):
+            offenders.append(path)
+    return offenders
+
+
+def main() -> int:
+    repo_root = Path(__file__).resolve().parent.parent
+    offenders = scan(repo_root)
+    if not offenders:
+        print("โœ… Container tag guard passed.")
+        return 0
+
+    print("โŒ Container tag guard failed. The following tests appear to boot 
containers but are missing a tag:")
+    for path in offenders:
+        rel = path.relative_to(repo_root)
+        print(f"  - {rel}")
+    print("\nAdd @SpringbootTest or @JettyMicroserviceTest to each class.")
+    return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/push-timings.py b/scripts/push-timings.py
new file mode 100644
index 0000000000..eecd9525ab
--- /dev/null
+++ b/scripts/push-timings.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+#
+# 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.
+#
+"""
+Report timing deltas from push timing JSONL history.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import statistics
+from collections import defaultdict
+from pathlib import Path
+
+
+def load_records(path: Path) -> list[dict]:
+    if not path.exists():
+        return []
+    rows = []
+    for line in path.read_text(encoding="utf-8").splitlines():
+        if not line.strip():
+            continue
+        try:
+            rows.append(json.loads(line))
+        except json.JSONDecodeError:
+            continue
+    return rows
+
+
+def fmt_seconds(value: float) -> str:
+    return f"{value:.1f}s"
+
+
+def fmt_delta(value: float) -> str:
+    sign = "+" if value >= 0 else ""
+    return f"{sign}{value * 100:.1f}%"
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Print push timing regression 
summary.")
+    parser.add_argument("--log", required=True, help="Path to timing JSONL")
+    parser.add_argument("--window", type=int, default=20, help="Rolling median 
window per execution")
+    args = parser.parse_args()
+
+    log_path = Path(args.log).expanduser()
+    records = load_records(log_path)
+    if not records:
+        print("๐Ÿ“Š Push-timing report: no timing history yet.")
+        return 0
+
+    latest_run = records[-1].get("run_id")
+    if not latest_run:
+        print("๐Ÿ“Š Push-timing report: missing run metadata in latest record.")
+        return 0
+
+    latest_records = [r for r in records if r.get("run_id") == latest_run and 
r.get("passed", True)]
+    if not latest_records:
+        print("๐Ÿ“Š Push-timing report: latest run did not produce successful 
timing rows.")
+        return 0
+
+    threshold = float(os.environ.get("JUNEAU_PUSH_TIMING_THRESHOLD", "0.05"))
+    print(f"๐Ÿ“Š Push-timing report (last {args.window} runs on this branch):")
+
+    prior_by_key: dict[tuple[str, str], list[float]] = defaultdict(list)
+    for row in records:
+        if row.get("run_id") == latest_run:
+            continue
+        key = (row.get("module", "?"), row.get("execution", "?"))
+        if row.get("passed", True):
+            prior_by_key[key].append(float(row.get("wallclock_s", 0.0)))
+
+    for row in sorted(latest_records, key=lambda r: (r.get("module", ""), 
r.get("execution", ""))):
+        module = row.get("module", "?")
+        execution = row.get("execution", "?")
+        wallclock = float(row.get("wallclock_s", 0.0))
+        key = (module, execution)
+        prior = prior_by_key.get(key, [])
+        if prior:
+            baseline = statistics.median(prior[-args.window:])
+            delta = (wallclock - baseline) / baseline if baseline > 0 else 0.0
+            print(f"   {module}/{execution:<18}: {fmt_seconds(wallclock):>6}  
(median {fmt_seconds(baseline)}, delta {fmt_delta(delta)})")
+            if abs(delta) > threshold:
+                print(
+                    f"   โš  {module}/{execution} wall-clock 
{fmt_seconds(wallclock)} is {fmt_delta(delta)} "
+                    f"vs rolling median {fmt_seconds(baseline)} (threshold 
{threshold * 100:.0f}%)"
+                )
+        else:
+            print(f"   {module}/{execution:<18}: {fmt_seconds(wallclock):>6}  
(first run, no baseline)")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/scripts/push.py b/scripts/push.py
index ccb5695889..8d77ca8f0a 100755
--- a/scripts/push.py
+++ b/scripts/push.py
@@ -294,6 +294,27 @@ def check_upstream_changes(repo_dir):
         return (False, f"Error checking upstream changes: {e}")
 
 
+def current_branch(repo_dir):
+    """Get current git branch name."""
+    try:
+        result = subprocess.run(
+            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
+            cwd=repo_dir,
+            capture_output=True,
+            text=True,
+            check=True
+        )
+        return result.stdout.strip()
+    except Exception:
+        return "unknown"
+
+
+def timing_log_path(repo_dir):
+    """Out-of-repo branch-specific timing history location."""
+    branch = current_branch(repo_dir).replace("/", "__")
+    return Path.home() / ".cache" / "juneau-push-timings" / f"{branch}.jsonl"
+
+
 def play_sound(success=True):  # NOSONAR python:S3776 -- Cognitive complexity 
is acceptable for this utility function
     """
     Play a system sound to indicate success or failure.
@@ -412,7 +433,11 @@ Examples:
         print(f"  {step_num}. Prompt for PGP passphrase (dummy call)")
         step_num += 1
         if not args.skip_tests:
-            print(f"  {step_num}. Run tests: python3 scripts/test.py --full")
+            print(f"  {step_num}. Verify container test tags: python3 
scripts/check-container-tags.py")
+            step_num += 1
+            print(f"  {step_num}. Run tests with timing capture: python3 
scripts/test.py --full --timing-log 
~/.cache/juneau-push-timings/<branch>.jsonl")
+            step_num += 1
+            print(f"  {step_num}. Print timing deltas: python3 
scripts/push-timings.py --log ~/.cache/juneau-push-timings/<branch>.jsonl")
             step_num += 1
         print(f"  {step_num}. Build and install: mvn clean package install 
-DskipTests")
         step_num += 1
@@ -437,12 +462,25 @@ Examples:
     
     # Step 1: Run tests (optional)
     if not args.skip_tests:
+        check_container_tags = script_dir / "check-container-tags.py"
+        if check_container_tags.exists():
+            if not run_command(
+                [sys.executable, str(check_container_tags)],
+                f"๐Ÿ”Ž Step {step_num}: Checking container test tags...",
+                juneau_root
+            ):
+                print("\nโŒ Build process aborted due to missing container test 
tags.")
+                play_sound(success=False)
+                return 1
+            step_num += 1
+
         test_script = script_dir / 'test.py'
+        timing_file = timing_log_path(juneau_root)
         if test_script.exists():
             print(f"\n๐Ÿงช Step {step_num}: Running tests via test.py...")
             try:
                 result = subprocess.run(
-                    [sys.executable, str(test_script), "--full"],
+                    [sys.executable, str(test_script), "--full", 
"--timing-log", str(timing_file)],
                     cwd=juneau_root,
                     check=False
                 )
@@ -465,6 +503,13 @@ Examples:
                 print("\nโŒ Build process aborted due to test failures.")
                 play_sound(success=False)
                 return 1
+        timing_report = script_dir / "push-timings.py"
+        if timing_report.exists():
+            run_command(
+                [sys.executable, str(timing_report), "--log", 
str(timing_file)],
+                f"๐Ÿ“Š Step {step_num}: Timing regression report...",
+                juneau_root
+            )
         step_num += 1
     else:
         print(f"\nโญ๏ธ  Step {step_num}: Skipping tests (--skip-tests flag)")
diff --git a/scripts/surefire-rollup.py b/scripts/surefire-rollup.py
new file mode 100644
index 0000000000..34f21a7af9
--- /dev/null
+++ b/scripts/surefire-rollup.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+#
+# 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.
+#
+"""
+Roll up Surefire XML results into timing buckets.
+"""
+
+from __future__ import annotations
+
+import argparse
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class BucketStats:
+    tests: int = 0
+    seconds: float = 0.0
+
+
+def parse_xml_dir(directory: Path) -> BucketStats:
+    stats = BucketStats()
+    if not directory.exists():
+        return stats
+    for xml_file in sorted(directory.glob("TEST-*.xml")):
+        root = ET.parse(xml_file).getroot()
+        stats.tests += int(root.attrib.get("tests", 0))
+        stats.seconds += float(root.attrib.get("time", 0.0))
+    return stats
+
+
+def source_for_class(module_dir: Path, class_name: str) -> Path | None:
+    if not class_name:
+        return None
+    parts = class_name.split(".")
+    if len(parts) < 2:
+        return None
+    return module_dir / "src" / "test" / "java" / 
Path("/".join(parts)).with_suffix(".java")
+
+
+def source_content(module_dir: Path, class_name: str) -> str:
+    source = source_for_class(module_dir, class_name)
+    if source and source.exists():
+        return source.read_text(encoding="utf-8")
+    return ""
+
+
+def container_bucket(module_dir: Path, class_name: str) -> str:
+    content = source_content(module_dir, class_name)
+    if "SpringbootTest" in content:
+        return "container.springboot"
+    if "JettyMicroserviceTest" in content:
+        return "container.jetty"
+    lower = class_name.lower()
+    if "springboot" in lower:
+        return "container.springboot"
+    if "jetty" in lower or "microservice" in lower:
+        return "container.jetty"
+    return ""
+
+
+def parse_container_subbuckets(module_dir: Path, container_dir: Path) -> 
dict[str, BucketStats]:
+    buckets: dict[str, BucketStats] = {
+        "container.springboot": BucketStats(),
+        "container.jetty": BucketStats(),
+    }
+    if not container_dir.exists():
+        return buckets
+    for xml_file in sorted(container_dir.glob("TEST-*.xml")):
+        root = ET.parse(xml_file).getroot()
+        class_name = root.attrib.get("name", "")
+        bucket = container_bucket(module_dir, class_name) or "container.jetty"
+        buckets[bucket].tests += int(root.attrib.get("tests", 0))
+        buckets[bucket].seconds += float(root.attrib.get("time", 0.0))
+    return buckets
+
+
+def parse_flat_reports(module_dir: Path, reports_root: Path):
+    core = BucketStats()
+    sub = {
+        "container.springboot": BucketStats(),
+        "container.jetty": BucketStats(),
+    }
+    for xml_file in sorted(reports_root.glob("TEST-*.xml")):
+        root = ET.parse(xml_file).getroot()
+        tests = int(root.attrib.get("tests", 0))
+        seconds = float(root.attrib.get("time", 0.0))
+        class_name = root.attrib.get("name", "")
+        content = source_content(module_dir, class_name)
+        bucket = container_bucket(module_dir, class_name)
+        if "SpringbootTest" in content:
+            sub["container.springboot"].tests += tests
+            sub["container.springboot"].seconds += seconds
+        elif "JettyMicroserviceTest" in content:
+            sub["container.jetty"].tests += tests
+            sub["container.jetty"].seconds += seconds
+        elif bucket.startswith("container."):
+            sub[bucket].tests += tests
+            sub[bucket].seconds += seconds
+        else:
+            core.tests += tests
+            core.seconds += seconds
+    container = BucketStats(
+        tests=sub["container.springboot"].tests + sub["container.jetty"].tests,
+        seconds=sub["container.springboot"].seconds + 
sub["container.jetty"].seconds,
+    )
+    return core, container, sub
+
+
+def bar(seconds: float, max_seconds: float, width: int = 20) -> str:
+    if max_seconds <= 0:
+        return ""
+    filled = int((seconds / max_seconds) * width)
+    filled = max(1 if seconds > 0 else 0, min(width, filled))
+    return "โ–“" * filled
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Aggregate Surefire reports 
by execution bucket.")
+    parser.add_argument("module", default="juneau-utest", nargs="?", 
help="Module path (default: juneau-utest)")
+    args = parser.parse_args()
+
+    repo_root = Path(__file__).resolve().parent.parent
+    module_dir = repo_root / args.module
+    reports_root = module_dir / "target" / "surefire-reports"
+
+    core_dir = reports_root / "core"
+    container_dir = reports_root / "container"
+    if core_dir.exists() or container_dir.exists():
+        core = parse_xml_dir(core_dir)
+        container = parse_xml_dir(container_dir)
+        sub = parse_container_subbuckets(module_dir, container_dir)
+    else:
+        core, container, sub = parse_flat_reports(module_dir, reports_root)
+
+    rows = [
+        ("core", core.tests, core.seconds),
+        ("container.springboot", sub["container.springboot"].tests, 
sub["container.springboot"].seconds),
+        ("container.jetty", sub["container.jetty"].tests, 
sub["container.jetty"].seconds),
+        ("container", container.tests, container.seconds),
+    ]
+    max_seconds = max((seconds for _, _, seconds in rows), default=0.0)
+
+    print(f"{args.module}/")
+    for name, tests, seconds in rows:
+        print(f"  {name:<20}: {bar(seconds, max_seconds)} ({tests} tests, 
{seconds:.3f}s)")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/scripts/test.py b/scripts/test.py
index 2a38dc81b1..2aaf9b5294 100755
--- a/scripts/test.py
+++ b/scripts/test.py
@@ -18,128 +18,225 @@ Usage:
     ./scripts/test.py [options]
 
 Options:
-    --build-only, -b    Only build (skip tests)
-    --test-only, -t     Only run tests (no build)
-    --full, -f          Clean build + run tests (default)
-    --verbose, -v       Show full Maven output
-    --help, -h          Show this help message
+    --build-only, -b         Only build (skip tests)
+    --test-only, -t          Only run tests (no build)
+    --full, -f               Clean build + run tests (default)
+    --verbose, -v            Show full Maven output
+    --no-container           Exclude @Tag("container") tests
+    --timing-log <path>      Append per-bucket timing JSONL records
+    --profile <module>       Run one-shot JFR profile for module tests
+    --help, -h               Show this help message
 """
 
+import argparse
+import json
+import re
 import subprocess
 import sys
-import os
+import xml.etree.ElementTree as ET
+from datetime import datetime, timezone
 from pathlib import Path
 
+
 def run_command(cmd, verbose=False):
-    """Run a command and return exit code and full output."""
-    script_dir = Path(__file__).parent
-    project_root = script_dir.parent
-    
-    print(f"Running: {cmd}")
-    print("-" * 80)
-    
-    if verbose:
-        # Show full output
-        result = subprocess.run(cmd, shell=True, cwd=str(project_root), 
capture_output=True, text=True)
-        print(result.stdout)
-        print(result.stderr, file=sys.stderr)
-        return result.returncode, result.stdout + result.stderr
-    else:
-        # Run command and capture all output, then show last lines
-        result = subprocess.run(
-            cmd,
-            shell=True,
-            cwd=str(project_root),
-            capture_output=True,
-            text=True
-        )
-        # Combine stdout and stderr
-        output = result.stdout + result.stderr
-        # Show last 50 lines
-        lines = output.splitlines()
-        if len(lines) > 50:
-            print('\n'.join(lines[-50:]))
-        else:
-            print(output)
-        return result.returncode, output
+       """Run a command and return exit code and full output."""
+       script_dir = Path(__file__).parent
+       project_root = script_dir.parent
+       print(f"Running: {cmd}")
+       print("-" * 80)
+       result = subprocess.run(cmd, shell=True, cwd=str(project_root), 
capture_output=True, text=True)
+       output = result.stdout + result.stderr
+       if verbose:
+               print(result.stdout)
+               print(result.stderr, file=sys.stderr)
+       else:
+               lines = output.splitlines()
+               print("\n".join(lines[-50:]) if len(lines) > 50 else output)
+       return result.returncode, output
 
-def build(verbose=False):
-    """Run Maven clean install without tests."""
-    return run_command("mvn clean install -DskipTests", verbose)
 
-def test(verbose=False):
-    """Run Maven tests."""
-    return run_command("mvn test -Drat.skip=true", verbose)
+def git_value(args):
+       try:
+               result = subprocess.run(["git", *args], 
cwd=str(Path(__file__).parent.parent), capture_output=True, text=True, 
check=True)
+               return result.stdout.strip()
+       except Exception:
+               return "unknown"
+
 
 def parse_test_results(output):
-    """Parse Maven test output and extract failure/error counts."""
-    import re
-    # Look for the last occurrence of: [ERROR] Tests run: 25916, Failures: 0, 
Errors: 12, Skipped: 1
-    # This will be the total across all modules
-    matches = list(re.finditer(r'\[ERROR\]\s+Tests 
run:\s+(\d+),\s+Failures:\s+(\d+),\s+Errors:\s+(\d+)', output))
-    if matches:
-        # Use the last match (final total)
-        match = matches[-1]
-        total = int(match.group(1))
-        failures = int(match.group(2))
-        errors = int(match.group(3))
-        return total, failures, errors
-    return None, None, None
+       matches = list(re.finditer(r"\[ERROR\]\s+Tests 
run:\s+(\d+),\s+Failures:\s+(\d+),\s+Errors:\s+(\d+)", output))
+       if matches:
+               match = matches[-1]
+               total = int(match.group(1))
+               failures = int(match.group(2))
+               errors = int(match.group(3))
+               return total, failures, errors
+       return None, None, None
+
+
+def parse_surefire_dir(directory: Path):
+       tests, seconds = 0, 0.0
+       if not directory.exists():
+               return tests, seconds
+       for xml_file in sorted(directory.glob("TEST-*.xml")):
+               root = ET.parse(xml_file).getroot()
+               tests += int(root.attrib.get("tests", 0))
+               seconds += float(root.attrib.get("time", 0.0))
+       return tests, seconds
+
+
+def container_bucket(module_dir: Path, class_name: str):
+       source = module_dir / "src" / "test" / "java" / 
Path("/".join(class_name.split("."))).with_suffix(".java")
+       if source.exists():
+               content = source.read_text(encoding="utf-8")
+               if "SpringbootTest" in content:
+                       return "container.springboot"
+               if "JettyMicroserviceTest" in content:
+                       return "container.jetty"
+       return "container.springboot" if "springboot" in class_name.lower() 
else "container.jetty"
+
+
+def parse_container_subbuckets(module_dir: Path, directory: Path):
+       buckets = {
+               "container.springboot": {"tests": 0, "seconds": 0.0},
+               "container.jetty": {"tests": 0, "seconds": 0.0},
+       }
+       if not directory.exists():
+               return buckets
+       for xml_file in sorted(directory.glob("TEST-*.xml")):
+               root = ET.parse(xml_file).getroot()
+               bucket = container_bucket(module_dir, root.attrib.get("name", 
""))
+               buckets[bucket]["tests"] += int(root.attrib.get("tests", 0))
+               buckets[bucket]["seconds"] += float(root.attrib.get("time", 
0.0))
+       return buckets
+
+
+def write_timing_log(path: Path, passed: bool):
+       path = path.expanduser()
+       path.parent.mkdir(parents=True, exist_ok=True)
+       repo_root = Path(__file__).parent.parent
+       module = "juneau-utest"
+       module_dir = repo_root / module
+       reports_root = module_dir / "target" / "surefire-reports"
+
+       core_tests, core_seconds = parse_surefire_dir(reports_root / "core")
+       container_tests, container_seconds = parse_surefire_dir(reports_root / 
"container")
+       container_sub = parse_container_subbuckets(module_dir, reports_root / 
"container")
+
+       run_id = 
f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{git_value(['rev-parse',
 '--short', 'HEAD'])}"
+       base = {
+               "ts": datetime.now(timezone.utc).isoformat(),
+               "run_id": run_id,
+               "branch": git_value(["rev-parse", "--abbrev-ref", "HEAD"]),
+               "commit": git_value(["rev-parse", "--short", "HEAD"]),
+               "module": module,
+               "passed": passed,
+       }
+       rows = [
+               {**base, "execution": "core", "wallclock_s": core_seconds, 
"test_count": core_tests},
+               {**base, "execution": "container", "wallclock_s": 
container_seconds, "test_count": container_tests},
+               {
+                       **base,
+                       "execution": "container.springboot",
+                       "wallclock_s": 
container_sub["container.springboot"]["seconds"],
+                       "test_count": 
container_sub["container.springboot"]["tests"],
+               },
+               {
+                       **base,
+                       "execution": "container.jetty",
+                       "wallclock_s": 
container_sub["container.jetty"]["seconds"],
+                       "test_count": container_sub["container.jetty"]["tests"],
+               },
+       ]
+       with path.open("a", encoding="utf-8") as f:
+               for row in rows:
+                       f.write(json.dumps(row) + "\n")
+       print(f"๐Ÿ•’ Timing metrics appended to {path}")
+
+
+def build(verbose=False):
+       return run_command("mvn clean install -DskipTests", verbose)
+
+
+def test(verbose=False, no_container=False):
+       cmd = "mvn test -Drat.skip=true"
+       if no_container:
+               cmd += " -DexcludedGroups=container"
+       return run_command(cmd, verbose)
+
+
+def profile(module, verbose=False):
+       ts = datetime.now().strftime("%Y%m%d-%H%M%S")
+       profile_dir = Path("target/profile-results")
+       profile_dir.mkdir(parents=True, exist_ok=True)
+       safe_module = module.replace("/", "-")
+       output_file = profile_dir / f"{safe_module}-{ts}.jfr"
+       argline = 
f"-XX:StartFlightRecording=filename={output_file},settings=profile,dumponexit=true"
+       cmd = f"mvn test -pl {module} -Drat.skip=true -DargLine='{argline}'"
+       code, out = run_command(cmd, verbose)
+       if code == 0:
+               print(f"\nโœ… JFR profile captured at {output_file}")
+       return code, out
+
 
 def main():  # NOSONAR python:S3776 -- Cognitive complexity is acceptable for 
this main function
-    args = sys.argv[1:]
-    
-    # Parse arguments
-    build_only = False
-    test_only = False
-    full = True
-    verbose = False
-    
-    for arg in args:
-        if arg in ['--help', '-h']:
-            print(__doc__)
-            return 0
-        elif arg in ['--build-only', '-b']:
-            build_only = True
-            full = False
-        elif arg in ['--test-only', '-t']:
-            test_only = True
-            full = False
-        elif arg in ['--full', '-f']:
-            full = True
-        elif arg in ['--verbose', '-v']:
-            verbose = True
-        else:
-            print(f"Unknown option: {arg}")
-            print(__doc__)
-            return 1
-    
-    # Execute commands
-    exit_code = 0
-    
-    if build_only or full:
-        exit_code, output = build(verbose)
-        if exit_code != 0:
-            print("\nโŒ Build failed!")
-            return exit_code
-        print("\nโœ… Build succeeded!")
-    
-    if test_only or full:
-        if full:
-            print("\n" + "=" * 80)
-        exit_code, output = test(verbose)
-        if exit_code != 0:
-            # Try to parse test results
-            _, failures, errors = parse_test_results(output)
-            if failures is not None and errors is not None:
-                failed_count = failures + errors
-                print(f"\nโŒ Tests failed! ({failed_count} failed: {failures} 
failures, {errors} errors)")
-            else:
-                print("\nโŒ Tests failed!")
-            return exit_code
-        print("\nโœ… Tests passed!")
-    
-    return exit_code
+       parser = argparse.ArgumentParser(add_help=False)
+       parser.add_argument("--build-only", "-b", action="store_true")
+       parser.add_argument("--test-only", "-t", action="store_true")
+       parser.add_argument("--full", "-f", action="store_true")
+       parser.add_argument("--verbose", "-v", action="store_true")
+       parser.add_argument("--no-container", action="store_true")
+       parser.add_argument("--timing-log")
+       parser.add_argument("--profile")
+       parser.add_argument("--help", "-h", action="store_true")
+       args, unknown = parser.parse_known_args()
+       if args.help:
+               print(__doc__)
+               return 0
+       if unknown:
+               print(f"Unknown option(s): {' '.join(unknown)}")
+               print(__doc__)
+               return 1
+
+       build_only = args.build_only
+       test_only = args.test_only
+       full = args.full or not (build_only or test_only or args.profile)
+       verbose = args.verbose
+
+       if args.profile:
+               exit_code, _ = profile(args.profile, verbose)
+               return exit_code
+
+       if build_only and test_only:
+               print("Cannot combine --build-only and --test-only")
+               return 1
+
+       exit_code = 0
+       last_test_output = ""
+       if build_only or full:
+               exit_code, _ = build(verbose)
+               if exit_code != 0:
+                       print("\nโŒ Build failed!")
+                       return exit_code
+               print("\nโœ… Build succeeded!")
+
+       if test_only or full:
+               if full:
+                       print("\n" + "=" * 80)
+               exit_code, last_test_output = test(verbose, 
no_container=args.no_container)
+               if exit_code != 0:
+                       _, failures, errors = 
parse_test_results(last_test_output)
+                       if failures is not None and errors is not None:
+                               print(f"\nโŒ Tests failed! ({failures + errors} 
failed: {failures} failures, {errors} errors)")
+                       else:
+                               print("\nโŒ Tests failed!")
+               else:
+                       print("\nโœ… Tests passed!")
+               if args.timing_log:
+                       write_timing_log(Path(args.timing_log), 
passed=(exit_code == 0))
+               if exit_code != 0:
+                       return exit_code
+       return exit_code
 
 if __name__ == '__main__':
     sys.exit(main())
diff --git a/todo/FINISHED-105-container-tag-perf-tracking.md 
b/todo/FINISHED-105-container-tag-perf-tracking.md
new file mode 100644
index 0000000000..4277e4043c
--- /dev/null
+++ b/todo/FINISHED-105-container-tag-perf-tracking.md
@@ -0,0 +1,54 @@
+# FINISHED-105 โ€” Container-test tag isolation + perf-regression tracking + 
profiling pass
+
+Source plan: 
[`TODO-105-container-tag-perf-tracking.md`](TODO-105-container-tag-perf-tracking.md).
+
+## Summary
+
+TODO-105 shipped the regression-detection and test-bucketing infrastructure 
requested after the
+FINISHED-79 slowdown incident. The landing keeps `juneau-utest` as a single 
Maven module while
+making container-heavy tests explicit and measurable.
+
+## What landed
+
+1. **Container-tag meta-annotations and adoption.**
+   - Added `@SpringbootTest` and `@JettyMicroserviceTest` in
+     `juneau-utest/src/test/java/org/apache/juneau/testing/annotations/`.
+   - Tagged the inventoried container-booting test classes (Spring Boot + 
Jetty/Microservice)
+     so they can be filtered as `container`, `springboot`, and `jetty`.
+
+2. **Dual Surefire execution buckets in `juneau-utest/pom.xml`.**
+   - Core execution excludes `container` and writes to
+     `target/surefire-reports/core/`.
+   - Container execution includes `container` and writes to
+     `target/surefire-reports/container/`.
+   - JaCoCo remains a single `.exec` workflow (no coverage-script changes 
required).
+
+3. **Container-tag discipline guard in push flow.**
+   - Added `scripts/check-container-tags.py`.
+   - Wired the guard into `scripts/push.py` before tests, so untagged 
container-booting tests
+     fail fast during `/push`.
+
+4. **Per-execution push timing capture and reporting.**
+   - Added `scripts/test.py --timing-log ...` plumbing to write JSONL records.
+   - Added `scripts/push-timings.py` to print rolling-median timing deltas per 
execution.
+   - `scripts/push.py` now captures and prints timing report output after 
tests.
+
+5. **Surefire XML rollup utility.**
+   - Added `scripts/surefire-rollup.py` for bucketed wall-clock and test-count 
summaries over
+     `core`, `container`, `container.springboot`, and `container.jetty`.
+
+6. **One-shot profiling support.**
+   - Added `scripts/test.py --profile <module>` with JFR output under
+     `target/profile-results/`.
+   - Profiling findings are intentionally tracked as follow-on optimization 
work in TODO-999,
+     not implemented in this landing.
+
+## Scope boundaries respected
+
+- `juneau-utest` was not split into new Maven modules.
+- No blocking policy was added for timing regressions; reporting remains 
warning-only.
+- Existing coverage scripts continue to use `juneau-utest/target/jacoco.exec`.
+
+## Follow-ups
+
+- Optimization candidates discovered via profiling remain queued under 
TODO-999.
diff --git a/todo/FINISHED.md b/todo/FINISHED.md
index 7562f78028..780f1d522e 100644
--- a/todo/FINISHED.md
+++ b/todo/FINISHED.md
@@ -12,6 +12,8 @@ In completion order, oldest-to-newest within the foundations 
+ mixin family + re
 
 ### Phase D โ€” SVL overhaul (newest)
 
+- **TODO-105** โ€” Container-test perf-signal hardening landed: `juneau-utest` 
now has explicit container-tag meta-annotations (`@SpringbootTest`, 
`@JettyMicroserviceTest`) on the 22 container-booting classes, Surefire 
executes core and container buckets separately, and 
`scripts/check-container-tags.py` is wired as a pre-push guard so new container 
tests cannot silently drift into the core timing bucket. `/push` now records 
per-execution timing history in `~/.cache/juneau-push-timings/<bra [...]
+
 - **TODO-102 + TODO-103** (joint landing) โ€” `#{...}` scripting syntax + 
`VarTemplate` compiled-template API. Combined infrastructure (single 
recursive-descent tokenizer/compiler + segment-array machinery) underpins both 
features so the tokenizer was built once and reused. **Hard break:** 11 
single-purpose transformation `Var` classes deleted (`IfVar`, `SwitchVar`, 
`CoalesceVar`, `NotEmptyVar`, `PatternMatchVar`, `PatternReplaceVar`, 
`PatternExtractVar`, `UpperCaseVar`, `LowerCaseVar`, `L [...]
 
 ### Phase C2 ancillary โ€” bundled-mixin SVL retrofit
diff --git a/todo/TODO-105-container-tag-perf-tracking.md 
b/todo/TODO-105-container-tag-perf-tracking.md
new file mode 100644
index 0000000000..a7b6eb1a18
--- /dev/null
+++ b/todo/TODO-105-container-tag-perf-tracking.md
@@ -0,0 +1,340 @@
+# TODO-105: Container-test tag isolation + perf-regression tracking + 
profiling pass
+
+Source: TODO.md headline bullet expanded 2026-05-26; pivoted same day from a 
module-split approach to a JUnit-5 tag-isolation approach after a revisit 
showed the tag mechanism solves the same goal without complicating the 
single-`.exec` JaCoCo workflow.
+
+## Goal
+
+Build the automatic regression-detection seam the FINISHED-79 incident proved 
we need (a `RestContext.build()` โ†’ `Settings.get().addSource(...)` bridge 
leaked `ConfigPropertySource` instances into a process-wide singleton via 
`MockRestClient`'s static `RestContext` cache, blowing the utest suite from 
~35s to ~560s; caught only by a human noticing the slowdown), then take a 
one-shot profiling pass to file optimization candidates into TODO-999.
+
+Four sub-deliverables:
+
+- **(a) Container-test tag isolation.** Introduce JUnit-5 meta-annotations 
(`@SpringbootTest`, `@JettyMicroserviceTest`) bundling `@Tag("container")` + a 
container-type sub-tag. Annotate the ~22 container-booting classes. Add a dual 
`<execution>` block to Surefire so `mvn test` runs core-only AND container 
suites separately with their own `reportsDirectory`. Single Maven module, 
single JaCoCo `.exec`, no coverage-workflow churn.
+- **(b) Per-execution timing capture in `/push`.** Enrich `scripts/push.py` so 
every `/push` appends a per-Surefire-execution wall-clock + test-count record 
to a `.push-timings.jsonl` log; add a reporter that flags >20% deltas from the 
rolling median, separately per execution (core vs container).
+- **(c) One-shot profiling pass.** `scripts/test.py --profile <module>` 
plumbing via JFR (or async-profiler). Run over the heaviest modules; file 
findings into 
[`TODO-999-final-prerelease-cleanup.md`](TODO-999-final-prerelease-cleanup.md)'s
 "Working notes" section.
+- **(d) Surefire XML rollup with container-type bucketing.** Python script 
reads `target/surefire-reports/core/` + `target/surefire-reports/container/` 
(the dual-execution output dirs from deliverable a) and produces a per-bucket 
wall-clock table (`core` / `container.springboot` / `container.jetty`) per push.
+
+## Implementation status (2026-05-26)
+
+- [x] Completed and archived as 
[`FINISHED-105-container-tag-perf-tracking.md`](FINISHED-105-container-tag-perf-tracking.md).
+- [x] Phase 1 implemented: container meta-annotations added, 22 inventoried 
classes tagged, dual-execution Surefire split in `juneau-utest/pom.xml`, and 
`scripts/check-container-tags.py` wired into `scripts/push.py`.
+- [x] Phase 2 implemented: `scripts/test.py --timing-log` writes per-execution 
JSONL metrics to `~/.cache/juneau-push-timings/<branch>.jsonl`; 
`scripts/push.py` runs `scripts/push-timings.py` and always prints timing 
numbers with >5% warning banners.
+- [x] Phase 3 implemented: `scripts/surefire-rollup.py` aggregates `core`, 
`container`, `container.springboot`, and `container.jetty` buckets from 
Surefire XML.
+- [x] Phase 4 support implemented: `scripts/test.py --profile <module>` emits 
one-shot JFR recordings under `target/profile-results/`.
+
+## Why
+
+- **The FINISHED-79 incident**: the perf regression sat in master for an 
unknown window before a human noticed `./scripts/test.py` was suddenly slow. No 
automated detection. The root cause turned out to be a process-wide singleton 
interaction (`MockRestClient`'s static `RestContext` cache + 
`Settings.get().addSource(...)` from `RestContext.build()`), invisible to 
per-test timing because *every* test slowed down proportionally.
+- **Today's `scripts/push.py` captures pass/fail only** โ€” no wall-clock, no 
per-module breakdown. The signal needed to catch FINISHED-79-class regressions 
exists in Surefire output but is never persisted.
+- **The container-booting tests are a small, slow tail.** Spring Boot context 
init + Jetty bind-port-and-handshake takes seconds per test class; a regression 
in core marshall code that's amortized over ~1237 fast in-process tests gets 
visually washed out by the ~22 slow container tests in the combined timing. 
**Isolating** the slow tail (without physically moving its classes) lets the 
core suite's wall-clock be a sensitive signal again.
+- **`MockRest`-based tests are NOT container tests.** They run in-process 
(Servlet API surface mocked via `juneau-rest-mock`), no socket bind, no servlet 
container. They stay in the core bucket.
+
+## Why tags instead of a Maven-module split
+
+Considered both. Comparison summary that drove the pivot:
+
+| Concern | Module split | JUnit `@Tag` (this plan) |
+|---|---|---|
+| JaCoCo coverage | Two `.exec` files; must merge via `mvn jacoco:merge` or 
change `scripts/coverage.py` | **Unchanged** โ€” single `.exec`, 
`scripts/coverage.py` untouched |
+| Maven dep graph | New POM, repackage shared test utils, possibly extract a 
`juneau-utest-common` | **Unchanged** โ€” single module |
+| Classpath / IDE config | Two test source roots, two run configs | 
**Unchanged** โ€” single source root |
+| Eclipse Run-As JUnit ergonomics | Pick the right project before launching | 
Tags-tab include/exclude works in any test launcher |
+| Per-bucket timing isolation | Surefire reports naturally bucketed by module 
| Two Surefire `<execution>` blocks โ†’ bucketed `reportsDirectory` |
+| Initial migration cost | ~22 file moves + new POM + dep wiring | ~22 
one-line annotation additions |
+| Reversibility | Painful to undo | Trivial โ€” delete the annotations |
+| Future drift risk | Module boundary enforces physically | Soft โ€” relies on a 
discipline-guard script (`scripts/check-container-tags.py`) |
+
+The discipline-drift risk is the only real cost; closing it with a guard 
script is sub-second and runs in `scripts/push.py` as a precondition. 
Everything else is strictly cheaper.
+
+## Non-goals
+
+- **Not splitting `juneau-utest` into multiple Maven modules.** Single module 
stays single module. JaCoCo `.exec`, `scripts/coverage.py`, dependency graph 
all untouched.
+- **Not implementing the optimization fixes** surfaced by the Phase 4 
profiling pass. Those land via TODO-999's release-cleanup batch. TODO-105 ships 
the infrastructure; TODO-999 ships the fixes. Hard scope boundary โ€” this PR 
must not grow optimization patches.
+- **Not changing test-framework versions or test idioms.** Annotate existing 
tests, don't rewrite them.
+- **Not splitting `org.apache.juneau.rest.*` MockRest tests out** of the core 
bucket. They stay in `!container`. MockRest is in-process.
+- **Not splitting REST-client tests** (rest/client + http/classic) out of the 
core bucket. They use MockRest, in-process.
+- **Not gating `/push` on a regression flag.** The reporter is warning-only. A 
>20% delta prints a banner; it does not block the commit. False positives kill 
the signal otherwise.
+- **Not building a long-term timing-trend dashboard.** Just the rolling-median 
delta detection.
+
+## Current test landscape (inventory baseline, captured 2026-05-26 at SHA 
`ba158b21ebf2589df375f0ff6d0defdb0ce85282`)
+
+Total `*Test.java` under `juneau-utest/src/test/java`: **1259**.
+
+### Container-bound tests (the annotation scope)
+
+**Spring Boot (10 classes using `@SpringBootTest` or equivalent boot):**
+
+| Class | Naming form |
+|---|---|
+| `rest/convention/BasicVersionResource_Springboot_Test.java` | clean suffix |
+| `rest/ops/BasicEchoResource_Springboot_Test.java` | clean suffix |
+| `rest/staticfiles/BasicStaticFilesResource_Springboot_Test.java` | clean 
suffix |
+| `rest/docs/BasicApiDocs_Springboot_Test.java` | clean suffix |
+| `rest/springboot/RestPathsRuntimeOverride_Springboot_Test.java` | clean 
suffix |
+| `rest/staticfiles/BasicStaticFilesResource_SpringbootMetaInf_Test.java` | 
**drift** โ€” `_SpringbootMetaInf_Test` |
+| 
`rest/springboot/SpringEnvironmentPropertySource_SpringbootIntegration_Test.java`
 | **drift** โ€” `_SpringbootIntegration_Test` |
+| `rest/docs/BasicApiDocs_Springboot_MultiOpenApiProvider_Test.java` | 
**drift** โ€” `_Springboot` mid-name, qualifier suffix |
+| `rest/springboot/SpringEnvironmentPropertySource_Test.java` | **drift** โ€” 
`@SpringBootTest` annotation, no `Springboot` in class name |
+| `rest/springboot/SpringBeanStore_Test.java` | **drift** โ€” `@SpringBootTest` 
annotation, no `Springboot` in class name |
+
+(5 clean / 5 drift.)
+
+**JettyMicroservice / Microservice (12 classes booting embedded Jetty via 
`JettyMicroservice` or `Microservice` builder):**
+
+| Class | Naming form |
+|---|---|
+| `rest/convention/BasicVersionResource_JettyMicroservice_Test.java` | clean 
suffix |
+| `rest/ops/BasicEchoResource_JettyMicroservice_Test.java` | clean suffix |
+| `rest/staticfiles/BasicStaticFilesResource_JettyMicroservice_Test.java` | 
clean suffix |
+| `rest/docs/BasicApiDocs_JettyMicroservice_Test.java` | clean suffix |
+| `microservice/Microservice_Builder_Test.java` | **drift** โ€” boots, 
class-name-as-SUT convention |
+| `microservice/Microservice_Inject_Test.java` | **drift** โ€” same |
+| `microservice/Microservice_Listener_Fanout_Test.java` | **drift** โ€” same |
+| `microservice/Microservice_OverridingBeanStore_Test.java` | **drift** โ€” same 
|
+| `microservice/Microservice_PushPopOverlay_Test.java` | **drift** โ€” same |
+| `microservice/jetty/JettyConfiguration_Test.java` | **drift** โ€” same |
+| `microservice/jetty/Rest_Paths_Test.java` | **drift** โ€” same |
+| `microservice/jetty/RestPathsRuntimeOverride_JettyMount_Test.java` | 
**drift** โ€” `_JettyMount_Test` |
+
+(4 clean / 8 drift.)
+
+**Microservice/* tests confirmed NOT container-bound (stay in `!container`):** 
`BasicMicroserviceListener_Test`, `LogConfig_Test`, `LogParser_Test`, 
`LogEntryFormatter_Test`, `ConsoleCommand_Test`, `LogsResource_Action_Test`, 
`LogsResource_PathTraversal_Test`, `DirectoryResource_Action_Test`, 
`DirectoryResource_PathTraversal_Test`, `microservice/jetty/JettyLogger_Test`.
+
+### Annotation-scope summary
+
+- **Classes that get `@SpringbootTest`: 10.**
+- **Classes that get `@JettyMicroserviceTest`: 12.**
+- **Classes left untagged (default `!container` bucket): ~1237.**
+- **Naming-convention drift: 13 of 22** (59%) โ€” those classes carry container 
semantics but don't follow the `_<Container>_Test.java` suffix. OQ #8 asks 
whether to rename them as a one-time consistency pass.
+
+### Push / test / coverage script integration seams
+
+- **`scripts/push.py`** runs `python3 scripts/test.py --full` once (currently 
lines 442-468) and inspects only the exit code. The natural insertion points 
for deliverable (b) and the discipline guard are *before* and *after* the test 
invocation respectively โ€” `check-container-tags.py` runs before tests as a 
precondition; the timing-log writer runs after tests complete.
+- **`scripts/test.py`** is the natural home for `--profile <module>` 
(deliverable c) and `--timing-log <path>` (deliverable b). It already shells 
`mvn test -Drat.skip=true`; the `--profile` variant becomes `mvn test -pl 
<module> -Drat.skip=true -DargLine='-XX:StartFlightRecording=...'`. The seam is 
the existing `run_command(...)` helper.
+- **`scripts/coverage.py`** reads exactly one `.exec` file: 
`juneau-utest/target/jacoco.exec`. **Unchanged by this plan.** The dual 
Surefire `<execution>` writes to the same `.exec` (JaCoCo appends across 
executions by default).
+
+## Meta-annotation design
+
+Two sibling typed meta-annotations under 
`juneau-utest/src/test/java/org/apache/juneau/testing/annotations/`:
+
+```java
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Tag("container")
+@Tag("springboot")
+public @interface SpringbootTest {}
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Tag("container")
+@Tag("jetty")
+public @interface JettyMicroserviceTest {}
+```
+
+Naming rationale: matches the existing `_Springboot_Test` / 
`_JettyMicroservice_Test` class-suffix convention so the annotation reads as a 
natural alias for the class name. The two-annotation pattern (not a 
parameterized enum) is forced by Java: a custom annotation with an enum 
`value()` cannot dynamically derive a second `@Tag` from its argument, so the 
sub-tag must be statically declared.
+
+Adoption pattern:
+
+```java
+@SpringbootTest
+public class BasicVersionResource_Springboot_Test { ... }
+
+@JettyMicroserviceTest
+public class BasicEchoResource_JettyMicroservice_Test { ... }
+```
+
+JUnit-5 tag-discovery (`AnnotationUtils.findRepeatableAnnotations(testClass, 
Tag.class)`) walks meta-annotation hierarchies, so `@SpringbootTest` matches a 
filter on `container`, `springboot`, or `container & springboot` identically to 
a direct `@Tag` would. Confirmed compatibility:
+
+- **Surefire** (`<groups>` / `<excludedGroups>`).
+- **Eclipse Run-As โ†’ JUnit Test โ†’ Tags tab** (passes the expression through to 
the JUnit-platform `Launcher` directly).
+- **IntelliJ Run Configurations โ†’ Test kind: Tags** (same mechanism).
+
+### Developer ergonomics enabled
+
+CLI:
+- `mvn test` โ€” runs both executions (core + container).
+- `mvn test -DexcludedGroups=container` โ€” core-only (fast inner loop, 
~30-something seconds today).
+- `mvn test -Dgroups=container` โ€” container-only.
+- `mvn test -Dgroups=springboot` โ€” just Spring Boot tests.
+- `mvn test -Dgroups=jetty` โ€” just Jetty Microservice tests.
+- `mvn test -Dgroups='container & jetty'` โ€” boolean expression.
+- `./scripts/test.py --no-container` โ€” convenience wrapper (sets 
`-DexcludedGroups=container`).
+
+Eclipse: Run As โ†’ JUnit Test โ†’ Run Configurations โ†’ Tags tab โ†’ enter 
`container` in Exclude (or any expression). Re-runs honor the saved filter.
+
+IntelliJ: Run Configurations โ†’ Test kind: Tags โ†’ enter `container` in Exclude.
+
+## Resolved design decisions
+
+_All eight OQAs answered 2026-05-26. Verbatim originals preserved below for 
archive value โ€” each carries an inline **Resolved** line._
+
+1. **Meta-annotation naming.** `@SpringbootTest` + `@JettyMicroserviceTest` 
(matches existing class-suffix convention, longer) vs `@Springboot` + `@Jetty` 
(shorter, but `@Jetty` is ambiguous โ€” could be misread as a generic Jetty 
marker rather than "this test boots `JettyMicroservice`") vs 
`@SpringbootContainerTest` + `@JettyMicroserviceContainerTest` (explicit but 
verbose).
+   - **Resolved 2026-05-26**: `@SpringbootTest` + `@JettyMicroserviceTest` 
(default, suffix-aligned).
+2. **Push-timings log location.** Three flavors:
+   - **`target/.push-timings.jsonl`** (gitignored under existing `**/target/` 
rule) โ€” per-developer, no sharing.
+   - **`scripts/.push-timings.jsonl`** (checked in) โ€” team-shared rolling 
history; every push grows the log โ†’ noisy diffs + merge conflicts.
+   - **`~/.cache/juneau-push-timings/<branch>.jsonl`** (out-of-repo) โ€” 
per-developer, branch-keyed, no repo footprint.
+
+   The "team-shared" flavor catches a regression *another* developer 
introduces; the per-developer flavors only catch regressions you yourself just 
pushed.
+   - **Resolved 2026-05-26**: out-of-repo 
(`~/.cache/juneau-push-timings/<branch>.jsonl`).
+3. **Regression-detection threshold.** Originally proposed >20% delta from 
rolling median.
+   - Per-execution (core vs container, separately) or global?
+   - Adjustable knob?
+   - Warn-only on first delta, escalate on N consecutive?
+   - What threshold value is reasonable?
+   - **Resolved 2026-05-26**:
+     - **Threshold = 5%** (default `JUNEAU_PUSH_TIMING_THRESHOLD=0.05`). 
Tighter than the originally-proposed 20% because catching subtler regressions 
early is the goal โ€” even a 5% perf creep across a few PRs is signal worth 
surfacing.
+     - **Per-execution** (core vs container, independent thresholds).
+     - **Warn-only always.** No escalation tier for v1.
+     - **Always print the numbers** at the end of every `/push`, regardless of 
whether the threshold is breached. The threshold only controls whether a โš  
warning banner is emitted ON TOP of the numbers. Per-execution timing report 
format:
+       ```
+       ๐Ÿ“Š Push-timing report (last 20 runs on this branch):
+          juneau-utest/core     : 28.4s  (median 27.9s, delta +1.8%)
+          juneau-utest/container: 18.6s  (median 18.2s, delta +2.2%)
+       ```
+       And when threshold breached:
+       ```
+       ๐Ÿ“Š Push-timing report (last 20 runs on this branch):
+          juneau-utest/core     : 41.2s  (median 27.9s, delta +47.7%)
+          โš  juneau-utest/core wall-clock 41.2s is +47.7% above rolling median 
27.9s (last 20 runs, threshold 5%)
+          juneau-utest/container: 18.6s  (median 18.2s, delta +2.2%)
+       ```
+4. **Profiling tooling pick.**
+   - **JFR** โ€” bundled with JDK, no install, slightly less detail on native 
frames, JSON/binary `.jfr` viewer in JDK Mission Control.
+   - **async-profiler** โ€” better flame graphs, native installer required 
(`brew install async-profiler` on macOS).
+   - **Resolved 2026-05-26**: JFR. Drop in async-profiler ad hoc if the v1 
pass surfaces a native-frame-shaped problem JFR can't see clearly.
+5. **Profiling output destination.** 
`target/profile-results/<module>-<ts>.jfr` (gitignored under `**/target/`) vs 
upload to a shared store.
+   - **Resolved 2026-05-26**: local-only 
(`target/profile-results/<module>-<ts>.jfr`).
+6. **Sequencing relative to TODO-94a/b/c** (auth-filter framework). Original 
plan said "suggested before TODO-94a/b/c land" so the auth-filter PRs land with 
regression-tracking in place. The narrower tag-based scope makes that 
sequencing low-cost โ€” TODO-105 is small enough to land in days.
+   - **Resolved 2026-05-26**: before TODO-94a/b/c. Update TODO.md execution 
order accordingly when TODO-105 lands (TODO-105 already sits earlier in Phase E 
than TODO-94 in Phase F, so the existing order already reflects this).
+7. **Container-detection heuristics in `check-container-tags.py`.** The guard 
script needs to identify a "container-booting test" without the tag. Proposed 
signals (the script flags any class that matches one but is not tagged):
+   - Class has `@SpringBootTest` annotation (any of its forms โ€” 
`@SpringBootTest`, `@AutoConfigureMockMvc`, etc.).
+   - Class has `@ExtendWith(SpringExtension.class)`.
+   - Class body or any nested class calls `JettyMicroservice.create()` / 
`JettyMicroservice.builder()`.
+   - Class body calls `Microservice.create()` / `Microservice.builder()` 
(excluding the legitimate `microservice/Microservice_*_Test` cases which 
actually want the tag).
+   - Class `extends` a known container-boot base class (none today, but TBD 
list).
+   - **Resolved 2026-05-26**: heuristic list above accepted as-is. Phase 1 
also adds raw `org.springframework.boot.SpringApplication.run(...)` and raw 
`org.eclipse.jetty.*.Server(...)` instantiation patterns to the scan to 
future-proof against bypass.
+8. **Drift-cleanup rename pass.** Should Phase 1 also rename the 13 
drift-cased classes to consistent `_Springboot_Test` / 
`_JettyMicroservice_Test` suffixes (for IDE-grep friendliness + signaling 
consistency), or leave the names as-is since the tag is the source of truth and 
renaming touches blame history?
+   - **Resolved 2026-05-26**: leave names as-is. The tag is the source of 
truth; drift-rename can land as a TODO-999 cleanup item if desired.
+
+## Phases
+
+### Phase 1 โ€” Tag introduction & adoption (deliverable a)
+
+1. **Create the meta-annotations** under 
`juneau-utest/src/test/java/org/apache/juneau/testing/annotations/`: 
`SpringbootTest.java`, `JettyMicroserviceTest.java`.
+2. **Annotate the 22 inventoried classes** with the appropriate 
meta-annotation. Mechanical โ€” one line per class.
+3. **Add `scripts/check-container-tags.py`** โ€” discipline guard. Scans 
`juneau-utest/src/test/java/**/*Test.java`, applies the OQ #7 heuristics to 
identify container-booting classes, asserts each carries `@SpringbootTest` or 
`@JettyMicroserviceTest`. Exits non-zero on a mismatch with a per-file list. 
Sub-second runtime.
+4. **Configure dual-execution Surefire** in `juneau-utest/pom.xml`:
+   ```xml
+   <plugin>
+       <artifactId>maven-surefire-plugin</artifactId>
+       <executions>
+           <execution>
+               <id>default-test</id>
+               <configuration>
+                   <excludedGroups>container</excludedGroups>
+                   
<reportsDirectory>${project.build.directory}/surefire-reports/core</reportsDirectory>
+               </configuration>
+           </execution>
+           <execution>
+               <id>container-test</id>
+               <phase>test</phase>
+               <goals><goal>test</goal></goals>
+               <configuration>
+                   <groups>container</groups>
+                   
<reportsDirectory>${project.build.directory}/surefire-reports/container</reportsDirectory>
+               </configuration>
+           </execution>
+       </executions>
+   </plugin>
+   ```
+   Both `<execution>`s run during the `test` phase. The `reportsDirectory` 
separation produces naturally bucketed Surefire output for deliverable (d).
+5. **Wire `scripts/check-container-tags.py` into `scripts/push.py`** as a 
precondition (before the test step). Failed check blocks the push with a clear 
error message and a per-class hint (`Add @SpringbootTest to <class>`).
+6. **Verify** โ€” `./scripts/test.py --full` is green; `mvn test 
-DexcludedGroups=container` runs only the core suite; `mvn test 
-Dgroups=container` runs only the container suite; `mvn test 
-Dgroups=springboot` runs only the 10 Spring Boot classes. Eclipse Run-As โ†’ 
JUnit with `container` in the Tags exclude list runs only the core suite.
+
+**Acceptance:** all 22 container-bound classes tagged; 
`scripts/check-container-tags.py` passes (and fails appropriately on a 
fabricated unTagged-but-container-booting test); dual-execution Surefire 
produces two `reportsDirectory` outputs; `scripts/coverage.py` produces 
identical aggregate coverage figures to pre-tag.
+
+### Phase 2 โ€” Push-timing infrastructure (deliverable b)
+
+1. **`scripts/test.py --timing-log <path>` flag.** When set, captures 
per-execution wall-clock and writes a JSONL record. Maven's Reactor Summary 
prints per-module times; Surefire's per-execution times come from the 
`target/surefire-reports/<dir>/TEST-*.xml` files (sum the `<testsuite 
time="โ€ฆ">` across the bucket dir). Record shape:
+   ```json
+   {"ts": "...", "branch": "...", "commit": "...", "module": "juneau-utest", 
"execution": "core", "wallclock_s": 28.4, "test_count": 1237, "passed": true}
+   {"ts": "...", "branch": "...", "commit": "...", "module": "juneau-utest", 
"execution": "container", "wallclock_s": 18.6, "test_count": 22, "passed": true}
+   ```
+2. **`scripts/push-timings.py` reporter.** Reads the last N records (default 
20) from the JSONL log, computes per-`(module, execution)` rolling median, 
compares the just-appended record's `wallclock_s` against the median.
+   - **Always print the numbers** โ€” every push prints a `๐Ÿ“Š Push-timing report` 
block with one row per `(module, execution)`, showing current wall-clock, 
rolling median, and percent delta. Format per OQ #3 resolution:
+     ```
+     ๐Ÿ“Š Push-timing report (last 20 runs on this branch):
+        juneau-utest/core     : 28.4s  (median 27.9s, delta +1.8%)
+        juneau-utest/container: 18.6s  (median 18.2s, delta +2.2%)
+     ```
+   - **Warning banner only when threshold breached** โ€” if `|current - median| 
/ median > threshold` (default `0.05` per OQ #3, env-var-tunable as 
`JUNEAU_PUSH_TIMING_THRESHOLD`), inject a โš  banner line immediately after the 
offending row:
+     ```
+        juneau-utest/core     : 41.2s  (median 27.9s, delta +47.7%)
+        โš  juneau-utest/core wall-clock 41.2s is +47.7% above rolling median 
27.9s (last 20 runs, threshold 5%)
+     ```
+   - Warning-only โ€” never returns non-zero exit code. Doesn't block the push.
+3. **Wire into `scripts/push.py`.** After the `python3 scripts/test.py --full` 
invocation succeeds:
+   - Pass `--timing-log <path>` per OQ #2 
(`~/.cache/juneau-push-timings/<branch>.jsonl` if going with out-of-repo).
+   - Run `python3 scripts/push-timings.py --log <path>` to surface deltas. 
Capture stdout, prefix with `๐Ÿ” Push-timing report:` for the user.
+   - Continue to the build + commit + push steps regardless of timing-report 
output.
+4. **Bootstrap the log.** First-run-with-empty-log behavior: skip delta check, 
just write the baseline record. Subsequent runs have data to compare against.
+
+**Acceptance:** running `./scripts/push.py "test message" --dry-run` (after 
extending dry-run to include timing read/write) on a clean working tree prints 
"first run, no baseline" โ†’ second `/push` on the same branch prints the 
per-execution timing block with delta percentages โ†’ fabricated 10% wall-clock 
blow-up in a unit test triggers the warning banner on the affected execution 
(above the 5% threshold) while the unaffected execution prints only its 
baseline numbers โ†’ `JUNEAU_PUSH_TIMING [...]
+
+### Phase 3 โ€” Container-type rollup (deliverable d)
+
+1. **`scripts/surefire-rollup.py`** โ€” walks both bucketed Surefire dirs:
+   - `<module>/target/surefire-reports/core/TEST-*.xml`
+   - `<module>/target/surefire-reports/container/TEST-*.xml`
+2. **Sub-bucket the container dir** by reading each test class's 
`@SpringbootTest` / `@JettyMicroserviceTest` annotation (or by class-suffix as 
a fallback for the drift cases). Two sub-buckets: `container.springboot`, 
`container.jetty`.
+3. **Aggregate** per-module per-bucket wall-clock totals. Output:
+   ```
+   juneau-utest/
+     core                : โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“ (1237 tests, 28.412s)
+     container.springboot: โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“ (10 tests, 11.812s)
+     container.jetty     : โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“โ–“ (12 tests, 6.781s)
+   ```
+4. **Integrate with Phase 2.** Same `--timing-log` writer optionally writes 
per-sub-bucket records; reporter optionally summarizes with `--by-container`.
+
+**Acceptance:** running the rollup over a fresh 
`target/surefire-reports/{core,container}/` produces a per-bucket table 
matching the inventory baseline (1237 core, 10 springboot, 12 jetty) โ†’ numbers 
match a hand-count of `<testsuite time="โ€ฆ">` values across each bucket dir.
+
+### Phase 4 โ€” Profiling pass + TODO-999 handoff (deliverable c)
+
+1. **`scripts/test.py --profile <module>` flag.** Adds 
`-DargLine='-XX:StartFlightRecording=โ€ฆ'` (JFR โ€” see OQ #4) or `-agentpath:` 
(async-profiler) to the `mvn test` invocation. Writes the `.jfr` (or `.html` 
flame graph) output to `target/profile-results/<module>-<ts>.jfr` per OQ #5.
+2. **One-shot profiling run** over the heaviest tagged buckets:
+   - `juneau-utest` core (1237 tests, the heavy work).
+   - `juneau-utest` container.springboot (slowest per-test).
+3. **Open each `.jfr` in JDK Mission Control** (or render flame graphs if 
using async-profiler). Look for:
+   - Hot allocation sites in `BeanContext.getClassMeta(...)` (recurring 
suspect per FINISHED-103's findings).
+   - Regex re-compilation in `UrlPathMatcher` and similar.
+   - Redundant `VarResolver.resolve(...)` calls (FINISHED-103 already 
addressed many via `VarTemplateCompiler` โ€” expect fewer findings here than 
initially anticipated).
+   - Serializer-session warm-up cost 
(first-`MockRestClient.create()`-per-class spike).
+   - Spring `Environment.getProperty(...)` walks on unknown keys 
(FINISHED-79's `SpringEnvironmentPropertySource` should memoize, but verify).
+4. **File findings into 
[`TODO-999-final-prerelease-cleanup.md`](TODO-999-final-prerelease-cleanup.md)'s
 "Working notes" section.** Format per existing TODO-999 convention: bullet per 
finding with file/line pointer + one-sentence summary + flame-graph screenshot 
or `.jfr` path. **Do NOT inline the fixes here.**
+
+**Acceptance:** `./scripts/test.py --profile juneau-utest` produces a `.jfr` 
file under `target/profile-results/` โ†’ at least one actionable finding is filed 
to TODO-999's working-notes section with a precise pointer 
(`juneau-marshall/.../BeanContext.java:LXXX`) โ†’ the finding rationale 
references the flame-graph evidence.
+
+## Acceptance criteria
+
+- **Phase 1**: 22 container-bound classes carry `@SpringbootTest` / 
`@JettyMicroserviceTest`; `scripts/check-container-tags.py` passes on the 
current tree and fails on a fabricated untagged container-booting test; 
dual-execution Surefire produces bucketed `reportsDirectory` outputs; 
`scripts/coverage.py` aggregate coverage matches pre-tag.
+- **Phase 2**: `scripts/push.py` invocation appends per-execution JSONL 
records per push; `scripts/push-timings.py` always prints the per-execution 
numbers + delta percentages, and surfaces a โš  warning banner when a fabricated 
>5% delta is introduced.
+- **Phase 3**: `scripts/surefire-rollup.py` produces a per-bucket table 
consistent with the inventory baseline.
+- **Phase 4**: at least one `.jfr` profile captured; at least one actionable 
optimization finding filed into TODO-999's "Working notes".
+
+## Risks
+
+- **Tag-discipline drift** โ€” a new test boots a container without 
`@SpringbootTest` / `@JettyMicroserviceTest`, sneaking into the core bucket and 
re-introducing the original "slow tests hide perf regressions" problem. 
Mitigation: `scripts/check-container-tags.py` as a `/push` precondition catches 
this on the next push attempt.
+- **`scripts/check-container-tags.py` heuristic gaps.** The detection 
heuristics in OQ #7 might miss a new container-boot pattern (e.g. a test that 
uses raw `SpringApplication.run(...)` instead of `@SpringBootTest`). 
Mitigation: review the heuristics quarterly; treat any FINISHED-79-class 
incident as a signal to extend the heuristic set.
+- **JUnit 5 tag-discovery quirks with meta-annotations.** JUnit-platform 
discovery walks meta-annotation hierarchies (verified upstream behavior) โ€” but 
a wrong-Surefire-plugin-version could behave differently. Mitigation: pin 
Surefire >= 3.0.0 in the parent POM; verify the dual-execution config with a 
smoke test before merging Phase 1.
+- **Surefire dual-execution interaction with `<testFailureIgnore>`.** Default 
behavior: if the first execution fails, the second doesn't run, which would 
hide container-test failures behind core-test failures. Mitigation: set 
`<testFailureIgnore>true</testFailureIgnore>` on the first execution and check 
the aggregate result in `scripts/test.py` post-hoc; OR sequence both executions 
and report combined pass/fail explicitly.
+- **JaCoCo append-on-second-execution.** JaCoCo's `prepare-agent` goal must 
use `append=true` for the second Surefire execution to add to (not overwrite) 
the first execution's `.exec` file. Default behavior; verify.
+- **JFR profiling startup overhead.** A short test run with JFR enabled has 
the JFR-init cost visible in the flame graph, which can mask real hotspots. 
Mitigation: profile only the heaviest buckets; use 
`-XX:StartFlightRecording=settings=profile` (medium-detail preset); ignore the 
first few seconds.
+- **Out-of-repo timing log (OQ #2) means CI runs lose history.** A push from a 
fresh `~/.cache/` produces no warnings ever. Acceptable if `/push` is 
developer-local-only; needs revisiting if `/push` ever runs in CI.
+
+## Related work
+
+- **FINISHED-79** 
([`FINISHED-79-value-annotation-config-bridge.md`](FINISHED-79-value-annotation-config-bridge.md))
 โ€” the perf-incident lesson learned. The `RestContext.build()` โ†’ 
`Settings.get().addSource(...)` bridge dropped during cleanup is what this TODO 
is designed to catch automatically next time.
+- **FINISHED-103** 
([`FINISHED-103-varresolver-template-compilation.md`](FINISHED-103-varresolver-template-compilation.md))
 โ€” already addressed many `VarResolver.resolve(...)` hotspots via the 
`VarTemplateCompiler` retrofit. Expect Phase 4 profiling to find *less* on the 
SVL side than initially anticipated; findings will skew toward `BeanContext` / 
serializer-session cold-start.
+- **TODO-94a/b/c** (auth-filter framework + SAML + OAuth) โ€” sequenced after 
TODO-105 so the auth-test PRs land with regression-tracking in place. The 
tag-based scope makes this sequencing cheap.
+- **TODO-999** 
([`TODO-999-final-prerelease-cleanup.md`](TODO-999-final-prerelease-cleanup.md))
 โ€” recipient of the Phase 4 profiling findings. Do NOT migrate any of those 
fixes into TODO-105. The drift-rename pass from OQ #8 is also a TODO-999 
candidate if not done here.
+- **TODO-95** (per-RestContext `@Value("${cfg-key}")` resolution follow-on to 
FINISHED-79). Independent of TODO-105 but motivated by the same incident.
diff --git a/todo/TODO.md b/todo/TODO.md
index 65f4785445..b5dd8033c5 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -33,8 +33,7 @@ Live work plan for the Juneau 9.5.0 development cycle. 
Completed work is tracked
 8. **TODO-89** โ€” `RateLimitGuard.Storage.snapshot()` SPI + 
`BasicAdminResource` enrichment. Small (~30 LOC + 2 test files), closes a known 
carry-over from FINISHED-77 (`"buckets": []` placeholder). Plan: 
`todo/TODO-89-ratelimit-storage-snapshot-spi.md`.
 9. **TODO-71** โ€” Doc-site script + Docusaurus search swap. Mostly already done 
(planning worker found `juneau-docs/scripts/build-docs.py` + 
`.github/workflows/deploy-docs.yml.disabled` already in place); this TODO is 
now a cutover/cleanup + the Algolia โ†’ `@easyops-cn/docusaurus-search-local` 
swap. Plan: `todo/TODO-71-docs-site-script-search-swap.md`.
 10. **TODO-88** โ€” YAML parser buffer-underflow on large OpenAPI 3.1 documents. 
Latent parser bug in `juneau-marshall`; opportunistic. Plan file TBD.
-11. **TODO-105** โ€” Test-suite split + perf-regression tracking + profiling 
pass. Lesson-learned from the FINISHED-79 perf incident (560s utest blow-up 
caught only by manual observation): build the tooling to catch this 
automatically next time. Three deliverables: (a) split `juneau-utest` into 
`juneau-utest` (core: marshall / commons / dto / svl / config) + 
`juneau-utest-rest` (everything REST-server-side, plus Microservice + Spring 
Boot integration tests) so a core-only perf regression d [...]
-12. **TODO-106** โ€” FINISHED-archive follow-up/deferral sweep. Comb through 
every `todo/FINISHED-<id>-*.md` archive (~55 older + ~15 recent post-TODO-81 = 
~70 files) looking at the "Known follow-ups / explicit deferrals" / 
"Carry-over" / "Open question" sections in each, plus inline `TODO: defer to 
TODO-<n>` style pointers in the archive narrative. Surface every item that was 
filed during execution but never promoted to a formal `[TODO-n]` bullet in 
`todo/TODO.md`. Output: one consolidate [...]
+11. **TODO-106** โ€” FINISHED-archive follow-up/deferral sweep. Comb through 
every `todo/FINISHED-<id>-*.md` archive (~55 older + ~15 recent post-TODO-81 = 
~70 files) looking at the "Known follow-ups / explicit deferrals" / 
"Carry-over" / "Open question" sections in each, plus inline `TODO: defer to 
TODO-<n>` style pointers in the archive narrative. Surface every item that was 
filed during execution but never promoted to a formal `[TODO-n]` bullet in 
`todo/TODO.md`. Output: one consolidate [...]
 
 **Phase F โ€” server-feature track (independent; can interleave anywhere):**
 
@@ -110,9 +109,3 @@ Live work plan for the Juneau 9.5.0 development cycle. 
Completed work is tracked
 
 - [TODO-106] FINISHED-archive follow-up/deferral sweep โ€” comb through every 
`todo/FINISHED-<id>-*.md` archive (~70 files post-TODO-103 landing) and surface 
"Known follow-ups / explicit deferrals / Carry-over / Open question" items that 
were filed during execution but never promoted to formal `[TODO-n]` bullets in 
`todo/TODO.md`. Many follow-ons are already explicitly tracked (TODO-92 โ† 
FINISHED-79 Phase 6, TODO-95 โ† FINISHED-79 perf cleanup, TODO-89 โ† FINISHED-77 
admin enrichment, TODO-9 [...]
 
-- [TODO-105] Test-suite split + perf-regression tracking + profiling pass. 
Driven by the lesson learned from the FINISHED-79 perf incident: a 
`RestContext.build()` โ†’ `Settings.get().addSource(...)` bridge leaked 
`ConfigPropertySource` instances into a process-wide singleton because 
`MockRestClient` caches `RestContext` statically and never invokes `destroy()`, 
blowing the utest suite from ~35s to ~560s. Caught only by manual 
`./scripts/test.py` observation after the user noticed the slow [...]
-  - **(a) Test-suite split.** Break `juneau-utest` into two Maven modules: 
`juneau-utest` (core only โ€” `juneau-marshall`, `juneau-commons`, `juneau-dto`, 
`juneau-svl`, `juneau-config`, `juneau-cp` โ€” i.e. everything not under 
`juneau-rest/` or `juneau-microservice/`) and a new `juneau-utest-rest` 
(everything REST-server-side, plus the `JettyMicroservice` + `@SpringBootTest` 
integration tests). Goal: a core-only perf regression like FINISHED-79's no 
longer hides behind REST-side wall-clock [...]
-  - **(b) Per-module timing capture in `/push`.** Wire `scripts/push.py` 
(currently runs `mvn test` once and logs only pass/fail) to record per-module 
wall-clock to a checked-in `target/.push-timings.jsonl` log (append-only, one 
JSON line per `/push` run: `{ts, branch, commit, module, wallclock_s, 
test_count, passed}`). Add a `scripts/push-timings.py` reporter that reads the 
last N runs, computes a rolling median per module, and flags deltas >20% as 
warnings on the next `/push` (warning- [...]
-  - **(c) One-shot profiling pass.** Run async-profiler or JFR 
(`scripts/test.py --profile <module>`) over the two largest test suites 
post-split (likely `juneau-utest-rest` and the `juneau-marshall` subset of 
`juneau-utest`) to surface concrete optimization candidates โ€” hot allocation 
sites in `BeanContext.getClassMeta(...)`, regex re-compilation in 
`UrlPathMatcher`, redundant `VarResolver.resolve(...)` calls (much of which 
TODO-103 already targets), serializer-session warm-up cost, etc [...]
-
-  Independent of all in-flight TODOs; no hard deps. **Suggested before 
TODO-94a/b/c land** so the auth-filter PRs (which add a fair amount of test 
surface) land with the new regression-tracking infrastructure already in place 
โ€” that way the auth tests show up as new entries in the per-module timing log 
from day one, not retrofitted later. Plan file TBD.


Reply via email to