From 9fdfed69b7182d0a433c04bec926781c70ce6f18 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 7 Sep 2026 13:45:36 +0100 Subject: [PATCH] Fold _canHavePrefixUri into Xobj._bits to shrink AttrXobj by 8 bytes NamedNodeXobj carries a single boolean, _canHavePrefixUri, to record whether a node was created through the DOM Level 1 factory methods. Xobj already ends on an 8-byte boundary, so that one byte costs AttrXobj a whole 8-byte slot: 88 bytes of fields become a 96-byte object with 7 bytes of padding. Xobj._bits already holds kind, domType and three flags, with the highest bit in use at 0x400. Moving the boolean there as CAN_HAVE_PREFIX_URI = 0x800 removes the field, and AttrXobj drops to 88 bytes. Measured with Unsafe.objectFieldOffset on JDK 17 and 21, with and without compressed oops. ElementXobj is unchanged at 96 - it adds a 4-byte _attributes reference on top of 88. Reported against POI as a 600MB workbook holding 2.35 million AttrXobj, where this is roughly 19MB. Refs https://github.com/apache/poi/issues/992 Co-Authored-By: Claude Opus 5 (1M context) --- .../apache/xmlbeans/impl/store/DomImpl.java | 4 +- .../xmlbeans/impl/store/NamedNodeXobj.java | 5 +- .../org/apache/xmlbeans/impl/store/Xobj.java | 6 +- .../impl/store/NodePrefixUriFlagTest.java | 104 ++++++++++++++++++ 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/apache/xmlbeans/impl/store/NodePrefixUriFlagTest.java diff --git a/src/main/java/org/apache/xmlbeans/impl/store/DomImpl.java b/src/main/java/org/apache/xmlbeans/impl/store/DomImpl.java index e8536e76a..e868b392e 100755 --- a/src/main/java/org/apache/xmlbeans/impl/store/DomImpl.java +++ b/src/main/java/org/apache/xmlbeans/impl/store/DomImpl.java @@ -661,7 +661,7 @@ public static Element document_createElement(Dom d, String name) { c.createElement(l.makeQualifiedQName("", name)); ElementXobj e = (ElementXobj) c.getDom(); c.release(); - e._canHavePrefixUri = false; + e.clearBit(Xobj.CAN_HAVE_PREFIX_URI); return e; } @@ -698,7 +698,7 @@ public static Attr document_createAttribute(Dom d, String name) { c.createAttr(l.makeQualifiedQName("", name)); AttrXobj e = (AttrXobj) c.getDom(); c.release(); - e._canHavePrefixUri = false; + e.clearBit(Xobj.CAN_HAVE_PREFIX_URI); return e; } diff --git a/src/main/java/org/apache/xmlbeans/impl/store/NamedNodeXobj.java b/src/main/java/org/apache/xmlbeans/impl/store/NamedNodeXobj.java index 01e2b994d..2ef9da433 100644 --- a/src/main/java/org/apache/xmlbeans/impl/store/NamedNodeXobj.java +++ b/src/main/java/org/apache/xmlbeans/impl/store/NamedNodeXobj.java @@ -18,13 +18,12 @@ abstract class NamedNodeXobj extends NodeXobj { NamedNodeXobj(Locale l, int kind, int domType) { super(l, kind, domType); - _canHavePrefixUri = true; + setBit(CAN_HAVE_PREFIX_URI); } public boolean nodeCanHavePrefixUri() { - return _canHavePrefixUri; + return bitIsSet(CAN_HAVE_PREFIX_URI); } - boolean _canHavePrefixUri; } diff --git a/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java b/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java index eb0ae6f6e..e4f293f6d 100644 --- a/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java +++ b/src/main/java/org/apache/xmlbeans/impl/store/Xobj.java @@ -567,8 +567,7 @@ final void setName(QName newName) { _name = newName; if (this instanceof NamedNodeXobj) { - NamedNodeXobj me = (NamedNodeXobj) this; - me._canHavePrefixUri = true; + setBit(CAN_HAVE_PREFIX_URI); } if (!isProcinst()) { @@ -1298,6 +1297,9 @@ final boolean bitIsClear(int mask) { static final int VACANT = 0x100; static final int STABLE_USER = 0x200; static final int INHIBIT_DISCONNECT = 0x400; + // only NamedNodeXobj reads this one - it lives here so that NamedNodeXobj + // needs no field of its own, which would cost 8 bytes per AttrXobj in padding + static final int CAN_HAVE_PREFIX_URI = 0x800; final boolean isVacant() { return bitIsSet(VACANT); diff --git a/src/test/java/org/apache/xmlbeans/impl/store/NodePrefixUriFlagTest.java b/src/test/java/org/apache/xmlbeans/impl/store/NodePrefixUriFlagTest.java new file mode 100644 index 000000000..98e1c0d32 --- /dev/null +++ b/src/test/java/org/apache/xmlbeans/impl/store/NodePrefixUriFlagTest.java @@ -0,0 +1,104 @@ +/* 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.xmlbeans.impl.store; + +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.namespace.QName; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * DOM Level 1 factory methods produce nodes with no namespace information, which + * the store tracks per node. These pin that behaviour across the three places the + * state is written. + */ +public class NodePrefixUriFlagTest { + + private static final String URI = "http://example.org/ns"; + + private static XmlObject parse() throws XmlException { + return XmlObject.Factory.parse(""); + } + + private static Document doc() throws XmlException { + return (Document) parse().getDomNode(); + } + + @Test + void level1ElementHasNoNamespaceInfo() throws XmlException { + Element e = doc().createElement("foo"); + + assertNull(e.getLocalName()); + assertNull(e.getNamespaceURI()); + assertNull(e.getPrefix()); + } + + @Test + void level2ElementKeepsNamespaceInfo() throws XmlException { + Element e = doc().createElementNS(URI, "p:foo"); + + assertEquals("foo", e.getLocalName()); + assertEquals(URI, e.getNamespaceURI()); + assertEquals("p", e.getPrefix()); + } + + @Test + void level1AttributeHasNoNamespaceInfo() throws XmlException { + Attr a = doc().createAttribute("bar"); + + assertNull(a.getLocalName()); + assertNull(a.getNamespaceURI()); + assertNull(a.getPrefix()); + } + + @Test + void level2AttributeKeepsNamespaceInfo() throws XmlException { + Attr a = doc().createAttributeNS(URI, "p:bar"); + + assertEquals("bar", a.getLocalName()); + assertEquals(URI, a.getNamespaceURI()); + assertEquals("p", a.getPrefix()); + } + + @Test + void renamingALevel1ElementRestoresNamespaceInfo() throws XmlException { + XmlObject xo = parse(); + Document doc = (Document) xo.getDomNode(); + + Element child = doc.createElement("child"); + doc.getDocumentElement().appendChild(child); + assertNull(child.getLocalName()); + + try (XmlCursor c = xo.newCursor()) { + c.toFirstChild(); // root + c.toFirstChild(); // child + c.setName(new QName(URI, "child", "p")); + } + + assertEquals("child", child.getLocalName()); + assertEquals(URI, child.getNamespaceURI()); + assertEquals("p", child.getPrefix()); + } +}