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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit d589bafcdb3eac3f539557881fe33559d36cb7c5
Merge: 5044f10852c 5fbcdd60dab
Author: Andi Huber <[email protected]>
AuthorDate: Fri Jun 13 14:39:41 2025 +0200

    CAUSEWAY-3891 and CAUSEWAY-3883: merges from maintenance branch
    
    - PDF.JS to play nice with browser caching
    - MemoryUsage new internal utility
    - new Wrapper memory leak tests

 commons/src/main/java/module-info.java             |   1 +
 .../commons/internal/debug/_MemoryUsage.java       |  98 +++++++++++++
 .../metamodel/context/HasMetaModelContext.java     |   5 +
 .../context/MetaModelContext_usingSpring.java      |   5 +
 .../metamodel/spec/impl/ObjectMemberAbstract.java  |   5 -
 .../runtimeservices/src/main/java/module-info.java |   1 +
 .../pdfjs/wkt/ui/components/PdfJsViewerPanel.java  |  36 ++---
 .../testdomain/jpa/JpaInventoryManager.java        |   6 +
 .../interact/WrapperInteraction_1_IntegTest.java   | 149 +++++++++++++++++++
 .../interact/WrapperInteraction_2_IntegTest.java   | 132 +++++++++++++++++
 .../interact/WrapperInteraction_3_IntegTest.java   | 158 +++++++++++++++++++++
 .../interact/WrapperInteraction_4_IntegTest.java   | 152 ++++++++++++++++++++
 .../WrapperInteraction_Caching_IntegTest.java      | 141 ++++++++++++++++++
 .../jpa/wrapper/JpaWrapperSyncTest.java            |  90 ++++++++++++
 .../WrapperFactoryMetaspaceMemoryLeakTest.java     | 107 ++++++++++++++
 15 files changed, 1060 insertions(+), 26 deletions(-)

diff --cc commons/src/main/java/module-info.java
index b74ac89285a,92e4474ad9c..27b06a6870d
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@@ -74,16 -73,16 +74,17 @@@ module org.apache.causeway.commons 
      requires transitive spring.beans;
      requires transitive spring.context;
      requires transitive spring.core;
 -    requires java.inject;
 -    requires java.annotation;
 +    requires transitive jakarta.xml.bind;
 +    requires transitive jakarta.inject;
 +    requires jakarta.annotation;
      requires com.sun.xml.bind;
 -    requires com.fasterxml.jackson.datatype.jsr310;
      requires com.fasterxml.jackson.dataformat.yaml;
 +    requires com.fasterxml.jackson.datatype.jsr310;
      requires com.fasterxml.jackson.datatype.jdk8;
+     requires java.management;
  
 -    opens org.apache.causeway.commons.internal.resources to java.xml.bind, 
com.sun.xml.bind; // JUnit test
 -    opens org.apache.causeway.commons.io to java.xml.bind, com.sun.xml.bind;
 -    exports org.apache.causeway.commons.memory;
 +    // JAXB JUnit test
 +    opens org.apache.causeway.commons.internal.resources to jakarta.xml.bind;
 +    opens org.apache.causeway.commons.io to jakarta.xml.bind;
  
  }
diff --cc 
commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
index 00000000000,00000000000..ab6958541a1
new file mode 100644
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/debug/_MemoryUsage.java
@@@ -1,0 -1,0 +1,98 @@@
++/*
++ *  Licensed to the Apache Software Foundation (ASF) under one
++ *  or more contributor license agreements.  See the NOTICE file
++ *  distributed with this work for additional information
++ *  regarding copyright ownership.  The ASF licenses this file
++ *  to you under the Apache License, Version 2.0 (the
++ *  "License"); you may not use this file except in compliance
++ *  with the License.  You may obtain a copy of the License at
++ *
++ *        http://www.apache.org/licenses/LICENSE-2.0
++ *
++ *  Unless required by applicable law or agreed to in writing,
++ *  software distributed under the License is distributed on an
++ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
++ *  KIND, either express or implied.  See the License for the
++ *  specific language governing permissions and limitations
++ *  under the License.
++ */
++package org.apache.causeway.commons.internal.debug;
++
++import lombok.SneakyThrows;
++
++import java.lang.management.ManagementFactory;
++import java.lang.management.MemoryPoolMXBean;
++import java.util.concurrent.Callable;
++
++/**
++ * <h1>- internal use only -</h1>
++ * <p>
++ * Memory Usage Utility
++ * <p>
++ * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this 
package! <br/>
++ * These may be changed or removed without notice!
++ * </p>
++ * @since 3.4.0
++ */
++public record _MemoryUsage(long usedInKibiBytes) {
++    
++    // -- UTILITIES
++    
++    static int indent = 0;
++
++    @SneakyThrows
++    public static <T> T measureMetaspace(final String desc, final Callable<T> 
runnable) {
++        var before = metaspace();
++        try {
++            indent++;
++            return runnable.call();
++        } finally {
++            var after = metaspace();
++            System.out.printf("%s%s : %s%n", spaces(indent), 
after.minus(before), desc);
++            indent--;
++        }
++    }
++    
++    public static void measureMetaspace(String desc, final Runnable runnable) 
{
++        var before = metaspace();
++        try {
++            runnable.run();
++        } finally {
++            var after = metaspace();
++            System.out.printf("%s : %s%n", after.minus(before), desc);
++        }
++    }
++    
++    // -- FACTORY
++    
++    private static _MemoryUsage metaspace() {
++        for (MemoryPoolMXBean pool : 
ManagementFactory.getMemoryPoolMXBeans()) {
++            if (pool.getName().contains("Metaspace")) {
++                return new _MemoryUsage(pool.getUsage());
++            }
++        }
++        throw new RuntimeException("Metaspace Usage not found");
++    }
++    
++    // -- NON CANONICAL CONSTRUCTOR
++
++    private _MemoryUsage(java.lang.management.MemoryUsage usage) {
++        this(usage.getUsed() / 1024);
++    }
++
++    @Override
++    public String toString() {
++        return String.format("%,d KiB", usedInKibiBytes);
++    }
++
++    // -- HELPER
++
++    private static String spaces(int indent) {
++        return " ".repeat(indent * 2);
++    }
++
++    private _MemoryUsage minus(_MemoryUsage before) {
++        return new _MemoryUsage(this.usedInKibiBytes - 
before.usedInKibiBytes);
++    }
++
++}
diff --cc 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java
index f023673783a,e542b43f0c3..fd2881f5fa6
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/HasMetaModelContext.java
@@@ -153,6 -150,10 +154,10 @@@ public interface HasMetaModelContext 
      default InteractionService getInteractionService() {
          return getMetaModelContext().getInteractionService();
      }
 -
++    
+     default CommandDtoFactory getCommandDtoFactory() {
+         return getMetaModelContext().getCommandDtoFactory();
+     }
  
      default Optional<UserLocale> currentUserLocale() {
          return getInteractionService().currentInteractionContext()
diff --cc 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java
index fa8b51e936b,23ee4ceed92..dc8c32a46ae
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/context/MetaModelContext_usingSpring.java
@@@ -158,6 -161,10 +159,10 @@@ class MetaModelContext_usingSpring exte
      private final InteractionService interactionService =
      getSingletonElseFail(InteractionService.class);
  
+     @Getter(lazy = true)
+     private final CommandDtoFactory commandDtoFactory =
+     getSingletonElseFail(CommandDtoFactory.class);
 -
++    
      @Override
      public final ManagedObject getHomePageAdapter() {
          final Object pojo = getHomePageResolverService().getHomePage();
diff --cc 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java
index 21cde68eefb,00000000000..cd53c40cb7c
mode 100644,000000..100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/impl/ObjectMemberAbstract.java
@@@ -1,358 -1,0 +1,353 @@@
 +/*
 + *  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.causeway.core.metamodel.spec.impl;
 +
 +import java.io.InvalidObjectException;
 +import java.io.ObjectInputStream;
 +import java.io.Serializable;
 +import java.util.Optional;
 +import java.util.UUID;
 +import java.util.function.Function;
 +import java.util.function.Supplier;
 +
 +import org.apache.causeway.applib.Identifier;
 +import org.apache.causeway.applib.annotation.Where;
 +import org.apache.causeway.applib.services.iactn.InteractionProvider;
 +import org.apache.causeway.commons.internal.assertions._Assert;
 +import org.apache.causeway.commons.internal.exceptions._Exceptions;
 +import org.apache.causeway.core.metamodel.consent.Consent;
 +import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 +import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
 +import org.apache.causeway.core.metamodel.context.MetaModelContext;
 +import org.apache.causeway.core.metamodel.facetapi.Facet;
 +import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
 +import org.apache.causeway.core.metamodel.facetapi.FeatureType;
 +import org.apache.causeway.core.metamodel.facets.FacetedMethod;
 +import org.apache.causeway.core.metamodel.facets.HasFacetedMethod;
 +import 
org.apache.causeway.core.metamodel.facets.all.described.MemberDescribedFacet;
 +import org.apache.causeway.core.metamodel.facets.all.help.HelpFacet;
 +import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet;
 +import 
org.apache.causeway.core.metamodel.facets.all.i8n.staatic.HasStaticText;
 +import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
 +import 
org.apache.causeway.core.metamodel.interactions.DisablingInteractionAdvisor;
 +import 
org.apache.causeway.core.metamodel.interactions.HidingInteractionAdvisor;
 +import org.apache.causeway.core.metamodel.interactions.InteractionContext;
 +import org.apache.causeway.core.metamodel.interactions.InteractionHead;
 +import org.apache.causeway.core.metamodel.interactions.InteractionUtils;
 +import org.apache.causeway.core.metamodel.interactions.acc.AccessContext;
 +import org.apache.causeway.core.metamodel.interactions.use.UsabilityContext;
 +import org.apache.causeway.core.metamodel.interactions.vis.VisibilityContext;
 +import org.apache.causeway.core.metamodel.object.ManagedObject;
 +import org.apache.causeway.core.metamodel.object.ManagedObjects;
- import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory;
 +import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 +import org.apache.causeway.core.metamodel.spec.feature.MixedInMember;
 +import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
 +import org.apache.causeway.schema.cmd.v2.CommandDto;
 +
 +import lombok.Getter;
 +import org.jspecify.annotations.NonNull;
 +
 +abstract class ObjectMemberAbstract
 +implements
 +    ObjectMember,
 +    HasFacetedMethod,
 +    HasMetaModelContext,
 +    Serializable {
 +    private static final long serialVersionUID = 1L;
 +
 +    @Getter(onMethod_ = {@Override}) private final @NonNull Identifier 
featureIdentifier;
 +    @Getter(onMethod_ = {@Override}) private final @NonNull FeatureType 
featureType;
 +    @Getter(onMethod_ = {@Override}) private final @NonNull FacetedMethod 
facetedMethod;
 +
 +    // -- CONSTRUCTOR
 +
 +    protected ObjectMemberAbstract(
 +            final Identifier featureIdentifier,
 +            final FacetedMethod facetedMethod,
 +            final FeatureType featureType) {
 +        this.featureIdentifier = featureIdentifier;
 +        this.facetedMethod = facetedMethod;
 +        this.featureType = featureType;
 +        if (getId() == null) {
 +            throw new IllegalArgumentException("Id must always be set");
 +        }
 +    }
 +
 +    // -- IDENTIFIERS
 +
 +    @Override
 +    public final String getId() {
 +        return getFeatureIdentifier().memberLogicalName();
 +    }
 +
 +    // -- INTERACTION HEAD
 +
 +    /**
 +     * To be overridden (only) by mixed in members!
 +     * @see MixedInMember
 +     */
 +    protected InteractionHead headFor(final ManagedObject ownerAdapter) {
 +        return InteractionHead.regular(ownerAdapter);
 +    }
 +
 +    // -- Name, Description, Help (convenience for facets)
 +
 +    @Override
 +    public final String getFriendlyName(final Supplier<ManagedObject> 
domainObjectProvider) {
 +
 +        var namedFacet = getFacet(MemberNamedFacet.class);
 +
 +        if(namedFacet==null) {
 +            throw _Exceptions.unrecoverable("no MemberNamedFacet preset on 
%s", getFeatureIdentifier());
 +        }
 +
 +        return namedFacet
 +            .getSpecialization()
 +            .fold(  textFacet->textFacet.translated(),
 +                    
textFacet->textFacet.textElseNull(headFor(domainObjectProvider.get()).target()));
 +    }
 +
 +    @Override
 +    public final Optional<String> getStaticFriendlyName() {
 +        return lookupFacet(MemberNamedFacet.class)
 +        .map(MemberNamedFacet::getSpecialization)
 +        .flatMap(specialization->specialization
 +                .fold(
 +                        textFacet->Optional.of(textFacet.translated()),
 +                        textFacet->Optional.empty()));
 +    }
 +
 +    @Override
 +    public final Optional<String> getDescription(final 
Supplier<ManagedObject> domainObjectProvider) {
 +        return lookupFacet(MemberDescribedFacet.class)
 +        .map(MemberDescribedFacet::getSpecialization)
 +        .map(specialization->specialization
 +                .fold(textFacet->textFacet.translated(),
 +                      
textFacet->textFacet.textElseNull(headFor(domainObjectProvider.get()).target())));
 +    }
 +
 +    @Override
 +    public final Optional<String> getStaticDescription() {
 +        return lookupFacet(MemberDescribedFacet.class)
 +                .map(MemberDescribedFacet::getSpecialization)
 +                .flatMap(specialization->specialization
 +                        .fold(
 +                                
textFacet->Optional.of(textFacet.translated()),
 +                                textFacet->Optional.empty()));
 +    }
 +
 +    @Override
 +    public final String getHelp() {
 +        final HelpFacet facet = getFacet(HelpFacet.class);
 +        return facet.value();
 +    }
 +
 +    // -- CANONICAL NAMING
 +
 +    @Override
 +    public final String getCanonicalFriendlyName() {
 +        return lookupFacet(MemberNamedFacet.class)
 +        .flatMap(MemberNamedFacet::getSharedFacetRanking)
 +        
.flatMap(facetRanking->facetRanking.getWinnerNonEventLowerOrEqualTo(MemberNamedFacet.class,
 Precedence.HIGH))
 +        .map(MemberNamedFacet::getSpecialization)
 +        .flatMap(specialization->specialization.left())
 +        .map(HasStaticText::translated)
 +        //we have a facet-post-processor to ensure following code path is 
unreachable,
 +        //however, we keep it in support of JUnit testing
 +        .orElseGet(()->getFeatureIdentifier().getMemberNaturalName());
 +    }
 +
 +    @Override
 +    public final Optional<String> getCanonicalDescription() {
 +        return lookupFacet(MemberDescribedFacet.class)
 +        .flatMap(MemberDescribedFacet::getSharedFacetRanking)
 +        
.flatMap(facetRanking->facetRanking.getWinnerNonEventLowerOrEqualTo(MemberDescribedFacet.class,
 Precedence.HIGH))
 +        .map(MemberDescribedFacet::getSpecialization)
 +        .flatMap(specialization->specialization.left())
 +        .map(HasStaticText::translated);
 +    }
 +
 +    // -- Hidden (or visible)
 +    /**
 +     * Create an {@link InteractionContext} to represent an attempt to view 
this
 +     * member (that is, to check if it is visible or not).
 +     *
 +     * <p>
 +     * Typically it is easier to just call
 +     * {@link ObjectMember#isVisible(ManagedObject, InteractionInitiatedBy, 
Where)}; this is
 +     * provided as API for symmetry with interactions (such as
 +     * {@link AccessContext} accesses) have no corresponding vetoing methods.
 +     */
 +    protected abstract VisibilityContext createVisibleInteractionContext(
 +            final ManagedObject target,
 +            final InteractionInitiatedBy interactionInitiatedBy,
 +            final Where where);
 +
 +    @Override
 +    public boolean isAlwaysHidden() {
 +        return HiddenFacet.isAlwaysHidden(getFacetHolder());
 +    }
 +
 +    /**
 +     * Loops over all {@link HidingInteractionAdvisor} {@link Facet}s and
 +     * returns <tt>true</tt> only if none hide the member.
 +     */
 +    @Override
 +    public Consent isVisible(
 +            final ManagedObject target,
 +            final InteractionInitiatedBy interactionInitiatedBy,
 +            final Where where) {
 +
 +        var visibilityContext = createVisibleInteractionContext(target, 
interactionInitiatedBy, where);
 +        return InteractionUtils.isVisibleResult(this, 
visibilityContext).createConsent();
 +    }
 +
 +    // -- Disabled (or enabled)
 +    /**
 +     * Create an {@link InteractionContext} to represent an attempt to
 +     * use this member (that is, to check if it is usable or not).
 +     *
 +     * <p>
 +     * Typically it is easier to just call
 +     * {@link ObjectMember#isUsable(ManagedObject, InteractionInitiatedBy, 
Where)}; this is
 +     * provided as API for symmetry with interactions (such as
 +     * {@link AccessContext} accesses) have no corresponding vetoing methods.
 +     */
 +    protected abstract UsabilityContext createUsableInteractionContext(
 +            final ManagedObject target,
 +            final InteractionInitiatedBy interactionInitiatedBy,
 +            final Where where);
 +
 +    /**
 +     * Loops over all {@link DisablingInteractionAdvisor} {@link Facet}s and
 +     * returns <tt>true</tt> only if none disables the member.
 +     */
 +    @Override
 +    public Consent isUsable(
 +            final ManagedObject target,
 +            final InteractionInitiatedBy interactionInitiatedBy,
 +            final Where where) {
 +
 +        var usabilityContext = createUsableInteractionContext(target, 
interactionInitiatedBy, where);
 +        return InteractionUtils.isUsableResult(this, 
usabilityContext).createConsent();
 +    }
 +
 +    // -- PREDICATES
 +
 +    @Override
 +    public final boolean isAction() {
 +        return featureType.isAction();
 +    }
 +
 +    @Override
 +    public final boolean isPropertyOrCollection() {
 +        return featureType.isPropertyOrCollection();
 +    }
 +
 +    @Override
 +    public final boolean isOneToManyAssociation() {
 +        return featureType.isCollection();
 +    }
 +
 +    @Override
 +    public final boolean isOneToOneAssociation() {
 +        return featureType.isProperty();
 +    }
 +
 +    // -- MIXIN ADAPTER FACTORY
 +
 +    protected ManagedObject mixinAdapterFor(
 +            final @NonNull ObjectSpecification mixinSpec,
 +            final @NonNull ManagedObject mixee) {
 +
 +        // nullable for action parameter mixins
 +        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mixee)) {
 +            return ManagedObject.empty(mixinSpec);
 +        }
 +
 +        var mixinPojo = 
getFactoryService().mixin(mixinSpec.getCorrespondingClass(), mixee.getPojo());
 +        return ManagedObject.mixin(mixinSpec, mixinPojo);
 +    }
 +
 +    // -- OBJECT CONTRACT
 +
 +    @Override
 +    public String toString() {
 +        return getStaticFriendlyName()
 +                .map(name->String.format("id=%s,name='%s'", getId(), name))
 +                .orElseGet(()->String.format("id=%s,name=imperative", 
getId()));
 +    }
 +
 +    // -- COMMAND SETUP
 +
 +    protected void setupCommand(
 +            final InteractionHead head,
 +            final Function<UUID, CommandDto> commandDtoFactory) {
 +
 +        var command = 
getInteractionContext().currentInteractionElseFail().getCommand();
 +
 +        _Assert.assertNotNull(command,
 +            "No command available with current thread, "
 +                + "are we missing an interaction context?");
 +
 +        if (command.getCommandDto() != null) {
 +            // guard here to prevent subsequent mixin actions from
 +            // trampling over the command's DTO
 +        } else {
 +            var dto = commandDtoFactory.apply(command.getInteractionId());
 +            command.updater().setCommandDtoAndIdentifier(dto);
 +        }
 +
 +    }
 +
 +    // -- DEPENDENCIES
 +
 +    protected InteractionProvider getInteractionContext() {
 +        return 
getServiceRegistry().lookupServiceElseFail(InteractionProvider.class);
 +    }
 +
-     protected CommandDtoFactory getCommandDtoFactory() {
-         return 
getServiceRegistry().lookupServiceElseFail(CommandDtoFactory.class);
-     }
- 
 +    @Override
 +    public String asciiId() {
 +        return 
getMetaModelContext().getAsciiIdentifierService().asciiIdFor(getId());
 +    }
 +
 +    // -- SERIALIZATION PROXY
 +
 +    protected final Object writeReplace() {
 +        return new SerializationProxy(this);
 +    }
 +
 +    protected final void readObject(final ObjectInputStream stream) throws 
InvalidObjectException {
 +        throw new InvalidObjectException("Proxy required");
 +    }
 +
 +    protected record SerializationProxy(Identifier identifier) implements 
Serializable {
 +        SerializationProxy(final ObjectMember objectMember) {
 +            this(objectMember.getFeatureIdentifier());
 +        }
 +        private Object readResolve() {
 +            return MetaModelContext.instanceElseFail()
 +                .getSpecificationLoader()
 +                .specForLogicalTypeElseFail(identifier.logicalType())
 +                .getMemberElseFail(
 +                        
identifier.getMemberNameAndParameterClassNamesIdentityString());
 +        }
 +    }
 +
 +}
diff --cc core/runtimeservices/src/main/java/module-info.java
index 219035955e9,e8a87d7f84e..f435d98c1ad
--- a/core/runtimeservices/src/main/java/module-info.java
+++ b/core/runtimeservices/src/main/java/module-info.java
@@@ -78,8 -79,8 +78,9 @@@ module org.apache.causeway.core.runtime
      requires spring.tx;
      requires org.apache.causeway.core.codegen.bytebuddy;
      requires spring.aop;
+     requires java.management;
 -
 +    
 +    opens org.apache.causeway.core.runtimeservices;
      opens org.apache.causeway.core.runtimeservices.wrapper;
 -    opens org.apache.causeway.core.runtimeservices.wrapper.proxy; //to 
org.apache.causeway.core.codegen.bytebuddy
 +    opens org.apache.causeway.core.runtimeservices.wrapper.handlers; //to 
org.apache.causeway.core.codegen.bytebuddy
  }
diff --cc 
extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java
index 3d761ece825,98f2b1781d5..3de5bda8de8
--- 
a/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java
+++ 
b/extensions/vw/pdfjs/wicket/ui/src/main/java/org/apache/causeway/extensions/pdfjs/wkt/ui/components/PdfJsViewerPanel.java
@@@ -36,9 -36,9 +36,11 @@@ import org.apache.wicket.request.cycle.
  import org.apache.wicket.request.handler.resource.ResourceRequestHandler;
  import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
  import org.apache.wicket.request.resource.ByteArrayResource;
++import org.jspecify.annotations.NonNull;
  
  import org.apache.causeway.applib.services.user.UserService;
  import org.apache.causeway.applib.value.Blob;
++import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.core.metamodel.object.MmUnwrapUtils;
  import org.apache.causeway.extensions.pdfjs.applib.config.PdfJsConfig;
  import org.apache.causeway.extensions.pdfjs.applib.config.Scale;
@@@ -49,8 -49,9 +51,6 @@@ import org.apache.causeway.viewer.wicke
  import org.apache.causeway.viewer.wicket.ui.util.Wkt;
  import org.apache.causeway.viewer.wicket.ui.util.WktComponents;
  
- import org.jspecify.annotations.NonNull;
 -import lombok.NonNull;
 -import lombok.val;
--
  import 
de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
  
  /**
@@@ -176,35 -177,36 +176,39 @@@ implements IRequestListener 
  
      @Override
      protected MarkupContainer createRegularFrame() {
 -        val blob = getBlob();
 -        if (blob == null) {
 +        var blob = getBlob();
-         if (blob == null) {
++        if (blob == null
++                || _NullSafe.isEmpty(blob.bytes())) { 
              return createShallowRegularFrame();
          }
  
 -        val scalarModel = scalarModel();
 +        var attributeModel = attributeModel();
  
 -        val regularFrame = new WebMarkupContainer(ID_SCALAR_IF_REGULAR);
 +        var regularFrame = new WebMarkupContainer(ID_SCALAR_IF_REGULAR);
  
 -        CharSequence documentUrl = urlFor(
++        var documentUrl = urlFor(
+                 new ListenerRequestHandler(
 -                        new PageAndComponentProvider(getPage(), this))) + 
"&noCache=" + System.currentTimeMillis();
 -        val pdfJsConfig =
 -                scalarModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class)
++                        new PageAndComponentProvider(getPage(), this)))
++                // adds a hash to the URL, such that browser caching works as 
desired 
++                + "&md5=" + blob.md5Hex();
 +        var pdfJsConfig =
-             attributeModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class)
++                
attributeModel.getMetaModel().lookupFacet(PdfJsViewerFacet.class)
                  .map(pdfJsViewerFacet->pdfJsViewerFacet.configFor(buildKey()))
                  .orElseGet(PdfJsConfig::new)
-                 .withDocumentUrl(urlFor(
-                         new ListenerRequestHandler(
-                                 new PageAndComponentProvider(getPage(), 
this))));
+                 .withDocumentUrl(documentUrl);
  
 -        val pdfJsPanel = new PdfJsPanel(ID_SCALAR_VALUE, pdfJsConfig);
 +        var pdfJsPanel = new PdfJsPanel(ID_SCALAR_VALUE, pdfJsConfig);
  
 -        val prevPageButton = createToolbarComponent("prevPage", pdfJsPanel);
 -        val nextPageButton = createToolbarComponent("nextPage", pdfJsPanel);
 -        val currentZoomSelect = createToolbarComponent("currentZoom", 
pdfJsPanel);
 -        val currentPageLabel = createToolbarComponent("currentPage", 
pdfJsPanel);
 -        val totalPagesLabel = createToolbarComponent("totalPages", 
pdfJsPanel);
 +        var prevPageButton = createToolbarComponent("prevPage", pdfJsPanel);
 +        var nextPageButton = createToolbarComponent("nextPage", pdfJsPanel);
 +        var currentZoomSelect = createToolbarComponent("currentZoom", 
pdfJsPanel);
 +        var currentPageLabel = createToolbarComponent("currentPage", 
pdfJsPanel);
 +        var totalPagesLabel = createToolbarComponent("totalPages", 
pdfJsPanel);
  
 -        val currentHeightSelect = createToolbarComponent("currentHeight", 
pdfJsPanel);
 -        val printButton = createToolbarComponent("print", pdfJsPanel);
 +        var currentHeightSelect = createToolbarComponent("currentHeight", 
pdfJsPanel);
 +        var printButton = createToolbarComponent("print", pdfJsPanel);
  
 -        val downloadResourceLink = Wkt.downloadLinkNoCache(ID_DOWNLOAD, 
asBlobResource(blob));
 +        var downloadResourceLink = Wkt.downloadLinkNoCache(ID_DOWNLOAD, 
asBlobResource(blob));
  
          regularFrame.addOrReplace(
                  pdfJsPanel, prevPageButton, nextPageButton, currentPageLabel, 
totalPagesLabel,
@@@ -230,9 -233,9 +234,9 @@@
          if (blob == null) {
              return createShallowCompactFrame();
          }
 -        val compactFrame = new WebMarkupContainer(ID_SCALAR_IF_COMPACT);
 -        val downloadLink = Wkt.add(compactFrame, 
Wkt.downloadLinkNoCache(ID_DOWNLOAD_IF_COMPACT, asBlobResource(blob)));
 -        Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.getName());
 +        var compactFrame = new WebMarkupContainer(ID_SCALAR_IF_COMPACT);
 +        var downloadLink = Wkt.add(compactFrame, 
Wkt.downloadLinkNoCache(ID_DOWNLOAD_IF_COMPACT, asBlobResource(blob)));
-         Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.getName());
++        Wkt.labelAdd(downloadLink, ID_FILE_NAME_IF_COMPACT, blob.name());
          return compactFrame;
      }
  
@@@ -282,28 -285,28 +286,18 @@@
                  new ResourceRequestHandler(asBlobResourceNoCache(blob), 
null));
      }
  
--//    @Override
--//    protected void setupInlinePrompt() {
--//        // not used
--//    }
--//
--//    @Override
--//    protected Component getValidationFeedbackReceiver() {
--//        return null; // not used
--//    }
--
      // -- HELPER
--
++    
      private Blob getBlob() {
 -        return (Blob) MmUnwrapUtils.single(scalarModel().getObject());
 +        return (Blob) MmUnwrapUtils.single(attributeModel().getObject());
      }
  
      private static ByteArrayResource asBlobResource(final @NonNull Blob blob) 
{
--        return new ByteArrayResource(blob.getMimeType().getBaseType(), 
blob.getBytes(), blob.getName());
++        return new ByteArrayResource(blob.mimeType().getBaseType(), 
blob.bytes(), blob.name());
      }
  
      private static ByteArrayResource asBlobResourceNoCache(final @NonNull 
Blob blob) {
--        final byte[] bytes = blob.getBytes();
++        final byte[] bytes = blob.bytes();
          return new ByteArrayResource("application/pdf", bytes) {
              private static final long serialVersionUID = 1L;
              @Override protected void configureResponse(
diff --cc 
regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java
index 3a7ae7b0c64,00000000000..de761d9ffa1
mode 100644,000000..100644
--- 
a/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java
+++ 
b/regressiontests/base-jpa/src/main/java/org/apache/causeway/testdomain/jpa/JpaInventoryManager.java
@@@ -1,62 -1,0 +1,68 @@@
 +/*
 + *  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.causeway.testdomain.jpa;
 +
 +import java.util.List;
 +
 +import jakarta.inject.Inject;
 +
 +import org.apache.causeway.applib.annotation.Action;
 +import org.apache.causeway.applib.annotation.Collection;
 +import org.apache.causeway.applib.annotation.DomainObject;
 +import org.apache.causeway.applib.annotation.Nature;
 +import org.apache.causeway.applib.domain.DomainObjectList.ActionDomainEvent;
 +import org.apache.causeway.applib.services.repository.RepositoryService;
 +import org.apache.causeway.testdomain.jpa.entities.JpaProduct;
 +
 +@DomainObject(nature = Nature.VIEW_MODEL)
 +public class JpaInventoryManager {
 +
 +    @Inject private RepositoryService repository;
 +
 +    // -- UPDATE PRODUCT PRICE
 +
 +    public static class UpdateProductPriceEvent extends ActionDomainEvent {}
 +
 +    @Action(
 +            domainEvent = UpdateProductPriceEvent.class,
 +            choicesFrom = "allProducts")
 +    public JpaProduct updateProductPrice(final JpaProduct product, final 
double newPrice) {
 +        product.setPrice(newPrice);
 +        return product;
 +    }
++    
++    // -- WRAPPER MEMORY LEAK TESTING
++    
++    @Action
++    public void foo() {
++    }
 +
 +    // -- COUNT PRODUCTS
 +
 +    @Action
 +    public int countProducts() {
 +        return getAllProducts().size();
 +    }
 +
 +    @Collection
 +    public List<JpaProduct> getAllProducts() {
 +        return repository.allInstances(JpaProduct.class);
 +    }
 +
 +}
diff --cc 
regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java
index 00000000000,c0c34b16f6f..0b60039686e
mode 000000,100644..100644
--- 
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java
+++ 
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_1_IntegTest.java
@@@ -1,0 -1,148 +1,149 @@@
+ /*
+  *  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.causeway.testdomain.interact;
+ 
 -import javax.inject.Inject;
++import jakarta.inject.Inject;
+ 
+ import org.junit.jupiter.api.Test;
 -import org.springframework.boot.test.context.SpringBootTest;
 -import org.springframework.test.context.TestPropertySource;
+ 
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
+ import static org.junit.jupiter.api.Assertions.assertThrows;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
+ 
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
+ import org.apache.causeway.applib.annotation.Action;
+ import org.apache.causeway.applib.annotation.DomainObject;
+ import org.apache.causeway.applib.annotation.Nature;
+ import org.apache.causeway.applib.services.wrapper.InvalidException;
+ import org.apache.causeway.core.config.presets.CausewayPresets;
+ import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
+ import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+ import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+ import org.apache.causeway.testdomain.conf.Configuration_headless;
+ import 
org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain;
+ import org.apache.causeway.testdomain.model.interaction.InteractionDemo;
+ import 
org.apache.causeway.testdomain.model.interaction.InteractionDemo_biArgEnabled;
+ import 
org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract;
+ 
+ import lombok.Data;
+ import lombok.RequiredArgsConstructor;
+ import lombok.val;
+ 
+ @SpringBootTest(
+         classes = {
+                 Configuration_headless.class,
+                 Configuration_usingInteractionDomain.class,
+                 WrapperInteraction_1_IntegTest.Customer.class,
+                 WrapperInteraction_1_IntegTest.ConcreteMixin.class,
+                 WrapperInteraction_1_IntegTest.ConcreteMixin2.class,
+         }
+ )
+ @TestPropertySource({
+     CausewayPresets.SilenceMetaModel,
+     CausewayPresets.SilenceProgrammingModel
+ })
+ class WrapperInteraction_1_IntegTest
+ extends InteractionTestAbstract {
+ 
+     @Data @DomainObject(nature = Nature.VIEW_MODEL)
+     static class Customer {
+         String name;
+         @Action public String who() { return name; }
+     }
+ 
+     // an abstract mixin class
+     static abstract class MixinAbstract<T extends Object> {
+         public T act(final String startTime, final String endTime) {
+             return null;
+         }
+     }
+ 
+     @Action
+     @RequiredArgsConstructor
+     public static class ConcreteMixin
+     extends MixinAbstract<String> {
+         @SuppressWarnings("unused")
+         private final Customer mixee;
+         @Override
+         public String act(final String startTime, final String endTime) {
+             return "acted";
+         }
+     }
+ 
+     @Action
+     @RequiredArgsConstructor
+     public static class ConcreteMixin2
+     extends MixinAbstract<String> {
+         @SuppressWarnings("unused")
+         private final Customer mixee;
+ 
+         @Override
+         public String act(final String startTime, final String endTime) {
+             return "acted2";
+         }
+     }
+ 
+     @Inject SpecificationLoader specificationLoader;
+ 
+     @Test
+     void mixinMemberNamedFacet_whenSharingSameAbstractMixin() {
+         val objectSpec = 
specificationLoader.specForType(Customer.class).get();
+ 
+         assertEquals(
+                 2L,
+                 objectSpec.streamRuntimeActions(MixedIn.INCLUDED)
+                 .filter(ObjectAction::isMixedIn)
+                 .peek(act->{
+                     //System.out.println("act: " + act);
+                     val memberNamedFacet = 
act.getFacet(MemberNamedFacet.class);
+                     assertNotNull(memberNamedFacet);
+                     assertTrue(memberNamedFacet.getSpecialization().isLeft());
+                 })
+                 .count());
+     }
+ 
+     @Test
+     void mixinActionValidation() {
+         InvalidException cause = assertThrows(InvalidException.class, ()-> {
+             wrapMixin(ConcreteMixin.class, new Customer()).act(null, "17:00");
+         });
+         assertEquals("'Start Time' is mandatory", cause.getMessage());
+ 
+         InvalidException cause2 = assertThrows(InvalidException.class, ()-> {
+             wrapMixin(ConcreteMixin2.class, new Customer()).act(null, 
"17:00");
+         });
+         assertEquals("'Start Time' is mandatory", cause2.getMessage());
+     }
+ 
+     @Test
+     void regularPropertyAccess() {
+         assertEquals("initial", wrapper.wrap(new 
InteractionDemo()).getString2());
+     }
+ 
+     @Test
+     void mixinActionAccess() {
+         assertEquals(3, wrapper.wrapMixin(InteractionDemo_biArgEnabled.class, 
new InteractionDemo()).act(1, 2));
+     }
+ 
+ 
+ }
diff --cc 
regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java
index 00000000000,9855529a747..60e1555cdfd
mode 000000,100644..100644
--- 
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java
+++ 
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_2_IntegTest.java
@@@ -1,0 -1,131 +1,132 @@@
+ /*
+  *  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.causeway.testdomain.interact;
+ 
 -import javax.inject.Inject;
++import jakarta.inject.Inject;
+ 
+ import org.junit.jupiter.api.Test;
 -import org.springframework.boot.test.context.SpringBootTest;
 -import org.springframework.test.context.TestPropertySource;
+ 
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
+ import static org.junit.jupiter.api.Assertions.assertThrows;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
+ 
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
+ import org.apache.causeway.applib.annotation.Action;
+ import org.apache.causeway.applib.annotation.DomainObject;
+ import org.apache.causeway.applib.annotation.Nature;
+ import org.apache.causeway.applib.services.wrapper.InvalidException;
+ import org.apache.causeway.core.config.presets.CausewayPresets;
+ import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
+ import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+ import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+ import org.apache.causeway.testdomain.conf.Configuration_headless;
+ import 
org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain;
+ import 
org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract;
+ 
+ import lombok.Data;
+ import lombok.val;
+ 
+ @SpringBootTest(
+         classes = {
+                 Configuration_headless.class,
+                 Configuration_usingInteractionDomain.class,
+                 WrapperInteraction_2_IntegTest.Customer.class,
+                 WrapperInteraction_2_IntegTest.Customer.ConcreteMixin.class,
+                 WrapperInteraction_2_IntegTest.Customer.ConcreteMixin2.class,
+         }
+ )
+ @TestPropertySource({
+     CausewayPresets.SilenceMetaModel,
+     CausewayPresets.SilenceProgrammingModel
+ })
+ class WrapperInteraction_2_IntegTest
+ extends InteractionTestAbstract {
+ 
+     @Data @DomainObject(nature = Nature.VIEW_MODEL)
+     public static class Customer {
+         String name;
+         @Action public String who() { return name; }
+ 
+         @Action
+         public class ConcreteMixin
+         extends MixinAbstract<String> {
+ 
+             @Override
+             public String act(final String startTime, final String endTime) {
+                 return "acted";
+             }
+         }
+ 
+         @Action
+         public class ConcreteMixin2
+         extends MixinAbstract<String> {
+ 
+             @Override
+             public String act(final String startTime, final String endTime) {
+                 return "acted2";
+             }
+         }
+ 
+     }
+ 
+     // an abstract mixin class
+     static abstract class MixinAbstract<T extends Object> {
+         public T act(final String startTime, final String endTime) {
+             return null;
+         }
+     }
+ 
+     @Inject SpecificationLoader specificationLoader;
+ 
+     @Test
+     void mixinMemberNamedFacet_whenSharingSameAbstractMixin() {
+         val objectSpec = 
specificationLoader.specForType(Customer.class).get();
+ 
+         assertEquals(
+                 2L,
+                 objectSpec.streamRuntimeActions(MixedIn.INCLUDED)
+                 .filter(ObjectAction::isMixedIn)
+                 .peek(act->{
+                     //System.out.println("act: " + act);
+                     val memberNamedFacet = 
act.getFacet(MemberNamedFacet.class);
+                     assertNotNull(memberNamedFacet);
+                     assertTrue(memberNamedFacet.getSpecialization().isLeft());
+                 })
+                 .count());
+     }
+ 
+     @Test
+     void mixinActionValidation() {
+ 
+         InvalidException cause = assertThrows(InvalidException.class, ()-> {
+             wrapMixin(Customer.ConcreteMixin.class, new Customer()).act(null, 
"17:00");
+         });
+         assertEquals("'Start Time' is mandatory", cause.getMessage());
+ 
+         InvalidException cause2 = assertThrows(InvalidException.class, ()-> {
+             wrapMixin(Customer.ConcreteMixin2.class, new 
Customer()).act(null, "17:00");
+         });
+         assertEquals("'Start Time' is mandatory", cause2.getMessage());
+     }
+ 
+ }
diff --cc 
regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java
index 00000000000,cb665b24b80..571710da007
mode 000000,100644..100644
--- 
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java
+++ 
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_3_IntegTest.java
@@@ -1,0 -1,157 +1,158 @@@
+ /*
+  *  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.causeway.testdomain.interact;
+ 
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.stream.Collectors;
+ 
 -import javax.inject.Inject;
++import jakarta.inject.Inject;
+ 
+ import org.assertj.core.api.Assertions;
+ import org.junit.jupiter.api.Test;
 -import org.springframework.boot.test.context.SpringBootTest;
 -import org.springframework.test.context.TestPropertySource;
+ 
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
+ 
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
+ import org.apache.causeway.applib.annotation.Action;
+ import org.apache.causeway.applib.annotation.DomainObject;
+ import org.apache.causeway.applib.annotation.Nature;
+ import org.apache.causeway.applib.annotation.Optionality;
+ import org.apache.causeway.applib.annotation.Property;
+ import org.apache.causeway.core.config.presets.CausewayPresets;
+ import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
+ import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+ import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+ import org.apache.causeway.testdomain.conf.Configuration_headless;
+ import 
org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain;
+ import 
org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract;
+ 
+ import lombok.Data;
+ import lombok.RequiredArgsConstructor;
+ import lombok.val;
+ 
+ @SpringBootTest(
+         classes = {
+                 Configuration_headless.class,
+                 Configuration_usingInteractionDomain.class,
+ 
+                 WrapperInteraction_3_IntegTest.Task.class,
+                 WrapperInteraction_3_IntegTest.Task.Succeeded.class,
+                 WrapperInteraction_3_IntegTest.Task.Failed.class,
+         }
+ )
+ @TestPropertySource({
+     CausewayPresets.SilenceMetaModel,
+     CausewayPresets.SilenceProgrammingModel
+ })
+ class WrapperInteraction_3_IntegTest
+ extends InteractionTestAbstract {
+ 
+     @Data @DomainObject(nature = Nature.VIEW_MODEL)
+     public static class Task {
+ 
+         @RequiredArgsConstructor
+         enum Outcome {
+             SUPER(true),
+             GREAT(true),
+             OK(true),
+             BAD(false),
+             TERRIBLE(false),
+             JUST_GIVE_UP(false),;
+ 
+             final boolean representsSuccess;
+ 
+             public static List<Outcome> successes() {
+                 return Arrays.stream(Outcome.values()).filter(x -> 
x.representsSuccess).collect(Collectors.toList());
+             }
+             public static List<Outcome> failures() {
+                 return Arrays.stream(Outcome.values()).filter(x -> ! 
x.representsSuccess).collect(Collectors.toList());
+             }
+         }
+ 
+         @Property(optionality = Optionality.OPTIONAL)
+         Outcome outcome;
+ 
+         @Action
+         public class Succeeded
+         extends MixinAbstract {
+             public List<Task.Outcome> choices0Act() { return 
Task.Outcome.successes(); }
+         }
+ 
+         @Action
+         public class Failed
+         extends MixinAbstract {
+             public List<Task.Outcome> choices0Act() { return 
Task.Outcome.failures(); }
+         }
+ 
+         // an abstract (inner) mixin class (required public if introspection 
policy does not process private methods)
+         public abstract class MixinAbstract {
+             public Task act(final Task.Outcome outcome) {
+                 Task.this.outcome = outcome;
+                 return Task.this;
+             }
+         }
+     }
+ 
+     @Inject SpecificationLoader specificationLoader;
+ 
+     @Test
+     void mixinMemberNamedFacet_whenSharingSameAbstractMixin() {
+ 
+         val mixinSpec1 = 
specificationLoader.specForType(Task.Succeeded.class).get();
+         val mixinSpec2 = 
specificationLoader.specForType(Task.Failed.class).get();
+ 
+         assertTrue(mixinSpec1.isMixin());
+         assertTrue(mixinSpec2.isMixin());
+ 
+         assertTrue(mixinSpec1.mixinFacetElseFail().isMixinFor(Task.class));
+         assertTrue(mixinSpec2.mixinFacetElseFail().isMixinFor(Task.class));
+ 
+         val objectSpec = specificationLoader.specForType(Task.class).get();
+ 
+         assertEquals(
+                 2L,
+                 objectSpec.streamRuntimeActions(MixedIn.INCLUDED)
+                 .filter(ObjectAction::isMixedIn)
+                 .peek(act->{
+                     val memberNamedFacet = 
act.getFacet(MemberNamedFacet.class);
+                     assertNotNull(memberNamedFacet);
+                     assertTrue(memberNamedFacet.getSpecialization().isLeft());
+                 })
+                 .count());
+     }
+ 
+     @Test
+     void mixinActionValidation() {
+ 
+         final Task task = new Task();
+ 
+         wrapMixin(Task.Succeeded.class, task).act(Task.Outcome.SUPER);
+         
Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.SUPER);
+ 
+         wrapMixin(Task.Failed.class, task).act(Task.Outcome.JUST_GIVE_UP);
+         
Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.JUST_GIVE_UP);
+     }
+ 
+ }
diff --cc 
regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java
index 00000000000,70287bff640..0fc536ade52
mode 000000,100644..100644
--- 
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java
+++ 
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_4_IntegTest.java
@@@ -1,0 -1,151 +1,152 @@@
+ /*
+  *  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.causeway.testdomain.interact;
+ 
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.stream.Collectors;
+ 
 -import javax.inject.Inject;
++import jakarta.inject.Inject;
+ 
+ import org.assertj.core.api.Assertions;
+ import org.junit.jupiter.api.Test;
 -import org.springframework.boot.test.context.SpringBootTest;
 -import org.springframework.test.context.TestPropertySource;
+ 
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotNull;
+ import static org.junit.jupiter.api.Assertions.assertTrue;
+ 
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
+ import org.apache.causeway.applib.annotation.Action;
+ import org.apache.causeway.applib.annotation.DomainObject;
+ import org.apache.causeway.applib.annotation.Nature;
+ import org.apache.causeway.applib.annotation.Optionality;
+ import org.apache.causeway.applib.annotation.Property;
+ import org.apache.causeway.core.config.presets.CausewayPresets;
+ import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
+ import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+ import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+ import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+ import org.apache.causeway.testdomain.conf.Configuration_headless;
+ import 
org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain;
+ import 
org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract;
+ 
+ import lombok.Data;
+ import lombok.RequiredArgsConstructor;
+ import lombok.val;
+ 
+ @SpringBootTest(
+         classes = {
+                 Configuration_headless.class,
+                 Configuration_usingInteractionDomain.class,
+                 WrapperInteraction_4_IntegTest.Task.class,
+                 WrapperInteraction_4_IntegTest.Task.Succeeded.class,
+                 WrapperInteraction_4_IntegTest.Task.Failed.class,
+         }
+ )
+ @TestPropertySource({
+     CausewayPresets.SilenceMetaModel,
+     CausewayPresets.SilenceProgrammingModel
+ })
+ class WrapperInteraction_4_IntegTest
+ extends InteractionTestAbstract {
+ 
+     @Data @DomainObject(nature = Nature.VIEW_MODEL)
+     public static class Task {
+ 
+         @RequiredArgsConstructor
+         enum Outcome {
+             SUPER(true),
+             GREAT(true),
+             OK(true),
+             BAD(false),
+             TERRIBLE(false),
+             JUST_GIVE_UP(false),;
+ 
+             final boolean representsSuccess;
+ 
+             public static List<Outcome> successes() {
+                 return Arrays.stream(Outcome.values()).filter(x -> 
x.representsSuccess).collect(Collectors.toList());
+             }
+             public static List<Outcome> failures() {
+                 return Arrays.stream(Outcome.values()).filter(x -> ! 
x.representsSuccess).collect(Collectors.toList());
+             }
+         }
+ 
+         @Property(optionality = Optionality.OPTIONAL)
+         Outcome outcome;
+ 
+         @Action
+         public static class Succeeded
+         extends MixinAbstract {
+             public Succeeded(final Task task) { super(task); }
+             public List<Task.Outcome> choices0Act() { return 
Task.Outcome.successes(); }
+         }
+ 
+         @Action
+         public static class Failed
+         extends MixinAbstract {
+             public Failed(final Task task) { super(task); }
+             public List<Task.Outcome> choices0Act() { return 
Task.Outcome.failures(); }
+         }
+ 
+         // an abstract mixin class (required public if introspection policy 
does not process private methods)
+         @RequiredArgsConstructor
+         public abstract static class MixinAbstract {
+             private final Task task;
+             public Task act(final Task.Outcome outcome) {
+                 task.outcome = outcome;
+                 return task;
+             }
+         }
+     }
+ 
+     @Inject SpecificationLoader specificationLoader;
+ 
+     @Test
+     void mixinMemberNamedFacet_whenSharingSameAbstractMixin() {
+         val objectSpec = specificationLoader.specForType(Task.class).get();
+ 
+         assertEquals(
+                 2L,
+                 objectSpec.streamRuntimeActions(MixedIn.INCLUDED)
+                 .filter(ObjectAction::isMixedIn)
+                 .peek(act->{
+                     //System.out.println("act: " + act);
+                     val memberNamedFacet = 
act.getFacet(MemberNamedFacet.class);
+                     assertNotNull(memberNamedFacet);
+                     assertTrue(memberNamedFacet.getSpecialization().isLeft());
+                 })
+                 .count());
+     }
+ 
+     @Test
+     void mixinActionValidation() {
+ 
+         final Task task = new Task();
+ 
+         wrapMixin(Task.Succeeded.class, task).act(Task.Outcome.SUPER);
+         
Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.SUPER);
+ 
+         wrapMixin(Task.Failed.class, task).act(Task.Outcome.JUST_GIVE_UP);
+         
Assertions.assertThat(task).extracting(Task::getOutcome).isEqualTo(Task.Outcome.JUST_GIVE_UP);
+     }
+ 
+ }
diff --cc 
regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
index 00000000000,cf2ba4cd808..c8763437802
mode 000000,100644..100644
--- 
a/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
+++ 
b/regressiontests/interact/src/test/java/org/apache/causeway/testdomain/interact/WrapperInteraction_Caching_IntegTest.java
@@@ -1,0 -1,144 +1,141 @@@
+ /*
+  *  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.causeway.testdomain.interact;
+ 
 -import lombok.Data;
 -import lombok.Getter;
 -import lombok.RequiredArgsConstructor;
 -
+ import java.util.concurrent.ExecutionException;
+ import java.util.concurrent.Future;
+ import java.util.concurrent.TimeUnit;
+ import java.util.concurrent.TimeoutException;
+ 
 -import javax.inject.Inject;
++import org.assertj.core.api.Assertions;
++import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Disabled;
++import org.junit.jupiter.api.Test;
++
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
+ 
+ import org.apache.causeway.applib.annotation.Action;
+ import org.apache.causeway.applib.annotation.DomainObject;
+ import org.apache.causeway.applib.annotation.Nature;
+ import org.apache.causeway.applib.annotation.SemanticsOf;
+ import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
+ import org.apache.causeway.core.config.presets.CausewayPresets;
 -import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+ import org.apache.causeway.testdomain.conf.Configuration_headless;
+ import 
org.apache.causeway.testdomain.model.interaction.Configuration_usingInteractionDomain;
+ import 
org.apache.causeway.testdomain.util.interaction.InteractionTestAbstract;
+ 
 -import org.assertj.core.api.Assertions;
 -import org.junit.jupiter.api.BeforeEach;
 -import org.junit.jupiter.api.Disabled;
 -import org.junit.jupiter.api.Test;
 -
 -import org.springframework.boot.test.context.SpringBootTest;
 -import org.springframework.test.context.TestPropertySource;
++import lombok.Data;
++import lombok.Getter;
++import lombok.RequiredArgsConstructor;
+ 
+ @SpringBootTest(
+         classes = {
+                 Configuration_headless.class,
+                 Configuration_usingInteractionDomain.class,
+                 WrapperInteraction_Caching_IntegTest.StatefulCalculator.class,
+                 
WrapperInteraction_Caching_IntegTest.StatefulCalculator_add.class
+         }
+ )
+ @TestPropertySource({
+     CausewayPresets.SilenceMetaModel,
+     CausewayPresets.SilenceProgrammingModel
+ })
+ class WrapperInteraction_Caching_IntegTest
+ extends InteractionTestAbstract {
+ 
+     @Data @DomainObject(nature = Nature.VIEW_MODEL)
+     static class StatefulCalculator {
+         @Getter int total;
+         @Action public Integer inc(final int amount) { return total += 
amount; }
+         @Action(semantics = SemanticsOf.IDEMPOTENT) public void reset() { 
total = 0; }
+     }
+ 
+     @Action
+     @RequiredArgsConstructor
+     public static class StatefulCalculator_add {
+         @SuppressWarnings("unused")
+         private final StatefulCalculator mixee;
+         public Integer act(final int amount) {
+             return mixee.inc(amount);
+         }
+     }
+ 
+     StatefulCalculator calculator1;
+     StatefulCalculator calculator2;
+ 
+     @BeforeEach
+     void before() {
+         calculator1 = new StatefulCalculator();
+         calculator2 = new StatefulCalculator();
+ 
+         Assertions.assertThat(calculator1.total).isEqualTo(0);
+         Assertions.assertThat(calculator2.total).isEqualTo(0);
+     }
+ 
+     @Test
+     void sync_wrapped() throws ExecutionException, InterruptedException, 
TimeoutException {
+ 
+         // when
+         wrap(calculator1).inc(5);
+         wrap(calculator2).inc(10);
+ 
+         // then
+         Assertions.assertThat(calculator1.total).isEqualTo(5);
+         Assertions.assertThat(calculator2.total).isEqualTo(10);
+     }
+ 
+     @Test
+     void sync_mixin() throws ExecutionException, InterruptedException, 
TimeoutException {
+ 
+         // when
+         wrapMixin(StatefulCalculator_add.class, calculator1).act(5);
+         wrapMixin(StatefulCalculator_add.class, calculator2).act(10);
+ 
+         // then
+         Assertions.assertThat(calculator1.total).isEqualTo(5);
+         Assertions.assertThat(calculator2.total).isEqualTo(10);
+     }
+ 
+     @Disabled
+     @Test
+     void async_wrapped() throws ExecutionException, InterruptedException, 
TimeoutException {
+ 
+         // when
+         AsyncControl<Integer> asyncControlForCalculator1 = 
AsyncControl.returning(Integer.class);
+         StatefulCalculator asyncCalculator1 = 
wrapperFactory.asyncWrap(calculator1, asyncControlForCalculator1);
+ 
+         AsyncControl<Integer> asyncControlForCalculator2 = 
AsyncControl.returning(Integer.class);
+         StatefulCalculator asyncCalculator2 = 
wrapperFactory.asyncWrap(calculator2, asyncControlForCalculator2);
+ 
+         asyncCalculator1.inc(12);
+         asyncCalculator2.inc(24);
+ 
+         // then
+         Future<Integer> future = asyncControlForCalculator1.getFuture();
+         Integer i = future.get(10, TimeUnit.SECONDS);
+         Assertions.assertThat(i.intValue()).isEqualTo(12);
+         Assertions.assertThat(calculator1.getTotal()).isEqualTo(12);
+ 
+         Assertions.assertThat(asyncControlForCalculator2.getFuture().get(10, 
TimeUnit.SECONDS).intValue()).isEqualTo(24);
+         Assertions.assertThat(calculator2.getTotal()).isEqualTo(24);
+     }
+ 
+ 
+ }
diff --cc 
regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/JpaWrapperSyncTest.java
index 00000000000,00000000000..a62821d1e69
new file mode 100644
--- /dev/null
+++ 
b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/JpaWrapperSyncTest.java
@@@ -1,0 -1,0 +1,90 @@@
++/*
++ *  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.causeway.testdomain.persistence.jpa.wrapper;
++
++import jakarta.inject.Inject;
++
++import org.junit.jupiter.api.AfterEach;
++import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Test;
++
++import static org.junit.jupiter.api.Assertions.assertEquals;
++
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
++import org.apache.causeway.applib.services.repository.RepositoryService;
++import org.apache.causeway.applib.services.wrapper.WrapperFactory;
++import org.apache.causeway.core.config.presets.CausewayPresets;
++import org.apache.causeway.testdomain.fixtures.EntityTestFixtures;
++import org.apache.causeway.testdomain.jpa.JpaInventoryManager;
++import org.apache.causeway.testdomain.jpa.JpaTestFixtures;
++import org.apache.causeway.testdomain.jpa.conf.Configuration_usingJpa;
++import org.apache.causeway.testdomain.jpa.entities.JpaBook;
++import org.apache.causeway.testdomain.jpa.entities.JpaProduct;
++import 
org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
++
++import lombok.val;
++
++@SpringBootTest(
++        classes = {
++                Configuration_usingJpa.class
++        },
++        properties = {
++                "spring.datasource.url=jdbc:h2:mem:JpaWrapperSyncTest"
++        }
++)
++@TestPropertySource(CausewayPresets.UseLog4j2Test)
++class JpaWrapperSyncTest extends CausewayIntegrationTestAbstract {
++
++    @Inject private RepositoryService repository;
++    @Inject private WrapperFactory wrapper;
++    @Inject private JpaTestFixtures testFixtures;
++
++    protected EntityTestFixtures.Lock lock;
++
++    @BeforeEach
++    void installFixture() {
++        this.lock = testFixtures.aquireLock();
++        lock.install();
++    }
++
++    @AfterEach
++    void uninstallFixture() {
++        this.lock.release();
++    }
++
++    @Test
++    void testWrapper_waitingOnDomainEvent() {
++
++        val inventoryManager = 
factoryService.viewModel(JpaInventoryManager.class);
++        val sumOfPrices = repository.allInstances(JpaProduct.class)
++                .stream()
++                .mapToDouble(JpaProduct::getPrice)
++                .sum();
++
++        assertEquals(167d, sumOfPrices, 1E-6);
++
++        val products = wrapper.wrap(inventoryManager).getAllProducts();
++
++        assertEquals(3, products.size());
++        assertEquals(JpaBook.class, products.get(0).getClass());
++    }
++
++}
diff --cc 
regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
index 00000000000,00000000000..18dd21de935
new file mode 100644
--- /dev/null
+++ 
b/regressiontests/persistence-jpa/src/test/java/org/apache/causeway/testdomain/persistence/jpa/wrapper/WrapperFactoryMetaspaceMemoryLeakTest.java
@@@ -1,0 -1,0 +1,107 @@@
++/*
++ *  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.causeway.testdomain.persistence.jpa.wrapper;
++
++import jakarta.inject.Inject;
++
++import org.junit.jupiter.api.AfterEach;
++import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Test;
++
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.test.context.TestPropertySource;
++
++import org.apache.causeway.applib.services.wrapper.WrapperFactory;
++import org.apache.causeway.commons.internal.base._Blackhole;
++import org.apache.causeway.commons.internal.debug._MemoryUsage;
++import org.apache.causeway.core.config.presets.CausewayPresets;
++import org.apache.causeway.testdomain.fixtures.EntityTestFixtures;
++import org.apache.causeway.testdomain.jpa.JpaInventoryManager;
++import org.apache.causeway.testdomain.jpa.JpaTestFixtures;
++import org.apache.causeway.testdomain.jpa.conf.Configuration_usingJpa;
++import org.apache.causeway.testdomain.jpa.entities.JpaProduct;
++import 
org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;
++
++@SpringBootTest(
++        classes = {
++                Configuration_usingJpa.class,
++        },
++        properties = {
++                "spring.datasource.url=jdbc:h2:mem:JpaWrapperSyncTest"
++        }
++)
++@TestPropertySource(CausewayPresets.UseLog4j2Test)
++class WrapperFactoryMetaspaceMemoryLeakTest extends 
CausewayIntegrationTestAbstract {
++
++    @Inject private WrapperFactory wrapper;
++    @Inject private JpaTestFixtures testFixtures;
++
++    protected EntityTestFixtures.Lock lock;
++
++    @BeforeEach
++    void installFixture() {
++        this.lock = testFixtures.aquireLock();
++        lock.install();
++    }
++
++    @AfterEach
++    void uninstallFixture() {
++        this.lock.release();
++    }
++
++    @Test
++    void testWrapper_waitingOnDomainEvent() throws InterruptedException {
++        _MemoryUsage.measureMetaspace("exercise", ()->{
++// with caching
++//            exercise(1, 0);         // 2,221 KB
++//            exercise(1, 2000);      // 3,839 KB. // some leakage from 
collections
++//            exercise(20, 0);        // 2,112 KB
++//            exercise(20, 2000);     // 3,875 KB
++//            exercise(2000, 0);      // 3,263 KB. // ? increased some, is it 
significant; a lot less than without caching
++//            exercise(2000, 200);    // 4,294 KB.
++//            exercise(20000, 0);     // 3,243 KB  // no noticeable leakage 
compared to 2000; MUCH less than without caching
++
++// without caching
++//            exercise(1, 0);        //   2,244 KB
++//            exercise(1, 2000);     //.  3,669 KB // some leakage from 
collections
++//            exercise(20, 0);       //   2,440 KB
++//            exercise(20, 2000);    //.  4,286 KB
++            exercise(2000, 0);     //  14,580 KB // significant leakage from 
20
++//            exercise(2000, 200);   //  20,423 KB
++//            exercise(20000, 0);    //.115,729 KB
++        });
++    }
++
++    private void exercise(int instances, int loops) {
++        for (int i = 0; i < instances; i++) {
++            var jpaInventoryManager = 
wrapper.wrap(factoryService.viewModel(JpaInventoryManager.class));
++            jpaInventoryManager.foo();
++
++            for (var j = 0; j < loops; j++) {
++                jpaInventoryManager
++                    .getAllProducts()
++                    .stream()
++                    .map(JpaProduct::getName)
++                    .forEach(_Blackhole::consume);
++            }
++        }
++    }
++}
++
++

Reply via email to