You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by ol...@apache.org on 2026/10/06 15:51:14 UTC

(daffodil) branch main updated: Inline MStack operations and size stacks to typical depth

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3430c5f9f Inline MStack operations and size stacks to typical depth
3430c5f9f is described below

commit 3430c5f9fa14c51a621b854ee096e7b3cc93a357
Author: olabusayoT <50...@users.noreply.github.com>
AuthorDate: Mon Oct 5 15:46:12 2026 -0400

    Inline MStack operations and size stacks to typical depth
    
    MStack's @specialized annotation has no effect under Scala 3, so push, pop, setTop and top on the Boolean, Int, Long and reference stacks boxed the element and went through type-dispatching array access. Make push, pop, setTop, top and bottom inline, so each call site expands with the stack's concrete element type and compiles to direct array loads and stores.
    
    Stacks take an initialSize, 32 by default. A diagnostic, off unless MStack.trackMaxSizeReached is set, records the deepest a stack has grown, for choosing a size; the flag is an inline val so the tracking compiles away when off. TestMStack covers the operations, growth and copyFrom.
    
    Stacks whose depth follows element, array or occurs nesting start at 16, and those tracking namespace bindings, JSON array names and unparse escape schemes start at 8. The parse escape scheme stack, points of uncertainty and input stream marks start at 16. Stacks that can fill past 16, such as the delimiter stacks, group index stacks, infoset walker and inputter, and next element resolver, keep 32. Stacks cloned for a suspended unparse are sized from the stack they copy.
    
    DAFFODIL-3065
---
 .../daffodil/io/InputSourceDataInputStream.scala   |   2 +-
 .../org/apache/daffodil/lib/util/MStack.scala      |  92 ++++++++++--------
 .../runtime1/infoset/JDOMInfosetInputter.scala     |   2 +-
 .../runtime1/infoset/JDOMInfosetOutputter.scala    |   2 +-
 .../runtime1/infoset/JsonInfosetInputter.scala     |   2 +-
 .../runtime1/infoset/JsonInfosetOutputter.scala    |   2 +-
 .../runtime1/infoset/ScalaXMLInfosetInputter.scala |   2 +-
 .../infoset/ScalaXMLInfosetOutputter.scala         |   2 +-
 .../runtime1/infoset/W3CDOMInfosetInputter.scala   |   2 +-
 .../runtime1/infoset/W3CDOMInfosetOutputter.scala  |   2 +-
 .../DaffodilParseOutputStreamContentHandler.scala  |   4 +-
 .../DaffodilUnparseContentHandlerImpl.scala        |   2 +-
 .../runtime1/processors/parsers/PState.scala       |  10 +-
 .../runtime1/processors/unparsers/UState.scala     |  14 +--
 .../org/apache/daffodil/lib/util/TestMStack.scala  | 106 +++++++++++++++++++++
 15 files changed, 181 insertions(+), 65 deletions(-)

diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/io/InputSourceDataInputStream.scala b/daffodil-core/src/main/scala/org/apache/daffodil/io/InputSourceDataInputStream.scala
index 8e39d7db4..b9e05b3d9 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/io/InputSourceDataInputStream.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/io/InputSourceDataInputStream.scala
@@ -111,7 +111,7 @@ final class InputSourceDataInputStream private (val inputSource: InputSource)
   }
 
   override final val cst: MarkState = new MarkState
-  val markStack = new MStackOf[MarkState]
+  val markStack = new MStackOf[MarkState](16)
   val markPool = new MarkPool()
 
   /**
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/lib/util/MStack.scala b/daffodil-core/src/main/scala/org/apache/daffodil/lib/util/MStack.scala
index 6c25a867b..6beea3738 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/lib/util/MStack.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/lib/util/MStack.scala
@@ -24,6 +24,12 @@ import Maybe.*
 object MStack {
   final case class Mark(val v: Int) extends AnyVal
   val nullMark = Mark(0)
+
+  /**
+   * Off by default: growing past initialSize isn't itself wrong, so paying
+   * this bookkeeping cost on every push isn't worth it normally.
+   */
+  final inline val trackMaxSizeReached = false
 }
 
 /**
@@ -33,35 +39,25 @@ object MStack {
  * catches improper initialization. These were not initializing properly,
  * so the idiom evolved to use the scala initializers.
  */
-final class MStackOfBoolean private ()
-  extends MStack[Boolean]((n: Int) => new Array[Boolean](n), false)
+final class MStackOfBoolean private (initialSize: Int)
+  extends MStack[Boolean]((n: Int) => new Array[Boolean](n), false, initialSize)
 
 object MStackOfBoolean {
-  def apply() = {
-    val stk = new MStackOfBoolean()
-    stk.init()
-    stk
-  }
+  def apply(initialSize: Int = 32) = new MStackOfBoolean(initialSize)
 }
 
-final class MStackOfInt extends MStack[Int]((n: Int) => new Array[Int](n), 0)
+final class MStackOfInt(initialSize: Int)
+  extends MStack[Int]((n: Int) => new Array[Int](n), 0, initialSize)
 
 object MStackOfInt {
-  def apply() = {
-    val stk = new MStackOfInt()
-    stk.init()
-    stk
-  }
+  def apply(initialSize: Int = 32) = new MStackOfInt(initialSize)
 }
 
-final class MStackOfLong extends MStack[Long]((n: Int) => new Array[Long](n), 0L)
+final class MStackOfLong(initialSize: Int)
+  extends MStack[Long]((n: Int) => new Array[Long](n), 0L, initialSize)
 
 object MStackOfLong {
-  def apply() = {
-    val stk = new MStackOfLong()
-    stk.init()
-    stk
-  }
+  def apply(initialSize: Int = 32) = new MStackOfLong(initialSize)
 }
 
 /**
@@ -75,11 +71,11 @@ object MStackOfLong {
  * So we use an Array[AnyRef] as the representation here, and we
  * convert null to Nope, and an actual object reference to One(x)
  */
-final class MStackOfMaybe[T <: AnyRef] {
+final class MStackOfMaybe[T <: AnyRef](initialSize: Int = 32) {
 
   override def toString = delegate.toString
 
-  private val delegate = new MStackOf[T]
+  private val delegate = new MStackOf[T](initialSize)
   private val nullT = null.asInstanceOf[T]
 
   def copyFrom(other: MStackOfMaybe[T]) = delegate.copyFrom(other.delegate)
@@ -120,6 +116,7 @@ final class MStackOfMaybe[T <: AnyRef] {
   def toListMaybe = delegate.toList.map { (x: AnyRef) =>
     Maybe(x) // Scala compiler bug without this cast
   }
+  def maxSizeReached = delegate.maxSizeReached
 }
 
 /**
@@ -135,7 +132,7 @@ final class MStackOfMaybe[T <: AnyRef] {
  * an object reference or null, and call Maybe(thing) explicitly outside the
  * iteration. Maybe(null) is Nope, and Maybe(thing) is One(thing) if thing is not null.
  */
-final class MStackOf[T <: AnyRef] extends Serializable {
+final class MStackOf[T <: AnyRef](initialSize: Int = 32) extends Serializable {
 
   override def toString = delegate.toString
 
@@ -143,7 +140,7 @@ final class MStackOf[T <: AnyRef] extends Serializable {
 
   @inline final def length = delegate.length
 
-  private val delegate = MStackOfAnyRef()
+  private val delegate = MStackOfAnyRef(initialSize)
 
   @inline final def mark = delegate.mark
   @inline final def reset(m: MStack.Mark) = delegate.reset(m)
@@ -156,6 +153,7 @@ final class MStackOf[T <: AnyRef] extends Serializable {
   @inline final def isEmpty = delegate.isEmpty
   def clear() = delegate.clear()
   def toList = delegate.toList
+  def maxSizeReached = delegate.maxSizeReached
 
   def iterator = delegate.iterator.asInstanceOf[ResettableIterator[T]]
 
@@ -163,15 +161,15 @@ final class MStackOf[T <: AnyRef] extends Serializable {
 
 }
 
-private[util] final class MStackOfAnyRef private ()
-  extends MStack[AnyRef]((n: Int) => new Array[AnyRef](n), null.asInstanceOf[AnyRef])
+private[util] final class MStackOfAnyRef private (initialSize: Int)
+  extends MStack[AnyRef](
+    (n: Int) => new Array[AnyRef](n),
+    null.asInstanceOf[AnyRef],
+    initialSize
+  )
 
 object MStackOfAnyRef {
-  def apply() = {
-    val stk = new MStackOfAnyRef()
-    stk.init()
-    stk
-  }
+  def apply(initialSize: Int = 32) = new MStackOfAnyRef(initialSize)
 }
 
 /**
@@ -184,16 +182,22 @@ object MStackOfAnyRef {
  */
 protected abstract class MStack[@specialized T] private[util] (
   arrayAllocator: (Int) => Array[T],
-  nullValue: T
+  nullValue: T,
+  initialSize: Int = 32
 ) {
 
   private var index = 0
-  private var table: Array[T] = null
+  private var table: Array[T] = arrayAllocator(initialSize)
 
-  def init(): Unit = {
-    index = 0
-    table = arrayAllocator(32)
-  }
+  private var maxSizeReached_ = 0
+
+  /**
+   * The largest this stack's length has ever grown to, across its whole
+   * lifetime (pops don't reduce it). Diagnostic only, for checking whether
+   * an initialSize is well-chosen. Always 0 unless
+   * MStack.trackMaxSizeReached is enabled.
+   */
+  final def maxSizeReached: Int = maxSizeReached_
 
   def copyFrom(other: MStack[T]): Unit = {
     this.index = other.index
@@ -212,6 +216,9 @@ protected abstract class MStack[@specialized T] private[util] (
       }
 
     }
+    if (MStack.trackMaxSizeReached && other.maxSizeReached_ > this.maxSizeReached_) {
+      this.maxSizeReached_ = other.maxSizeReached_
+    }
   }
   // private var currentIteratorIndex = -1
 
@@ -250,10 +257,13 @@ protected abstract class MStack[@specialized T] private[util] (
    *
    *  @param x The element to push
    */
-  @inline final def push(x: T): Unit = {
+  inline def push(x: T): Unit = {
     if (index == table.length) table = growArray(table)
     table(index) = x
     index += 1
+    if (MStack.trackMaxSizeReached && index > maxSizeReached_) {
+      maxSizeReached_ = index
+    }
   }
 
   /**
@@ -261,7 +271,7 @@ protected abstract class MStack[@specialized T] private[util] (
    *
    *  @return the element on top of the stack
    */
-  @inline final def pop(): T = {
+  inline def pop(): T = {
     if (index == 0) Assert.usageError("Stack empty")
     index -= 1
     val x = table(index)
@@ -275,7 +285,7 @@ protected abstract class MStack[@specialized T] private[util] (
    *
    * @param x The element to set to the top of the stack
    */
-  @inline final def setTop(x: T): Unit = {
+  inline def setTop(x: T): Unit = {
     if (index == 0) Assert.usageError("Stack empty")
     table(index - 1) = x
   }
@@ -288,9 +298,9 @@ protected abstract class MStack[@specialized T] private[util] (
    *
    *  @return the element on top of the stack.
    */
-  @inline final def top: T = table(index - 1)
+  inline def top: T = table(index - 1)
 
-  @inline final def bottom: T = table(0)
+  inline def bottom: T = table(0)
 
   @inline final def isEmpty: Boolean = index == 0
 
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetInputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetInputter.scala
index 8f89e283c..2bdb324cc 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetInputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetInputter.scala
@@ -57,7 +57,7 @@ class JDOMInfosetInputter(doc: Document) extends api.infoset.InfosetInputter {
    * Element.
    */
   private val stack = {
-    val s = new MStackOf[(Element, Iterator[Content])]
+    val s = new MStackOf[(Element, Iterator[Content])](16)
     val docChildren = doc.getContent.iterator
     if (!docChildren.hasNext) {
       throw new InvalidInfosetException("Document does not contain a root element")
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetOutputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetOutputter.scala
index d0b8c431d..e7b5bfcb1 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetOutputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JDOMInfosetOutputter.scala
@@ -30,7 +30,7 @@ import org.apache.daffodil.lib.xml.XMLUtils
 
 class JDOMInfosetOutputter extends JJDOMInfosetOutputter {
 
-  private val stack = new MStackOf[org.jdom2.Parent]
+  private val stack = new MStackOf[org.jdom2.Parent](16)
   private var result: Maybe[org.jdom2.Document] = Maybe.Nope
   private val xsiNS = org.jdom2.Namespace.getNamespace("xsi", XMLUtils.XSI_NAMESPACE.toString)
 
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetInputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetInputter.scala
index a76b4adc3..2d0c70466 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetInputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetInputter.scala
@@ -53,7 +53,7 @@ class JsonInfosetInputter(input: java.io.InputStream) extends api.infoset.Infose
    * array we enter is pushed onto this stack and is used as the name for all
    * StartElement events immediately inside that array.
    */
-  private val arrayNameStack = new MStackOf[String]()
+  private val arrayNameStack = new MStackOf[String](8)
 
   /**
    * The jackson library has no concept of an end event for values (which
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetOutputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetOutputter.scala
index 861f49f48..2f5e153ed 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetOutputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/JsonInfosetOutputter.scala
@@ -48,7 +48,7 @@ class JsonInfosetOutputter private (writer: java.io.BufferedWriter, pretty: Bool
   // used to determine if a comma should be written. We write commas before
   // writing start content if it is not the first child (i.e. top of the stack
   // is false).
-  private val isFirstChildStack = MStackOfBoolean()
+  private val isFirstChildStack = MStackOfBoolean(16)
 
   private val stringEncoder = JsonStringEncoder.getInstance()
 
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetInputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetInputter.scala
index f8ecbf990..27175d448 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetInputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetInputter.scala
@@ -47,7 +47,7 @@ class ScalaXMLInfosetInputter(rootNode: Node) extends api.infoset.InfosetInputte
    * Start/EndDocument events.
    */
   private val stack = {
-    val s = new MStackOf[(Elem, Iterator[Node])]
+    val s = new MStackOf[(Elem, Iterator[Node])](16)
 
     val iter = rootNode match {
       case e: Elem => e.iterator
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetOutputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetOutputter.scala
index e30534fcc..424842b6b 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetOutputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/ScalaXMLInfosetOutputter.scala
@@ -35,7 +35,7 @@ import org.apache.daffodil.lib.xml.XMLUtils
 class ScalaXMLInfosetOutputter(showFreedInfo: Boolean = false)
   extends JScalaXMLInfosetOutputter {
 
-  protected val stack = new MStackOf[ListBuffer[scala.xml.Node]]
+  protected val stack = new MStackOf[ListBuffer[scala.xml.Node]](16)
   private var resultNode: Maybe[scala.xml.Node] = Maybe.Nope
 
   def reset()
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetInputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetInputter.scala
index 9cedb5f83..e2cb4d1c6 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetInputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetInputter.scala
@@ -52,7 +52,7 @@ class W3CDOMInfosetInputter(doc: Document) extends api.infoset.InfosetInputter {
    * Element.
    */
   private val stack = {
-    val s = new MStackOf[(Element, Iterator[Node])]
+    val s = new MStackOf[(Element, Iterator[Node])](16)
     val iterator = new IterableNodeList(doc.getChildNodes)
     if (!iterator.hasNext) {
       throw new InvalidInfosetException("Document does not contain a root element")
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetOutputter.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetOutputter.scala
index b362842b7..556cae380 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetOutputter.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/infoset/W3CDOMInfosetOutputter.scala
@@ -37,7 +37,7 @@ import org.w3c.dom.Node
 class W3CDOMInfosetOutputter extends JW3CDOMInfosetOutputter {
 
   private var document: Document = null
-  private val stack = new MStackOf[Node]
+  private val stack = new MStackOf[Node](16)
   private var result: Maybe[Document] = Maybe.Nope
 
   def reset()
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilParseOutputStreamContentHandler.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilParseOutputStreamContentHandler.scala
index 6cb597e73..88b1c90d6 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilParseOutputStreamContentHandler.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilParseOutputStreamContentHandler.scala
@@ -62,9 +62,9 @@ class DaffodilParseOutputStreamContentHandler(out: OutputStream, pretty: Boolean
    * used to maintain the correct scope of activePrefixMapping throughout processing. It is also used
    * to reset the activePrefixMapping after processing each element.
    */
-  private lazy val activePrefixMappingContextStack = new MStackOf[NamespaceBinding]
+  private lazy val activePrefixMappingContextStack = new MStackOf[NamespaceBinding](8)
   private val outputNewlineStack: MStackOfBoolean = {
-    val s = MStackOfBoolean()
+    val s = MStackOfBoolean(16)
     s.push(false)
     s
   }
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilUnparseContentHandlerImpl.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilUnparseContentHandlerImpl.scala
index 4a13d5a07..9b6d4a082 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilUnparseContentHandlerImpl.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/DaffodilUnparseContentHandlerImpl.scala
@@ -137,7 +137,7 @@ class DaffodilUnparseContentHandlerImpl(dp: DFDL.DataProcessor, output: DFDL.Out
    * added in startElement() so they can be quickly removed in endElement() by
    * popping off the stack.
    */
-  private lazy val prefixMappingTrackingStack = new MStackOf[NamespaceBinding]
+  private lazy val prefixMappingTrackingStack = new MStackOf[NamespaceBinding](8)
 
   /**
    * SAXInfosetEvents are buffered into this array before sent to the
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/PState.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/PState.scala
index 521dc2d1f..bfec3ede7 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/PState.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/parsers/PState.scala
@@ -114,8 +114,8 @@ object MPState {
 
 class MPState private () {
 
-  val arrayIterationIndexStack = MStackOfLong()
-  val occursIndexStack = MStackOfLong()
+  val arrayIterationIndexStack = MStackOfLong(16)
+  val occursIndexStack = MStackOfLong(16)
 
   def moveOverOneArrayIterationIndexOnly() =
     arrayIterationIndexStack.push(arrayIterationIndexStack.pop() + 1)
@@ -131,7 +131,7 @@ class MPState private () {
 
   // TODO: it doesn't look anything is actually reading the value of childindex
   // stack. Can we get rid of it?
-  val childIndexStack = MStackOfLong()
+  val childIndexStack = MStackOfLong(16)
   def moveOverOneElementChildOnly() = childIndexStack.push(childIndexStack.pop() + 1)
   def childPos = {
     val res = childIndexStack.top
@@ -141,7 +141,7 @@ class MPState private () {
   val delimiters = new ArrayBuffer[DFADelimiter]
   val delimitersLocalIndexStack = MStackOfInt()
 
-  val escapeSchemeEVCache = new MStackOfMaybe[EscapeSchemeParserHelper]
+  val escapeSchemeEVCache = new MStackOfMaybe[EscapeSchemeParserHelper](16)
 
   private def init(): Unit = {
     arrayIterationIndexStack.push(1L)
@@ -225,7 +225,7 @@ final class PState private (
    * discard/resetTo/resolvePointOfUncertainty functions to simplify this
    * logic.
    */
-  val pointsOfUncertainty = new MStackOf[PState.Mark]()
+  val pointsOfUncertainty = new MStackOf[PState.Mark](16)
 
   override def dataStream = One(dataInputStream)
 
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/unparsers/UState.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/unparsers/UState.scala
index 27a438187..d60290ce7 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/unparsers/UState.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/unparsers/UState.scala
@@ -554,7 +554,7 @@ final class UStateMain private (
         // reallyjust need the top for cloning for suspensions, but that
         // requires changes to how the escape schema cache is accessed, which
         // isn't a trivial change.
-        val esClone = new MStackOfMaybe[EscapeSchemeUnparserHelper]()
+        val esClone = new MStackOfMaybe[EscapeSchemeUnparserHelper](escapeSchemeEVCache.length)
         esClone.copyFrom(escapeSchemeEVCache)
         Maybe(esClone)
       } else {
@@ -564,7 +564,7 @@ final class UStateMain private (
       if (!delimiterStack.isEmpty) {
         // If there are any delimiters, then we need to clone them all since
         // they may be needed for escaping
-        val dsClone = new MStackOf[DelimiterStackUnparseNode]()
+        val dsClone = new MStackOf[DelimiterStackUnparseNode](delimiterStack.length)
         dsClone.copyFrom(delimiterStack)
         Maybe(dsClone)
       } else {
@@ -663,15 +663,15 @@ final class UStateMain private (
     if (currentInfosetNodeStack.isEmpty) Nope
     else currentInfosetNodeStack.top
 
-  override val currentInfosetNodeStack = new MStackOfMaybe[DINode]
+  override val currentInfosetNodeStack = new MStackOfMaybe[DINode](16)
 
-  override val arrayIterationIndexStack = MStackOfLong()
+  override val arrayIterationIndexStack = MStackOfLong(16)
   arrayIterationIndexStack.push(1L)
   override def moveOverOneArrayIterationIndexOnly() =
     arrayIterationIndexStack.setTop(arrayIterationIndexStack.top + 1)
   override def arrayIterationPos = arrayIterationIndexStack.top
 
-  override val occursIndexStack = MStackOfLong()
+  override val occursIndexStack = MStackOfLong(16)
   occursIndexStack.push(1L)
   override def moveOverOneOccursIndexOnly() = occursIndexStack.setTop(occursIndexStack.top + 1)
   override def occursPos = occursIndexStack.top
@@ -683,12 +683,12 @@ final class UStateMain private (
 
   // TODO: it doesn't look anything is actually reading the value of childindex
   // stack. Can we get rid of it?
-  override val childIndexStack = MStackOfLong()
+  override val childIndexStack = MStackOfLong(16)
   childIndexStack.push(1L)
   override def moveOverOneElementChildOnly() = childIndexStack.setTop(childIndexStack.top + 1)
   override def childPos = childIndexStack.top
 
-  override lazy val escapeSchemeEVCache = new MStackOfMaybe[EscapeSchemeUnparserHelper]
+  override lazy val escapeSchemeEVCache = new MStackOfMaybe[EscapeSchemeUnparserHelper](8)
 
   val delimiterStack = new MStackOf[DelimiterStackUnparseNode]()
   override def pushDelimiters(node: DelimiterStackUnparseNode) = delimiterStack.push(node)
diff --git a/daffodil-core/src/test/scala/org/apache/daffodil/lib/util/TestMStack.scala b/daffodil-core/src/test/scala/org/apache/daffodil/lib/util/TestMStack.scala
index b4cebcdbc..5905e6deb 100644
--- a/daffodil-core/src/test/scala/org/apache/daffodil/lib/util/TestMStack.scala
+++ b/daffodil-core/src/test/scala/org/apache/daffodil/lib/util/TestMStack.scala
@@ -17,6 +17,11 @@
 
 package org.apache.daffodil.lib.util
 
+import org.apache.daffodil.lib.util.Maybe.*
+
+import org.junit.Assert.*
+import org.junit.Test
+
 /**
  * Compare MStack performance to ArrayStack. It should be faster for primitives
  */
@@ -24,6 +29,107 @@ class TestMStack {
 
   var junk: Long = 0
 
+  // Copying into a sized destination must match copying into a default one.
+  @Test def testMStackOfCopyFromSizedDestinationMatchesDefault(): Unit = {
+    val source = new MStackOf[String]
+    source.push("a")
+    source.push("b")
+    source.push("c")
+
+    val sizedDest = new MStackOf[String](source.length)
+    sizedDest.copyFrom(source)
+
+    val defaultDest = new MStackOf[String]
+    defaultDest.copyFrom(source)
+
+    assertEquals(defaultDest.toList, sizedDest.toList)
+    assertEquals(3, sizedDest.length)
+    assertEquals("c", sizedDest.top)
+  }
+
+  // A sized-to-exact-depth destination must still grow past capacity.
+  @Test def testMStackOfSizedDestinationStillGrowsCorrectly(): Unit = {
+    val source = new MStackOf[String]
+    source.push("a")
+    source.push("b")
+
+    val sizedDest = new MStackOf[String](source.length)
+    sizedDest.copyFrom(source)
+
+    sizedDest.push("c")
+    sizedDest.push("d")
+    sizedDest.push("e")
+
+    assertEquals(5, sizedDest.length)
+    assertEquals("e", sizedDest.top)
+    assertEquals(List("e", "d", "c", "b", "a"), sizedDest.toList)
+  }
+
+  /** Same sized-clone pattern, but for MStackOfMaybe. */
+  @Test def testMStackOfMaybeCopyFromSizedDestinationMatchesDefault(): Unit = {
+    val source = new MStackOfMaybe[String]
+    source.push(One("x"))
+    source.push(Nope)
+    source.push(One("z"))
+
+    val sizedDest = new MStackOfMaybe[String](source.length)
+    sizedDest.copyFrom(source)
+
+    val defaultDest = new MStackOfMaybe[String]
+    defaultDest.copyFrom(source)
+
+    assertEquals(defaultDest.toListMaybe, sizedDest.toListMaybe)
+    assertEquals(3, sizedDest.length)
+    assertEquals(One("z"), sizedDest.top)
+    assertEquals(One("z"), sizedDest.pop)
+    assertEquals(Nope, sizedDest.pop)
+    assertEquals(One("x"), sizedDest.pop)
+    assertTrue(sizedDest.isEmpty)
+  }
+
+  /** Same sized-construction pattern, for the primitive-specialized variants. */
+  @Test def testMStackOfBooleanSizedConstructionWorks(): Unit = {
+    val stk = MStackOfBoolean(3)
+    stk.push(true)
+    stk.push(false)
+    stk.push(true)
+    assertEquals(3, stk.length)
+    assertEquals(true, stk.pop())
+    assertEquals(false, stk.pop())
+    assertEquals(true, stk.pop())
+  }
+
+  @Test def testMStackOfIntSizedConstructionWorks(): Unit = {
+    val stk = MStackOfInt(2)
+    stk.push(1)
+    stk.push(2)
+    stk.push(3)
+    assertEquals(3, stk.length)
+    assertEquals(3, stk.pop())
+    assertEquals(2, stk.pop())
+    assertEquals(1, stk.pop())
+  }
+
+  @Test def testMStackOfLongSizedConstructionWorks(): Unit = {
+    val stk = MStackOfLong(1)
+    stk.push(10L)
+    stk.push(20L)
+    assertEquals(2, stk.length)
+    assertEquals(20L, stk.pop())
+    assertEquals(10L, stk.pop())
+  }
+
+  // trackMaxSizeReached is a `final val`; confirms it's off by default
+  // and maxSizeReached stays 0 while it is.
+  @Test def testMaxSizeReachedDisabledByDefault(): Unit = {
+    assertEquals(false, MStack.trackMaxSizeReached)
+    val stk = new MStackOf[String]
+    stk.push("a")
+    stk.push("b")
+    stk.push("c")
+    assertEquals(0, stk.maxSizeReached)
+  }
+
   /**
    * This test compares MStackOfLong to ArrayStack[Long].
    *