diff --git a/bundles/sirix-core/src/main/java/io/sirix/BinaryEncodingVersion.java b/bundles/sirix-core/src/main/java/io/sirix/BinaryEncodingVersion.java index 8b73e88ff..59886c733 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/BinaryEncodingVersion.java +++ b/bundles/sirix-core/src/main/java/io/sirix/BinaryEncodingVersion.java @@ -36,8 +36,9 @@ public enum BinaryEncodingVersion { /** - * Zero-copy format: slot offsets array + raw slotMemory blob. Enables direct buffer slice as page - * storage without copying. + * Unified page format: Header(32B) + Bitmap(128B) + Directory(8KB) + Heap. + * FlyweightNode records bind directly to page memory for zero-copy reads. + * Non-FlyweightNode records are serialized to the heap at commit time. */ V0((byte) 0); diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/Databases.java b/bundles/sirix-core/src/main/java/io/sirix/access/Databases.java index 707ace5da..f16441a16 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/Databases.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/Databases.java @@ -36,7 +36,6 @@ * @author Sebastian Graf, University of Konstanz */ public final class Databases { - /** * Private constructor to prevent instantiation. */ @@ -596,4 +595,5 @@ public static synchronized void reinitializeBufferManagerForTesting(long recordP logger.info("BufferManager reinitialized for testing"); } + } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/ResourceStoreImpl.java b/bundles/sirix-core/src/main/java/io/sirix/access/ResourceStoreImpl.java index ccd08ee27..63f7222dc 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/ResourceStoreImpl.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/ResourceStoreImpl.java @@ -60,7 +60,10 @@ public R getOpenResourceSession(final Path resourceFile) { @Override public void close() { - resourceSessions.forEach((_, resourceSession) -> resourceSession.close()); + resourceSessions.forEach((resourceFile, resourceSession) -> { + resourceSession.close(); + allResourceSessions.removeObject(resourceFile, resourceSession); + }); resourceSessions.clear(); } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeHashing.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeHashing.java index 3f6adab01..e1b6c005c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeHashing.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeHashing.java @@ -70,13 +70,25 @@ public void setAutoCommit(final boolean value) { /** * Adapting the structure with a hash for all ancestors only with insert. + * Uses the current cursor position as the start node. * * @throws SirixIOException if an I/O error occurs */ public void adaptHashesWithAdd() { + adaptHashesWithAdd(nodeReadOnlyTrx.getNodeKey()); + } + + /** + * Adapting the structure with a hash for all ancestors only with insert. + * Accepts the start node key directly, avoiding a moveTo to position the cursor. + * + * @param startNodeKey the node key to start hashing from + * @throws SirixIOException if an I/O error occurs + */ + public void adaptHashesWithAdd(final long startNodeKey) { if (!bulkInsert || autoCommit) { switch (hashType) { - case ROLLING -> rollingAdd(); + case ROLLING -> rollingAdd(startNodeKey); case POSTORDER -> postorderAdd(); case NONE -> { } @@ -190,7 +202,8 @@ private void postorderAdd() { protected abstract StructNode getStructuralNode(); private void persistNode(final Node node) { - storageEngineWriter.updateRecordSlot(node, IndexType.DOCUMENT, -1); + // Ensure the mutated node is stored in the TIL's modified page. + storageEngineWriter.persistRecord(node, IndexType.DOCUMENT, -1); } /** @@ -290,10 +303,14 @@ private static void setRemoveDescendants(final long startDescendantCount, final * * @throws SirixIOException if an I/O error occurs */ - private void rollingAdd() { - // start with hash to add - final Node startNode = storageEngineWriter.prepareRecordForModification(nodeReadOnlyTrx.getNodeKey(), IndexType.DOCUMENT, -1); - final long startNodeKey = startNode.getNodeKey(); + private void rollingAdd(final long startNodeKey) { + // Position cursor on start node — needed for getStructuralNode().getDescendantCount() + // and to restore position at end. The caller eliminated the moveTo before adaptForInsert + // which is the bigger win (adaptForInsert is called unconditionally; rollingAdd only + // when hashing is enabled). + nodeReadOnlyTrx.moveTo(startNodeKey); + // start with hash to add — startNodeKey passed directly + final Node startNode = storageEngineWriter.prepareRecordForModification(startNodeKey, IndexType.DOCUMENT, -1); // Capture all needed values from startNode before any subsequent prepareRecordForModification // calls, which may return the same write-path singleton and overwrite startNode's fields. final long startParentKey = startNode.getParentKey(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeReadOnlyTrx.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeReadOnlyTrx.java index 855f7ec3d..42347158f 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeReadOnlyTrx.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeReadOnlyTrx.java @@ -1,28 +1,30 @@ package io.sirix.access.trx.node; -import io.brackit.query.atomic.QNm; import io.sirix.access.ResourceConfiguration; import io.sirix.access.User; import io.sirix.access.trx.page.NodeStorageEngineReader; -import io.sirix.api.*; +import io.sirix.api.ItemList; +import io.sirix.api.NodeCursor; +import io.sirix.api.NodeReadOnlyTrx; +import io.sirix.api.NodeTrx; +import io.sirix.api.ResourceSession; +import io.sirix.api.StorageEngineReader; +import io.sirix.api.StorageEngineWriter; import io.sirix.cache.PageGuard; import io.sirix.exception.SirixIOException; import io.sirix.index.IndexType; +import io.sirix.node.DeltaVarIntCodec; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.interfaces.DataRecord; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableNode; -import io.sirix.node.layout.FixedSlotRecordMaterializer; -import io.sirix.node.layout.FixedSlotRecordProjector; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; import io.sirix.node.BytesIn; import io.sirix.node.MemorySegmentBytesIn; import io.sirix.node.json.ArrayNode; import io.sirix.node.json.BooleanNode; +import io.sirix.node.json.JsonDocumentRootNode; import io.sirix.node.json.NumberNode; import io.sirix.node.json.ObjectBooleanNode; import io.sirix.node.json.ObjectKeyNode; @@ -30,9 +32,10 @@ import io.sirix.node.json.ObjectNullNode; import io.sirix.node.json.ObjectNumberNode; import io.sirix.node.json.ObjectStringNode; -import io.sirix.node.json.JsonDocumentRootNode; import io.sirix.node.json.NullNode; import io.sirix.node.json.StringNode; +import io.sirix.node.interfaces.FlyweightNode; +import io.sirix.node.interfaces.Node; import io.sirix.node.xml.AttributeNode; import io.sirix.node.xml.CommentNode; import io.sirix.node.xml.ElementNode; @@ -41,7 +44,9 @@ import io.sirix.node.xml.TextNode; import io.sirix.node.xml.XmlDocumentRootNode; import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.PageLayout; import io.sirix.service.xml.xpath.AtomicValue; +import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import io.sirix.utils.NamePageHash; import org.checkerframework.checker.index.qual.NonNegative; @@ -62,7 +67,7 @@ * @param the type of node cursor */ public abstract class AbstractNodeReadOnlyTrx - implements InternalNodeReadOnlyTrx { + implements InternalNodeReadOnlyTrx, NodeCursor, NodeReadOnlyTrx { /** * ID of transaction. @@ -75,12 +80,12 @@ public abstract class AbstractNodeReadOnlyTrx resourceSession; @@ -93,81 +98,115 @@ public abstract class AbstractNodeReadOnlyTrx itemList; - - // ==================== CURSOR SLOT STATE ==================== - + + // ==================== FLYWEIGHT CURSOR STATE ==================== + // These fields enable zero-allocation navigation by reading directly from MemorySegment + + /** + * The raw MemorySegment containing the current node's serialized data. + * When in flyweight mode, getters read directly from this segment. + */ + private MemorySegment currentSlot; + /** * The current node's key (used for delta decoding). */ private long currentNodeKey; - + /** * The current node's kind. */ private NodeKind currentNodeKind; - + + /** + * The current node's DeweyID bytes (may be null if DeweyIDs not stored). + */ + private byte[] currentDeweyId; + /** - * Page guard protecting the current page from eviction. MUST be released when moving to a different - * node or closing the transaction. + * Page guard protecting the current page from eviction. + * MUST be released when moving to a different node or closing the transaction. */ private PageGuard currentPageGuard; - + /** - * The page key of the currently held page guard. Used to detect same-page moves and avoid guard - * release/reacquire overhead. + * The page key of the currently held page guard. + * Used to detect same-page moves and avoid guard release/reacquire overhead. */ private long currentPageKey = -1; - + /** - * The current page reference (same page as currentPageGuard). Cached to avoid re-lookup when moving - * within the same page. + * The current page reference (same page as currentPageGuard). + * Cached to avoid re-lookup when moving within the same page. */ private KeyValueLeafPage currentPage; - + /** - * Slot offset of the current singleton node in {@link #currentPage}. + * Reusable BytesIn instance for reading node data. + * Avoids allocation on every moveTo() call. */ - private int currentSlotOffset = -1; - + private final MemorySegmentBytesIn reusableBytesIn = new MemorySegmentBytesIn(MemorySegment.NULL); + /** - * Reusable BytesIn instance for reading node data. Avoids allocation on every moveTo() call. + * Whether the transaction is in flyweight mode (reading from currentSlot). + * When false, falls back to using currentNode object. */ - private final MemorySegmentBytesIn reusableBytesIn = new MemorySegmentBytesIn(MemorySegment.NULL); - + private boolean flyweightMode = true; + /** - * Resource configuration cached for hash type checks. + * Preallocated array for caching field offsets within currentSlot. + * Indices are defined by FIELD_* constants. + * This avoids re-parsing varints on each getter call. */ - protected final ResourceConfiguration resourceConfig; - + protected final int[] cachedFieldOffsets = new int[16]; + + // Field offset indices for cachedFieldOffsets array + protected static final int FIELD_PARENT_KEY = 0; + protected static final int FIELD_PREV_REVISION = 1; + protected static final int FIELD_LAST_MOD_REVISION = 2; + protected static final int FIELD_RIGHT_SIBLING_KEY = 3; + protected static final int FIELD_LEFT_SIBLING_KEY = 4; + protected static final int FIELD_FIRST_CHILD_KEY = 5; + protected static final int FIELD_LAST_CHILD_KEY = 6; + protected static final int FIELD_CHILD_COUNT = 7; + protected static final int FIELD_DESCENDANT_COUNT = 8; + protected static final int FIELD_HASH = 9; + protected static final int FIELD_NAME_KEY = 10; + protected static final int FIELD_PATH_NODE_KEY = 11; + protected static final int FIELD_VALUE = 12; + protected static final int FIELD_END = 13; // End marker for offset validation + /** - * Whether singleton-mode hot-path rebinding is enabled for this resource. JSON and XML resources - * use singleton rebinding. + * Resource configuration cached for hash type checks. */ - private final boolean singletonOptimizedResource; + protected final ResourceConfiguration resourceConfig; /** - * Whether the current resource is an XML resource. + * Cached {@link NodeStorageEngineReader} resolved once from {@link #storageEngineReader}. + * For read-only transactions, this is the reader itself. + * For write transactions, this is the delegate reader extracted from the writer. + * Used by {@link #moveTo(long)} to enable singleton mode without per-call instanceof checks. */ - private final boolean xmlSingletonResource; + private NodeStorageEngineReader cachedNodeReader; /** - * Tracks if Dewey bytes are already bound for the current singleton. Deferred binding avoids a - * byte[] allocation on every moveTo. + * Cached {@link StorageEngineWriter} reference, non-null only for write transactions. + * Used by {@link #moveToSingletonSlowPath} to resolve TIL modified pages. */ - private boolean singletonDeweyBound = true; + private StorageEngineWriter cachedWriter; /** * Constructor. * - * @param trxId the transaction ID + * @param trxId the transaction ID * @param pageReadTransaction the underlying read-only page transaction - * @param documentNode the document root node - * @param resourceSession The resource session for the current transaction - * @param itemList Read-transaction-exclusive item list. + * @param documentNode the document root node + * @param resourceSession The resource manager for the current transaction + * @param itemList Read-transaction-exclusive item list. */ - protected AbstractNodeReadOnlyTrx(final @NonNegative int trxId, - final @NonNull StorageEngineReader pageReadTransaction, final @NonNull N documentNode, - final InternalResourceSession resourceSession, final ItemList itemList) { + protected AbstractNodeReadOnlyTrx(final @NonNegative int trxId, final @NonNull StorageEngineReader pageReadTransaction, + final @NonNull N documentNode, final InternalResourceSession resourceSession, + final ItemList itemList) { this.itemList = itemList; this.resourceSession = requireNonNull(resourceSession); this.id = trxId; @@ -175,12 +214,13 @@ protected AbstractNodeReadOnlyTrx(final @NonNegative int trxId, this.currentNode = requireNonNull(documentNode); this.isClosed = false; this.resourceConfig = resourceSession.getResourceConfig(); + this.cachedNodeReader = resolveNodeReader(pageReadTransaction); + this.cachedWriter = (pageReadTransaction instanceof StorageEngineWriter w) ? w : null; - // Initialize cursor state from document node. + // Initialize flyweight state from document node this.currentNodeKey = documentNode.getNodeKey(); this.currentNodeKind = documentNode.getKind(); - this.xmlSingletonResource = documentNode.getKind() == NodeKind.XML_DOCUMENT; - this.singletonOptimizedResource = documentNode.getKind() == NodeKind.JSON_DOCUMENT || xmlSingletonResource; + this.flyweightMode = false; // Start with object mode for document node } @Override @@ -190,16 +230,22 @@ public N getCurrentNode() { } // When in singleton mode, create a snapshot (deep copy) of the singleton. - if (singletonMode && currentSingleton != null) { + // Snapshot semantics are required because singleton instances are reused across moveTo calls. + if (SINGLETON_ENABLED && singletonMode && currentSingleton != null) { currentNode = createSingletonSnapshot(); + return currentNode; + } + + if (FLYWEIGHT_ENABLED && flyweightMode && currentSlot != null) { + currentNode = deserializeToSnapshot(); } return currentNode; } - + /** - * Create a deep copy snapshot of the current singleton node. The snapshot is a new object with all - * values copied, safe to hold across cursor moves. + * Create a deep copy snapshot of the current singleton node. + * The snapshot is a new object with all values copied, safe to hold across cursor moves. * * @return a snapshot of the current singleton */ @@ -218,24 +264,40 @@ private N createSingletonSnapshot() { case OBJECT_BOOLEAN_VALUE -> (N) ((ObjectBooleanNode) currentSingleton).toSnapshot(); case OBJECT_NULL_VALUE -> (N) ((ObjectNullNode) currentSingleton).toSnapshot(); case JSON_DOCUMENT -> (N) ((JsonDocumentRootNode) currentSingleton).toSnapshot(); - case XML_DOCUMENT -> (N) ((XmlDocumentRootNode) currentSingleton).toSnapshot(); case ELEMENT -> (N) ((ElementNode) currentSingleton).toSnapshot(); case ATTRIBUTE -> (N) ((AttributeNode) currentSingleton).toSnapshot(); - case NAMESPACE -> (N) ((NamespaceNode) currentSingleton).toSnapshot(); case TEXT -> (N) ((TextNode) currentSingleton).toSnapshot(); case COMMENT -> (N) ((CommentNode) currentSingleton).toSnapshot(); case PROCESSING_INSTRUCTION -> (N) ((PINode) currentSingleton).toSnapshot(); + case NAMESPACE -> (N) ((NamespaceNode) currentSingleton).toSnapshot(); + case XML_DOCUMENT -> (N) ((XmlDocumentRootNode) currentSingleton).toSnapshot(); default -> throw new IllegalStateException("Unexpected singleton kind: " + currentNodeKind); }; } + + /** + * Deserialize the current slot to a node object (snapshot). + * This is the escape hatch for code that needs a stable node reference. + * Called by getCurrentNode() when in flyweight mode. + * + * @return the deserialized node + */ + @SuppressWarnings("unchecked") + protected N deserializeToSnapshot() { + // Use the same deserialization as normal read path + var bytesIn = new MemorySegmentBytesIn(currentSlot); + var record = resourceConfig.recordPersister.deserialize(bytesIn, currentNodeKey, currentDeweyId, resourceConfig); + return (N) record; + } @Override public void setCurrentNode(final @Nullable N currentNode) { assertNotClosed(); this.currentNode = currentNode; - + if (currentNode != null) { - // Disable singleton mode and use the provided node object. + // Disable flyweight and singleton modes - use the provided node object + this.flyweightMode = false; this.singletonMode = false; this.currentSingleton = null; this.currentNodeKey = currentNode.getNodeKey(); @@ -263,7 +325,7 @@ public Optional getUser() { @Override public boolean moveToPrevious() { assertNotClosed(); - // Use cursor getters to avoid unnecessary materialization. + // Use flyweight getters to avoid node materialization if (hasLeftSibling()) { // Left sibling node. boolean leftSiblMove = moveTo(getLeftSiblingKey()); @@ -281,7 +343,7 @@ public boolean moveToPrevious() { public NodeKind getLeftSiblingKind() { assertNotClosed(); if (hasLeftSibling()) { - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); moveToLeftSibling(); final NodeKind leftSiblingKind = getKind(); @@ -294,16 +356,19 @@ public NodeKind getLeftSiblingKind() { @Override public long getLeftSiblingKey() { assertNotClosed(); - if (singletonMode && currentSingleton instanceof StructNode sn) { + if (SINGLETON_ENABLED && singletonMode && currentSingleton instanceof StructNode sn) { return sn.getLeftSiblingKey(); } + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] >= 0) { + return DeltaVarIntCodec.decodeDeltaFromSegment(currentSlot, cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY], currentNodeKey); + } return getStructuralNode().getLeftSiblingKey(); } @Override public boolean hasLeftSibling() { assertNotClosed(); - if (singletonMode) { + if ((SINGLETON_ENABLED && singletonMode) || (FLYWEIGHT_ENABLED && flyweightMode)) { return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } return getStructuralNode().hasLeftSibling(); @@ -312,7 +377,7 @@ public boolean hasLeftSibling() { @Override public boolean moveToLeftSibling() { assertNotClosed(); - // Use cursor getter and avoid unnecessary materialization. + // Use flyweight getter if available to avoid node materialization if (!hasLeftSibling()) { return false; } @@ -334,17 +399,7 @@ public String nameForKey(final int key) { @Override public long getPathNodeKey() { assertNotClosed(); - final NodeKind kind = getKind(); - if (kind == NodeKind.XML_DOCUMENT || kind == NodeKind.JSON_DOCUMENT) { - return 0; - } - - final ImmutableNode node; - if (singletonMode && currentSingleton != null) { - node = currentSingleton; - } else { - node = getCurrentNode(); - } + final ImmutableNode node = getCurrentNode(); if (node instanceof NameNode) { return ((NameNode) node).getPathNodeKey(); } @@ -354,6 +409,9 @@ public long getPathNodeKey() { if (node instanceof ArrayNode arrayNode) { return arrayNode.getPathNodeKey(); } + if (node.getKind() == NodeKind.XML_DOCUMENT || node.getKind() == NodeKind.JSON_DOCUMENT) { + return 0; + } return -1; } @@ -390,7 +448,7 @@ public boolean moveToParent() { @Override public boolean moveToFirstChild() { assertNotClosed(); - // Use cursor getter and avoid unnecessary materialization. + // Use flyweight getter if available to avoid node materialization if (!hasFirstChild()) { return false; } @@ -406,20 +464,39 @@ public boolean moveTo(final long nodeKey) { return moveToItemList(nodeKey); } - if (singletonOptimizedResource && storageEngineReader instanceof NodeStorageEngineReader reader) { - return moveToSingleton(nodeKey, reader); + // Use singleton mode for READ-ONLY transactions (cachedWriter == null). + // Write transactions fall through to moveToLegacy for now — the writer's overridden + // getRecord() provides TIL-aware page resolution that moveToSingleton needs. + if (SINGLETON_ENABLED && cachedNodeReader != null && cachedWriter == null) { + return moveToSingleton(nodeKey, cachedNodeReader); + } + + // Write path: use moveToSingletonWrite for TIL-aware singleton mode + if (SINGLETON_ENABLED && cachedWriter != null && cachedNodeReader != null) { + return moveToSingletonWrite(nodeKey, cachedNodeReader, cachedWriter); } - // Fallback to traditional object mode. + // Fallback to traditional object mode return moveToLegacy(nodeKey); } - + + /** + * Toggle for flyweight mode. Set to true to enable zero-allocation optimization. + * Note: Flyweight mode reads directly from MemorySegment but has varint parsing overhead. + */ + private static final boolean FLYWEIGHT_ENABLED = false; + + /** + * Toggle for singleton mode. Set to true to enable singleton node reuse. + * Singleton mode uses mutable singleton nodes that are repopulated on each moveTo(). + * When combined with cache checking, uses cached records when available. + */ + private static final boolean SINGLETON_ENABLED = true; + // ==================== SINGLETON NODE INSTANCES ==================== // These mutable singleton nodes are reused across moveTo() operations. - // Each supported JSON/XML node type has a dedicated singleton instance. - - private static final QNm EMPTY_QNM = new QNm(""); - + // Each JSON node type has a dedicated singleton instance. + private ObjectNode singletonObject; private ArrayNode singletonArray; private ObjectKeyNode singletonObjectKey; @@ -431,73 +508,146 @@ public boolean moveTo(final long nodeKey) { private ObjectNumberNode singletonObjectNumber; private ObjectBooleanNode singletonObjectBoolean; private ObjectNullNode singletonObjectNull; - private JsonDocumentRootNode singletonJsonDocumentRoot; - private XmlDocumentRootNode singletonXmlDocumentRoot; + private JsonDocumentRootNode singletonJsonDocRoot; private ElementNode singletonElement; private AttributeNode singletonAttribute; - private NamespaceNode singletonNamespace; private TextNode singletonText; private CommentNode singletonComment; private PINode singletonPI; - + private NamespaceNode singletonNamespace; + private XmlDocumentRootNode singletonXmlDocRoot; + /** * Whether currently in singleton mode (using singleton nodes). */ private boolean singletonMode = false; - + /** * The current singleton node (set when in singletonMode). */ private ImmutableNode currentSingleton; - /** - * Move to a node using singleton mode (zero allocation). Repopulates a mutable singleton instance - * from serialized data. NO allocation happens here - only when getCurrentNode() is called. + * Array-based singleton lookup indexed by NodeKind.getId(). + * Replaces the 19-case switch in getSingletonForKind with O(1) array access. + * Lazily populated on first access per kind. Max NodeKind ID is 55. + */ + private final ImmutableNode[] singletonByKindId = new ImmutableNode[56]; + + /** + * Move to a node using flyweight mode (zero allocation). + * Reads directly from MemorySegment without creating node objects. + * + * @param nodeKey the node key to move to + * @param reader the storage engine reader + * @return true if the move was successful + */ + private boolean moveToFlyweight(final long nodeKey, final NodeStorageEngineReader reader) { + // Try singleton mode first (preferred for JSON nodes) + if (SINGLETON_ENABLED) { + return moveToSingleton(nodeKey, reader); + } + + if (!FLYWEIGHT_ENABLED) { + return moveToLegacy(nodeKey); + } + + // Lookup slot directly without deserializing + // NOTE: We acquire the new guard BEFORE releasing the old one to ensure + // the transaction state remains valid if the lookup fails. + var slotLocation = reader.lookupSlotWithGuard(nodeKey, IndexType.DOCUMENT, -1); + if (slotLocation == null) { + // Node not found - keep the current position unchanged + return false; + } + + // Read node kind from first byte + MemorySegment data = slotLocation.data(); + byte kindByte = data.get(java.lang.foreign.ValueLayout.JAVA_BYTE, 0); + NodeKind kind = NodeKind.getKind(kindByte); + + // Check for deleted node + if (kind == NodeKind.DELETE) { + // Release the newly acquired guard, keep current position unchanged + slotLocation.guard().close(); + return false; + } + + // Move succeeded - now release the previous page guard + releaseCurrentPageGuard(); + + // Update flyweight state (NO ALLOCATION) + this.currentSlot = data; + this.currentNodeKey = nodeKey; + this.currentNodeKind = kind; + this.currentDeweyId = slotLocation.page().getDeweyIdAsByteArray(slotLocation.offset()); + this.currentPageGuard = slotLocation.guard(); + this.flyweightMode = true; + this.currentNode = null; // Invalidate cached node object + + // Parse and cache field offsets for fast getter access + parseFieldOffsets(); + + return true; + } + + /** + * Move to a node using singleton mode (zero allocation). + * Repopulates a mutable singleton instance from serialized data. + * NO allocation happens here - only when getCurrentNode() is called. * * @param nodeKey the node key to move to - * @param reader the storage engine reader + * @param reader the storage engine reader * @return true if the move was successful */ private boolean moveToSingleton(final long nodeKey, final NodeStorageEngineReader reader) { - // Calculate target page key to check for same-page access - final long targetPageKey = reader.pageKey(nodeKey, IndexType.DOCUMENT); - final int slotOffset = StorageEngineReader.recordPageOffset(nodeKey); + // Inline pageKey: all index types use exponent 10, avoids assertNotClosed + switch overhead + final long targetPageKey = nodeKey >> Constants.NDP_NODE_COUNT_EXPONENT; + final int slotOffset = (int) (nodeKey & ((1 << Constants.NDP_NODE_COUNT_EXPONENT) - 1)); + MemorySegment data; KeyValueLeafPage page; // OPTIMIZATION: Check if we're moving within the same page if (currentPageKey == targetPageKey && currentPage != null && !currentPage.isClosed()) { // Same page! Skip guard management entirely page = currentPage; + + // Check records[] first: Java objects are authoritative during write transactions + // (modifications via prepareRecordForModification are NOT synced back to page heap) + final DataRecord fromRecords = page.getRecord(slotOffset); + if (fromRecords != null) { + if (fromRecords.getKind() == NodeKind.DELETE) { + return false; + } + @SuppressWarnings("unchecked") + final N node = (N) fromRecords; + this.currentNode = node; + this.currentNodeKind = (NodeKind) fromRecords.getKind(); + this.currentNodeKey = nodeKey; + this.currentSingleton = null; + this.singletonMode = false; + this.flyweightMode = false; + return true; + } + + data = page.getSlot(slotOffset); + if (data == null) { + // Slot not found on current page - try overflow or fail + return moveToSingletonSlowPath(nodeKey, reader); + } } else { // Different page - use the slow path with guard management return moveToSingletonSlowPath(nodeKey, reader); } - // Read slot metadata without allocating an asSlice - final int dataLength = page.getSlotDataLength(slotOffset); - if (dataLength < 0) { - // Slot not found on current page - try overflow or fail - return moveToSingletonSlowPath(nodeKey, reader); - } - final MemorySegment slotMemory = page.getSlotMemory(); - final long baseOffset = page.getSlotDataOffset(slotOffset); + // Read node kind from first byte + byte kindByte = data.get(java.lang.foreign.ValueLayout.JAVA_BYTE, 0); + NodeKind kind = NodeKind.getKind(kindByte); - final boolean fixedSlotFormat = page.isFixedSlotFormat(slotOffset); - final NodeKind kind; - if (fixedSlotFormat) { - kind = page.getFixedSlotNodeKind(slotOffset); - if (kind == null) { - return moveToLegacy(nodeKey); - } - } else { - // Read node kind from first byte (zero-copy: read directly from slotMemory). - final byte kindByte = slotMemory.get(java.lang.foreign.ValueLayout.JAVA_BYTE, baseOffset); - kind = NodeKind.getKind(kindByte); - if (kind == NodeKind.DELETE) { - return false; - } + // Check for deleted node + if (kind == NodeKind.DELETE) { + return false; } // Get singleton instance for this node type @@ -507,598 +657,394 @@ private boolean moveToSingleton(final long nodeKey, final NodeStorageEngineReade return moveToLegacy(nodeKey); } - // Populate singleton from slot bytes (no allocation on the singleton path). - // Note: no guard management needed, we're on the same page. - // Dewey bytes are bound lazily on demand to avoid byte[] allocation per moveTo. - if (fixedSlotFormat) { - if (!populateSingletonFromFixedSlot(singleton, kind, slotMemory, baseOffset, dataLength, nodeKey, null, page)) { - return moveToLegacy(nodeKey); + // Check if this is a flyweight record in slotted page + final boolean isFlyweightSlot = page.getSlottedPage() != null && page.getSlotNodeKindId(slotOffset) > 0 + && singleton instanceof FlyweightNode; + if (isFlyweightSlot) { + final FlyweightNode fn = (FlyweightNode) singleton; + // Bind flyweight directly to slotted page (zero-copy, no legacy parsing) + final int heapOffset = PageLayout.getDirHeapOffset(page.getSlottedPage(), slotOffset); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + fn.bind(page.getSlottedPage(), recordBase, nodeKey, slotOffset); + // Propagate FSST symbol table for compressed string nodes + propagateFsstToFlyweight(fn, page); + // Propagate DeweyID from page to flyweight node (stored inline after record data). + // setDeweyIDBytes stores raw bytes lazily — no SirixDeweyID parsing until getDeweyID() called. + // MUST always set (even null) to clear stale DeweyID from previous singleton reuse. + if (resourceConfig.areDeweyIDsStored && fn instanceof Node node) { + node.setDeweyIDBytes(page.getDeweyIdAsByteArray(slotOffset)); } } else { - reusableBytesIn.reset(slotMemory, baseOffset + 1); - populateSingleton(singleton, reusableBytesIn, nodeKey, null, kind, page); + // Legacy format: populate from serialized data (NO ALLOCATION) + // Reuse BytesIn instance - just reset to new segment and offset (skip kind byte) + reusableBytesIn.reset(data, 1); + // Only fetch DeweyID if actually stored (avoids byte[] allocation) + byte[] deweyId = resourceConfig.areDeweyIDsStored ? page.getDeweyIdAsByteArray(slotOffset) : null; + populateSingleton(singleton, reusableBytesIn, nodeKey, deweyId, kind, page); } // Update state - we're in singleton mode now (page guard unchanged) this.currentSingleton = singleton; this.currentNodeKind = kind; this.currentNodeKey = nodeKey; - this.currentSlotOffset = slotOffset; - this.singletonDeweyBound = !resourceConfig.areDeweyIDsStored; - this.currentNode = null; // Clear - will be created lazily by getCurrentNode() + this.currentNode = null; // Clear - will be created lazily by getCurrentNode() this.singletonMode = true; + this.flyweightMode = false; return true; } /** - * Slow path for moveToSingleton when moving to a different page. Handles guard acquisition and - * release. + * Slow path for moveToSingleton when moving to a different page (read-only transactions only). + * Uses the reader's lookupSlotWithGuard with guard management. */ private boolean moveToSingletonSlowPath(final long nodeKey, final NodeStorageEngineReader reader) { - // Get raw slot data with full guard management var slotLocation = reader.lookupSlotWithGuard(nodeKey, IndexType.DOCUMENT, -1); if (slotLocation == null) { return false; } - final KeyValueLeafPage slotPage = slotLocation.page(); - final int slotOff = slotLocation.offset(); - final MemorySegment slotMemory = slotPage.getSlotMemory(); - final long baseOffset = slotPage.getSlotDataOffset(slotOff); - final int dataLength = slotPage.getSlotDataLength(slotOff); - final boolean fixedSlotFormat = slotPage.isFixedSlotFormat(slotOff); - final NodeKind kind; - if (fixedSlotFormat) { - kind = slotPage.getFixedSlotNodeKind(slotOff); - if (kind == null) { - slotLocation.guard().close(); - return moveToLegacy(nodeKey); - } - } else { - final byte kindByte = slotMemory.get(java.lang.foreign.ValueLayout.JAVA_BYTE, baseOffset); - kind = NodeKind.getKind(kindByte); - if (kind == NodeKind.DELETE) { - slotLocation.guard().close(); + return moveToSingletonFromPage(nodeKey, slotLocation.page(), reader, + nodeKey >> Constants.NDP_NODE_COUNT_EXPONENT, slotLocation.guard()); + } + + /** + * Move to a node on a given page using singleton mode. + * Shared logic for both write (TIL modified page) and read (guarded page) paths. + * + * @param nodeKey the node key + * @param page the page to read from + * @param reader the storage engine reader (for pageKey calculation) + * @param pageKey the pre-calculated page key + * @param newGuard the new page guard (null for TIL pages which don't need guarding) + * @return true if move succeeded + */ + private boolean moveToSingletonFromPage(final long nodeKey, final KeyValueLeafPage page, + final NodeStorageEngineReader reader, final long pageKey, final @Nullable PageGuard newGuard) { + final int slotOff = (int) (nodeKey & ((1 << Constants.NDP_NODE_COUNT_EXPONENT) - 1)); + + // Check records[] first: Java objects are authoritative during write transactions + // (modifications via prepareRecordForModification are NOT synced back to page heap) + final DataRecord fromRecords = page.getRecord(slotOff); + if (fromRecords != null) { + if (fromRecords.getKind() == NodeKind.DELETE) { + if (newGuard != null) { + newGuard.close(); + } return false; } + releaseCurrentPageGuard(); + @SuppressWarnings("unchecked") + final N node = (N) fromRecords; + this.currentNode = node; + this.currentNodeKind = (NodeKind) fromRecords.getKind(); + this.currentNodeKey = nodeKey; + this.currentSingleton = null; + this.singletonMode = false; + this.flyweightMode = false; + this.currentPageGuard = newGuard; + this.currentPage = page; + this.currentPageKey = pageKey; + return true; + } + + // Get slot data from page heap + MemorySegment data = page.getSlot(slotOff); + if (data == null) { + if (newGuard != null) { + newGuard.close(); + } + return false; + } + + // Read node kind from first byte + byte kindByte = data.get(java.lang.foreign.ValueLayout.JAVA_BYTE, 0); + NodeKind kind = NodeKind.getKind(kindByte); + + // Check for deleted node + if (kind == NodeKind.DELETE) { + if (newGuard != null) { + newGuard.close(); + } + return false; } // Get singleton instance for this node type ImmutableNode singleton = getSingletonForKind(kind); if (singleton == null) { // No singleton for this type (e.g., document root), fall back to legacy - slotLocation.guard().close(); + if (newGuard != null) { + newGuard.close(); + } return moveToLegacy(nodeKey); } // Release previous page guard ONLY NOW (after we know the new page is valid) releaseCurrentPageGuard(); - // Populate singleton from serialized data (NO ALLOCATION) - // Reuse BytesIn instance - just reset to new segment and offset (skip kind byte) - // Dewey bytes are bound lazily on demand to avoid byte[] allocation per moveTo. - if (fixedSlotFormat) { - if (!populateSingletonFromFixedSlot(singleton, kind, slotMemory, baseOffset, dataLength, nodeKey, null, - slotPage)) { - slotLocation.guard().close(); - return moveToLegacy(nodeKey); + // Check if this is a flyweight record in slotted page + final boolean isFlyweight = page.getSlottedPage() != null && page.getSlotNodeKindId(slotOff) > 0 + && singleton instanceof FlyweightNode; + if (isFlyweight) { + final FlyweightNode fn = (FlyweightNode) singleton; + // Bind flyweight directly to slotted page (zero-copy, no legacy parsing) + final int heapOffset = PageLayout.getDirHeapOffset(page.getSlottedPage(), slotOff); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + fn.bind(page.getSlottedPage(), recordBase, nodeKey, slotOff); + // Propagate FSST symbol table for compressed string nodes + propagateFsstToFlyweight(fn, page); + // Propagate DeweyID from page to flyweight node (stored inline after record data). + // setDeweyIDBytes stores raw bytes lazily — no SirixDeweyID parsing until getDeweyID() called. + // MUST always set (even null) to clear stale DeweyID from previous singleton reuse. + if (resourceConfig.areDeweyIDsStored && fn instanceof Node node) { + node.setDeweyIDBytes(page.getDeweyIdAsByteArray(slotOff)); } } else { - reusableBytesIn.reset(slotMemory, baseOffset + 1); - populateSingleton(singleton, reusableBytesIn, nodeKey, null, kind, slotPage); + // Legacy format: populate from serialized data (NO ALLOCATION) + reusableBytesIn.reset(data, 1); + byte[] deweyId = resourceConfig.areDeweyIDsStored + ? page.getDeweyIdAsByteArray(slotOff) : null; + populateSingleton(singleton, reusableBytesIn, nodeKey, deweyId, kind, page); } // Update state - we're in singleton mode now with new page - this.currentPageGuard = slotLocation.guard(); - this.currentPage = slotLocation.page(); - this.currentPageKey = reader.pageKey(nodeKey, IndexType.DOCUMENT); + this.currentPageGuard = newGuard; + this.currentPage = page; + this.currentPageKey = pageKey; this.currentSingleton = singleton; this.currentNodeKind = kind; this.currentNodeKey = nodeKey; - this.currentSlotOffset = slotOff; - this.singletonDeweyBound = !resourceConfig.areDeweyIDsStored; - this.currentNode = null; // Clear - will be created lazily by getCurrentNode() + this.currentNode = null; // Clear - will be created lazily by getCurrentNode() this.singletonMode = true; + this.flyweightMode = false; return true; } - private boolean populateSingletonFromFixedSlot(final ImmutableNode singleton, final NodeKind kind, - final MemorySegment slotMemory, final long baseOffset, final int dataLength, final long nodeKey, - final byte[] deweyIdBytes, final KeyValueLeafPage page) { - final NodeKindLayout layout = kind.layoutDescriptor(); - if (!layout.isFixedSlotSupported() || dataLength < layout.fixedSlotSizeInBytes()) { - return false; - } - // Reject payload-bearing nodes with non-VALUE_BLOB payload refs (e.g., ATTRIBUTE_VECTOR) - if (layout.payloadRefCount() > 0 && !layout.hasSupportedPayloads()) { - return false; - } + /** + * Write-transaction singleton moveTo. Uses the writer's TIL for page resolution (modified pages). + * Same-page optimization caches the modified page between calls. + * Falls back to moveToLegacy if the page is not in TIL. + * + * @param nodeKey the node key to move to + * @param reader the underlying storage engine reader (for pageKey calculation) + * @param writer the storage engine writer (for TIL page resolution) + * @return true if the move was successful + */ + private boolean moveToSingletonWrite(final long nodeKey, final NodeStorageEngineReader reader, + final StorageEngineWriter writer) { + // Inline pageKey: all index types use exponent 10, avoids assertNotClosed + switch overhead + final long targetPageKey = nodeKey >> Constants.NDP_NODE_COUNT_EXPONENT; + final int slotOffset = (int) (nodeKey & ((1 << Constants.NDP_NODE_COUNT_EXPONENT) - 1)); + KeyValueLeafPage page; - switch (kind) { - case JSON_DOCUMENT -> { - if (!(singleton instanceof JsonDocumentRootNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case XML_DOCUMENT -> { - if (!(singleton instanceof XmlDocumentRootNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT -> { - if (!(singleton instanceof ObjectNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case ARRAY -> { - if (!(singleton instanceof ArrayNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT_KEY -> { - if (!(singleton instanceof ObjectKeyNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setNameKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.NAME_KEY)); - node.clearCachedName(); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case BOOLEAN_VALUE -> { - if (!(singleton instanceof BooleanNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setValue(SlotLayoutAccessors.readBooleanField(slotMemory, baseOffset, layout, StructuralField.BOOLEAN_VALUE)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case NULL_VALUE -> { - if (!(singleton instanceof NullNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT_BOOLEAN_VALUE -> { - if (!(singleton instanceof ObjectBooleanNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setValue(SlotLayoutAccessors.readBooleanField(slotMemory, baseOffset, layout, StructuralField.BOOLEAN_VALUE)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT_NULL_VALUE -> { - if (!(singleton instanceof ObjectNullNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case NAMESPACE -> { - if (!(singleton instanceof NamespaceNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.URI_KEY)); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case ELEMENT -> { - if (!(singleton instanceof ElementNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.LOCAL_NAME_KEY)); - FixedSlotRecordMaterializer.readInlineVectorPayload(node, slotMemory, baseOffset, layout, 0, true); - FixedSlotRecordMaterializer.readInlineVectorPayload(node, slotMemory, baseOffset, layout, 1, false); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case STRING_VALUE -> { - if (!(singleton instanceof StringNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazySource and - // metadataParsed=true - final long strPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int strLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - final int strFlags = SlotLayoutAccessors.readPayloadFlags(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + strPointer, strLength, (strFlags & 1) != 0); - // Propagate FSST symbol table for decompression (use pre-parsed symbols) - final byte[] strFsstSymbols = page.getFsstSymbolTable(); - if (strFsstSymbols != null && strFsstSymbols.length > 0) { - node.setFsstSymbolTable(strFsstSymbols, page.getParsedFsstSymbols()); - } - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT_STRING_VALUE -> { - if (!(singleton instanceof ObjectStringNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazySource and - // metadataParsed=true - final long objStrPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int objStrLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - final int objStrFlags = SlotLayoutAccessors.readPayloadFlags(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + objStrPointer, objStrLength, (objStrFlags & 1) != 0); - // Propagate FSST symbol table for decompression (use pre-parsed symbols) - final byte[] objStrFsstSymbols = page.getFsstSymbolTable(); - if (objStrFsstSymbols != null && objStrFsstSymbols.length > 0) { - node.setFsstSymbolTable(objStrFsstSymbols, page.getParsedFsstSymbols()); - } - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case NUMBER_VALUE -> { - if (!(singleton instanceof NumberNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - // setLazyNumberValue BEFORE bindFixedSlotLazy — setLazyNumberValue sets lazySource and - // metadataParsed=true - final long numPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int numLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - node.setLazyNumberValue(slotMemory, baseOffset + numPointer, numLength); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case OBJECT_NUMBER_VALUE -> { - if (!(singleton instanceof ObjectNumberNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - // setLazyNumberValue BEFORE bindFixedSlotLazy — setLazyNumberValue sets lazySource and - // metadataParsed=true - final long objNumPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int objNumLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - node.setLazyNumberValue(slotMemory, baseOffset + objNumPointer, objNumLength); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; - } - case TEXT -> { - if (!(singleton instanceof TextNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazySource and - // metadataParsed=true - final long textPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int textLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - final int textFlags = SlotLayoutAccessors.readPayloadFlags(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + textPointer, textLength, (textFlags & 1) != 0); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; + // Same-page fast path: reuse cached modified page + if (currentPageKey == targetPageKey && currentPage != null && !currentPage.isClosed()) { + page = currentPage; + } else { + // Different page: get modified page from writer's TIL + page = writer.getModifiedPageForRead(targetPageKey, IndexType.DOCUMENT, -1); + if (page == null) { + // Page not in TIL — fall back to legacy (allocating) moveTo + return moveToLegacy(nodeKey); } - case COMMENT -> { - if (!(singleton instanceof CommentNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazyValueSource - final long commentPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int commentLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - final int commentFlags = SlotLayoutAccessors.readPayloadFlags(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + commentPointer, commentLength, (commentFlags & 1) != 0); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; + // Release previous guard (if any) and update page tracking + // TIL pages don't need guarding — they're pinned by the transaction + if (currentPageGuard != null) { + currentPageGuard.close(); + currentPageGuard = null; } - case ATTRIBUTE -> { - if (!(singleton instanceof AttributeNode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.URI_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazyValueSource - final long attrPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int attrLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + attrPointer, attrLength); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; + currentPage = page; + currentPageKey = targetPageKey; + } + + // Check records[] first: authoritative for writes (prepareRecordForModification stores here) + final DataRecord fromRecords = page.getRecord(slotOffset); + if (fromRecords != null) { + if (fromRecords.getKind() == NodeKind.DELETE) { + return false; } - case PROCESSING_INSTRUCTION -> { - if (!(singleton instanceof PINode node)) { - return false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(slotMemory, baseOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(slotMemory, baseOffset, layout, StructuralField.LOCAL_NAME_KEY)); - // setLazyRawValue BEFORE bindFixedSlotLazy — setLazyRawValue sets lazyValueSource - final long piPointer = SlotLayoutAccessors.readPayloadPointer(slotMemory, baseOffset, layout, 0); - final int piLength = SlotLayoutAccessors.readPayloadLength(slotMemory, baseOffset, layout, 0); - final int piFlags = SlotLayoutAccessors.readPayloadFlags(slotMemory, baseOffset, layout, 0); - node.setLazyRawValue(slotMemory, baseOffset + piPointer, piLength, (piFlags & 1) != 0); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - node.bindFixedSlotLazy(slotMemory, baseOffset, layout); - return true; + @SuppressWarnings("unchecked") + final N node = (N) fromRecords; + this.currentNode = node; + this.currentNodeKind = (NodeKind) fromRecords.getKind(); + this.currentNodeKey = nodeKey; + this.currentSingleton = null; + this.singletonMode = false; + this.flyweightMode = false; + return true; + } + + // Check slot data on modified page + final MemorySegment data = page.getSlot(slotOffset); + if (data == null) { + // Not in modified page heap either — fall back to legacy + return moveToLegacy(nodeKey); + } + + // Read node kind from first byte + final byte kindByte = data.get(java.lang.foreign.ValueLayout.JAVA_BYTE, 0); + final NodeKind kind = NodeKind.getKind(kindByte); + + if (kind == NodeKind.DELETE) { + return false; + } + + // Get singleton instance for this node type + final ImmutableNode singleton = getSingletonForKind(kind); + if (singleton == null) { + return moveToLegacy(nodeKey); + } + + // Bind singleton to page data (zero allocation) + // Cache slottedPage locally to avoid repeated virtual calls to getSlottedPage() + final MemorySegment sp = page.getSlottedPage(); + if (sp != null && singleton instanceof FlyweightNode fn) { + final int heapOffset = PageLayout.getDirHeapOffset(sp, slotOffset); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + fn.bind(sp, recordBase, nodeKey, slotOffset); + // Propagate DeweyID lazily — no SirixDeweyID parsing until getDeweyID() called. + // MUST always set (even null) to clear stale DeweyID from previous singleton reuse. + if (resourceConfig.areDeweyIDsStored && fn instanceof Node node) { + node.setDeweyIDBytes(page.getDeweyIdAsByteArray(slotOffset)); } - default -> { - return false; + } else { + // Legacy format: populate singleton from serialized data (NO ALLOCATION) + reusableBytesIn.reset(data, 1); + final byte[] deweyId = resourceConfig.areDeweyIDsStored + ? page.getDeweyIdAsByteArray(slotOffset) : null; + populateSingleton(singleton, reusableBytesIn, nodeKey, deweyId, kind, page); + } + + // Update state — singleton mode, no guard needed for TIL pages + this.currentSingleton = singleton; + this.currentNodeKind = kind; + this.currentNodeKey = nodeKey; + this.currentNode = null; + this.singletonMode = true; + this.flyweightMode = false; + + return true; + } + + /** + * Propagate FSST symbol table from page to a flyweight string node. + * Required for lazy decompression of FSST-compressed strings in singleton mode. + */ + private static void propagateFsstToFlyweight(final FlyweightNode fn, final KeyValueLeafPage page) { + final byte[] fsstTable = page.getFsstSymbolTable(); + if (fsstTable != null && fsstTable.length > 0) { + if (fn instanceof StringNode sn) { + sn.setFsstSymbolTable(fsstTable); + } else if (fn instanceof ObjectStringNode osn) { + osn.setFsstSymbolTable(fsstTable); } } } /** - * Get the singleton instance for a given node kind. Lazily creates singletons on first use. + * Get the singleton instance for a given node kind. + * Lazily creates singletons on first use. * * @param kind the node kind * @return the singleton instance, or null if not supported */ private ImmutableNode getSingletonForKind(NodeKind kind) { + final int id = kind.getId() & 0xFF; + if (id >= singletonByKindId.length) { + return null; + } + ImmutableNode singleton = singletonByKindId[id]; + if (singleton != null) { + return singleton; + } + singleton = createSingletonForKind(kind); + if (singleton != null) { + singletonByKindId[id] = singleton; + } + return singleton; + } + + /** + * Create a singleton instance for the given node kind (cold path, called once per kind). + */ + private ImmutableNode createSingletonForKind(NodeKind kind) { return switch (kind) { - case OBJECT -> { - if (singletonObject == null) { - singletonObject = - new ObjectNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObject; - } - case ARRAY -> { - if (singletonArray == null) { - singletonArray = - new ArrayNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonArray; - } - case OBJECT_KEY -> { - if (singletonObjectKey == null) { - singletonObjectKey = - new ObjectKeyNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObjectKey; - } - case STRING_VALUE -> { - if (singletonString == null) { - singletonString = new StringNode(0, 0, 0, 0, 0, 0, 0, null, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonString; - } - case NUMBER_VALUE -> { - if (singletonNumber == null) { - singletonNumber = new NumberNode(0, 0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonNumber; - } - case BOOLEAN_VALUE -> { - if (singletonBoolean == null) { - singletonBoolean = - new BooleanNode(0, 0, 0, 0, 0, 0, 0, false, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonBoolean; - } - case NULL_VALUE -> { - if (singletonNull == null) { - singletonNull = new NullNode(0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonNull; - } - case OBJECT_STRING_VALUE -> { - if (singletonObjectString == null) { - singletonObjectString = - new ObjectStringNode(0, 0, 0, 0, 0, null, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObjectString; - } - case OBJECT_NUMBER_VALUE -> { - if (singletonObjectNumber == null) { - singletonObjectNumber = - new ObjectNumberNode(0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObjectNumber; - } - case OBJECT_BOOLEAN_VALUE -> { - if (singletonObjectBoolean == null) { - singletonObjectBoolean = - new ObjectBooleanNode(0, 0, 0, 0, 0, false, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObjectBoolean; - } - case OBJECT_NULL_VALUE -> { - if (singletonObjectNull == null) { - singletonObjectNull = new ObjectNullNode(0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonObjectNull; - } - case JSON_DOCUMENT -> { - if (singletonJsonDocumentRoot == null) { - singletonJsonDocumentRoot = new JsonDocumentRootNode(Fixed.DOCUMENT_NODE_KEY.getStandardProperty(), - Fixed.NULL_NODE_KEY.getStandardProperty(), Fixed.NULL_NODE_KEY.getStandardProperty(), 0, 0, - resourceConfig.nodeHashFunction); - } - yield singletonJsonDocumentRoot; - } - case XML_DOCUMENT -> { - if (singletonXmlDocumentRoot == null) { - singletonXmlDocumentRoot = new XmlDocumentRootNode(Fixed.DOCUMENT_NODE_KEY.getStandardProperty(), - Fixed.NULL_NODE_KEY.getStandardProperty(), Fixed.NULL_NODE_KEY.getStandardProperty(), 0, 0, - resourceConfig.nodeHashFunction); - } - yield singletonXmlDocumentRoot; - } - case ELEMENT -> { - if (singletonElement == null) { - singletonElement = new ElementNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - resourceConfig.nodeHashFunction, (byte[]) null, null, null, EMPTY_QNM); - } - yield singletonElement; - } - case ATTRIBUTE -> { - if (singletonAttribute == null) { - singletonAttribute = new AttributeNode(0, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], - resourceConfig.nodeHashFunction, (byte[]) null, EMPTY_QNM); - } - yield singletonAttribute; - } - case NAMESPACE -> { - if (singletonNamespace == null) { - singletonNamespace = - new NamespaceNode(0, 0, 0, 0, 0, 0, 0, 0, 0, resourceConfig.nodeHashFunction, (byte[]) null, EMPTY_QNM); - } - yield singletonNamespace; - } - case TEXT -> { - if (singletonText == null) { - singletonText = - new TextNode(0, 0, 0, 0, 0, 0, 0, new byte[0], false, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonText; - } - case COMMENT -> { - if (singletonComment == null) { - singletonComment = - new CommentNode(0, 0, 0, 0, 0, 0, 0, new byte[0], false, resourceConfig.nodeHashFunction, (byte[]) null); - } - yield singletonComment; - } - case PROCESSING_INSTRUCTION -> { - if (singletonPI == null) { - singletonPI = new PINode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], false, - resourceConfig.nodeHashFunction, (byte[]) null, EMPTY_QNM); - } - yield singletonPI; - } - // Other types fall back to legacy mode. + case OBJECT -> singletonObject = new ObjectNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case ARRAY -> singletonArray = new ArrayNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case OBJECT_KEY -> singletonObjectKey = new ObjectKeyNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case STRING_VALUE -> singletonString = new StringNode(0, 0, 0, 0, 0, 0, 0, null, + resourceConfig.nodeHashFunction, (byte[]) null); + case NUMBER_VALUE -> singletonNumber = new NumberNode(0, 0, 0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case BOOLEAN_VALUE -> singletonBoolean = new BooleanNode(0, 0, 0, 0, 0, 0, 0, false, + resourceConfig.nodeHashFunction, (byte[]) null); + case NULL_VALUE -> singletonNull = new NullNode(0, 0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case OBJECT_STRING_VALUE -> singletonObjectString = new ObjectStringNode(0, 0, 0, 0, 0, null, + resourceConfig.nodeHashFunction, (byte[]) null); + case OBJECT_NUMBER_VALUE -> singletonObjectNumber = new ObjectNumberNode(0, 0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case OBJECT_BOOLEAN_VALUE -> singletonObjectBoolean = new ObjectBooleanNode(0, 0, 0, 0, 0, false, + resourceConfig.nodeHashFunction, (byte[]) null); + case OBJECT_NULL_VALUE -> singletonObjectNull = new ObjectNullNode(0, 0, 0, 0, 0, + resourceConfig.nodeHashFunction, (byte[]) null); + case JSON_DOCUMENT -> singletonJsonDocRoot = new JsonDocumentRootNode(0, resourceConfig.nodeHashFunction); + case ELEMENT -> singletonElement = new ElementNode(0, resourceConfig.nodeHashFunction); + case ATTRIBUTE -> singletonAttribute = new AttributeNode(0, resourceConfig.nodeHashFunction); + case TEXT -> singletonText = new TextNode(0, resourceConfig.nodeHashFunction); + case COMMENT -> singletonComment = new CommentNode(0, resourceConfig.nodeHashFunction); + case PROCESSING_INSTRUCTION -> singletonPI = new PINode(0, resourceConfig.nodeHashFunction); + case NAMESPACE -> singletonNamespace = new NamespaceNode(0, resourceConfig.nodeHashFunction); + case XML_DOCUMENT -> singletonXmlDocRoot = new XmlDocumentRootNode(0, resourceConfig.nodeHashFunction); default -> null; }; } - + /** * Populate a singleton node from serialized data. * * @param singleton the singleton to populate - * @param source the BytesIn source positioned after the kind byte - * @param nodeKey the node key - * @param deweyId the DeweyID bytes - * @param kind the node kind + * @param source the BytesIn source positioned after the kind byte + * @param nodeKey the node key + * @param deweyId the DeweyID bytes + * @param kind the node kind */ - private void populateSingleton(ImmutableNode singleton, BytesIn source, long nodeKey, byte[] deweyId, - NodeKind kind, KeyValueLeafPage page) { + private void populateSingleton(ImmutableNode singleton, BytesIn source, + long nodeKey, byte[] deweyId, NodeKind kind, + KeyValueLeafPage page) { switch (kind) { - case OBJECT -> - ((ObjectNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case ARRAY -> - ((ArrayNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case OBJECT_KEY -> - ((ObjectKeyNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); + case OBJECT -> ((ObjectNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case ARRAY -> ((ArrayNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case OBJECT_KEY -> ((ObjectKeyNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); case STRING_VALUE -> { StringNode stringNode = (StringNode) singleton; stringNode.readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - // Propagate FSST symbol table for decompression (use pre-parsed symbols) + // Propagate FSST symbol table for decompression byte[] fsstSymbolTable = page.getFsstSymbolTable(); if (fsstSymbolTable != null && fsstSymbolTable.length > 0) { - stringNode.setFsstSymbolTable(fsstSymbolTable, page.getParsedFsstSymbols()); + stringNode.setFsstSymbolTable(fsstSymbolTable); } } - case NUMBER_VALUE -> - ((NumberNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case BOOLEAN_VALUE -> - ((BooleanNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case NULL_VALUE -> - ((NullNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); + case NUMBER_VALUE -> ((NumberNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case BOOLEAN_VALUE -> ((BooleanNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case NULL_VALUE -> ((NullNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); case OBJECT_STRING_VALUE -> { ObjectStringNode objectStringNode = (ObjectStringNode) singleton; objectStringNode.readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - // Propagate FSST symbol table for decompression (use pre-parsed symbols) + // Propagate FSST symbol table for decompression byte[] fsstSymbolTable = page.getFsstSymbolTable(); if (fsstSymbolTable != null && fsstSymbolTable.length > 0) { - objectStringNode.setFsstSymbolTable(fsstSymbolTable, page.getParsedFsstSymbols()); + objectStringNode.setFsstSymbolTable(fsstSymbolTable); } } case OBJECT_NUMBER_VALUE -> ((ObjectNumberNode) singleton).readFrom(source, nodeKey, deweyId, @@ -1109,27 +1055,27 @@ private void populateSingleton(ImmutableNode singleton, BytesIn source, long resourceConfig.nodeHashFunction, resourceConfig); case JSON_DOCUMENT -> ((JsonDocumentRootNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); + case ELEMENT -> ((ElementNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case ATTRIBUTE -> ((AttributeNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case TEXT -> ((TextNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case COMMENT -> ((CommentNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case PROCESSING_INSTRUCTION -> ((PINode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); + case NAMESPACE -> ((NamespaceNode) singleton).readFrom(source, nodeKey, deweyId, + resourceConfig.nodeHashFunction, resourceConfig); case XML_DOCUMENT -> ((XmlDocumentRootNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case ELEMENT -> - ((ElementNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case ATTRIBUTE -> - ((AttributeNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case NAMESPACE -> - ((NamespaceNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case TEXT -> - ((TextNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case COMMENT -> - ((CommentNode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); - case PROCESSING_INSTRUCTION -> - ((PINode) singleton).readFrom(source, nodeKey, deweyId, resourceConfig.nodeHashFunction, resourceConfig); default -> throw new IllegalStateException("Unexpected singleton kind: " + kind); } } - + /** - * Move to an item in the item list (negative keys). Falls back to object mode since item list uses - * objects. + * Move to an item in the item list (negative keys). + * Falls back to object mode since item list uses objects. * * @param nodeKey the negative node key * @return true if the move was successful @@ -1138,9 +1084,10 @@ private boolean moveToItemList(final long nodeKey) { if (itemList.size() > 0) { DataRecord item = itemList.getItem(nodeKey); if (item != null) { - // Move succeeded - release previous page guard and switch to object mode. + // Move succeeded - release previous page guard and switch to object mode releaseCurrentPageGuard(); - // noinspection unchecked + flyweightMode = false; + //noinspection unchecked setCurrentNode((N) item); this.currentNodeKey = nodeKey; return true; @@ -1149,9 +1096,9 @@ private boolean moveToItemList(final long nodeKey) { // Item not found - keep the current position unchanged return false; } - + /** - * Legacy object-based moveTo path. + * Legacy object-based moveTo for when flyweight mode is not available. * * @param nodeKey the node key to move to * @return true if the move was successful @@ -1160,43 +1107,580 @@ private boolean moveToLegacy(final long nodeKey) { DataRecord newNode; try { newNode = storageEngineReader.getRecord(nodeKey, IndexType.DOCUMENT, -1); - } catch (final SirixIOException | UncheckedIOException | IllegalArgumentException | IllegalStateException e) { + } catch (final SirixIOException | UncheckedIOException | IllegalArgumentException e) { newNode = null; } if (newNode == null) { return false; } else { - // Only release guard if we were in singleton mode. - if (singletonMode) { + // Only release guard if we were in flyweight/singleton mode + if (flyweightMode || singletonMode) { releaseCurrentPageGuard(); + flyweightMode = false; singletonMode = false; } - // noinspection unchecked + //noinspection unchecked setCurrentNode((N) newNode); this.currentNodeKey = nodeKey; return true; } } - + /** - * Release the current page guard if one is held. This allows the page to be evicted if needed. + * Release the current page guard if one is held. + * This allows the page to be evicted if needed. */ protected void releaseCurrentPageGuard() { if (currentPageGuard != null) { currentPageGuard.close(); currentPageGuard = null; + currentPage = null; + currentPageKey = -1; + } + } + + /** + * Parse the field offsets from the current slot for fast getter access. + * This is called once per moveTo() and caches the byte offset of each field. + *

+ * The slot format starts with: + * - Byte 0: NodeKind byte + * - Byte 1+: Node-specific fields (varints) + *

+ * Field order varies by node kind, but common structural nodes follow: + * - parentKey (delta varint) + * - prevRev (signed varint) + * - lastModRev (signed varint) + * - [pathNodeKey for ARRAY] (delta varint) + * - rightSiblingKey (delta varint) + * - leftSiblingKey (delta varint) + * - firstChildKey (delta varint) for structural nodes + * - lastChildKey (delta varint) for structural nodes + * - childCount (signed varint) if storeChildCount + * - hash (8 bytes fixed) if hashType != NONE + * - descendantCount (signed varint) if hashType != NONE + */ + protected void parseFieldOffsets() { + // Start after NodeKind byte + int offset = 1; + + switch (currentNodeKind) { + case OBJECT -> parseObjectNodeOffsets(offset); + case ARRAY -> parseArrayNodeOffsets(offset); + case OBJECT_KEY -> parseObjectKeyNodeOffsets(offset); + // Non-object value nodes have siblings (used in arrays) + case STRING_VALUE -> parseStringValueNodeOffsets(offset); + case NUMBER_VALUE -> parseNumberValueNodeOffsets(offset); + case BOOLEAN_VALUE -> parseBooleanValueNodeOffsets(offset); + case NULL_VALUE -> parseNullValueNodeOffsets(offset); + // Object value nodes have no siblings (single child of ObjectKeyNode) + case OBJECT_STRING_VALUE -> parseObjectStringValueNodeOffsets(offset); + case OBJECT_NUMBER_VALUE -> parseObjectNumberValueNodeOffsets(offset); + case OBJECT_BOOLEAN_VALUE -> parseObjectBooleanValueNodeOffsets(offset); + case OBJECT_NULL_VALUE -> parseObjectNullValueNodeOffsets(offset); + case JSON_DOCUMENT -> { + // JSON_DOCUMENT has special serialization - fall back to object mode + // for simplicity. Document root is typically only visited once. + java.util.Arrays.fill(cachedFieldOffsets, -1); + } + default -> { + // For unsupported node kinds, set all offsets to -1 (use object fallback) + java.util.Arrays.fill(cachedFieldOffsets, -1); + } + } + } + + /** + * Parse field offsets for OBJECT node. + * Format: parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, + * firstChildKey, lastChildKey, [childCount], [hash, descendantCount] + */ + private void parseObjectNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + // Optional childCount + if (resourceConfig.storeChildCount()) { + cachedFieldOffsets[FIELD_CHILD_COUNT] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + } else { + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + } + + // Optional hash and descendant count + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; // Fixed 8 bytes for hash + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + } + + // OBJECT nodes don't have nameKey/pathNodeKey/value + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for ARRAY node. + * Format: parentKey, prevRev, lastModRev, pathNodeKey, rightSiblingKey, leftSiblingKey, + * firstChildKey, lastChildKey, [childCount], [hash, descendantCount] + */ + private void parseArrayNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + if (resourceConfig.storeChildCount()) { + cachedFieldOffsets[FIELD_CHILD_COUNT] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + } else { + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + } + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + } + + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for OBJECT_KEY node. + * Format: parentKey, prevRev, lastModRev, pathNodeKey, rightSiblingKey, + * leftSiblingKey, firstChildKey, nameKey, [hash, descendantCount] + */ + private void parseObjectKeyNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_NAME_KEY] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + } + + // OBJECT_KEY has no lastChildKey, childCount + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for STRING_VALUE/OBJECT_STRING_VALUE node. + * Format: parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, [hash], valueLength, value + */ + private void parseStringValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + // Value starts at current offset (length + bytes) + cachedFieldOffsets[FIELD_VALUE] = offset; + + // Leaf nodes don't have children + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; // Value parsing done on demand + } + + /** + * Parse field offsets for NUMBER_VALUE/OBJECT_NUMBER_VALUE node. + * Format: parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, [hash], numberValue + */ + private void parseNumberValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + cachedFieldOffsets[FIELD_VALUE] = offset; + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for BOOLEAN_VALUE/OBJECT_BOOLEAN_VALUE node. + * Format: parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, [hash], booleanValue + */ + private void parseBooleanValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + cachedFieldOffsets[FIELD_VALUE] = offset; + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for NULL_VALUE node (used in arrays). + * Format: parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, [hash] + */ + private void parseNullValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; } - currentPage = null; - currentPageKey = -1; - currentSlotOffset = -1; - singletonDeweyBound = true; + + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + // ==================== OBJECT_* VALUE NODES (no siblings) ==================== + + /** + * Parse field offsets for OBJECT_STRING_VALUE node. + * Format: parentKey, prevRev, lastModRev, [hash], stringValue + * Note: No sibling keys - single child of ObjectKeyNode. + */ + private void parseObjectStringValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + cachedFieldOffsets[FIELD_VALUE] = offset; + + // No siblings for object value nodes + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for OBJECT_NUMBER_VALUE node. + * Format: parentKey, prevRev, lastModRev, [hash], numberValue + * Note: No sibling keys - single child of ObjectKeyNode. + */ + private void parseObjectNumberValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + cachedFieldOffsets[FIELD_VALUE] = offset; + + // No siblings for object value nodes + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for OBJECT_BOOLEAN_VALUE node. + * Format: parentKey, prevRev, lastModRev, [hash], booleanValue + * Note: No sibling keys - single child of ObjectKeyNode. + */ + private void parseObjectBooleanValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + cachedFieldOffsets[FIELD_VALUE] = offset; + + // No siblings for object value nodes + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for OBJECT_NULL_VALUE node. + * Format: parentKey, prevRev, lastModRev, [hash] + * Note: No sibling keys - single child of ObjectKeyNode. + */ + private void parseObjectNullValueNodeOffsets(int offset) { + cachedFieldOffsets[FIELD_PARENT_KEY] = offset; + offset += DeltaVarIntCodec.deltaLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_PREV_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + if (resourceConfig.hashType != HashType.NONE) { + cachedFieldOffsets[FIELD_HASH] = offset; + offset += 8; + } else { + cachedFieldOffsets[FIELD_HASH] = -1; + } + + // No siblings for object value nodes + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; + } + + /** + * Parse field offsets for JSON_DOCUMENT node. + * Format: firstChildKey (varint), descendantCount (8 bytes) + * Note: JSON_DOCUMENT has fixed parent, prevRev, lastModRev values, not serialized. + */ + private void parseJsonDocumentNodeOffsets(int offset) { + // JSON_DOCUMENT has fixed values for these, not serialized: + // - parentKey = NULL_NODE_KEY + // - prevRev = NULL_REVISION_NUMBER + // - lastModRev = NULL_REVISION_NUMBER + cachedFieldOffsets[FIELD_PARENT_KEY] = -1; // Fixed value, not serialized + cachedFieldOffsets[FIELD_PREV_REVISION] = -1; // Fixed value + cachedFieldOffsets[FIELD_LAST_MOD_REVISION] = -1; // Fixed value + + // firstChildKey is a plain varint (not delta encoded) + cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] = offset; + offset += DeltaVarIntCodec.varintLength(currentSlot, offset); + + // In JSON_DOCUMENT, firstChildKey == lastChildKey + cachedFieldOffsets[FIELD_LAST_CHILD_KEY] = -1; // Same as firstChildKey + + // descendantCount is always stored as 8-byte long + cachedFieldOffsets[FIELD_DESCENDANT_COUNT] = offset; + offset += 8; + + // These fields don't exist for JSON_DOCUMENT + cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_LEFT_SIBLING_KEY] = -1; + cachedFieldOffsets[FIELD_CHILD_COUNT] = -1; + cachedFieldOffsets[FIELD_HASH] = -1; + cachedFieldOffsets[FIELD_NAME_KEY] = -1; + cachedFieldOffsets[FIELD_PATH_NODE_KEY] = -1; + cachedFieldOffsets[FIELD_VALUE] = -1; + cachedFieldOffsets[FIELD_END] = offset; } @Override public boolean moveToRightSibling() { assertNotClosed(); - // Use cursor getter and avoid unnecessary materialization. + // Use flyweight getter if available to avoid node materialization if (!hasRightSibling()) { return false; } @@ -1206,7 +1690,7 @@ public boolean moveToRightSibling() { @Override public long getNodeKey() { assertNotClosed(); - if (singletonMode) { + if ((FLYWEIGHT_ENABLED && flyweightMode) || (SINGLETON_ENABLED && singletonMode)) { return currentNodeKey; } return getCurrentNode().getNodeKey(); @@ -1215,20 +1699,23 @@ public long getNodeKey() { @Override public long getHash() { assertNotClosed(); - if (singletonMode) { - return currentSingleton != null - ? currentSingleton.getHash() - : 0L; + if (SINGLETON_ENABLED && singletonMode) { + return currentSingleton != null ? currentSingleton.getHash() : 0L; + } + if (FLYWEIGHT_ENABLED && flyweightMode) { + if (cachedFieldOffsets[FIELD_HASH] >= 0) { + return DeltaVarIntCodec.readLongFromSegment(currentSlot, cachedFieldOffsets[FIELD_HASH]); + } + N node = getCurrentNode(); + return node != null ? node.getHash() : 0L; } - return currentNode != null - ? currentNode.getHash() - : 0L; + return currentNode != null ? currentNode.getHash() : 0L; } @Override public NodeKind getKind() { assertNotClosed(); - if (singletonMode) { + if ((FLYWEIGHT_ENABLED && flyweightMode) || (SINGLETON_ENABLED && singletonMode)) { return currentNodeKind; } return getCurrentNode().getKind(); @@ -1236,12 +1723,11 @@ public NodeKind getKind() { /** * Make sure that the transaction is not yet closed when calling this method. - * Uses {@code assert} so the volatile read of {@code isClosed} is eliminated - * when assertions are disabled (the production default), removing a memory - * barrier from every getter on the read hot path. */ public void assertNotClosed() { - assert !isClosed : "Transaction is already closed."; + if (isClosed) { + throw new IllegalStateException("Transaction is already closed."); + } } /** @@ -1262,6 +1748,25 @@ public StorageEngineReader getPageTransaction() { public final void setPageReadTransaction(@Nullable final StorageEngineReader pageReadTransaction) { assertNotClosed(); storageEngineReader = pageReadTransaction; + cachedNodeReader = resolveNodeReader(pageReadTransaction); + cachedWriter = (pageReadTransaction instanceof StorageEngineWriter w) ? w : null; + } + + /** + * Resolve the underlying {@link NodeStorageEngineReader} from a storage engine reader. + * For read-only transactions, this is the reader itself. + * For write transactions (where the reader is a {@link StorageEngineWriter}), + * extracts the delegate reader via {@link StorageEngineWriter#getStorageEngineReader()}. + */ + private static NodeStorageEngineReader resolveNodeReader(@Nullable final StorageEngineReader reader) { + if (reader instanceof NodeStorageEngineReader r) { + return r; + } + if (reader instanceof StorageEngineWriter w + && w.getStorageEngineReader() instanceof NodeStorageEngineReader r) { + return r; + } + return null; } @Override @@ -1276,22 +1781,29 @@ public final long getMaxNodeKey() { * @return structural node instance of current node */ public final StructNode getStructuralNode() { - if (singletonMode && currentSingleton instanceof StructNode structNode) { + // In flyweight mode, materialize if needed + N node = getCurrentNode(); + if (node instanceof StructNode structNode) { return structNode; } + return new io.sirix.node.NullNode(node); + } - // Materialize a structural node only when needed. - final N node = getCurrentNode(); - if (node instanceof final StructNode structNode) { + @Override + public final StructNode getStructuralNodeView() { + if (currentNode instanceof StructNode structNode) { return structNode; } - return new io.sirix.node.NullNode(node); + if (SINGLETON_ENABLED && singletonMode && currentSingleton instanceof StructNode structNode) { + return structNode; + } + return getStructuralNode(); } @Override public boolean moveToNextFollowing() { assertNotClosed(); - // Use cursor getters to avoid unnecessary materialization. + // Use flyweight getters to avoid node materialization while (!hasRightSibling() && hasParent()) { moveToParent(); } @@ -1301,7 +1813,7 @@ public boolean moveToNextFollowing() { @Override public boolean hasNode(final @NonNegative long key) { assertNotClosed(); - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); final boolean retVal = moveTo(key); // Restore to the saved position @@ -1312,8 +1824,8 @@ public boolean hasNode(final @NonNegative long key) { @Override public boolean hasParent() { assertNotClosed(); - if (singletonMode && currentSingleton != null) { - return currentSingleton.hasParent(); + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_PARENT_KEY] >= 0) { + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } return getCurrentNode().hasParent(); } @@ -1321,7 +1833,7 @@ public boolean hasParent() { @Override public boolean hasFirstChild() { assertNotClosed(); - if (singletonMode) { + if ((SINGLETON_ENABLED && singletonMode) || (FLYWEIGHT_ENABLED && flyweightMode)) { return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } return getStructuralNode().hasFirstChild(); @@ -1330,7 +1842,7 @@ public boolean hasFirstChild() { @Override public boolean hasRightSibling() { assertNotClosed(); - if (singletonMode) { + if ((SINGLETON_ENABLED && singletonMode) || (FLYWEIGHT_ENABLED && flyweightMode)) { return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } return getStructuralNode().hasRightSibling(); @@ -1339,37 +1851,46 @@ public boolean hasRightSibling() { @Override public long getRightSiblingKey() { assertNotClosed(); - if (singletonMode && currentSingleton instanceof StructNode sn) { + if (SINGLETON_ENABLED && singletonMode && currentSingleton instanceof StructNode sn) { return sn.getRightSiblingKey(); } + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY] >= 0) { + return DeltaVarIntCodec.decodeDeltaFromSegment(currentSlot, cachedFieldOffsets[FIELD_RIGHT_SIBLING_KEY], currentNodeKey); + } return getStructuralNode().getRightSiblingKey(); } @Override public long getFirstChildKey() { assertNotClosed(); - if (singletonMode && currentSingleton instanceof StructNode sn) { + if (SINGLETON_ENABLED && singletonMode && currentSingleton instanceof StructNode sn) { return sn.getFirstChildKey(); } + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_FIRST_CHILD_KEY] >= 0) { + return DeltaVarIntCodec.decodeDeltaFromSegment(currentSlot, cachedFieldOffsets[FIELD_FIRST_CHILD_KEY], currentNodeKey); + } return getStructuralNode().getFirstChildKey(); } @Override public long getParentKey() { assertNotClosed(); - if (singletonMode && currentSingleton != null) { + if (SINGLETON_ENABLED && singletonMode && currentSingleton != null) { return currentSingleton.getParentKey(); } + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_PARENT_KEY] >= 0) { + return DeltaVarIntCodec.decodeDeltaFromSegment(currentSlot, cachedFieldOffsets[FIELD_PARENT_KEY], currentNodeKey); + } return getCurrentNode().getParentKey(); } @Override public NodeKind getParentKind() { assertNotClosed(); - if (getParentKey() == Fixed.NULL_NODE_KEY.getStandardProperty()) { + final long parentKey = getParentKey(); + if (parentKey == Fixed.NULL_NODE_KEY.getStandardProperty()) { return NodeKind.UNKNOWN; } - // Save current position using cursor-compatible getters. final long savedNodeKey = getNodeKey(); moveToParent(); final NodeKind parentKind = getKind(); @@ -1380,7 +1901,7 @@ public NodeKind getParentKind() { @Override public boolean moveToNext() { assertNotClosed(); - // Use cursor getter directly. + // Use flyweight getter if available if (hasRightSibling()) { // Right sibling node. return moveTo(getRightSiblingKey()); @@ -1392,7 +1913,7 @@ public boolean moveToNext() { @Override public boolean hasLastChild() { assertNotClosed(); - // If it has a first child, it has a last child. + // Use flyweight getter - if it has a first child, it also has a last child return hasFirstChild(); } @@ -1400,7 +1921,7 @@ public boolean hasLastChild() { public NodeKind getLastChildKind() { assertNotClosed(); if (hasLastChild()) { - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); moveToLastChild(); final NodeKind lastChildKind = getKind(); @@ -1414,7 +1935,7 @@ public NodeKind getLastChildKind() { public NodeKind getFirstChildKind() { assertNotClosed(); if (hasFirstChild()) { - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); moveToFirstChild(); final NodeKind firstChildKind = getKind(); @@ -1428,7 +1949,7 @@ public NodeKind getFirstChildKind() { public long getLastChildKey() { assertNotClosed(); if (hasLastChild()) { - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); moveToLastChild(); final long lastChildNodeKey = getNodeKey(); @@ -1441,13 +1962,16 @@ public long getLastChildKey() { @Override public long getChildCount() { assertNotClosed(); + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_CHILD_COUNT] >= 0) { + return DeltaVarIntCodec.decodeSignedFromSegment(currentSlot, cachedFieldOffsets[FIELD_CHILD_COUNT]); + } return getStructuralNode().getChildCount(); } @Override public boolean hasChildren() { assertNotClosed(); - if (singletonMode) { + if ((SINGLETON_ENABLED && singletonMode) || (FLYWEIGHT_ENABLED && flyweightMode)) { return hasFirstChild(); } return getStructuralNode().hasFirstChild(); @@ -1456,6 +1980,9 @@ public boolean hasChildren() { @Override public long getDescendantCount() { assertNotClosed(); + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_DESCENDANT_COUNT] >= 0) { + return DeltaVarIntCodec.decodeSignedFromSegment(currentSlot, cachedFieldOffsets[FIELD_DESCENDANT_COUNT]); + } return getStructuralNode().getDescendantCount(); } @@ -1469,7 +1996,7 @@ public NodeKind getPathKind() { public NodeKind getRightSiblingKind() { assertNotClosed(); if (hasRightSibling()) { - // Save current position using cursor-compatible getters. + // Save current position using flyweight-compatible getters final long savedNodeKey = getNodeKey(); moveToRightSibling(); final NodeKind rightSiblingKind = getKind(); @@ -1494,58 +2021,24 @@ public CommitCredentials getCommitCredentials() { @Override public SirixDeweyID getDeweyID() { assertNotClosed(); - if (singletonMode) { - bindSingletonDeweyBytesIfNeeded(); - return currentSingleton != null - ? currentSingleton.getDeweyID() - : null; - } - return currentNode != null - ? currentNode.getDeweyID() - : null; - } - - private void bindSingletonDeweyBytesIfNeeded() { - if (singletonDeweyBound || currentSingleton == null || !resourceConfig.areDeweyIDsStored || currentPage == null - || currentSlotOffset < 0) { - return; + if (SINGLETON_ENABLED && singletonMode) { + return currentSingleton != null ? currentSingleton.getDeweyID() : null; } - - final byte[] deweyIdBytes = currentPage.getDeweyIdAsByteArray(currentSlotOffset); - switch (currentNodeKind) { - // JSON node kinds - case JSON_DOCUMENT -> ((JsonDocumentRootNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT -> ((ObjectNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case ARRAY -> ((ArrayNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT_KEY -> ((ObjectKeyNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case STRING_VALUE -> ((StringNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT_STRING_VALUE -> ((ObjectStringNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case NUMBER_VALUE -> ((NumberNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT_NUMBER_VALUE -> ((ObjectNumberNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case BOOLEAN_VALUE -> ((BooleanNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT_BOOLEAN_VALUE -> ((ObjectBooleanNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case NULL_VALUE -> ((NullNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case OBJECT_NULL_VALUE -> ((ObjectNullNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - // XML node kinds - case XML_DOCUMENT -> ((XmlDocumentRootNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case ELEMENT -> ((ElementNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case ATTRIBUTE -> ((AttributeNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case NAMESPACE -> ((NamespaceNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case TEXT -> ((TextNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case COMMENT -> ((CommentNode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - case PROCESSING_INSTRUCTION -> ((PINode) currentSingleton).setDeweyIDBytes(deweyIdBytes); - default -> { - // Unknown singleton kind - harmless. + if (FLYWEIGHT_ENABLED && flyweightMode) { + if (currentDeweyId != null) { + return new SirixDeweyID(currentDeweyId); } + N node = getCurrentNode(); + return node != null ? node.getDeweyID() : null; } - singletonDeweyBound = true; + return currentNode != null ? currentNode.getDeweyID() : null; } @Override public int getPreviousRevisionNumber() { assertNotClosed(); - if (singletonMode && currentSingleton != null) { - return currentSingleton.getPreviousRevisionNumber(); + if (FLYWEIGHT_ENABLED && flyweightMode && cachedFieldOffsets[FIELD_PREV_REVISION] >= 0) { + return DeltaVarIntCodec.decodeSignedFromSegment(currentSlot, cachedFieldOffsets[FIELD_PREV_REVISION]); } return getCurrentNode().getPreviousRevisionNumber(); } @@ -1554,31 +2047,45 @@ public int getPreviousRevisionNumber() { public boolean isClosed() { return isClosed; } - + /** - * Check if singleton mode is currently active. Package-private for testing purposes. + * Check if flyweight mode is currently active. + * Package-private for testing purposes. + * + * @return true if flyweight mode is active (reading directly from MemorySegment) + */ + boolean isFlyweightMode() { + return flyweightMode; + } + + /** + * Check if singleton mode is currently active. + * Package-private for testing purposes. * * @return true if singleton mode is active (using mutable singleton nodes) */ boolean isSingletonMode() { return singletonMode; } - + /** - * Check if zero-allocation mode is active. Package-private for testing purposes. + * Check if zero-allocation mode is active (either flyweight or singleton). + * Package-private for testing purposes. * - * @return true if zero-allocation mode is active + * @return true if any zero-allocation mode is active */ boolean isZeroAllocationMode() { - return singletonMode; + return flyweightMode || singletonMode; } @Override public void close() { if (!isClosed) { - // Release page guard first to allow page eviction. + // Release flyweight state and page guard FIRST to allow page eviction releaseCurrentPageGuard(); - + currentSlot = null; + flyweightMode = false; + // Callback on session to make sure everything is cleaned up. resourceSession.closeReadTransaction(id); diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeTrxImpl.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeTrxImpl.java index d47ec8959..c2d37283f 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeTrxImpl.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/AbstractNodeTrxImpl.java @@ -21,9 +21,11 @@ import io.sirix.index.path.summary.PathSummaryWriter; import io.sirix.node.SirixDeweyID; import io.sirix.node.interfaces.DataRecord; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableNode; +import io.sirix.page.KeyValueLeafPage; import io.sirix.page.UberPage; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.Nullable; @@ -370,7 +372,15 @@ protected void checkAccessAndCommit() { } protected final void persistUpdatedRecord(final DataRecord record) { - storageEngineWriter.updateRecordSlot(record, IndexType.DOCUMENT, -1); + if (record instanceof FlyweightNode fn && fn.isWriteSingleton() && fn.getOwnerPage() != null) { + return; // Bound write singleton — mutations already on heap via inlined setters + } + // Ensure the mutated record is stored in the TIL's modified page. + // For records obtained via prepareRecordForModification(), the page is already + // in the TIL and the record is in records[] — this is a safe (redundant) setRecord. + // For records obtained via nodeReadOnlyTrx.getCurrentNode() (e.g., setName/setValue), + // this ensures the page enters the TIL and the record gets into records[]. + storageEngineWriter.persistRecord(record, IndexType.DOCUMENT, -1); } /** @@ -396,6 +406,9 @@ private void intermediateCommitIfRequired() { * @param revNumber revision number */ private void reInstantiate(final @NonNegative int trxID, final @NonNegative int revNumber) { + // Save the current cursor position. getNodeKey() reads from a Java field, always valid. + final long currentNodeKey = nodeReadOnlyTrx.getNodeKey(); + // Reset page transaction to new uber page. resourceSession.closeNodePageWriteTransaction(getId()); storageEngineWriter = @@ -414,6 +427,11 @@ private void reInstantiate(final @NonNegative int trxID, final @NonNegative int updateOperationsOrdered.clear(); reInstantiateIndexes(); + + // Re-read the current node from the new page transaction. + // FlyweightNode getters read from the page MemorySegment; after closing the old transaction, + // that MemorySegment is stale. Re-reading creates a fresh node from the new transaction. + nodeReadOnlyTrx.moveTo(currentNodeKey); } protected abstract AbstractNodeHashing reInstantiateNodeHashing(StorageEngineWriter storageEngineWriter); @@ -442,6 +460,9 @@ public synchronized W rollback() { nodeReadOnlyTrx.assertNotClosed(); + // Save the current cursor position before closing the old page transaction. + final long rollbackNodeKey = nodeReadOnlyTrx.getNodeKey(); + // Reset modification counter. modificationCount = 0L; @@ -470,6 +491,9 @@ public synchronized W rollback() { reInstantiateIndexes(); + // Re-read the current node from the new page transaction (FlyweightNode binding is stale). + nodeReadOnlyTrx.moveTo(rollbackNodeKey); + if (lock != null) { lock.unlock(); } @@ -495,6 +519,9 @@ public W revertTo(final int revision) { nodeReadOnlyTrx.assertNotClosed(); resourceSession.assertAccess(revision); + // Save the current cursor position before closing the old page transaction. + final long revertNodeKey = nodeReadOnlyTrx.getNodeKey(); + // Close current page transaction. final int trxID = getId(); final int revNumber = getRevisionNumber(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/IndexController.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/IndexController.java index 2e2bf6550..8a257367f 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/IndexController.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/IndexController.java @@ -96,6 +96,15 @@ default boolean hasNameIndex() { return containsIndex(IndexType.NAME); } + /** + * Fast-path check: returns {@code true} if any primitive index (path, name, or CAS) exists. + * Used on the insert hot path to skip expensive moveTo + snapshot operations + * when no indexes are defined. + */ + default boolean hasAnyPrimitiveIndex() { + return hasPathIndex() || hasNameIndex() || hasCASIndex(); + } + /** * Determines if an index of the specified type is available. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/InternalNodeReadOnlyTrx.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/InternalNodeReadOnlyTrx.java index a52ef5307..beb0de957 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/InternalNodeReadOnlyTrx.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/InternalNodeReadOnlyTrx.java @@ -13,6 +13,14 @@ public interface InternalNodeReadOnlyTrx extends NodeCu StructNode getStructuralNode(); + /** + * Returns a live view of the current structural node without allocating a snapshot. + * When the cursor is in singleton mode, returns the singleton directly (zero-alloc). + * The returned reference must NOT be retained across moveTo/prepareRecordForModification + * calls — extract needed values into local primitives immediately. + */ + StructNode getStructuralNodeView(); + void assertNotClosed(); void setPageReadTransaction(StorageEngineReader trx); diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/RecordToRevisionsIndex.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/RecordToRevisionsIndex.java index 067e1ab9c..7eb334337 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/RecordToRevisionsIndex.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/RecordToRevisionsIndex.java @@ -49,6 +49,5 @@ public void addRevisionToRecordToRevisionsIndex(long recordKey) { final RevisionReferencesNode revisionReferencesNode = storageEngineWriter.prepareRecordForModification(recordKey, IndexType.RECORD_TO_REVISIONS, 0); revisionReferencesNode.addRevision(storageEngineWriter.getRevisionNumber()); - storageEngineWriter.updateRecordSlot(revisionReferencesNode, IndexType.RECORD_TO_REVISIONS, 0); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeFactoryImpl.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeFactoryImpl.java index 34861279c..d129c0688 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeFactoryImpl.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeFactoryImpl.java @@ -1,6 +1,5 @@ package io.sirix.access.trx.node.json; -import io.sirix.access.ResourceConfiguration; import io.sirix.api.StorageEngineWriter; import io.sirix.index.IndexType; import io.sirix.index.path.summary.PathNode; @@ -10,6 +9,7 @@ import io.sirix.node.delegates.NameNodeDelegate; import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.delegates.StructNodeDelegate; +import io.sirix.node.interfaces.DataRecord; import io.sirix.node.json.ArrayNode; import io.sirix.node.json.BooleanNode; import io.sirix.node.json.NullNode; @@ -19,18 +19,22 @@ import io.sirix.node.json.ObjectNode; import io.sirix.node.json.ObjectNullNode; import io.sirix.node.json.ObjectNumberNode; +import io.sirix.node.json.JsonDocumentRootNode; import io.sirix.node.json.ObjectStringNode; import io.sirix.node.json.StringNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.PageLayout; import io.sirix.page.PathSummaryPage; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; -import io.sirix.settings.StringCompressionType; import io.sirix.utils.NamePageHash; import net.openhft.hashing.LongHashFunction; import io.brackit.query.atomic.QNm; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; +import java.lang.foreign.MemorySegment; + import static java.util.Objects.requireNonNull; /** @@ -40,6 +44,9 @@ */ final class JsonNodeFactoryImpl implements JsonNodeFactory { + /** Cached null node key constant — avoids enum method call in hot path. */ + private static final long NULL_KEY = Fixed.NULL_NODE_KEY.getStandardProperty(); + /** * Hash function used to hash nodes. */ @@ -69,6 +76,7 @@ final class JsonNodeFactoryImpl implements JsonNodeFactory { private final ObjectBooleanNode reusableObjectBooleanNode; private final ObjectNumberNode reusableObjectNumberNode; private final ObjectStringNode reusableObjectStringNode; + private final JsonDocumentRootNode reusableJsonDocumentRootNode; /** * Constructor. @@ -111,177 +119,22 @@ final class JsonNodeFactoryImpl implements JsonNodeFactory { hashFunction, (SirixDeweyID) null); this.reusableObjectStringNode = new ObjectStringNode(0, 0, Constants.NULL_REVISION_NUMBER, revisionNumber, 0, new byte[0], hashFunction, (SirixDeweyID) null, false, null); - } - - private long nextNodeKey() { - return storageEngineWriter.getActualRevisionRootPage().getMaxNodeKeyInDocumentIndex() + 1; - } - - private ObjectNode bindObjectNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final SirixDeweyID id) { - final ObjectNode node = reusableObjectNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setFirstChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setLastChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setChildCount(0); - node.setDescendantCount(0); - node.setHash(0); - node.setDeweyID(id); - return node; - } - - private ArrayNode bindArrayNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final long pathNodeKey, final SirixDeweyID id) { - final ArrayNode node = reusableArrayNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPathNodeKey(pathNodeKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setFirstChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setLastChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setChildCount(0); - node.setDescendantCount(0); - node.setHash(0); - node.setDeweyID(id); - return node; - } - - private ObjectKeyNode bindObjectKeyNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final long pathNodeKey, final int nameKey, final String name, final long objectValueKey, - final SirixDeweyID id) { - final ObjectKeyNode node = reusableObjectKeyNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPathNodeKey(pathNodeKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setFirstChildKey(objectValueKey); - node.setNameKey(nameKey); - node.setName(name); - node.setDescendantCount(0); - node.setHash(0); - node.setDeweyID(id); - return node; - } - - private NullNode bindNullNode(final long nodeKey, final long parentKey, final long leftSibKey, final long rightSibKey, - final SirixDeweyID id) { - final NullNode node = reusableNullNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setHash(0); - node.setDeweyID(id); - return node; - } - - private BooleanNode bindBooleanNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final boolean boolValue, final SirixDeweyID id) { - final BooleanNode node = reusableBooleanNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setHash(0); - node.setValue(boolValue); - node.setDeweyID(id); - return node; - } - - private NumberNode bindNumberNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final Number value, final SirixDeweyID id) { - final NumberNode node = reusableNumberNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setHash(0); - node.setValue(value); - node.setDeweyID(id); - return node; - } - - private StringNode bindStringNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final byte[] value, final boolean isCompressed, final byte[] fsstSymbolTable, - final SirixDeweyID id) { - final StringNode node = reusableStringNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setHash(0); - node.setRawValue(value, isCompressed, fsstSymbolTable); - node.setDeweyID(id); - return node; - } - private ObjectNullNode bindObjectNullNode(final long nodeKey, final long parentKey, final SirixDeweyID id) { - final ObjectNullNode node = reusableObjectNullNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setDeweyID(id); - return node; - } - - private ObjectBooleanNode bindObjectBooleanNode(final long nodeKey, final long parentKey, final boolean boolValue, - final SirixDeweyID id) { - final ObjectBooleanNode node = reusableObjectBooleanNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setValue(boolValue); - node.setDeweyID(id); - return node; - } - - private ObjectNumberNode bindObjectNumberNode(final long nodeKey, final long parentKey, final Number value, - final SirixDeweyID id) { - final ObjectNumberNode node = reusableObjectNumberNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setValue(value); - node.setDeweyID(id); - return node; - } - - private ObjectStringNode bindObjectStringNode(final long nodeKey, final long parentKey, final byte[] value, - final boolean isCompressed, final byte[] fsstSymbolTable, final SirixDeweyID id) { - final ObjectStringNode node = reusableObjectStringNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setRawValue(value, isCompressed, fsstSymbolTable); - node.setDeweyID(id); - return node; + this.reusableJsonDocumentRootNode = new JsonDocumentRootNode(0, hashFunction); + + // Mark all singletons as write singletons so setRecord skips records[] storage. + reusableJsonDocumentRootNode.setWriteSingleton(true); + reusableObjectNode.setWriteSingleton(true); + reusableArrayNode.setWriteSingleton(true); + reusableObjectKeyNode.setWriteSingleton(true); + reusableNullNode.setWriteSingleton(true); + reusableBooleanNode.setWriteSingleton(true); + reusableNumberNode.setWriteSingleton(true); + reusableStringNode.setWriteSingleton(true); + reusableObjectNullNode.setWriteSingleton(true); + reusableObjectBooleanNode.setWriteSingleton(true); + reusableObjectNumberNode.setWriteSingleton(true); + reusableObjectStringNode.setWriteSingleton(true); } @Override @@ -310,110 +163,338 @@ public PathNode createPathNode(final @NonNegative long parentKey, final long lef @Override public ArrayNode createJsonArrayNode(long parentKey, long leftSibKey, long rightSibKey, long pathNodeKey, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final ArrayNode node = bindArrayNode(nodeKey, parentKey, leftSibKey, rightSibKey, pathNodeKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableArrayNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ArrayNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableArrayNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + NULL_KEY, NULL_KEY, pathNodeKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0, 0, 0); + kvl.completeDirectWrite(NodeKind.ARRAY.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableArrayNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableArrayNode.setOwnerPage(kvl); + reusableArrayNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableArrayNode; } @Override public ObjectNode createJsonObjectNode(long parentKey, long leftSibKey, long rightSibKey, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final ObjectNode node = bindObjectNode(nodeKey, parentKey, leftSibKey, rightSibKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableObjectNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ObjectNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + NULL_KEY, NULL_KEY, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0, 0, 0); + kvl.completeDirectWrite(NodeKind.OBJECT.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectNode.setOwnerPage(kvl); + reusableObjectNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectNode; } @Override public NullNode createJsonNullNode(long parentKey, long leftSibKey, long rightSibKey, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final NullNode node = bindNullNode(nodeKey, parentKey, leftSibKey, rightSibKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableNullNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = NullNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableNullNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber); + kvl.completeDirectWrite(NodeKind.NULL_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableNullNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableNullNode.setOwnerPage(kvl); + reusableNullNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableNullNode; } @Override public ObjectKeyNode createJsonObjectKeyNode(long parentKey, long leftSibKey, long rightSibKey, long pathNodeKey, String name, long objectValueKey, SirixDeweyID id) { final int localNameKey = storageEngineWriter.createNameKey(name, NodeKind.OBJECT_KEY); - final long nodeKey = nextNodeKey(); - final ObjectKeyNode node = bindObjectKeyNode(nodeKey, parentKey, leftSibKey, rightSibKey, pathNodeKey, localNameKey, - name, objectValueKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableObjectKeyNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ObjectKeyNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectKeyNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + objectValueKey, localNameKey, pathNodeKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0, 0); + kvl.completeDirectWrite(NodeKind.OBJECT_KEY.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectKeyNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectKeyNode.setOwnerPage(kvl); + reusableObjectKeyNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectKeyNode; } @Override public StringNode createJsonStringNode(long parentKey, long leftSibKey, long rightSibKey, byte[] value, boolean doCompress, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - - // For FSST, page-level symbol tables are required for decoding. - // Until symbol table plumbing is complete, keep stored values uncompressed. - final ResourceConfiguration config = storageEngineWriter.getResourceSession().getResourceConfig(); - final boolean shouldUseCompression = doCompress && config.stringCompressionType == StringCompressionType.FSST; - if (shouldUseCompression) { - // Intentionally not compressing here: no symbol table is available on this path yet. - } - final boolean isCompressed = false; - - final StringNode node = bindStringNode(nodeKey, parentKey, leftSibKey, rightSibKey, value, isCompressed, null, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 55 + value.length, deweyIdLen); + final int recordBytes = StringNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableStringNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, value, false); + kvl.completeDirectWrite(NodeKind.STRING_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableStringNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableStringNode.setOwnerPage(kvl); + reusableStringNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableStringNode; } @Override public BooleanNode createJsonBooleanNode(long parentKey, long leftSibKey, long rightSibKey, boolean boolValue, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final BooleanNode node = bindBooleanNode(nodeKey, parentKey, leftSibKey, rightSibKey, boolValue, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableBooleanNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = BooleanNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableBooleanNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, boolValue); + kvl.completeDirectWrite(NodeKind.BOOLEAN_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableBooleanNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableBooleanNode.setOwnerPage(kvl); + reusableBooleanNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableBooleanNode; } @Override public NumberNode createJsonNumberNode(long parentKey, long leftSibKey, long rightSibKey, Number value, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final NumberNode node = bindNumberNode(nodeKey, parentKey, leftSibKey, rightSibKey, value, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableNumberNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = NumberNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableNumberNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, value); + kvl.completeDirectWrite(NodeKind.NUMBER_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableNumberNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableNumberNode.setOwnerPage(kvl); + reusableNumberNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableNumberNode; } @Override public ObjectNullNode createJsonObjectNullNode(long parentKey, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final ObjectNullNode node = bindObjectNullNode(nodeKey, parentKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableObjectNullNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ObjectNullNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectNullNode.getHeapOffsets(), nodeKey, parentKey, + Constants.NULL_REVISION_NUMBER, revisionNumber); + kvl.completeDirectWrite(NodeKind.OBJECT_NULL_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectNullNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectNullNode.setOwnerPage(kvl); + reusableObjectNullNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectNullNode; } @Override public ObjectStringNode createJsonObjectStringNode(long parentKey, byte[] value, boolean doCompress, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - - // For FSST, page-level symbol tables are required for decoding. - // Until symbol table plumbing is complete, keep stored values uncompressed. - final ResourceConfiguration config = storageEngineWriter.getResourceSession().getResourceConfig(); - final boolean shouldUseCompression = doCompress && config.stringCompressionType == StringCompressionType.FSST; - if (shouldUseCompression) { - // Intentionally not compressing here: no symbol table is available on this path yet. - } - final boolean isCompressed = false; - - final ObjectStringNode node = bindObjectStringNode(nodeKey, parentKey, value, isCompressed, null, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 55 + value.length, deweyIdLen); + final int recordBytes = ObjectStringNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectStringNode.getHeapOffsets(), nodeKey, parentKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, value, false); + kvl.completeDirectWrite(NodeKind.OBJECT_STRING_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectStringNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectStringNode.setOwnerPage(kvl); + reusableObjectStringNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectStringNode; } @Override public ObjectBooleanNode createJsonObjectBooleanNode(long parentKey, boolean boolValue, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final ObjectBooleanNode node = bindObjectBooleanNode(nodeKey, parentKey, boolValue, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableObjectBooleanNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ObjectBooleanNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectBooleanNode.getHeapOffsets(), nodeKey, parentKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, boolValue); + kvl.completeDirectWrite(NodeKind.OBJECT_BOOLEAN_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectBooleanNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectBooleanNode.setOwnerPage(kvl); + reusableObjectBooleanNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectBooleanNode; } @Override public ObjectNumberNode createJsonObjectNumberNode(long parentKey, Number value, SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - final ObjectNumberNode node = bindObjectNumberNode(nodeKey, parentKey, value, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableObjectNumberNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = ObjectNumberNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableObjectNumberNode.getHeapOffsets(), nodeKey, parentKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, value); + kvl.completeDirectWrite(NodeKind.OBJECT_NUMBER_VALUE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableObjectNumberNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableObjectNumberNode.setOwnerPage(kvl); + reusableObjectNumberNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableObjectNumberNode; } @Override public DeweyIDNode createDeweyIdNode(long nodeKey, @NonNull SirixDeweyID id) { return storageEngineWriter.createRecord(new DeweyIDNode(nodeKey, id), IndexType.DEWEYID_TO_RECORDID, 0); } + + /** + * Bind the correct write singleton to a slotted page slot for zero-allocation modification. + * Reads the nodeKindId from the page directory, selects the matching singleton, unbinds if + * currently bound elsewhere, binds to the slot, and propagates DeweyID. + * + * @param page the KeyValueLeafPage containing the slotted page + * @param offset the slot index (0-1023) + * @param nodeKey the record key + * @return the bound write singleton, or null if the slot is not a JSON node type + */ + DataRecord bindWriteSingleton(final KeyValueLeafPage page, final int offset, final long nodeKey) { + final MemorySegment slottedPage = page.getSlottedPage(); + if (slottedPage == null || !PageLayout.isSlotPopulated(slottedPage, offset)) { + return null; + } + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, offset); + final int heapOffset = PageLayout.getDirHeapOffset(slottedPage, offset); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + final byte[] deweyIdBytes = page.getDeweyIdAsByteArray(offset); + + // Concrete-type switch eliminates 3 itable stubs per bind (bind, setDeweyIDBytes, setOwnerPage). + // Each case is monomorphic — JVM can inline directly. + // setDeweyIDBytes stores raw bytes lazily (no SirixDeweyID parsing). + // No setOwnerPage(null) needed — setDeweyIDBytes doesn't trigger resize. + return switch (nodeKindId) { + case 24 -> { // OBJECT + reusableObjectNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectNode.setOwnerPage(page); + yield reusableObjectNode; + } + case 25 -> { // ARRAY + reusableArrayNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableArrayNode.setDeweyIDBytes(deweyIdBytes); + reusableArrayNode.setOwnerPage(page); + yield reusableArrayNode; + } + case 26 -> { // OBJECT_KEY + reusableObjectKeyNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectKeyNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectKeyNode.setOwnerPage(page); + yield reusableObjectKeyNode; + } + case 27 -> { // BOOLEAN_VALUE + reusableBooleanNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableBooleanNode.setDeweyIDBytes(deweyIdBytes); + reusableBooleanNode.setOwnerPage(page); + yield reusableBooleanNode; + } + case 28 -> { // NUMBER_VALUE + reusableNumberNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableNumberNode.setDeweyIDBytes(deweyIdBytes); + reusableNumberNode.setOwnerPage(page); + yield reusableNumberNode; + } + case 29 -> { // NULL_VALUE + reusableNullNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableNullNode.setDeweyIDBytes(deweyIdBytes); + reusableNullNode.setOwnerPage(page); + yield reusableNullNode; + } + case 30 -> { // STRING_VALUE + reusableStringNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableStringNode.setDeweyIDBytes(deweyIdBytes); + reusableStringNode.setOwnerPage(page); + reusableStringNode.setFsstSymbolTable(page.getFsstSymbolTable()); + yield reusableStringNode; + } + case 31 -> { // JSON_DOCUMENT + reusableJsonDocumentRootNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableJsonDocumentRootNode.setDeweyIDBytes(deweyIdBytes); + reusableJsonDocumentRootNode.setOwnerPage(page); + yield reusableJsonDocumentRootNode; + } + case 40 -> { // OBJECT_STRING_VALUE + reusableObjectStringNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectStringNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectStringNode.setOwnerPage(page); + reusableObjectStringNode.setFsstSymbolTable(page.getFsstSymbolTable()); + yield reusableObjectStringNode; + } + case 41 -> { // OBJECT_BOOLEAN_VALUE + reusableObjectBooleanNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectBooleanNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectBooleanNode.setOwnerPage(page); + yield reusableObjectBooleanNode; + } + case 42 -> { // OBJECT_NUMBER_VALUE + reusableObjectNumberNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectNumberNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectNumberNode.setOwnerPage(page); + yield reusableObjectNumberNode; + } + case 43 -> { // OBJECT_NULL_VALUE + reusableObjectNullNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableObjectNullNode.setDeweyIDBytes(deweyIdBytes); + reusableObjectNullNode.setOwnerPage(page); + yield reusableObjectNullNode; + } + default -> null; + }; + } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeHashing.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeHashing.java index 1f8a17889..61de27ca4 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeHashing.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/json/JsonNodeHashing.java @@ -26,7 +26,7 @@ final class JsonNodeHashing extends AbstractNodeHashing value) { @Override public JsonNodeTrx insertStringValueAsFirstChild(final String value) { requireNonNull(value); - final byte[] textValue = getBytes(value); - return insertPrimitiveAsChild( - (parentKey, id) -> nodeFactory.createJsonObjectStringNode(parentKey, textValue, useTextCompression, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonStringNode(parentKey, leftSib, rightSib, textValue, - useTextCompression, id), - true, true); + return insertPrimitiveAsChild(PrimitiveNodeType.STRING, getBytes(value), null, false, true, true); } @Override public JsonNodeTrx insertStringValueAsLastChild(final String value) { requireNonNull(value); - final byte[] textValue = getBytes(value); - return insertPrimitiveAsChild( - (parentKey, id) -> nodeFactory.createJsonObjectStringNode(parentKey, textValue, useTextCompression, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonStringNode(parentKey, leftSib, rightSib, textValue, - useTextCompression, id), - true, false); + return insertPrimitiveAsChild(PrimitiveNodeType.STRING, getBytes(value), null, false, true, false); } private long getPathNodeKey(StructNode structNode) { @@ -1293,56 +1333,50 @@ private long getPathNodeKey(StructNode structNode) { return pathNodeKey; } - private void adaptNodesAndHashesForInsertAsChild(final ImmutableJsonNode node) { - // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); - adaptForInsert((StructNode) node); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + private void adaptNodesAndHashesForInsertAsChild(final long nodeKey, final long parentKey, + final long leftSibKey, final long rightSibKey) { + // Pass structural keys directly — eliminates moveTo before adaptForInsert. + // Old code did: moveTo(nodeKey) → adaptForInsert(getStructuralNodeView()) → moveTo(nodeKey) → hash. + // New code: adaptForInsert(keys) → hash(nodeKey) → moveTo(nodeKey). + // Net: eliminated 1 moveTo (the first one before adaptForInsert). + adaptForInsert(nodeKey, parentKey, leftSibKey, rightSibKey); + nodeHashing.adaptHashesWithAdd(nodeKey); + // Restore cursor to new node — callers expect this (e.g., notifyPrimitiveIndexChange). + // When hashing runs, rollingAdd already does moveTo(startNodeKey) at end, so this is + // only needed when hashing is NONE (bulkInsert && !autoCommit). + nodeReadOnlyTrx.moveTo(nodeKey); } @Override public JsonNodeTrx insertStringValueAsLeftSibling(final String value) { requireNonNull(value); - final byte[] textValue = getBytes(value); - return insertPrimitiveAsSibling((parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonStringNode(parentKey, - leftSib, rightSib, textValue, useTextCompression, id), true); + return insertPrimitiveAsSibling(PrimitiveNodeType.STRING, getBytes(value), null, false, true); } @Override public JsonNodeTrx insertStringValueAsRightSibling(final String value) { requireNonNull(value); - final byte[] textValue = getBytes(value); - return insertPrimitiveAsSibling((parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonStringNode(parentKey, - leftSib, rightSib, textValue, useTextCompression, id), false); + return insertPrimitiveAsSibling(PrimitiveNodeType.STRING, getBytes(value), null, false, false); } @Override public JsonNodeTrx insertBooleanValueAsFirstChild(boolean value) { - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectBooleanNode(parentKey, value, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonBooleanNode(parentKey, leftSib, rightSib, value, - id), - true, true); + return insertPrimitiveAsChild(PrimitiveNodeType.BOOLEAN, null, null, value, true, true); } @Override public JsonNodeTrx insertBooleanValueAsLastChild(boolean value) { - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectBooleanNode(parentKey, value, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonBooleanNode(parentKey, leftSib, rightSib, value, - id), - true, false); + return insertPrimitiveAsChild(PrimitiveNodeType.BOOLEAN, null, null, value, true, false); } @Override public JsonNodeTrx insertBooleanValueAsLeftSibling(boolean value) { - return insertPrimitiveAsSibling((parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonBooleanNode(parentKey, - leftSib, rightSib, value, id), true); + return insertPrimitiveAsSibling(PrimitiveNodeType.BOOLEAN, null, null, value, true); } @Override public JsonNodeTrx insertBooleanValueAsRightSibling(boolean value) { - return insertPrimitiveAsSibling((parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonBooleanNode(parentKey, - leftSib, rightSib, value, id), false); + return insertPrimitiveAsSibling(PrimitiveNodeType.BOOLEAN, null, null, value, false); } private void checkPrecondition() { @@ -1351,40 +1385,66 @@ private void checkPrecondition() { } } - private void insertAsSibling(final ImmutableJsonNode node) { - // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); - adaptForInsert((StructNode) node); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); - - // Get the path node key. - final long pathNodeKey; + private void insertAsSibling(final long nodeKey, final long parentKey, + final long leftSibKey, final long rightSibKey, final ImmutableNode insertedNode) { + // Pass structural keys directly — eliminates moveTo before adaptForInsert. + adaptForInsert(nodeKey, parentKey, leftSibKey, rightSibKey); + nodeHashing.adaptHashesWithAdd(nodeKey); + // Restore cursor to new node (rollingAdd does this when hashing, but not when NONE). + nodeReadOnlyTrx.moveTo(nodeKey); - if (buildPathSummary) { - moveToParentObjectKeyArrayOrDocumentRoot(); - pathNodeKey = getPathNodeKey(nodeReadOnlyTrx.getStructuralNode()); - } else { - pathNodeKey = 0; + if (indexController.hasAnyPrimitiveIndex()) { + // Resolve path node key and notify indexes — only when indexes exist. + final long pathNodeKey; + if (buildPathSummary) { + moveToParentObjectKeyArrayOrDocumentRoot(); + pathNodeKey = getPathNodeKey(nodeReadOnlyTrx.getStructuralNodeView()); + nodeReadOnlyTrx.moveTo(nodeKey); + } else { + pathNodeKey = 0; + } + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, + (ImmutableNode) nodeReadOnlyTrx.getStructuralNodeView(), pathNodeKey); } - - nodeReadOnlyTrx.setCurrentNode(node); - - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); } - @FunctionalInterface - private interface ObjectKeyNodeCreator { - StructNode create(long parentKey, SirixDeweyID id); + /** + * Enumeration of primitive node types to avoid lambda allocation on the insert hot path. + */ + private enum PrimitiveNodeType { + STRING, NUMBER, BOOLEAN, NULL } - @FunctionalInterface - private interface SiblingNodeCreator { - StructNode create(long parentKey, long leftSibKey, long rightSibKey, SirixDeweyID id); + /** + * Create an object-key child node (direct child of ObjectKeyNode) based on primitive type. + */ + private StructNode createObjectKeyNode(final PrimitiveNodeType type, final long parentKey, + final byte[] stringValue, final Number numberValue, final boolean booleanValue, final SirixDeweyID id) { + return switch (type) { + case STRING -> nodeFactory.createJsonObjectStringNode(parentKey, stringValue, useTextCompression, id); + case NUMBER -> nodeFactory.createJsonObjectNumberNode(parentKey, numberValue, id); + case BOOLEAN -> nodeFactory.createJsonObjectBooleanNode(parentKey, booleanValue, id); + case NULL -> nodeFactory.createJsonObjectNullNode(parentKey, id); + }; } - private JsonNodeTrx insertPrimitiveAsChild(final ObjectKeyNodeCreator objectKeyCreator, - final SiblingNodeCreator arrayCreator, final boolean notifyIndex, final boolean isFirstChild) { + /** + * Create a sibling node (child of array) based on primitive type. + */ + private StructNode createSiblingNode(final PrimitiveNodeType type, final long parentKey, + final long leftSibKey, final long rightSibKey, final byte[] stringValue, final Number numberValue, + final boolean booleanValue, final SirixDeweyID id) { + return switch (type) { + case STRING -> nodeFactory.createJsonStringNode(parentKey, leftSibKey, rightSibKey, + stringValue, useTextCompression, id); + case NUMBER -> nodeFactory.createJsonNumberNode(parentKey, leftSibKey, rightSibKey, numberValue, id); + case BOOLEAN -> nodeFactory.createJsonBooleanNode(parentKey, leftSibKey, rightSibKey, booleanValue, id); + case NULL -> nodeFactory.createJsonNullNode(parentKey, leftSibKey, rightSibKey, id); + }; + } + + private JsonNodeTrx insertPrimitiveAsChild(final PrimitiveNodeType type, final byte[] stringValue, + final Number numberValue, final boolean booleanValue, final boolean notifyIndex, final boolean isFirstChild) { if (lock != null) { lock.lock(); } @@ -1399,43 +1459,52 @@ private JsonNodeTrx insertPrimitiveAsChild(final ObjectKeyNodeCreator objectKeyC checkAccessAndCommit(); } - final StructNode structNode = nodeReadOnlyTrx.getStructuralNode(); - final long pathNodeKey = notifyIndex + final StructNode structNode = nodeReadOnlyTrx.getStructuralNodeView(); + final long pathNodeKey = (notifyIndex && indexController.hasAnyPrimitiveIndex()) ? getPathNodeKey(structNode) : 0; final long parentKey = structNode.getNodeKey(); + final long firstChildKey = structNode.getFirstChildKey(); + final long lastChildKey = structNode.getLastChildKey(); final SirixDeweyID id; final StructNode node; + final long leftSibKey; + final long rightSibKey; if (kind == NodeKind.OBJECT_KEY) { id = deweyIDManager.newRecordValueID(); - node = objectKeyCreator.create(parentKey, id); + leftSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); + rightSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); + node = createObjectKeyNode(type, parentKey, stringValue, numberValue, booleanValue, id); } else if (isFirstChild) { id = deweyIDManager.newFirstChildID(); - final long rightSibKey = structNode.getFirstChildKey(); - final long leftSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); - node = arrayCreator.create(parentKey, leftSibKey, rightSibKey, id); + leftSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); + rightSibKey = firstChildKey; + node = createSiblingNode(type, parentKey, leftSibKey, rightSibKey, stringValue, numberValue, booleanValue, id); } else { - id = structNode.getFirstChildKey() == Fixed.NULL_NODE_KEY.getStandardProperty() + id = firstChildKey == Fixed.NULL_NODE_KEY.getStandardProperty() ? deweyIDManager.newFirstChildID() : deweyIDManager.newLastChildID(); - final long leftSibKey = structNode.getLastChildKey(); - final long rightSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); - node = arrayCreator.create(parentKey, leftSibKey, rightSibKey, id); + leftSibKey = lastChildKey; + rightSibKey = Fixed.NULL_NODE_KEY.getStandardProperty(); + node = createSiblingNode(type, parentKey, leftSibKey, rightSibKey, stringValue, numberValue, booleanValue, id); } - adaptNodesAndHashesForInsertAsChild((ImmutableJsonNode) node); + final long nodeKey = node.getNodeKey(); - if (notifyIndex) { - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + adaptNodesAndHashesForInsertAsChild(nodeKey, parentKey, leftSibKey, rightSibKey); + + if (notifyIndex && indexController.hasAnyPrimitiveIndex()) { + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, + (ImmutableNode) nodeReadOnlyTrx.getStructuralNodeView(), pathNodeKey); } if (getParentKind() != NodeKind.OBJECT_KEY && !nodeHashing.isBulkInsert()) { - adaptUpdateOperationsForInsert(id, node.getNodeKey()); + adaptUpdateOperationsForInsert(id, nodeKey); } if (storeNodeHistory) { - nodeToRevisionsIndex.addToRecordToRevisionsIndex(node.getNodeKey()); + nodeToRevisionsIndex.addToRecordToRevisionsIndex(nodeKey); } return this; @@ -1446,7 +1515,8 @@ private JsonNodeTrx insertPrimitiveAsChild(final ObjectKeyNodeCreator objectKeyC } } - private JsonNodeTrx insertPrimitiveAsSibling(final SiblingNodeCreator nodeCreator, final boolean isLeftSibling) { + private JsonNodeTrx insertPrimitiveAsSibling(final PrimitiveNodeType type, final byte[] stringValue, + final Number numberValue, final boolean booleanValue, final boolean isLeftSibling) { if (lock != null) { lock.lock(); } @@ -1455,7 +1525,7 @@ private JsonNodeTrx insertPrimitiveAsSibling(final SiblingNodeCreator nodeCreato checkAccessAndCommit(); checkPrecondition(); - final StructNode currentNode = nodeReadOnlyTrx.getStructuralNode(); + final StructNode currentNode = nodeReadOnlyTrx.getStructuralNodeView(); final long parentKey = currentNode.getParentKey(); final long leftSibKey; @@ -1471,16 +1541,18 @@ private JsonNodeTrx insertPrimitiveAsSibling(final SiblingNodeCreator nodeCreato id = deweyIDManager.newRightSiblingID(); } - final StructNode node = nodeCreator.create(parentKey, leftSibKey, rightSibKey, id); + final StructNode node = createSiblingNode(type, parentKey, leftSibKey, rightSibKey, + stringValue, numberValue, booleanValue, id); + final long nodeKey = node.getNodeKey(); - insertAsSibling((ImmutableJsonNode) node); + insertAsSibling(nodeKey, parentKey, leftSibKey, rightSibKey, (ImmutableNode) node); if (!nodeHashing.isBulkInsert()) { - adaptUpdateOperationsForInsert(id, node.getNodeKey()); + adaptUpdateOperationsForInsert(id, nodeKey); } if (storeNodeHistory) { - nodeToRevisionsIndex.addToRecordToRevisionsIndex(node.getNodeKey()); + nodeToRevisionsIndex.addToRecordToRevisionsIndex(nodeKey); } return this; @@ -1494,25 +1566,19 @@ private JsonNodeTrx insertPrimitiveAsSibling(final SiblingNodeCreator nodeCreato @Override public JsonNodeTrx insertNumberValueAsFirstChild(Number value) { requireNonNull(value); - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectNumberNode(parentKey, value, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNumberNode(parentKey, leftSib, rightSib, value, id), - true, true); + return insertPrimitiveAsChild(PrimitiveNodeType.NUMBER, null, value, false, true, true); } @Override public JsonNodeTrx insertNumberValueAsLastChild(Number value) { requireNonNull(value); - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectNumberNode(parentKey, value, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNumberNode(parentKey, leftSib, rightSib, value, id), - true, false); + return insertPrimitiveAsChild(PrimitiveNodeType.NUMBER, null, value, false, true, false); } @Override public JsonNodeTrx insertNumberValueAsLeftSibling(Number value) { requireNonNull(value); - return insertPrimitiveAsSibling( - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNumberNode(parentKey, leftSib, rightSib, value, id), - true); + return insertPrimitiveAsSibling(PrimitiveNodeType.NUMBER, null, value, false, true); } @Override @@ -1523,35 +1589,27 @@ public JsonNodeReadOnlyTrx nodeReadOnlyTrxDelegate() { @Override public JsonNodeTrx insertNumberValueAsRightSibling(Number value) { requireNonNull(value); - return insertPrimitiveAsSibling( - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNumberNode(parentKey, leftSib, rightSib, value, id), - false); + return insertPrimitiveAsSibling(PrimitiveNodeType.NUMBER, null, value, false, false); } @Override public JsonNodeTrx insertNullValueAsFirstChild() { - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectNullNode(parentKey, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNullNode(parentKey, leftSib, rightSib, id), false, - true); + return insertPrimitiveAsChild(PrimitiveNodeType.NULL, null, null, false, false, true); } @Override public JsonNodeTrx insertNullValueAsLastChild() { - return insertPrimitiveAsChild((parentKey, id) -> nodeFactory.createJsonObjectNullNode(parentKey, id), - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNullNode(parentKey, leftSib, rightSib, id), false, - false); + return insertPrimitiveAsChild(PrimitiveNodeType.NULL, null, null, false, false, false); } @Override public JsonNodeTrx insertNullValueAsLeftSibling() { - return insertPrimitiveAsSibling( - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNullNode(parentKey, leftSib, rightSib, id), true); + return insertPrimitiveAsSibling(PrimitiveNodeType.NULL, null, null, false, true); } @Override public JsonNodeTrx insertNullValueAsRightSibling() { - return insertPrimitiveAsSibling( - (parentKey, leftSib, rightSib, id) -> nodeFactory.createJsonNullNode(parentKey, leftSib, rightSib, id), false); + return insertPrimitiveAsSibling(PrimitiveNodeType.NULL, null, null, false, false); } /** @@ -1676,7 +1734,17 @@ private void notifyPrimitiveIndexChange(final IndexController.ChangeType type, f final QNm name; if (kind == NodeKind.OBJECT_KEY && indexController.hasNameIndex() && node instanceof ObjectKeyNode objectKeyNode) { - name = objectKeyNode.getName(); + // getName() may be null for flyweight-bound nodes (cachedName is a Java field, not in MemorySegment). + // Fall back to resolving the name from the name key via the storage engine. + QNm resolvedName = objectKeyNode.getName(); + if (resolvedName == null) { + final int nameKey = objectKeyNode.getNameKey(); + final String localName = storageEngineWriter.getName(nameKey, NodeKind.OBJECT_KEY); + if (localName != null) { + resolvedName = new QNm(localName); + } + } + name = resolvedName; } else { name = null; } @@ -1990,22 +2058,19 @@ public JsonNodeTrx setNumberValue(final Number value) { // //////////////////////////////////////////////////////////// /** - * Adapting everything for insert operations. + * Adapting everything for insert operations. Accepts the new node's structural keys directly + * to avoid a moveTo call — the caller already knows these values from the factory call. * - * @param structNode pointer of the new node to be inserted + * @param structNodeKey the new node's key + * @param parentKey the new node's parent key + * @param leftSibKey the new node's left sibling key + * @param rightSibKey the new node's right sibling key * @throws SirixIOException if anything weird happens */ - private void adaptForInsert(final StructNode structNode) { - assert structNode != null; - // Capture all needed keys from structNode before any prepareRecordForModification calls. - // With write-path singletons, prepareRecordForModification for a node of the same kind - // would overwrite the singleton, invalidating prior references. - final long structNodeKey = structNode.getNodeKey(); - final long parentKey = structNode.getParentKey(); - final long leftSibKey = structNode.getLeftSiblingKey(); - final long rightSibKey = structNode.getRightSiblingKey(); - final boolean hasLeft = structNode.hasLeftSibling(); - final boolean hasRight = structNode.hasRightSibling(); + private void adaptForInsert(final long structNodeKey, final long parentKey, + final long leftSibKey, final long rightSibKey) { + final boolean hasLeft = leftSibKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + final boolean hasRight = rightSibKey != Fixed.NULL_NODE_KEY.getStandardProperty(); // Phase 1: Update parent — childCount + firstChild/lastChild if no siblings. // Complete all parent modifications and persist BEFORE acquiring any sibling singletons. @@ -2156,7 +2221,14 @@ protected AbstractNodeHashing reInstantiateN @Override protected JsonNodeFactory reInstantiateNodeFactory(StorageEngineWriter storageEngineWriter) { - return new JsonNodeFactoryImpl(hashFunction, storageEngineWriter); + final var factory = new JsonNodeFactoryImpl(hashFunction, storageEngineWriter); + wireWriteSingletonBinder(factory, storageEngineWriter); + return factory; + } + + private static void wireWriteSingletonBinder(final JsonNodeFactoryImpl factory, + final StorageEngineWriter storageEngineWriter) { + storageEngineWriter.setWriteSingletonBinder(factory::bindWriteSingleton); } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/InsertPos.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/InsertPos.java index 989454e82..81b071d70 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/InsertPos.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/InsertPos.java @@ -26,6 +26,7 @@ import io.sirix.index.IndexType; import io.sirix.node.NodeKind; import io.sirix.node.interfaces.DataRecord; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.StructNode; import io.sirix.settings.Fixed; import io.brackit.query.atomic.QNm; @@ -359,6 +360,10 @@ abstract void processMove(final StructNode fromNode, final StructNode toNode, fi abstract void insertNode(final XmlNodeTrx wtx, final XmlNodeReadOnlyTrx rtx) throws SirixException; private static void persistUpdatedRecord(final XmlNodeTrx wtx, final DataRecord record) { - wtx.getStorageEngineWriter().updateRecordSlot(record, IndexType.DOCUMENT, -1); + if (record instanceof FlyweightNode fn && fn.isWriteSingleton() && fn.getOwnerPage() != null) { + return; // Bound write singleton — mutations already on heap via inlined setters + } + // Ensure the mutated record is stored in the TIL's modified page. + wtx.getStorageEngineWriter().persistRecord(record, IndexType.DOCUMENT, -1); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlDeweyIDManager.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlDeweyIDManager.java index fd9bac62d..0a3303077 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlDeweyIDManager.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlDeweyIDManager.java @@ -7,6 +7,7 @@ import io.sirix.index.IndexType; import io.sirix.node.SirixDeweyID; import io.sirix.node.interfaces.DataRecord; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; import io.sirix.node.interfaces.StructNode; import io.sirix.node.xml.ElementNode; @@ -182,7 +183,11 @@ SirixDeweyID newAttributeID() { } private void persistUpdatedRecord(final DataRecord record) { - storageEngineWriter.updateRecordSlot(record, IndexType.DOCUMENT, -1); + if (record instanceof FlyweightNode fn && fn.isWriteSingleton() && fn.getOwnerPage() != null) { + return; // Bound write singleton — mutations already on heap via inlined setters + } + // Ensure the mutated record is stored in the TIL's modified page. + storageEngineWriter.persistRecord(record, IndexType.DOCUMENT, -1); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImpl.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImpl.java index 9cb4c03cf..8334e02a7 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImpl.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImpl.java @@ -8,12 +8,16 @@ import io.sirix.node.delegates.NameNodeDelegate; import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.delegates.StructNodeDelegate; +import io.sirix.node.interfaces.DataRecord; import io.sirix.node.xml.AttributeNode; import io.sirix.node.xml.CommentNode; import io.sirix.node.xml.ElementNode; import io.sirix.node.xml.NamespaceNode; import io.sirix.node.xml.PINode; import io.sirix.node.xml.TextNode; +import io.sirix.node.xml.XmlDocumentRootNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.PageLayout; import io.sirix.page.PathSummaryPage; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; @@ -25,6 +29,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; +import java.lang.foreign.MemorySegment; import java.util.zip.Deflater; import static java.util.Objects.requireNonNull; @@ -36,6 +41,8 @@ */ final class XmlNodeFactoryImpl implements XmlNodeFactory { + private static final long NULL_KEY = Fixed.NULL_NODE_KEY.getStandardProperty(); + /** * {@link StorageEngineWriter} implementation. */ @@ -62,6 +69,7 @@ final class XmlNodeFactoryImpl implements XmlNodeFactory { private final LongArrayList reusableElementAttributeKeys; private final LongArrayList reusableElementNamespaceKeys; private final ElementNode reusableElementNode; + private final XmlDocumentRootNode reusableXmlDocumentRootNode; /** * Constructor. @@ -97,129 +105,16 @@ final class XmlNodeFactoryImpl implements XmlNodeFactory { this.reusableCommentNode = new CommentNode(0, 0, Constants.NULL_REVISION_NUMBER, revisionNumber, Fixed.NULL_NODE_KEY.getStandardProperty(), Fixed.NULL_NODE_KEY.getStandardProperty(), 0, new byte[0], false, hashFunction, (SirixDeweyID) null); - } - - private long nextNodeKey() { - return storageEngineWriter.getActualRevisionRootPage().getMaxNodeKeyInDocumentIndex() + 1; - } - - private AttributeNode bindAttributeNode(final long nodeKey, final long parentKey, final QNm name, final byte[] value, - final long pathNodeKey, final int prefixKey, final int localNameKey, final int uriKey, final SirixDeweyID id) { - final AttributeNode node = reusableAttributeNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPathNodeKey(pathNodeKey); - node.setPrefixKey(prefixKey); - node.setLocalNameKey(localNameKey); - node.setURIKey(uriKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRawValue(value); - node.setHash(0); - node.setName(name); - node.setDeweyID(id); - return node; - } - - private NamespaceNode bindNamespaceNode(final long nodeKey, final long parentKey, final QNm name, - final long pathNodeKey, final int prefixKey, final int uriKey, final SirixDeweyID id) { - final NamespaceNode node = reusableNamespaceNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPathNodeKey(pathNodeKey); - node.setPrefixKey(prefixKey); - node.setLocalNameKey(-1); - node.setURIKey(uriKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setName(name); - node.setDeweyID(id); - return node; - } - - private PINode bindPINode(final long nodeKey, final long parentKey, final long leftSibKey, final long rightSibKey, - final QNm target, final byte[] content, final long pathNodeKey, final int prefixKey, final int localNameKey, - final int uriKey, final boolean isCompressed, final SirixDeweyID id) { - final PINode node = reusablePINode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setFirstChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setLastChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setChildCount(0); - node.setDescendantCount(0); - node.setPathNodeKey(pathNodeKey); - node.setPrefixKey(prefixKey); - node.setLocalNameKey(localNameKey); - node.setURIKey(uriKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRawValue(content); - node.setCompressed(isCompressed); - node.setHash(0); - node.setName(target); - node.setDeweyID(id); - return node; - } - - private ElementNode bindElementNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final QNm name, final long pathNodeKey, final int prefixKey, final int localNameKey, - final int uriKey, final SirixDeweyID id) { - reusableElementAttributeKeys.clear(); - reusableElementNamespaceKeys.clear(); - final ElementNode node = reusableElementNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setFirstChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setLastChildKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setChildCount(0); - node.setDescendantCount(0); - node.setPathNodeKey(pathNodeKey); - node.setPrefixKey(prefixKey); - node.setLocalNameKey(localNameKey); - node.setURIKey(uriKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setHash(0); - node.setName(name); - node.setDeweyID(id); - return node; - } - - private TextNode bindTextNode(final long nodeKey, final long parentKey, final long leftSibKey, final long rightSibKey, - final byte[] value, final boolean isCompressed, final SirixDeweyID id) { - final TextNode node = reusableTextNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setRawValue(value); - node.setCompressed(isCompressed); - node.setHash(0); - node.setDeweyID(id); - return node; - } + this.reusableXmlDocumentRootNode = new XmlDocumentRootNode(0, hashFunction); - private CommentNode bindCommentNode(final long nodeKey, final long parentKey, final long leftSibKey, - final long rightSibKey, final byte[] value, final boolean isCompressed, final SirixDeweyID id) { - final CommentNode node = reusableCommentNode; - node.setNodeKey(nodeKey); - node.setParentKey(parentKey); - node.setPreviousRevision(Constants.NULL_REVISION_NUMBER); - node.setLastModifiedRevision(revisionNumber); - node.setRightSiblingKey(rightSibKey); - node.setLeftSiblingKey(leftSibKey); - node.setRawValue(value); - node.setCompressed(isCompressed); - node.setHash(0); - node.setDeweyID(id); - return node; + // Mark all singletons as write singletons so setRecord skips records[] storage. + reusableElementNode.setWriteSingleton(true); + reusableAttributeNode.setWriteSingleton(true); + reusableNamespaceNode.setWriteSingleton(true); + reusablePINode.setWriteSingleton(true); + reusableTextNode.setWriteSingleton(true); + reusableCommentNode.setWriteSingleton(true); + reusableXmlDocumentRootNode.setWriteSingleton(true); } @Override @@ -258,25 +153,53 @@ public ElementNode createElementNode(final @NonNegative long parentKey, final @N ? storageEngineWriter.createNameKey(name.getPrefix(), NodeKind.ELEMENT) : -1; final int localNameKey = storageEngineWriter.createNameKey(name.getLocalName(), NodeKind.ELEMENT); - final long nodeKey = nextNodeKey(); - final ElementNode node = bindElementNode(nodeKey, parentKey, leftSibKey, rightSibKey, name, pathNodeKey, prefixKey, - localNameKey, uriKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableElementNode.estimateSerializedSize(), deweyIdLen); + reusableElementAttributeKeys.clear(); + reusableElementNamespaceKeys.clear(); + final int recordBytes = ElementNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableElementNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + NULL_KEY, NULL_KEY, pathNodeKey, prefixKey, localNameKey, uriKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0, 0, 0); + kvl.completeDirectWrite(NodeKind.ELEMENT.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableElementNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableElementNode.setOwnerPage(kvl); + reusableElementNode.setDeweyIDAfterCreation(id, deweyIdBytes); + reusableElementNode.setName(name); + return reusableElementNode; } @Override public TextNode createTextNode(final @NonNegative long parentKey, final @NonNegative long leftSibKey, final @NonNegative long rightSibKey, final byte[] value, final boolean isCompressed, final SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - // Compress value if needed final boolean compression = isCompressed && value.length > 10; final byte[] compressedValue = compression ? Compression.compress(value, Deflater.HUFFMAN_ONLY) : value; - final TextNode node = bindTextNode(nodeKey, parentKey, leftSibKey, rightSibKey, compressedValue, compression, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 55 + compressedValue.length, deweyIdLen); + final int recordBytes = TextNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableTextNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, compressedValue, compression); + kvl.completeDirectWrite(NodeKind.TEXT.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableTextNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableTextNode.setOwnerPage(kvl); + reusableTextNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableTextNode; } @Override @@ -287,10 +210,24 @@ public AttributeNode createAttributeNode(final @NonNegative long parentKey, fina ? storageEngineWriter.createNameKey(name.getPrefix(), NodeKind.ATTRIBUTE) : -1; final int localNameKey = storageEngineWriter.createNameKey(name.getLocalName(), NodeKind.ATTRIBUTE); - final long nodeKey = nextNodeKey(); - final AttributeNode node = - bindAttributeNode(nodeKey, parentKey, name, value, pathNodeKey, prefixKey, localNameKey, uriKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 64 + value.length, deweyIdLen); + final int recordBytes = AttributeNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableAttributeNode.getHeapOffsets(), nodeKey, parentKey, pathNodeKey, + prefixKey, localNameKey, uriKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, value); + kvl.completeDirectWrite(NodeKind.ATTRIBUTE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableAttributeNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableAttributeNode.setOwnerPage(kvl); + reusableAttributeNode.setDeweyIDAfterCreation(id, deweyIdBytes); + reusableAttributeNode.setName(name); + return reusableAttributeNode; } @Override @@ -301,9 +238,24 @@ public NamespaceNode createNamespaceNode(final @NonNegative long parentKey, fina ? storageEngineWriter.createNameKey(name.getPrefix(), NodeKind.NAMESPACE) : -1; - final long nodeKey = nextNodeKey(); - final NamespaceNode node = bindNamespaceNode(nodeKey, parentKey, name, pathNodeKey, prefixKey, uriKey, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + reusableNamespaceNode.estimateSerializedSize(), deweyIdLen); + final int recordBytes = NamespaceNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableNamespaceNode.getHeapOffsets(), nodeKey, parentKey, pathNodeKey, + prefixKey, -1, uriKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0); + kvl.completeDirectWrite(NodeKind.NAMESPACE.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableNamespaceNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableNamespaceNode.setOwnerPage(kvl); + reusableNamespaceNode.setDeweyIDAfterCreation(id, deweyIdBytes); + reusableNamespaceNode.setName(name); + return reusableNamespaceNode; } @Override @@ -315,29 +267,126 @@ public PINode createPINode(final @NonNegative long parentKey, final @NonNegative : -1; final int localNameKey = storageEngineWriter.createNameKey(target.getLocalName(), NodeKind.PROCESSING_INSTRUCTION); final int uriKey = storageEngineWriter.createNameKey(target.getNamespaceURI(), NodeKind.NAMESPACE); - final long nodeKey = nextNodeKey(); final boolean compression = isCompressed && content.length > 10; final byte[] compressedContent = compression ? Compression.compress(content, Deflater.HUFFMAN_ONLY) : content; - final PINode node = bindPINode(nodeKey, parentKey, leftSibKey, rightSibKey, target, compressedContent, pathNodeKey, - prefixKey, localNameKey, uriKey, compression, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 64 + compressedContent.length, deweyIdLen); + final int recordBytes = PINode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusablePINode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + NULL_KEY, NULL_KEY, pathNodeKey, prefixKey, localNameKey, uriKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, 0, 0, + compressedContent, compression); + kvl.completeDirectWrite(NodeKind.PROCESSING_INSTRUCTION.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusablePINode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusablePINode.setOwnerPage(kvl); + reusablePINode.setDeweyIDAfterCreation(id, deweyIdBytes); + reusablePINode.setName(target); + return reusablePINode; } @Override public CommentNode createCommentNode(final @NonNegative long parentKey, final @NonNegative long leftSibKey, final @NonNegative long rightSibKey, final byte[] value, final boolean isCompressed, final SirixDeweyID id) { - final long nodeKey = nextNodeKey(); - // Compress value if needed final boolean compression = isCompressed && value.length > 10; final byte[] compressedValue = compression ? Compression.compress(value, Deflater.HUFFMAN_ONLY) : value; - final CommentNode node = - bindCommentNode(nodeKey, parentKey, leftSibKey, rightSibKey, compressedValue, compression, id); - return storageEngineWriter.createRecord(node, IndexType.DOCUMENT, -1); + storageEngineWriter.allocateForDocumentCreation(); + final KeyValueLeafPage kvl = storageEngineWriter.getAllocKvl(); + final long nodeKey = storageEngineWriter.getAllocNodeKey(); + final int slotOffset = storageEngineWriter.getAllocSlotOffset(); + final byte[] deweyIdBytes = (id != null && kvl.areDeweyIDsStored()) ? id.toBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + final long absOffset = kvl.prepareHeapForDirectWrite( + 55 + compressedValue.length, deweyIdLen); + final int recordBytes = CommentNode.writeNewRecord(kvl.getSlottedPage(), absOffset, + reusableCommentNode.getHeapOffsets(), nodeKey, parentKey, rightSibKey, leftSibKey, + Constants.NULL_REVISION_NUMBER, revisionNumber, compressedValue, compression); + kvl.completeDirectWrite(NodeKind.COMMENT.getId(), nodeKey, slotOffset, recordBytes, deweyIdBytes); + reusableCommentNode.bind(kvl.getSlottedPage(), absOffset, nodeKey, slotOffset); + reusableCommentNode.setOwnerPage(kvl); + reusableCommentNode.setDeweyIDAfterCreation(id, deweyIdBytes); + return reusableCommentNode; + } + + /** + * Bind the correct write singleton to a slotted page slot for zero-allocation modification. + * Reads the nodeKindId from the page directory, selects the matching singleton, binds to the + * slot, and propagates DeweyID. + * + * @param page the KeyValueLeafPage containing the slotted page + * @param offset the slot index (0-1023) + * @param nodeKey the record key + * @return the bound write singleton, or null if the slot is not an XML node type + */ + DataRecord bindWriteSingleton(final KeyValueLeafPage page, final int offset, final long nodeKey) { + final MemorySegment slottedPage = page.getSlottedPage(); + if (slottedPage == null || !PageLayout.isSlotPopulated(slottedPage, offset)) { + return null; + } + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, offset); + final int heapOffset = PageLayout.getDirHeapOffset(slottedPage, offset); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + final byte[] deweyIdBytes = page.getDeweyIdAsByteArray(offset); + + // Concrete-type switch eliminates 3 itable stubs per bind (bind, setDeweyIDBytes, setOwnerPage). + // Each case is monomorphic — JVM can inline directly. + // setDeweyIDBytes stores raw bytes lazily (no SirixDeweyID parsing). + // No setOwnerPage(null) needed — setDeweyIDBytes doesn't trigger resize. + return switch (nodeKindId) { + case 1 -> { // ELEMENT + reusableElementNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableElementNode.setDeweyIDBytes(deweyIdBytes); + reusableElementNode.setOwnerPage(page); + yield reusableElementNode; + } + case 2 -> { // ATTRIBUTE + reusableAttributeNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableAttributeNode.setDeweyIDBytes(deweyIdBytes); + reusableAttributeNode.setOwnerPage(page); + yield reusableAttributeNode; + } + case 3 -> { // TEXT + reusableTextNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableTextNode.setDeweyIDBytes(deweyIdBytes); + reusableTextNode.setOwnerPage(page); + yield reusableTextNode; + } + case 7 -> { // PROCESSING_INSTRUCTION + reusablePINode.bind(slottedPage, recordBase, nodeKey, offset); + reusablePINode.setDeweyIDBytes(deweyIdBytes); + reusablePINode.setOwnerPage(page); + yield reusablePINode; + } + case 8 -> { // COMMENT + reusableCommentNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableCommentNode.setDeweyIDBytes(deweyIdBytes); + reusableCommentNode.setOwnerPage(page); + yield reusableCommentNode; + } + case 9 -> { // XML_DOCUMENT + reusableXmlDocumentRootNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableXmlDocumentRootNode.setDeweyIDBytes(deweyIdBytes); + reusableXmlDocumentRootNode.setOwnerPage(page); + yield reusableXmlDocumentRootNode; + } + case 13 -> { // NAMESPACE + reusableNamespaceNode.bind(slottedPage, recordBase, nodeKey, offset); + reusableNamespaceNode.setDeweyIDBytes(deweyIdBytes); + reusableNamespaceNode.setOwnerPage(page); + yield reusableNamespaceNode; + } + default -> null; + }; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeTrxImpl.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeTrxImpl.java index 5ed7d46f1..022461bdd 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeTrxImpl.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/node/xml/XmlNodeTrxImpl.java @@ -146,6 +146,11 @@ final class XmlNodeTrxImpl extends useTextCompression = resourceSession.getResourceConfig().useTextCompression; deweyIDManager = new XmlDeweyIDManager(this); + // Wire write singleton binder for zero-allocation write path. + if (nodeFactory instanceof XmlNodeFactoryImpl factoryImpl) { + wireWriteSingletonBinder(factoryImpl, storageEngineWriter); + } + // Register index listeners for any existing indexes. // This is critical for subsequent write transactions to update indexes on node modifications. final var existingIndexDefs = indexController.getIndexes().getIndexDefs(); @@ -543,13 +548,13 @@ public XmlNodeTrx insertElementAsFirstChild(final QNm name) { : 0; final SirixDeweyID id = deweyIDManager.newFirstChildID(); final ElementNode node = nodeFactory.createElementNode(parentKey, leftSibKey, rightSibKey, name, pathNodeKey, id); + final long nodeKey = node.getNodeKey(); - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASFIRSTCHILD); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -590,13 +595,13 @@ public XmlNodeTrx insertElementAsLeftSibling(final QNm name) { final SirixDeweyID id = deweyIDManager.newLeftSiblingID(); final ElementNode node = nodeFactory.createElementNode(parentKey, leftSibKey, rightSibKey, name, pathNodeKey, id); + final long nodeKey = node.getNodeKey(); - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASLEFTSIBLING); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -629,7 +634,15 @@ public XmlNodeTrx insertElementAsRightSibling(final QNm name) { final long parentKey = currentNode.getParentKey(); final long leftSibKey = currentNode.getNodeKey(); final long rightSibKey = currentNode.getRightSiblingKey(); - moveToParent(); + final boolean moveResult = moveToParent(); + if (!moveResult) { + throw new IllegalStateException("moveToParent() failed from nodeKey=" + key + + " kind=" + currentKind + " parentKey=" + parentKey); + } + if (getKind() != NodeKind.ELEMENT && getKind() != NodeKind.XML_DOCUMENT) { + throw new IllegalStateException("After moveToParent(), expected ELEMENT or XML_DOCUMENT but got kind=" + + getKind() + " nodeKey=" + getNodeKey() + " from child nodeKey=" + key + " kind=" + currentKind); + } final long pathNodeKey = buildPathSummary ? pathSummaryWriter.getPathNodeKey(name, NodeKind.ELEMENT) : 0; @@ -637,13 +650,13 @@ public XmlNodeTrx insertElementAsRightSibling(final QNm name) { final SirixDeweyID id = deweyIDManager.newRightSiblingID(); final ElementNode node = nodeFactory.createElementNode(parentKey, leftSibKey, rightSibKey, name, pathNodeKey, id); + final long nodeKey = node.getNodeKey(); - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASRIGHTSIBLING); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -818,12 +831,12 @@ private XmlNodeTrx pi(final String target, final String content, final InsertPos : 0; final PINode node = nodeFactory.createPINode(pk.parentKey(), pk.leftSibKey(), pk.rightSibKey(), targetName, processingContent, useTextCompression, pathNodeKey, pk.id()); + final long nodeKey = node.getNodeKey(); // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, pk.pos()); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); return this; } finally { @@ -881,12 +894,12 @@ private XmlNodeTrx comment(final String value, final InsertPosition insert) { final byte[] commentValue = getBytes(value); final CommentNode node = nodeFactory.createCommentNode(pk.parentKey(), pk.leftSibKey(), pk.rightSibKey(), commentValue, useTextCompression, pk.id()); + final long nodeKey = node.getNodeKey(); // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, pk.pos()); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); return this; } finally { @@ -933,15 +946,15 @@ public XmlNodeTrx insertTextAsFirstChild(final String value) { final SirixDeweyID id = deweyIDManager.newFirstChildID(); final TextNode node = nodeFactory.createTextNode(parentKey, leftSibKey, rightSibKey, textValue, useTextCompression, id); + final long nodeKey = node.getNodeKey(); // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASFIRSTCHILD); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); + moveTo(nodeKey); // Index text value. - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -1002,22 +1015,22 @@ public XmlNodeTrx insertTextAsLeftSibling(final String value) { final SirixDeweyID id = deweyIDManager.newLeftSiblingID(); final TextNode node = nodeFactory.createTextNode(parentKey, leftSibKey, rightSibKey, textValue, useTextCompression, id); + final long nodeKey = node.getNodeKey(); // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASLEFTSIBLING); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); // Get the path node key. + moveTo(nodeKey); moveToParent(); final long pathNodeKey = isElement() ? getNameNode().getPathNodeKey() : -1; - nodeReadOnlyTrx.setCurrentNode(node); + moveTo(nodeKey); // Index text value. - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -1080,22 +1093,22 @@ public XmlNodeTrx insertTextAsRightSibling(final String value) { final SirixDeweyID id = deweyIDManager.newRightSiblingID(); final TextNode node = nodeFactory.createTextNode(parentKey, leftSibKey, rightSibKey, textValue, useTextCompression, id); + final long nodeKey = node.getNodeKey(); // Adapt local nodes and hashes. - nodeReadOnlyTrx.setCurrentNode(node); adaptForInsert(node, InsertPos.ASRIGHTSIBLING); - nodeReadOnlyTrx.setCurrentNode(node); - nodeHashing.adaptHashesWithAdd(); + nodeHashing.adaptHashesWithAdd(nodeKey); // Get the path node key. + moveTo(nodeKey); moveToParent(); final long pathNodeKey = isElement() ? getNameNode().getPathNodeKey() : -1; - nodeReadOnlyTrx.setCurrentNode(node); + moveTo(nodeKey); // Index text value. - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); return this; } finally { @@ -1175,16 +1188,17 @@ public XmlNodeTrx insertAttribute(final QNm name, @NonNull final String value, @ final SirixDeweyID id = deweyIDManager.newAttributeID(); final AttributeNode node = nodeFactory.createAttributeNode(elementKey, name, attValue, pathNodeKey, id); + final long nodeKey = node.getNodeKey(); - final Node parentNode = storageEngineWriter.prepareRecordForModification(node.getParentKey(), IndexType.DOCUMENT, -1); - ((ElementNode) parentNode).insertAttribute(node.getNodeKey()); + final Node parentNode = storageEngineWriter.prepareRecordForModification(elementKey, IndexType.DOCUMENT, -1); + ((ElementNode) parentNode).insertAttribute(nodeKey); persistUpdatedRecord(parentNode); - nodeReadOnlyTrx.setCurrentNode(node); + moveTo(nodeKey); nodeHashing.adaptHashesWithAdd(); // Index text value. - notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, node, pathNodeKey); + notifyPrimitiveIndexChange(IndexController.ChangeType.INSERT, nodeReadOnlyTrx.getCurrentNode(), pathNodeKey); if (move == Movement.TOPARENT) { moveToParent(); @@ -1235,12 +1249,13 @@ public XmlNodeTrx insertNamespace(@NonNull final QNm name, @NonNull final Moveme final SirixDeweyID id = deweyIDManager.newNamespaceID(); final NamespaceNode node = nodeFactory.createNamespaceNode(elementKey, name, pathNodeKey, id); + final long nodeKey = node.getNodeKey(); - final Node parentNode = storageEngineWriter.prepareRecordForModification(node.getParentKey(), IndexType.DOCUMENT, -1); - ((ElementNode) parentNode).insertNamespace(node.getNodeKey()); + final Node parentNode = storageEngineWriter.prepareRecordForModification(elementKey, IndexType.DOCUMENT, -1); + ((ElementNode) parentNode).insertNamespace(nodeKey); persistUpdatedRecord(parentNode); - nodeReadOnlyTrx.setCurrentNode(node); + moveTo(nodeKey); nodeHashing.adaptHashesWithAdd(); if (move == Movement.TOPARENT) { moveToParent(); @@ -1475,12 +1490,9 @@ public XmlNodeTrx setName(final QNm name) { if (!getName().equals(name)) { checkAccessAndCommit(); - final NameNode node; - if (currentKind == NodeKind.ELEMENT || currentKind == NodeKind.PROCESSING_INSTRUCTION) { - node = (NameNode) nodeReadOnlyTrx.getStructuralNode(); - } else { - node = (NameNode) nodeReadOnlyTrx.getCurrentNode(); - } + // Get a TIL-owned copy via prepareRecordForModification (proper COW). + final NameNode node = + (NameNode) storageEngineWriter.prepareRecordForModification(getNodeKey(), IndexType.DOCUMENT, -1); final long oldHash = node.computeHash(bytes); // Remove old keys from mapping. @@ -1505,6 +1517,7 @@ public XmlNodeTrx setName(final QNm name) { : -1; // Set new keys for current node. + final long savedNodeKey = node.getNodeKey(); node.setLocalNameKey(localNameKey); node.setURIKey(uriKey); node.setPrefixKey(prefixKey); @@ -1515,13 +1528,18 @@ public XmlNodeTrx setName(final QNm name) { PathSummaryWriter.OPType.SETNAME); } + // Re-acquire the record: adaptPathForChangedNode may have rebound the write + // singleton to a different node via resetPathNodeKey → prepareRecordForModification. + final NameNode node2 = + (NameNode) storageEngineWriter.prepareRecordForModification(savedNodeKey, IndexType.DOCUMENT, -1); + // Set path node key. - node.setPathNodeKey(buildPathSummary + node2.setPathNodeKey(buildPathSummary ? pathSummaryWriter.getNodeKey() : 0); - nodeReadOnlyTrx.setCurrentNode((ImmutableXmlNode) node); - persistUpdatedRecord(node); + nodeReadOnlyTrx.setCurrentNode((ImmutableXmlNode) node2); + persistUpdatedRecord(node2); nodeHashing.adaptHashedWithUpdate(oldHash); } @@ -1560,13 +1578,11 @@ public XmlNodeTrx setValue(final String value) { final long pathNodeKey = getPathNodeKey(); moveTo(nodeKey); - final ValueNode node; - if (currentKind == NodeKind.TEXT || currentKind == NodeKind.COMMENT - || currentKind == NodeKind.PROCESSING_INSTRUCTION) { - node = (ValueNode) nodeReadOnlyTrx.getStructuralNode(); - } else { - node = (ValueNode) nodeReadOnlyTrx.getCurrentNode(); - } + // Get a TIL-owned copy via prepareRecordForModification (proper COW). + // This ensures mutations don't leak to read-only transactions that share + // the same cached page reference. + final ValueNode node = + (ValueNode) storageEngineWriter.prepareRecordForModification(nodeKey, IndexType.DOCUMENT, -1); // Remove old value from indexes before mutating the node. notifyPrimitiveIndexChange(IndexController.ChangeType.DELETE, (ImmutableNode) node, pathNodeKey); final long oldHash = node.computeHash(bytes); @@ -2074,6 +2090,14 @@ protected AbstractNodeHashing reInstantiat @Override protected XmlNodeFactory reInstantiateNodeFactory(StorageEngineWriter storageEngineWriter) { - return new XmlNodeFactoryImpl(resourceSession.getResourceConfig().nodeHashFunction, storageEngineWriter); + final var factory = new XmlNodeFactoryImpl( + resourceSession.getResourceConfig().nodeHashFunction, storageEngineWriter); + wireWriteSingletonBinder(factory, storageEngineWriter); + return factory; + } + + private static void wireWriteSingletonBinder(final XmlNodeFactoryImpl factory, + final StorageEngineWriter storageEngineWriter) { + storageEngineWriter.setWriteSingletonBinder(factory::bindWriteSingleton); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/AbstractForwardingStorageEngineWriter.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/AbstractForwardingStorageEngineWriter.java index a5f27d9c1..a916e361f 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/AbstractForwardingStorageEngineWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/AbstractForwardingStorageEngineWriter.java @@ -46,8 +46,8 @@ public V prepareRecordForModification(@NonNegative long r } @Override - public void updateRecordSlot(@NonNull DataRecord record, @NonNull IndexType indexType, int index) { - delegate().updateRecordSlot(record, indexType, index); + public void persistRecord(@NonNull DataRecord record, @NonNull IndexType indexType, int index) { + delegate().persistRecord(record, indexType, index); } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/HOTTrieWriter.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/HOTTrieWriter.java index dcd414df4..ecd65a590 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/HOTTrieWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/HOTTrieWriter.java @@ -507,14 +507,13 @@ public byte[] handleLeafSplit(@NonNull StorageEngineWriter storageEngineReader, } else { // Root split - create new BiNode as root // CRITICAL: When pathDepth=0, leafRef may be the same object as rootReference. - // We need to create a SEPARATE reference for the left child to avoid circular references. + // We need a SEPARATE reference for the left child, and it MUST be in the log + // so that HOTIndirectPage.commit() can find and write it to disk. PageReference leftChildRef; if (leafRef == rootReference) { - // Create a new reference for the left child that points to the leaf's log entry + // Put the left child in the log so commit can find it by identity leftChildRef = new PageReference(); - leftChildRef.setLogKey(leafRef.getLogKey()); - leftChildRef.setKey(fullPage.getPageKey()); - leftChildRef.setPage(fullPage); + log.put(leftChildRef, PageContainer.getInstance(fullPage, fullPage)); } else { leftChildRef = leafRef; } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineReader.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineReader.java index cd660c93c..7e8adcbfc 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineReader.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineReader.java @@ -30,36 +30,21 @@ import io.sirix.api.NodeTrx; import io.sirix.api.StorageEngineReader; import io.sirix.api.ResourceSession; -import io.sirix.cache.BufferManager; -import io.sirix.cache.Cache; -import io.sirix.cache.FrameReusedException; -import io.sirix.cache.IndexLogKey; -import io.sirix.cache.PageContainer; +import io.sirix.cache.*; import io.sirix.cache.PageGuard; -import io.sirix.cache.RevisionRootPageCacheKey; -import io.sirix.cache.TransactionIntentLog; import io.sirix.exception.SirixIOException; import io.sirix.index.IndexType; import io.sirix.io.Reader; import io.sirix.node.DeletedNode; import io.sirix.node.MemorySegmentBytesIn; import io.sirix.node.NodeKind; +import io.sirix.node.SirixDeweyID; import io.sirix.node.interfaces.DataRecord; +import io.sirix.node.interfaces.FlyweightNode; +import io.sirix.node.interfaces.Node; import io.sirix.node.json.ObjectStringNode; import io.sirix.node.json.StringNode; -import io.sirix.page.CASPage; -import io.sirix.page.DeweyIDPage; -import io.sirix.page.HOTIndirectPage; -import io.sirix.page.HOTLeafPage; -import io.sirix.page.IndirectPage; -import io.sirix.page.KeyValueLeafPage; -import io.sirix.page.NamePage; -import io.sirix.page.OverflowPage; -import io.sirix.page.PageReference; -import io.sirix.page.PathPage; -import io.sirix.page.PathSummaryPage; -import io.sirix.page.RevisionRootPage; -import io.sirix.page.UberPage; +import io.sirix.page.*; import io.sirix.page.interfaces.KeyValuePage; import io.sirix.page.interfaces.Page; import io.sirix.page.interfaces.PageFragmentKey; @@ -84,7 +69,7 @@ import static java.util.Objects.requireNonNull; /** - * Storage engine reader. The only thing shared amongst transactions is the resource session. + * Page read-only transaction. The only thing shared amongst transactions is the resource manager. * Everything else is exclusive to this transaction. It is required that only a single thread has * access to this transaction. */ @@ -94,13 +79,12 @@ public final class NodeStorageEngineReader implements StorageEngineReader { /** * Enable path summary cache debugging. - * * @see DiagnosticSettings#PATH_SUMMARY_DEBUG */ private static final boolean DEBUG_PATH_SUMMARY = DiagnosticSettings.PATH_SUMMARY_DEBUG; private record RecordPage(int index, IndexType indexType, long recordPageKey, int revision, - PageReference pageReference, KeyValueLeafPage page) { + PageReference pageReference, KeyValueLeafPage page) { } /** @@ -119,12 +103,12 @@ private record RecordPage(int index, IndexType indexType, long recordPageKey, in final InternalResourceSession resourceSession; /** - * The revision number, this storage engine reader is bound to. + * The revision number, this page trx is bound to. */ private final int revisionNumber; /** - * Determines if storage engine reader is closed or not. + * Determines if page reading transaction is closed or not. */ private volatile boolean isClosed; @@ -159,19 +143,22 @@ private record RecordPage(int index, IndexType indexType, long recordPageKey, in private final long resourceId; /** - * Epoch tracker ticket for this transaction (for MVCC-aware eviction). Registered when transaction - * opens, deregistered when it closes. + * Epoch tracker ticket for this transaction (for MVCC-aware eviction). + * Registered when transaction opens, deregistered when it closes. */ private final RevisionEpochTracker.Ticket epochTicket; /** * Current page guard - protects the page where cursor is currently positioned. *

- * Guard lifecycle: - Acquired when cursor moves to a page - Released when cursor moves to a - * DIFFERENT page - Released on transaction close + * Guard lifecycle: + * - Acquired when cursor moves to a page + * - Released when cursor moves to a DIFFERENT page + * - Released on transaction close *

- * This matches database cursor semantics: only the "current" page is guarded. Node keys are - * primitives (copied from MemorySegments), so old pages can be evicted after cursor moves away. + * This matches database cursor semantics: only the "current" page is guarded. + * Node keys are primitives (copied from MemorySegments), so old pages can be + * evicted after cursor moves away. */ private PageGuard currentPageGuard; @@ -186,8 +173,9 @@ private record RecordPage(int index, IndexType indexType, long recordPageKey, in private final NamePage namePage; /** - * Most recently read pages by type and index. Using specific fields instead of generic cache for - * clear ownership and lifecycle. Index-aware: NAME/PATH/CAS can have multiple indexes (0-3). + * Most recently read pages by type and index. + * Using specific fields instead of generic cache for clear ownership and lifecycle. + * Index-aware: NAME/PATH/CAS can have multiple indexes (0-3). */ private RecordPage mostRecentDocumentPage; private RecordPage mostRecentChangedNodesPage; @@ -199,21 +187,21 @@ private record RecordPage(int index, IndexType indexType, long recordPageKey, in private RecordPage mostRecentDeweyIdPage; /** - * Reusable IndexLogKey to avoid allocations on every getRecord/lookupSlot call. Safe to reuse - * because this transaction is single-threaded (see class javadoc). + * Reusable IndexLogKey to avoid allocations on every getRecord/lookupSlot call. + * Safe to reuse because this transaction is single-threaded (see class javadoc). */ private final IndexLogKey reusableIndexLogKey = new IndexLogKey(null, 0, 0, 0); /** * Standard constructor. * - * @param trxId the transaction-ID. - * @param resourceSession the resource session - * @param uberPage {@link UberPage} to start reading from - * @param revision key of revision to read from uber page - * @param reader to read stored pages for this transaction + * @param trxId the transaction-ID. + * @param resourceSession the resource manager + * @param uberPage {@link UberPage} to start reading from + * @param revision key of revision to read from uber page + * @param reader to read stored pages for this transaction * @param resourceBufferManager caches in-memory reconstructed pages - * @param trxIntentLog the transaction intent log (can be {@code null}) + * @param trxIntentLog the transaction intent log (can be {@code null}) * @throws SirixIOException if reading of the persistent storage fails */ public NodeStorageEngineReader(final int trxId, @@ -260,7 +248,7 @@ private Page loadPage(final PageReference reference) { // logKey might still be set, so don't assert it's NULL_ID_INT } - // if (trxIntentLog == null) { + // if (trxIntentLog == null) { // REMOVED INCORRECT ASSERTION: logKey can be != NULL_ID_INT if page was in TIL then cleared // assert reference.getLogKey() == Constants.NULL_ID_INT; page = resourceBufferManager.getPageCache().get(reference, (_, _) -> { @@ -275,18 +263,17 @@ private Page loadPage(final PageReference reference) { reference.setPage(page); } return page; - // } + // } - // if (reference.getKey() != Constants.NULL_ID_LONG || reference.getLogKey() != - // Constants.NULL_ID_INT) { - // page = pageReader.read(reference, resourceSession.getResourceConfig()); - // } + // if (reference.getKey() != Constants.NULL_ID_LONG || reference.getLogKey() != Constants.NULL_ID_INT) { + // page = pageReader.read(reference, resourceSession.getResourceConfig()); + // } // - // if (page != null) { - // putIntoPageCache(reference, page); - // reference.setPage(page); - // } - // return page; + // if (page != null) { + // putIntoPageCache(reference, page); + // reference.setPage(page); + // } + // return page; } @Override @@ -298,9 +285,7 @@ public boolean hasTrxIntentLog() { private Page getFromTrxIntentLog(PageReference reference) { // Try to get it from the transaction log if it's present. final PageContainer cont = trxIntentLog.get(reference); - return cont == null - ? null - : cont.getComplete(); + return cont == null ? null : cont.getComplete(); } @Override @@ -349,14 +334,13 @@ public V getRecord(final long recordKey, @NonNull final I // OPTIMIZATION: Reuse IndexLogKey instance to avoid allocation on every getRecord call reusableIndexLogKey.setIndexType(indexType) - .setRecordPageKey(recordPageKey) - .setIndexNumber(index) - .setRevisionNumber(revisionNumber); + .setRecordPageKey(recordPageKey) + .setIndexNumber(index) + .setRevisionNumber(revisionNumber); // $CASES-OMITTED$ final PageReferenceToPage pageReferenceToPage = switch (indexType) { - case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> - getRecordPage(reusableIndexLogKey); + case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> getRecordPage(reusableIndexLogKey); default -> throw new IllegalStateException(); }; @@ -366,7 +350,7 @@ public V getRecord(final long recordKey, @NonNull final I final var dataRecord = getValue(((KeyValueLeafPage) pageReferenceToPage.page), recordKey); - // noinspection unchecked + //noinspection unchecked return (V) checkItemIfDeleted(dataRecord); } @@ -375,60 +359,102 @@ public DataRecord getValue(final KeyValuePage page, final final var offset = StorageEngineReader.recordPageOffset(nodeKey); DataRecord record = page.getRecord(offset); if (record == null) { - var data = page.getSlot(offset); - if (data != null) { - record = getDataRecord(nodeKey, offset, data, page); + // Unified page path: check directory for flyweight vs legacy format + if (page instanceof KeyValueLeafPage kvlPage) { + record = getRecordFromSlottedPage(kvlPage, nodeKey, offset); + } else { + var data = page.getSlot(offset); + if (data != null) { + record = getDataRecord(nodeKey, offset, data, page); + } } if (record != null) { return record; } + // Overflow page fallback try { final PageReference reference = page.getPageReference(nodeKey); if (reference != null && reference.getKey() != Constants.NULL_ID_LONG) { - data = ((OverflowPage) pageReader.read(reference, resourceSession.getResourceConfig())).getData(); + final var data = ((OverflowPage) pageReader.read(reference, resourceSession.getResourceConfig())).getData(); + record = getDataRecord(nodeKey, offset, data, page); } else { return null; } } catch (final SirixIOException e) { return null; } - record = getDataRecord(nodeKey, offset, data, page); + } else if (page instanceof KeyValueLeafPage kvlPage) { + // Record was found in records[] cache. Ensure FSST symbol table is propagated + // (needed when a cached page from the write transaction is reused by a read session + // — the FSST table is built at commit time but records[] may already be populated) + propagateFsstSymbolTableToRecord(record, kvlPage); } return record; } + /** + * Get a record from a slotted page. Flyweight records (nodeKindId > 0) are + * created via FlyweightNodeFactory and bound directly to page memory. + * Legacy records (nodeKindId == 0) are deserialized from the heap bytes. + */ @SuppressWarnings({"unchecked", "rawtypes"}) - private DataRecord getDataRecord(long key, int offset, MemorySegment data, KeyValuePage page) { - final boolean fixedSlotFormat = page instanceof KeyValueLeafPage kvPage && kvPage.isFixedSlotFormat(offset); - if (fixedSlotFormat) { - throw new IllegalStateException( - "Fixed-slot bytes must be read through singleton cursor access (moveTo/lookupSlotWithGuard) " - + "or writer-specific fixed-slot materialization (key=" + key + ", slot=" + offset + ")."); + private DataRecord getRecordFromSlottedPage(final KeyValueLeafPage kvlPage, + final long nodeKey, final int offset) { + final MemorySegment slottedPage = kvlPage.getSlottedPage(); + if (!PageLayout.isSlotPopulated(slottedPage, offset)) { + return null; + } + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, offset); + if (nodeKindId > 0) { + // Flyweight format: create binding shell and bind to page memory (zero-copy read) + final FlyweightNode fn = FlyweightNodeFactory.createAndBind( + slottedPage, offset, nodeKey, resourceConfig.nodeHashFunction); + // Propagate DeweyID from page to flyweight node (stored inline after record data) + if (resourceConfig.areDeweyIDsStored && fn instanceof Node node) { + final byte[] deweyIdBytes = kvlPage.getDeweyIdAsByteArray(offset); + if (deweyIdBytes != null) { + node.setDeweyID(new SirixDeweyID(deweyIdBytes)); + } + } + // Propagate FSST symbol table to flyweight string nodes for lazy decompression + propagateFsstSymbolTableToRecord((DataRecord) fn, kvlPage); + // Do NOT cache FlyweightNode in page's records[] — pages are shared between transactions. + return (DataRecord) fn; + } else { + // Legacy format in slotted page heap: deserialize normally + final var data = kvlPage.getSlot(offset); + if (data != null) { + final var record = getDataRecord(nodeKey, offset, data, kvlPage); + return record; + } + return null; } + } - final byte[] deweyIdBytes = page.getDeweyIdAsByteArray(offset); - final DataRecord record = deserializeCompactRecord(key, data, deweyIdBytes); + @SuppressWarnings({"unchecked", "rawtypes"}) + private DataRecord getDataRecord(long key, int offset, MemorySegment data, KeyValuePage page) { + var record = resourceConfig.recordPersister.deserialize(new MemorySegmentBytesIn(data), + key, + page.getDeweyIdAsByteArray(offset), + resourceConfig); // Propagate FSST symbol table to string nodes for lazy decompression // Only KeyValueLeafPage has FSST symbol table support if (page instanceof KeyValueLeafPage kvPage) { propagateFsstSymbolTableToRecord(record, kvPage); - kvPage.onRecordRematerialized(); } - // Use raw type to avoid generic mismatch with different KeyValuePage implementations. - ((KeyValuePage) page).setRecord(record); + // Do NOT cache deserialized records in the page's records[] here. + // Pages in the buffer manager are shared between transactions. + // Caching creates shared mutable references that violate snapshot isolation: + // a write transaction mutating the cached record would be visible to + // read-only transactions reading from the same committed page. return record; } - private DataRecord deserializeCompactRecord(final long key, final MemorySegment data, final byte[] deweyIdBytes) { - return resourceConfig.recordPersister.deserialize(new MemorySegmentBytesIn(data), key, deweyIdBytes, - resourceConfig); - } - /** - * Propagate FSST symbol table from page to string nodes. This enables lazy decompression when - * getValue() is called. + * Propagate FSST symbol table from page to string nodes. + * This enables lazy decompression when getValue() is called. */ private void propagateFsstSymbolTableToRecord(DataRecord record, KeyValueLeafPage page) { if (record == null || page == null) { @@ -436,29 +462,27 @@ private void propagateFsstSymbolTableToRecord(DataRecord record, KeyValueLeafPag } final byte[] fsstSymbolTable = page.getFsstSymbolTable(); if (fsstSymbolTable != null && fsstSymbolTable.length > 0) { - final byte[][] parsedSymbols = page.getParsedFsstSymbols(); if (record instanceof StringNode stringNode) { - stringNode.setFsstSymbolTable(fsstSymbolTable, parsedSymbols); + stringNode.setFsstSymbolTable(fsstSymbolTable); } else if (record instanceof ObjectStringNode objectStringNode) { - objectStringNode.setFsstSymbolTable(fsstSymbolTable, parsedSymbols); + objectStringNode.setFsstSymbolTable(fsstSymbolTable); } } } // ==================== FLYWEIGHT CURSOR SUPPORT ==================== - + /** - * Record containing slot location data for zero-allocation access. Holds all information needed to - * read node fields directly from memory. + * Record containing slot location data for zero-allocation access. + * Holds all information needed to read node fields directly from memory. * - * @param page the KeyValueLeafPage containing the slot + * @param page the KeyValueLeafPage containing the slot * @param offset the slot offset within the page (for DeweyID lookup) - * @param data the MemorySegment containing the serialized node data - * @param guard the PageGuard protecting the page from eviction + * @param data the MemorySegment containing the serialized node data + * @param guard the PageGuard protecting the page from eviction */ - public record SlotLocation(KeyValueLeafPage page, int offset, MemorySegment data, PageGuard guard) { - } - + public record SlotLocation(KeyValueLeafPage page, int offset, MemorySegment data, PageGuard guard) {} + /** * Result of lookupSlotOrCached - either a cached record or a slot location. */ @@ -466,19 +490,18 @@ public record SlotOrCachedResult(DataRecord cachedRecord, SlotLocation slotLocat public boolean hasCachedRecord() { return cachedRecord != null; } - public boolean hasSlotLocation() { return slotLocation != null; } } - + /** - * Lookup a node, returning cached record if available, otherwise slot location. This does ONE page - * lookup and checks the cache first, avoiding double lookups. + * Lookup a node, returning cached record if available, otherwise slot location. + * This does ONE page lookup and checks the cache first, avoiding double lookups. * * @param recordKey the node key to look up * @param indexType the index type - * @param index the index number + * @param index the index number * @return SlotOrCachedResult with either cachedRecord or slotLocation, or both null if not found */ public SlotOrCachedResult lookupSlotOrCached(final long recordKey, @NonNull final IndexType indexType, @@ -493,14 +516,13 @@ public SlotOrCachedResult lookupSlotOrCached(final long recordKey, @NonNull fina final long recordPageKey = pageKey(recordKey, indexType); // OPTIMIZATION: Reuse IndexLogKey instance to avoid allocation reusableIndexLogKey.setIndexType(indexType) - .setRecordPageKey(recordPageKey) - .setIndexNumber(index) - .setRevisionNumber(revisionNumber); + .setRecordPageKey(recordPageKey) + .setIndexNumber(index) + .setRevisionNumber(revisionNumber); // Get the page reference (uses cache) - ONE lookup for both paths final PageReferenceToPage pageReferenceToPage = switch (indexType) { - case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> - getRecordPage(reusableIndexLogKey); + case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> getRecordPage(reusableIndexLogKey); default -> null; }; @@ -521,7 +543,7 @@ public SlotOrCachedResult lookupSlotOrCached(final long recordKey, @NonNull fina // Record was deleted - return not found return new SlotOrCachedResult(null, null); } - + // Not cached - acquire guard and return slot location page.acquireGuard(); @@ -557,29 +579,28 @@ public SlotOrCachedResult lookupSlotOrCached(final long recordKey, @NonNull fina } /** - * Lookup a slot directly without deserializing to a node object. This is the core method for - * zero-allocation flyweight cursor access. + * Lookup a slot directly without deserializing to a node object. + * This is the core method for zero-allocation flyweight cursor access. *

- * IMPORTANT: The returned PageGuard MUST be closed when the slot is no longer needed. Failure to - * close the guard will prevent page eviction and cause memory issues. + * IMPORTANT: The returned PageGuard MUST be closed when the slot is no longer needed. + * Failure to close the guard will prevent page eviction and cause memory issues. *

* Usage: - * *

{@code
    * var location = reader.lookupSlotWithGuard(nodeKey, IndexType.DOCUMENT, -1);
    * if (location != null) {
-   *   try {
-   *     // Read directly from location.data()
-   *     long parentKey = DeltaVarIntCodec.decodeDeltaFromSegment(location.data(), offset, nodeKey);
-   *   } finally {
-   *     location.guard().close();
-   *   }
+   *     try {
+   *         // Read directly from location.data()
+   *         long parentKey = DeltaVarIntCodec.decodeDeltaFromSegment(location.data(), offset, nodeKey);
+   *     } finally {
+   *         location.guard().close();
+   *     }
    * }
    * }
* * @param recordKey the node key to lookup * @param indexType the index type (typically DOCUMENT for regular nodes) - * @param index the index number (-1 for DOCUMENT) + * @param index the index number (-1 for DOCUMENT) * @return SlotLocation with page guard, or null if not found */ public SlotLocation lookupSlotWithGuard(long recordKey, IndexType indexType, int index) { @@ -593,14 +614,13 @@ public SlotLocation lookupSlotWithGuard(long recordKey, IndexType indexType, int final long recordPageKey = pageKey(recordKey, indexType); // OPTIMIZATION: Reuse IndexLogKey instance to avoid allocation reusableIndexLogKey.setIndexType(indexType) - .setRecordPageKey(recordPageKey) - .setIndexNumber(index) - .setRevisionNumber(revisionNumber); + .setRecordPageKey(recordPageKey) + .setIndexNumber(index) + .setRevisionNumber(revisionNumber); // Get the page reference final PageReferenceToPage pageReferenceToPage = switch (indexType) { - case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> - getRecordPage(reusableIndexLogKey); + case DOCUMENT, CHANGED_NODES, RECORD_TO_REVISIONS, PATH_SUMMARY, PATH, CAS, NAME -> getRecordPage(reusableIndexLogKey); default -> throw new IllegalStateException("Unsupported index type: " + indexType); }; @@ -755,11 +775,11 @@ public record PageReferenceToPage(PageReference reference, Page page) { /** * Prefetch a page into the cache without blocking on the result. *

- * This method loads the specified page into the buffer cache so that subsequent accesses to nodes - * on that page will be cache hits. + * This method loads the specified page into the buffer cache so that + * subsequent accesses to nodes on that page will be cache hits. *

- * Called by prefetching axes (e.g., PrefetchingDescendantAxis) to asynchronously load pages that - * will be needed soon. + * Called by prefetching axes (e.g., PrefetchingDescendantAxis) to + * asynchronously load pages that will be needed soon. * * @param recordPageKey the page key to prefetch * @param indexType the index type (typically DOCUMENT) @@ -778,9 +798,9 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { // This prevents TOCTOU race where page is closed between check and guard acquisition closeCurrentPageGuard(); page.acquireGuard(); - + // Now check if page is still valid (not closed, memory not released) - if (!page.isClosed() && page.getSlotMemory() != null) { + if (!page.isClosed() && page.getSlottedPage() != null) { currentPageGuard = PageGuard.wrapAlreadyGuarded(page); return new PageReferenceToPage(pathSummaryRecordPage.pageReference, page); } else { @@ -792,8 +812,10 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { } // Check the most recent page for this type/index - var cachedPage = getMostRecentPage(indexLogKey.getIndexType(), indexLogKey.getIndexNumber(), - indexLogKey.getRecordPageKey(), indexLogKey.getRevisionNumber()); + var cachedPage = getMostRecentPage(indexLogKey.getIndexType(), + indexLogKey.getIndexNumber(), + indexLogKey.getRecordPageKey(), + indexLogKey.getRevisionNumber()); if (cachedPage != null) { var page = cachedPage.page(); @@ -802,9 +824,9 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { // This prevents TOCTOU race where page is closed between check and guard acquisition closeCurrentPageGuard(); page.acquireGuard(); - + // Now check if page is still valid (not closed, memory not released) - if (!page.isClosed() && page.getSlotMemory() != null) { + if (!page.isClosed() && page.getSlottedPage() != null) { currentPageGuard = PageGuard.wrapAlreadyGuarded(page); return new PageReferenceToPage(cachedPage.pageReference, page); } else { @@ -818,7 +840,8 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { // Second: Traverse trie. final var pageReferenceToRecordPage = getLeafPageReference(indexLogKey.getRecordPageKey(), - indexLogKey.getIndexNumber(), requireNonNull(indexLogKey.getIndexType())); + indexLogKey.getIndexNumber(), + requireNonNull(indexLogKey.getIndexType())); if (pageReferenceToRecordPage == null) { return null; @@ -844,12 +867,12 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { if (DEBUG_PATH_SUMMARY && indexLogKey.getIndexType() == IndexType.PATH_SUMMARY && page instanceof KeyValueLeafPage kvp) { - LOGGER.debug("[PATH_SUMMARY-NORMAL] -> Got page from cache: pageKey={}, revision={}", kvp.getPageKey(), - kvp.getRevision()); + LOGGER.debug("[PATH_SUMMARY-NORMAL] -> Got page from cache: pageKey={}, revision={}", + kvp.getPageKey(), + kvp.getRevision()); } - // CRITICAL: Handle case where page doesn't exist (e.g., temporal queries accessing non-existent - // revisions) + // CRITICAL: Handle case where page doesn't exist (e.g., temporal queries accessing non-existent revisions) if (page == null) { // Page doesn't exist for this revision/index - this can happen with temporal queries // Return null to signal page not found (caller should handle gracefully) @@ -863,8 +886,7 @@ public PageReferenceToPage getRecordPage(@NonNull IndexLogKey indexLogKey) { // PATH_SUMMARY bypass for write transactions - REQUIRED due to cache key limitations // RecordPageCache uses PageReference (key + logKey) as cache key, which doesn't include revision. - // Different revisions of the same page can have the same PageReference → cache returns wrong - // revision. + // Different revisions of the same page can have the same PageReference → cache returns wrong revision. // Bypass loads directly from disk to avoid stale cached pages. if (DEBUG_PATH_SUMMARY) { LOGGER.debug("\n[PATH_SUMMARY-DECISION] Using bypass (write trx):"); @@ -908,15 +930,9 @@ private RecordPage getMostRecentPage(IndexType indexType, int index, long record case CHANGED_NODES -> mostRecentChangedNodesPage; case RECORD_TO_REVISIONS -> mostRecentRecordToRevisionsPage; case PATH_SUMMARY -> pathSummaryRecordPage; - case PATH -> index < mostRecentPathPages.length - ? mostRecentPathPages[index] - : null; - case CAS -> index < mostRecentCasPages.length - ? mostRecentCasPages[index] - : null; - case NAME -> index < mostRecentNamePages.length - ? mostRecentNamePages[index] - : null; + case PATH -> index < mostRecentPathPages.length ? mostRecentPathPages[index] : null; + case CAS -> index < mostRecentCasPages.length ? mostRecentCasPages[index] : null; + case NAME -> index < mostRecentNamePages.length ? mostRecentNamePages[index] : null; case DEWEYID_TO_RECORDID -> mostRecentDeweyIdPage; default -> null; }; @@ -955,27 +971,21 @@ private void setMostRecentPage(IndexType indexType, int index, RecordPage page) yield old; } case PATH -> { - RecordPage old = index < mostRecentPathPages.length - ? mostRecentPathPages[index] - : null; + RecordPage old = index < mostRecentPathPages.length ? mostRecentPathPages[index] : null; if (index < mostRecentPathPages.length) { mostRecentPathPages[index] = page; } yield old; } case CAS -> { - RecordPage old = index < mostRecentCasPages.length - ? mostRecentCasPages[index] - : null; + RecordPage old = index < mostRecentCasPages.length ? mostRecentCasPages[index] : null; if (index < mostRecentCasPages.length) { mostRecentCasPages[index] = page; } yield old; } case NAME -> { - RecordPage old = index < mostRecentNamePages.length - ? mostRecentNamePages[index] - : null; + RecordPage old = index < mostRecentNamePages.length ? mostRecentNamePages[index] : null; if (index < mostRecentNamePages.length) { mostRecentNamePages[index] = page; } @@ -998,14 +1008,16 @@ private void setMostRecentPage(IndexType indexType, int index, RecordPage page) /** * Close a mostRecent page if it has been orphaned (evicted from cache). *

- * Pages still in cache will be closed by the cache eviction process. Orphaned pages (no longer in - * cache) need to have any remaining guards released and then be closed. + * Pages still in cache will be closed by the cache eviction process. + * Orphaned pages (no longer in cache) need to have any remaining guards released + * and then be closed. *

- * IMPORTANT: For orphaned pages (not in cache), any remaining guards must be from this transaction - * since: 1. Pages can only be evicted/replaced in cache if guardCount == 0 2. If a page has guards - * and is not in cache, those guards are from transactions that hold mostRecent references to a page - * instance that was replaced in cache 3. When this transaction closes, we must release our guard to - * allow the page to close + * IMPORTANT: For orphaned pages (not in cache), any remaining guards must be from + * this transaction since: + * 1. Pages can only be evicted/replaced in cache if guardCount == 0 + * 2. If a page has guards and is not in cache, those guards are from transactions + * that hold mostRecent references to a page instance that was replaced in cache + * 3. When this transaction closes, we must release our guard to allow the page to close * * @param recordPage the record page to potentially close */ @@ -1046,9 +1058,9 @@ private void closeMostRecentPageIfOrphaned(RecordPage recordPage) { /** * Load a page from the buffer manager's cache, or from storage if not cached. *

- * Uses atomic compute() to prevent race conditions between cache lookup and guard acquisition. - * Guards are acquired inside the compute block to ensure the page cannot be evicted before this - * transaction has protected it. + * Uses atomic compute() to prevent race conditions between cache lookup and + * guard acquisition. Guards are acquired inside the compute block to ensure + * the page cannot be evicted before this transaction has protected it. * * @param indexLogKey the index log key for lookup * @param pageReferenceToRecordPage reference to the page @@ -1057,8 +1069,8 @@ private void closeMostRecentPageIfOrphaned(RecordPage recordPage) { @Nullable private Page getFromBufferManager(@NonNull IndexLogKey indexLogKey, PageReference pageReferenceToRecordPage) { if (DEBUG_PATH_SUMMARY && indexLogKey.getIndexType() == IndexType.PATH_SUMMARY && LOGGER.isDebugEnabled()) { - LOGGER.debug("Path summary cache lookup: key={}, revision={}", pageReferenceToRecordPage.getKey(), - indexLogKey.getRevisionNumber()); + LOGGER.debug("Path summary cache lookup: key={}, revision={}", + pageReferenceToRecordPage.getKey(), indexLogKey.getRevisionNumber()); } final ResourceConfiguration config = resourceSession.getResourceConfig(); @@ -1067,8 +1079,8 @@ private Page getFromBufferManager(@NonNull IndexLogKey indexLogKey, PageReferenc // This saves 100% of the allocation and copy overhead for reads if (config.versioningType == VersioningType.FULL) { KeyValueLeafPage page = resourceBufferManager.getRecordPageCache() - .getOrLoadAndGuard(pageReferenceToRecordPage, - ref -> (KeyValueLeafPage) pageReader.read(ref, config)); + .getOrLoadAndGuard(pageReferenceToRecordPage, + ref -> (KeyValueLeafPage) pageReader.read(ref, config)); if (page != null) { pageReferenceToRecordPage.setPage(page); @@ -1079,10 +1091,9 @@ private Page getFromBufferManager(@NonNull IndexLogKey indexLogKey, PageReferenc } // Other versioning types: load fragments → combine → cache - KeyValueLeafPage page = - resourceBufferManager.getRecordPageCache() - .getOrLoadAndGuard(pageReferenceToRecordPage, - ref -> (KeyValueLeafPage) loadDataPageFromDurableStorageAndCombinePageFragments(ref)); + KeyValueLeafPage page = resourceBufferManager.getRecordPageCache() + .getOrLoadAndGuard(pageReferenceToRecordPage, + ref -> (KeyValueLeafPage) loadDataPageFromDurableStorageAndCombinePageFragments(ref)); if (page != null) { pageReferenceToRecordPage.setPage(page); @@ -1104,7 +1115,9 @@ private void setMostRecentlyReadRecordPage(@NonNull IndexLogKey indexLogKey, @No if (DEBUG_PATH_SUMMARY && recordPage != null) { LOGGER.debug( "[PATH_SUMMARY-REPLACE] Replacing old pathSummaryRecordPage: oldPageKey={}, newPageKey={}, trxIntentLog={}", - pathSummaryRecordPage.page.getPageKey(), recordPage.getPageKey(), (trxIntentLog != null)); + pathSummaryRecordPage.page.getPageKey(), + recordPage.getPageKey(), + (trxIntentLog != null)); } if (trxIntentLog == null) { @@ -1120,24 +1133,33 @@ private void setMostRecentlyReadRecordPage(@NonNull IndexLogKey indexLogKey, @No // But if bypass is disabled for testing, they might be cached // Remove from cache before closing to prevent "closed page in cache" errors pathSummaryRecordPage.pageReference.setPage(null); - // resourceBufferManager.getRecordPageCache().remove(pathSummaryRecordPage.pageReference); + //resourceBufferManager.getRecordPageCache().remove(pathSummaryRecordPage.pageReference); if (!pathSummaryRecordPage.page.isClosed()) { if (DEBUG_PATH_SUMMARY) { LOGGER.debug("[PATH_SUMMARY-REPLACE] -> Write trx: Closing bypassed page pageKey={}, revision={}", - pathSummaryRecordPage.page.getPageKey(), pathSummaryRecordPage.page.getRevision()); + pathSummaryRecordPage.page.getPageKey(), + pathSummaryRecordPage.page.getRevision()); } pathSummaryRecordPage.page.close(); } } } - pathSummaryRecordPage = new RecordPage(indexLogKey.getIndexNumber(), indexLogKey.getIndexType(), - indexLogKey.getRecordPageKey(), indexLogKey.getRevisionNumber(), pageReference, recordPage); + pathSummaryRecordPage = new RecordPage(indexLogKey.getIndexNumber(), + indexLogKey.getIndexType(), + indexLogKey.getRecordPageKey(), + indexLogKey.getRevisionNumber(), + pageReference, + recordPage); } else { // Set as most recent page for this type/index (auto-unpins previous) - var newRecordPage = new RecordPage(indexLogKey.getIndexNumber(), indexLogKey.getIndexType(), - indexLogKey.getRecordPageKey(), indexLogKey.getRevisionNumber(), pageReference, recordPage); + var newRecordPage = new RecordPage(indexLogKey.getIndexNumber(), + indexLogKey.getIndexType(), + indexLogKey.getRecordPageKey(), + indexLogKey.getRevisionNumber(), + pageReference, + recordPage); setMostRecentPage(indexLogKey.getIndexType(), indexLogKey.getIndexNumber(), newRecordPage); } } @@ -1145,9 +1167,9 @@ private void setMostRecentlyReadRecordPage(@NonNull IndexLogKey indexLogKey, @No /** * Load a page from storage and combine with historical fragments for versioning. *

- * This method handles the versioning reconstruction by loading the current page fragment and - * combining it with previous revisions according to the configured versioning strategy (e.g., - * incremental, differential, full). + * This method handles the versioning reconstruction by loading the current page + * fragment and combining it with previous revisions according to the configured + * versioning strategy (e.g., incremental, differential, full). * * @param pageReferenceToRecordPage reference to the page to load * @return the combined page, or null if no page exists at this reference @@ -1201,8 +1223,9 @@ private Page getInMemoryPageInstance(@NonNull IndexLogKey indexLogKey, if (trxIntentLog == null || indexLogKey.getIndexType() != IndexType.PATH_SUMMARY) { var kvLeafPage = ((KeyValueLeafPage) page); if (DEBUG_PATH_SUMMARY && indexLogKey.getIndexType() == IndexType.PATH_SUMMARY) { - LOGGER.debug("[PATH_SUMMARY-SWIZZLED] Found swizzled page: pageKey={}, revision={}", kvLeafPage.getPageKey(), - kvLeafPage.getRevision()); + LOGGER.debug("[PATH_SUMMARY-SWIZZLED] Found swizzled page: pageKey={}, revision={}", + kvLeafPage.getPageKey(), + kvLeafPage.getRevision()); } // Fast path: Use swizzled page directly if still valid @@ -1210,15 +1233,15 @@ private Page getInMemoryPageInstance(@NonNull IndexLogKey indexLogKey, // This prevents TOCTOU race where page is closed between check and guard acquisition closeCurrentPageGuard(); kvLeafPage.acquireGuard(); - + // Now check if page is still valid (not closed, memory not released) - if (!kvLeafPage.isClosed() && kvLeafPage.getSlotMemory() != null) { + if (!kvLeafPage.isClosed() && kvLeafPage.getSlottedPage() != null) { currentPageGuard = PageGuard.wrapAlreadyGuarded(kvLeafPage); } else { // Swizzled page was closed - release guard and reload kvLeafPage.releaseGuard(); - pageReferenceToRecordPage.setPage(null); // Clear stale swizzled reference - return null; // Signal caller to reload + pageReferenceToRecordPage.setPage(null); // Clear stale swizzled reference + return null; // Signal caller to reload } } return page; @@ -1240,19 +1263,18 @@ PageReference getLeafPageReference(final PageReference pageReferenceToSubtree, f } /** - * Result of loading page fragments, including pages, original fragment keys, and storage key for - * pages[0]. + * Result of loading page fragments, including pages, original fragment keys, and storage key for pages[0]. */ record PageFragmentsResult(List> pages, List originalKeys, - long storageKeyForFirstFragment) { + long storageKeyForFirstFragment) { } /** * Dereference key/value page reference and get all page fragments from revision-trees. *

- * For versioning systems (incremental, differential), a complete page may be composed of multiple - * fragments from different revisions. This method loads all required fragments and returns them in - * order for combining. + * For versioning systems (incremental, differential), a complete page may be composed + * of multiple fragments from different revisions. This method loads all required + * fragments and returns them in order for combining. * * @param pageReference reference pointing to the first (most recent) page fragment * @return result containing all page fragments and their original keys @@ -1265,23 +1287,31 @@ PageFragmentsResult getPageFragments(final PageReference pageReference) { // FULL versioning fast path: Page IS complete - use RecordPageCache directly // This bypasses the fragment cache since there are no fragments to combine if (config.versioningType == VersioningType.FULL) { - final var pageReferenceWithKey = - new PageReference().setKey(pageReference.getKey()).setDatabaseId(databaseId).setResourceId(resourceId); + final var pageReferenceWithKey = new PageReference() + .setKey(pageReference.getKey()) + .setDatabaseId(databaseId) + .setResourceId(resourceId); // Copy hash for checksum verification if (pageReference.getHash() != null) { pageReferenceWithKey.setHash(pageReference.getHash()); } KeyValueLeafPage page = resourceBufferManager.getRecordPageCache() - .getOrLoadAndGuard(pageReferenceWithKey, - key -> (KeyValueLeafPage) pageReader.read(key, config)); + .getOrLoadAndGuard(pageReferenceWithKey, + key -> (KeyValueLeafPage) pageReader.read(key, config)); if (page != null && !page.isClosed()) { - return new PageFragmentsResult(java.util.Collections.singletonList(page), java.util.Collections.emptyList(), - pageReference.getKey()); + return new PageFragmentsResult( + java.util.Collections.singletonList(page), + java.util.Collections.emptyList(), + pageReference.getKey() + ); } - return new PageFragmentsResult(java.util.Collections.emptyList(), java.util.Collections.emptyList(), - pageReference.getKey()); + return new PageFragmentsResult( + java.util.Collections.emptyList(), + java.util.Collections.emptyList(), + pageReference.getKey() + ); } // Other versioning types: load fragments from RecordPageFragmentCache @@ -1300,10 +1330,9 @@ PageFragmentsResult getPageFragments(final PageReference pageReference) { } // Load first fragment atomically with guard - KeyValueLeafPage page = - resourceBufferManager.getRecordPageFragmentCache() - .getOrLoadAndGuard(pageReferenceWithKey, - key -> (KeyValueLeafPage) pageReader.read(key, resourceSession.getResourceConfig())); + KeyValueLeafPage page = resourceBufferManager.getRecordPageFragmentCache() + .getOrLoadAndGuard(pageReferenceWithKey, + key -> (KeyValueLeafPage) pageReader.read(key, resourceSession.getResourceConfig())); assert page != null && !page.isClosed(); pages.add(page); @@ -1338,28 +1367,27 @@ private CompletableFuture> readPage(final PageFragmentK KeyValueLeafPage pageFromCache = resourceBufferManager.getRecordPageFragmentCache().getAndGuard(pageReference); if (pageFromCache != null) { - assert pageFragmentKey.revision() == pageFromCache.getRevision() - : "Revision mismatch: key=" + pageFragmentKey.revision() + ", page=" + pageFromCache.getRevision(); + assert pageFragmentKey.revision() == pageFromCache.getRevision() : + "Revision mismatch: key=" + pageFragmentKey.revision() + ", page=" + pageFromCache.getRevision(); return CompletableFuture.completedFuture(pageFromCache); } // Cache miss - load async, then cache with atomic guard acquisition final var reader = resourceSession.createReader(); - return reader.readAsync(pageReference, resourceSession.getResourceConfig()).thenApply(loadedPage -> { - reader.close(); - assert pageFragmentKey.revision() == ((KeyValuePage) loadedPage).getRevision() - : "Revision mismatch: key=" + pageFragmentKey.revision() + ", page=" - + ((KeyValuePage) loadedPage).getRevision(); - - // Atomic cache-or-store with guard (handles race with other threads) - // If another thread cached the page first, returns that cached page with guard. - // Otherwise caches our loaded page with guard. - KeyValueLeafPage cachedPage = - resourceBufferManager.getRecordPageFragmentCache() - .getOrLoadAndGuard(pageReference, _ -> (KeyValueLeafPage) loadedPage); - - return (KeyValuePage) cachedPage; - }); + return reader.readAsync(pageReference, resourceSession.getResourceConfig()) + .thenApply(loadedPage -> { + reader.close(); + assert pageFragmentKey.revision() == ((KeyValuePage) loadedPage).getRevision() : + "Revision mismatch: key=" + pageFragmentKey.revision() + ", page=" + ((KeyValuePage) loadedPage).getRevision(); + + // Atomic cache-or-store with guard (handles race with other threads) + // If another thread cached the page first, returns that cached page with guard. + // Otherwise caches our loaded page with guard. + KeyValueLeafPage cachedPage = resourceBufferManager.getRecordPageFragmentCache() + .getOrLoadAndGuard(pageReference, _ -> (KeyValueLeafPage) loadedPage); + + return (KeyValuePage) cachedPage; + }); } static CompletableFuture> sequence(List> listOfCompletableFutures) { @@ -1374,8 +1402,8 @@ static CompletableFuture> sequence(List> listOf * nodes, Path index nodes or Name index nodes). * * @param revisionRoot {@link RevisionRootPage} instance - * @param indexType the index type - * @param index the index to use + * @param indexType the index type + * @param index the index to use */ PageReference getPageReference(final RevisionRootPage revisionRoot, final IndexType indexType, final int index) { assert revisionRoot != null; @@ -1390,7 +1418,7 @@ PageReference getPageReference(final RevisionRootPage revisionRoot, final IndexT case NAME -> getNamePage(revisionRoot).getIndirectPageReference(index); case PATH_SUMMARY -> getPathSummaryPage(revisionRoot).getIndirectPageReference(index); default -> - throw new IllegalStateException("Only defined for node, path summary, text value and attribute value pages!"); + throw new IllegalStateException("Only defined for node, path summary, text value and attribute value pages!"); }; } @@ -1399,7 +1427,7 @@ PageReference getPageReference(final RevisionRootPage revisionRoot, final IndexT * * @param reference reference to dereference * @return dereferenced page - * @throws SirixIOException if something odd happens within the creation process + * @throws SirixIOException if something odd happens within the creation process * @throws NullPointerException if {@code reference} is {@code null} */ @Override @@ -1411,7 +1439,7 @@ public IndirectPage dereferenceIndirectPageReference(final PageReference referen * Find reference pointing to leaf page of an indirect tree. * * @param startReference start reference pointing to the indirect tree - * @param pageKey key to look up in the indirect tree + * @param pageKey key to look up in the indirect tree * @return reference denoted by key pointing to the leaf page * @throws SirixIOException if an I/O error occurs */ @@ -1429,8 +1457,8 @@ public PageReference getReferenceToLeafOfSubtree(final PageReference startRefere final int maxHeight = getCurrentMaxIndirectPageTreeLevel(indexType, indexNumber, revisionRootPage); // Iterate through all levels. - for (int level = inpLevelPageCountExp.length - maxHeight, - height = inpLevelPageCountExp.length; level < height; level++) { + for (int level = inpLevelPageCountExp.length - maxHeight, height = inpLevelPageCountExp.length; + level < height; level++) { final Page derefPage = dereferenceIndirectPageReference(reference); if (derefPage == null) { reference = null; @@ -1467,9 +1495,7 @@ public long pageKey(@NonNegative final long recordKey, @NonNull final IndexType public int getCurrentMaxIndirectPageTreeLevel(final IndexType indexType, final int index, final RevisionRootPage revisionRootPage) { final int maxLevel; - final RevisionRootPage currentRevisionRootPage = revisionRootPage == null - ? rootPage - : revisionRootPage; + final RevisionRootPage currentRevisionRootPage = revisionRootPage == null ? rootPage : revisionRootPage; // $CASES-OMITTED$ maxLevel = switch (indexType) { @@ -1504,8 +1530,8 @@ public String toString() { } /** - * Close the current page guard if one is active. Should be called before fetching a different page - * or when transaction closes. + * Close the current page guard if one is active. + * Should be called before fetching a different page or when transaction closes. *

* Package-private to allow NodeStorageEngineWriter to release guards before TIL operations. */ @@ -1522,15 +1548,13 @@ void closeCurrentPageGuard() { } /** - * Get the page that the current page guard is protecting. Used by NodeStorageEngineWriter for - * acquiring additional guards on the current page. + * Get the page that the current page guard is protecting. + * Used by NodeStorageEngineWriter for acquiring additional guards on the current page. * * @return the current page, or null if no page is currently guarded */ public KeyValueLeafPage getCurrentPage() { - return currentPageGuard != null - ? currentPageGuard.page() - : null; + return currentPageGuard != null ? currentPageGuard.page() : null; } @Override @@ -1621,13 +1645,13 @@ public CommitCredentials getCommitCredentials() { @Override public @Nullable HOTLeafPage getHOTLeafPage(@NonNull IndexType indexType, int indexNumber) { assertNotClosed(); - + // CRITICAL: Use getActualRevisionRootPage() to get the current revision root, // which for write transactions is the NEW revision root page where HOT pages are stored. // Using the old 'rootPage' field would fail for write transactions because the HOT // pages are stored against the new revision's PathPage/CASPage/NamePage references. final RevisionRootPage actualRootPage = getActualRevisionRootPage(); - + // Get the root reference for the index final PageReference rootRef = switch (indexType) { case PATH -> { @@ -1653,11 +1677,11 @@ public CommitCredentials getCommitCredentials() { } default -> null; }; - + if (rootRef == null) { return null; } - + // FIRST: Check transaction log for uncommitted pages (write transactions) // This must be checked before anything else since uncommitted pages won't // be on the reference or in the buffer cache @@ -1675,23 +1699,23 @@ public CommitCredentials getCommitCredentials() { } } } - + // Check if page is swizzled (directly on reference) if (rootRef.getPage() instanceof HOTLeafPage hotLeaf) { return hotLeaf; } - + // For uncommitted pages with no log key, we're done if (rootRef.getKey() < 0 && rootRef.getLogKey() < 0) { return null; } - + // Try to get from buffer cache Page cachedPage = resourceBufferManager.getRecordPageCache().get(rootRef); if (cachedPage instanceof HOTLeafPage hotLeaf) { return hotLeaf; } - + // Load from storage with proper versioning fragment combining if (rootRef.getKey() >= 0) { try { @@ -1701,17 +1725,15 @@ public CommitCredentials getCommitCredentials() { return null; } } - + return null; } - + /** * Load a HOTLeafPage from storage with proper versioning fragment combining. * - *

- * For FULL versioning, loads a single complete page. For INCREMENTAL/DIFFERENTIAL/SLIDING_SNAPSHOT, - * loads all fragments and combines them. - *

+ *

For FULL versioning, loads a single complete page. + * For INCREMENTAL/DIFFERENTIAL/SLIDING_SNAPSHOT, loads all fragments and combines them.

* * @param pageRef the page reference * @return the combined HOTLeafPage, or null if not found @@ -1719,29 +1741,29 @@ public CommitCredentials getCommitCredentials() { private @Nullable HOTLeafPage loadHOTLeafPageWithVersioning(PageReference pageRef) { final VersioningType versioningType = resourceConfig.versioningType; final int revsToRestore = resourceConfig.maxNumberOfRevisionsToRestore; - + // FULL versioning fast path: Page on disk IS complete - load directly if (versioningType == VersioningType.FULL) { Page loadedPage = pageReader.read(pageRef, resourceConfig); - if (loadedPage instanceof HOTLeafPage hotLeaf) { + if (loadedPage instanceof HOTLeafPage hotLeaf) { pageRef.setPage(hotLeaf); - return hotLeaf; + return hotLeaf; } return null; } - + // Other versioning types: load fragments and combine List fragments = loadHOTPageFragments(pageRef); if (fragments.isEmpty()) { return null; } - + // Combine fragments using VersioningType HOTLeafPage combinedPage = versioningType.combineHOTLeafPages(fragments, revsToRestore, this); pageRef.setPage(combinedPage); return combinedPage; } - + /** * Load all HOTLeafPage fragments for versioning reconstruction. * @@ -1750,44 +1772,46 @@ public CommitCredentials getCommitCredentials() { */ private List loadHOTPageFragments(PageReference pageRef) { final List fragments = new ArrayList<>(); - + // Load the first (most recent) fragment Page firstPage = pageReader.read(pageRef, resourceConfig); if (!(firstPage instanceof HOTLeafPage hotLeaf)) { return fragments; } fragments.add(hotLeaf); - + // Check if this is already a complete page (for FULL versioning) or no fragments List pageFragments = pageRef.getPageFragments(); if (pageFragments.isEmpty()) { return fragments; } - + // Load additional fragments from the versioning chain // Note: Fragment keys don't include hashes - only the first fragment can be verified // Future improvement: Store fragment hashes in PageFragmentKey for complete verification for (PageFragmentKey fragmentKey : pageFragments) { - PageReference fragmentRef = - new PageReference().setKey(fragmentKey.key()).setDatabaseId(databaseId).setResourceId(resourceId); - + PageReference fragmentRef = new PageReference() + .setKey(fragmentKey.key()) + .setDatabaseId(databaseId) + .setResourceId(resourceId); + Page fragmentPage = pageReader.read(fragmentRef, resourceConfig); if (fragmentPage instanceof HOTLeafPage hotFragment) { fragments.add(hotFragment); } } - + return fragments; } - + @Override public @Nullable Page loadHOTPage(@NonNull PageReference reference) { assertNotClosed(); - + if (reference == null) { return null; } - + // FIRST: Check transaction log for uncommitted pages (write transactions) if (trxIntentLog != null) { final PageContainer container = trxIntentLog.get(reference); @@ -1803,24 +1827,24 @@ private List loadHOTPageFragments(PageReference pageRef) { } } } - + // Check if page is swizzled (directly on reference) Page swizzled = reference.getPage(); if (swizzled instanceof HOTLeafPage || swizzled instanceof HOTIndirectPage) { return swizzled; } - + // For uncommitted pages with no log key, we're done if (reference.getKey() < 0 && reference.getLogKey() < 0) { return null; } - + // Try to get from buffer cache Page cachedPage = resourceBufferManager.getRecordPageCache().get(reference); if (cachedPage instanceof HOTLeafPage || cachedPage instanceof HOTIndirectPage) { return cachedPage; } - + // Load from storage (only if key >= 0) if (reference.getKey() >= 0) { try { @@ -1832,7 +1856,7 @@ private List loadHOTPageFragments(PageReference pageRef) { reference.setPage(loadedPage); return loadedPage; } - + if (loadedPage instanceof HOTLeafPage) { // HOTLeafPage needs proper versioning fragment combining HOTLeafPage combinedPage = loadHOTLeafPageWithVersioning(reference); @@ -1843,7 +1867,7 @@ private List loadHOTPageFragments(PageReference pageRef) { return null; } } - + return null; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineWriter.java b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineWriter.java index c70cff98d..0807d930c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/access/trx/page/NodeStorageEngineWriter.java @@ -21,7 +21,6 @@ package io.sirix.access.trx.page; -import io.brackit.query.atomic.QNm; import io.sirix.access.ResourceConfiguration; import io.sirix.access.User; import io.sirix.access.trx.node.CommitCredentials; @@ -39,48 +38,24 @@ import io.sirix.exception.SirixIOException; import io.sirix.io.SerializationBufferPool; import io.sirix.node.PooledBytesOut; -import io.sirix.node.MemorySegmentBytesIn; -import io.sirix.node.MemorySegmentBytesOut; import io.sirix.utils.OS; import io.sirix.index.IndexType; import io.sirix.io.Writer; import io.sirix.node.DeletedNode; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; -import io.sirix.node.json.ArrayNode; -import io.sirix.node.json.BooleanNode; -import io.sirix.node.json.JsonDocumentRootNode; -import io.sirix.node.json.NullNode; -import io.sirix.node.json.NumberNode; -import io.sirix.node.json.ObjectBooleanNode; -import io.sirix.node.json.ObjectKeyNode; -import io.sirix.node.json.ObjectNode; -import io.sirix.node.json.ObjectNullNode; -import io.sirix.node.json.ObjectNumberNode; -import io.sirix.node.json.ObjectStringNode; -import io.sirix.node.json.StringNode; -import io.sirix.node.xml.AttributeNode; -import io.sirix.node.xml.CommentNode; -import io.sirix.node.xml.NamespaceNode; -import io.sirix.node.xml.PINode; -import io.sirix.node.xml.TextNode; -import io.sirix.node.xml.ElementNode; -import io.sirix.node.xml.XmlDocumentRootNode; -import io.sirix.node.layout.FixedSlotRecordMaterializer; -import io.sirix.node.layout.FixedSlotRecordProjector; -import io.sirix.node.layout.NodeKindLayout; import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.interfaces.DataRecord; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.page.CASPage; import io.sirix.page.DeweyIDPage; import io.sirix.page.HOTIndirectPage; import io.sirix.page.HOTLeafPage; import io.sirix.page.IndirectPage; import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.PageLayout; import io.sirix.page.NamePage; -import io.sirix.page.PageConstants; import io.sirix.page.PageKind; import io.sirix.page.PageReference; import io.sirix.page.PathPage; @@ -91,7 +66,6 @@ import io.sirix.page.interfaces.KeyValuePage; import io.sirix.page.interfaces.Page; import io.sirix.settings.Constants; -import io.sirix.settings.DiagnosticSettings; import io.sirix.settings.Fixed; import io.sirix.settings.VersioningType; import io.sirix.node.BytesOut; @@ -102,9 +76,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.foreign.MemorySegment; import java.io.IOException; import java.io.OutputStream; -import java.lang.foreign.MemorySegment; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -134,17 +108,7 @@ final class NodeStorageEngineWriter extends AbstractForwardingStorageEngineReade private static final Logger LOGGER = LoggerFactory.getLogger(NodeStorageEngineWriter.class); - /** - * Debug flag for memory leak tracking. - * - * @see DiagnosticSettings#MEMORY_LEAK_TRACKING - */ - private static final boolean DEBUG_MEMORY_LEAKS = DiagnosticSettings.MEMORY_LEAK_TRACKING; - private BytesOut bufferBytes = Bytes.elasticOffHeapByteBuffer(Writer.FLUSH_SIZE); - private MemorySegmentBytesOut demotionBuffer; - private MemorySegment fixedSlotProjectionBuffer; - /** * Page writer to serialize. @@ -232,29 +196,14 @@ private record IndexLogKeyToPageContainer(IndexType indexType, long recordPageKe */ private IndexLogKeyToPageContainer mostRecentPathSummaryPageContainer; - // Write-path singletons — one per XML/JSON node kind (lazily allocated) - private ObjectNode writeObjectSingleton; - private ArrayNode writeArraySingleton; - private ObjectKeyNode writeObjectKeySingleton; - private StringNode writeStringSingleton; - private NumberNode writeNumberSingleton; - private BooleanNode writeBooleanSingleton; - private NullNode writeNullSingleton; - private ObjectStringNode writeObjectStringSingleton; - private ObjectNumberNode writeObjectNumberSingleton; - private ObjectBooleanNode writeObjectBooleanSingleton; - private ObjectNullNode writeObjectNullSingleton; - private JsonDocumentRootNode writeJsonDocumentSingleton; - private XmlDocumentRootNode writeXmlDocumentSingleton; - private ElementNode writeElementSingleton; - private TextNode writeTextSingleton; - private CommentNode writeCommentSingleton; - private AttributeNode writeAttributeSingleton; - private NamespaceNode writeNamespaceSingleton; - private PINode writePISingleton; - private final LinkedHashMap pageContainerCache; + /** + * Optional binder for write-path singletons. + * When set, prepareRecordForModification uses factory singletons instead of allocating new nodes. + */ + private WriteSingletonBinder writeSingletonBinder; + /** * Constructor. * @@ -343,6 +292,11 @@ public HOTTrieWriter getHOTTrieWriter() { return hotTrieWriter; } + @Override + public void setWriteSingletonBinder(final WriteSingletonBinder binder) { + this.writeSingletonBinder = binder; + } + @Override public BytesOut newBufferedBytesInstance() { bufferBytes = Bytes.elasticOffHeapByteBuffer(Writer.FLUSH_SIZE); @@ -385,31 +339,18 @@ public DataRecord prepareRecordForModification(final long recordKey, @NonNull fi return record; } - // Try fixed-slot singleton path — zero allocation hot path. - // The write singleton is populated from fixed-slot bytes (authoritative state). - // Callers must use the returned object immediately, mutate, and persist via - // updateRecordSlot before the next prepareRecordForModification call for the - // same node kind (use-persist-discard rule). - if (modifiedPage instanceof KeyValueLeafPage modifiedLeafPage && modifiedLeafPage.isFixedSlotFormat(recordOffset)) { - final NodeKind nodeKind = modifiedLeafPage.getFixedSlotNodeKind(recordOffset); - if (nodeKind != null) { - final long dataOffset = modifiedLeafPage.getSlotDataOffset(recordOffset); - if (dataOffset >= 0) { - final DataRecord singleton = getOrCreateWriteSingleton(nodeKind); - if (singleton != null) { - final int dataLength = modifiedLeafPage.getSlotDataLength(recordOffset); - final byte[] deweyIdBytes = modifiedLeafPage.getDeweyIdAsByteArray(recordOffset); - if (FixedSlotRecordMaterializer.populateExisting(singleton, nodeKind, recordKey, - modifiedLeafPage.getSlotMemory(), dataOffset, dataLength, deweyIdBytes, - storageEngineReader.getResourceSession().getResourceConfig())) { - return singleton; // Zero-allocation hot path - } - } - } + // Zero-allocation fast path: bind write singleton to modified page's slotted page. + // Write singletons are NOT stored in records[], so this path is hit on every access + // to a previously created/modified record. The bind is cheap (4 field assignments). + if (writeSingletonBinder != null && modifiedPage instanceof KeyValueLeafPage kvl + && kvl.hasSlottedPageSlot(recordKey)) { + record = writeSingletonBinder.bind(kvl, recordOffset, recordKey); + if (record != null) { + return record; } } - // Try compact-format / records[] on the modified page. + // Try deserialization from modified page's slotted page (non-singleton path). record = storageEngineReader.getValue(modifiedPage, recordKey); if (record != null) { modifiedPage.setRecord(record); @@ -417,132 +358,81 @@ record = storageEngineReader.getValue(modifiedPage, recordKey); } // Fall back to complete (on-disk) page. - final DataRecord oldRecord = storageEngineReader.getValue(cont.getCompleteAsKeyValuePage(), recordKey); + final var completePage = cont.getCompleteAsKeyValuePage(); + + // Zero-copy path: copy raw slot bytes from complete page to modified page, then bind. + if (writeSingletonBinder != null && modifiedPage instanceof KeyValueLeafPage kvlMod + && completePage instanceof KeyValueLeafPage kvlComplete) { + final MemorySegment srcPage = kvlComplete.getSlottedPage(); + if (srcPage != null && PageLayout.isSlotPopulated(srcPage, recordOffset) + && PageLayout.getDirNodeKindId(srcPage, recordOffset) > 0) { + kvlMod.copySlotFromPage(kvlComplete, recordOffset); + record = writeSingletonBinder.bind(kvlMod, recordOffset, recordKey); + if (record != null) { + return record; + } + } + } + + // Fallback for non-binder, binder returned null, or non-FlyweightNode (nodeKindId=0) + final DataRecord oldRecord = storageEngineReader.getValue(completePage, recordKey); if (oldRecord == null) { + final int offset = StorageEngineReader.recordPageOffset(recordKey); + final var kvlComplete = (KeyValueLeafPage) completePage; + final var slottedPage = kvlComplete.getSlottedPage(); + final boolean slotPopulated = slottedPage != null + && PageLayout.isSlotPopulated(slottedPage, offset); + final var slotData = completePage.getSlot(offset); + final int populatedCount = slottedPage != null + ? PageLayout.getPopulatedCount(slottedPage) : -1; throw new SirixIOException("Cannot retrieve record from cache: (key: " + recordKey + ") (indexType: " + indexType - + ") (index: " + index + ")"); + + ") (index: " + index + ") (slotPopulated: " + slotPopulated + + ") (populatedCount: " + populatedCount + + ") (slotData: " + (slotData != null ? slotData.byteSize() + " bytes" : "null") + + ") (completePage.pageKey: " + completePage.getPageKey() + + ") (completePage.revision: " + completePage.getRevision() + + ") (modifiedPage.pageKey: " + modifiedPage.getPageKey() + ")"); } record = oldRecord; - // Project disk-read records to fixed-slot format if eligible. - // Fixed-slot bytes are now authoritative. Don't cache in records[] — - // next access will use write singleton via fixed-slot path. - if (modifiedPage instanceof KeyValueLeafPage modifiedLeafPage && indexType == IndexType.DOCUMENT) { - final ResourceConfiguration resourceConfiguration = storageEngineReader.getResourceSession().getResourceConfig(); - if (tryProjectRecordIntoFixedSlot(record, modifiedLeafPage, resourceConfiguration)) { - return record; - } + // Unbind flyweight from complete page — ensures mutations go to Java fields, + // not the old revision's MemorySegment. setRecord will re-serialize to modified page. + if (record instanceof FlyweightNode fn && fn.isBound()) { + fn.unbind(); } - // Non-fixed-slot: cache in records[] for stable reference identity. + modifiedPage.setRecord(record); return record; } - @Override - public void updateRecordSlot(@NonNull final DataRecord record, @NonNull final IndexType indexType, final int index) { - storageEngineReader.assertNotClosed(); - requireNonNull(record); - requireNonNull(indexType); - checkArgument(record.getNodeKey() >= 0, "recordKey must be >= 0!"); - - final long recordKey = record.getNodeKey(); - final long recordPageKey = storageEngineReader.pageKey(recordKey, indexType); - final PageContainer container = prepareRecordPage(recordPageKey, index, indexType); - final KeyValuePage modifiedPage = container.getModifiedAsKeyValuePage(); - - if (!(modifiedPage instanceof KeyValueLeafPage modifiedLeafPage)) { - modifiedPage.setRecord(record); - return; - } + // ==================== DIRECT-TO-HEAP CREATION ==================== - final ResourceConfiguration resourceConfiguration = storageEngineReader.getResourceSession().getResourceConfig(); - if (tryProjectRecordIntoFixedSlot(record, modifiedLeafPage, resourceConfiguration)) { - // Fixed-slot projection succeeded — fixed-slot bytes are the authoritative - // in-memory representation. No records[] entry needed; compactFixedSlotsForCommit - // handles serialization at commit time. - return; - } + /** Reusable allocation result — zero-alloc on hot path. */ + private KeyValueLeafPage allocKvl; + private int allocSlotOffset; + private long allocNodeKey; - // Non-fixed-slot records (payload-bearing kinds like StringNode/NumberNode): - // store in records[] for commit-time serialization via processEntries(). - modifiedPage.setRecord(record); + @Override + public void allocateForDocumentCreation() { + storageEngineReader.assertNotClosed(); + final long nodeKey = newRevisionRootPage.incrementAndGetMaxNodeKeyInDocumentIndex(); + final long recordPageKey = storageEngineReader.pageKey(nodeKey, IndexType.DOCUMENT); + final PageContainer cont = prepareRecordPage(recordPageKey, -1, IndexType.DOCUMENT); + this.allocKvl = (KeyValueLeafPage) cont.getModifiedAsKeyValuePage(); + this.allocSlotOffset = (int) (nodeKey + - ((nodeKey >> Constants.NDP_NODE_COUNT_EXPONENT) << Constants.NDP_NODE_COUNT_EXPONENT)); + this.allocNodeKey = nodeKey; } - private boolean tryProjectRecordIntoFixedSlot(final DataRecord record, final KeyValueLeafPage modifiedLeafPage, - final ResourceConfiguration resourceConfiguration) { - if (!(record.getKind() instanceof NodeKind nodeKind)) { - return false; - } - - final NodeKindLayout layout = nodeKind.layoutDescriptor(); - if (!layout.isFixedSlotSupported()) { - return false; - } - - // Only VALUE_BLOB payload refs are supported for inline projection. - if (!layout.hasSupportedPayloads()) { - return false; - } - - final int fixedSlotSize = layout.fixedSlotSizeInBytes(); - if (fixedSlotSize <= 0) { - return false; - } - - // Compute inline payload length for payload-bearing nodes (strings, numbers). - final int inlinePayloadLength = FixedSlotRecordProjector.computeInlinePayloadLength(record, layout); - if (inlinePayloadLength < 0) { - return false; - } - - final int totalSlotSize = fixedSlotSize + inlinePayloadLength; - if (totalSlotSize > PageConstants.MAX_RECORD_SIZE) { - return false; - } - - final int recordOffset = StorageEngineReader.recordPageOffset(record.getNodeKey()); - long dataOffset = modifiedLeafPage.getSlotDataOffset(recordOffset); - int dataLength = (dataOffset >= 0) - ? modifiedLeafPage.getSlotDataLength(recordOffset) - : -1; - if (dataOffset < 0 || dataLength != totalSlotSize) { - final MemorySegment projectionBuffer = ensureFixedSlotProjectionBuffer(totalSlotSize); - modifiedLeafPage.setSlot(projectionBuffer.asSlice(0, totalSlotSize), recordOffset); - dataOffset = modifiedLeafPage.getSlotDataOffset(recordOffset); - dataLength = (dataOffset >= 0) - ? modifiedLeafPage.getSlotDataLength(recordOffset) - : -1; - if (dataOffset < 0 || dataLength != totalSlotSize) { - return false; - } - } - - if (!FixedSlotRecordProjector.project(record, layout, modifiedLeafPage.getSlotMemory(), dataOffset)) { - return false; - } - modifiedLeafPage.markSlotAsFixedFormat(recordOffset, nodeKind); - - if (resourceConfiguration.areDeweyIDsStored) { - final byte[] deweyId = record.getDeweyIDAsBytes(); - if (deweyId != null && deweyId.length > 0) { - modifiedLeafPage.setDeweyId(deweyId, recordOffset); - } - } + @Override + public KeyValueLeafPage getAllocKvl() { return allocKvl; } - return true; - } + @Override + public int getAllocSlotOffset() { return allocSlotOffset; } - private MemorySegment ensureFixedSlotProjectionBuffer(final int requiredSize) { - if (fixedSlotProjectionBuffer == null || fixedSlotProjectionBuffer.byteSize() < requiredSize) { - int newSize = 256; - while (newSize < requiredSize) { - newSize <<= 1; - } - fixedSlotProjectionBuffer = MemorySegment.ofArray(new byte[newSize]); - } - return fixedSlotProjectionBuffer; - } + @Override + public long getAllocNodeKey() { return allocNodeKey; } @Override public DataRecord createRecord(@NonNull final DataRecord record, @NonNull final IndexType indexType, @@ -595,69 +485,35 @@ public DataRecord createRecord(@NonNull final DataRecord record, @NonNull final final long recordPageKey = storageEngineReader.pageKey(createdRecordKey, indexType); final PageContainer cont = prepareRecordPage(recordPageKey, index, indexType); final KeyValuePage modified = cont.getModifiedAsKeyValuePage(); - final boolean storedAsReusableProxy = - persistReusableProxyRecordIfSupported(record, createdRecordKey, indexType, modified); - if (!storedAsReusableProxy) { - modified.setRecord(record); - if (modified instanceof KeyValueLeafPage modifiedLeafPage && modifiedLeafPage.shouldDemoteRecords(indexType)) { - if (demotionBuffer == null) { - demotionBuffer = new MemorySegmentBytesOut(256); - } - modifiedLeafPage.demoteRecordsToSlots(storageEngineReader.getResourceSession().getResourceConfig(), demotionBuffer); + if (modified instanceof KeyValueLeafPage kvl) { + if (record instanceof FlyweightNode fn) { + final int offset = (int) (createdRecordKey + - ((createdRecordKey >> Constants.NDP_NODE_COUNT_EXPONENT) + << Constants.NDP_NODE_COUNT_EXPONENT)); + kvl.serializeNewRecord(fn, createdRecordKey, offset); + } else { + kvl.setNewRecord(record); } + } else { + modified.setRecord(record); } return record; } - private boolean persistReusableProxyRecordIfSupported(final DataRecord record, final long createdRecordKey, - @NonNull final IndexType indexType, final KeyValuePage modified) { - if (indexType != IndexType.DOCUMENT || !(record instanceof ReusableNodeProxy) - || !(modified instanceof KeyValueLeafPage modifiedLeafPage) || record.getNodeKey() != createdRecordKey) { - return false; - } - - if (demotionBuffer == null) { - demotionBuffer = new MemorySegmentBytesOut(256); - } - demotionBuffer.clear(); - - final ResourceConfiguration resourceConfiguration = storageEngineReader.getResourceSession().getResourceConfig(); - if (tryProjectRecordIntoFixedSlot(record, modifiedLeafPage, resourceConfiguration)) { - return true; - } - - resourceConfiguration.recordPersister.serialize(demotionBuffer, record, resourceConfiguration); - final var serializedRecord = demotionBuffer.getDestination(); - - if (serializedRecord.byteSize() > PageConstants.MAX_RECORD_SIZE) { - final DataRecord detachedRecord = materializeDetachedRecord(serializedRecord, record, resourceConfiguration); - modified.setRecord(detachedRecord); - if (modifiedLeafPage.shouldDemoteRecords(indexType)) { - modifiedLeafPage.demoteRecordsToSlots(resourceConfiguration, demotionBuffer); - } - return true; - } - - final int recordOffset = StorageEngineReader.recordPageOffset(record.getNodeKey()); - modifiedLeafPage.setSlot(serializedRecord, recordOffset); - modifiedLeafPage.markSlotAsCompactFormat(recordOffset); - - if (resourceConfiguration.areDeweyIDsStored) { - final byte[] deweyId = record.getDeweyIDAsBytes(); - if (deweyId != null && deweyId.length > 0) { - modifiedLeafPage.setDeweyId(deweyId, recordOffset); - } + @Override + public void persistRecord(@NonNull final DataRecord record, @NonNull final IndexType indexType, final int index) { + if (record instanceof FlyweightNode fn && fn.isWriteSingleton() && fn.getOwnerPage() != null) { + return; // Bound write singleton — mutations already on heap } + storageEngineReader.assertNotClosed(); + requireNonNull(record); + requireNonNull(indexType); - return true; - } - - private DataRecord materializeDetachedRecord(final java.lang.foreign.MemorySegment serializedRecord, - final DataRecord sourceRecord, final ResourceConfiguration resourceConfiguration) { - return resourceConfiguration.recordPersister.deserialize(new MemorySegmentBytesIn(serializedRecord), - sourceRecord.getNodeKey(), sourceRecord.getDeweyIDAsBytes(), resourceConfiguration); + final long recordPageKey = storageEngineReader.pageKey(record.getNodeKey(), indexType); + final PageContainer cont = prepareRecordPage(recordPageKey, index, indexType); + cont.getModifiedAsKeyValuePage().setRecord(record); } @Override @@ -691,7 +547,17 @@ public V getRecord(final long recordKey, @NonNull final I final PageContainer pageCont = getPageContainer(recordPageKey, index, indexType); if (pageCont == null) { - return storageEngineReader.getRecord(recordKey, indexType, index); + // Fallback to underlying reader. The reader may return a FlyweightNode bound to a page + // whose MemorySegment lifecycle is managed by the reader (guard-based eviction). + // Since the writer cannot hold the reader's page guard, the segment may be freed and + // reused at any time (e.g., by the clock sweeper between successive reader calls). + // Unbinding materializes all fields to Java primitives, making the node independent + // of the page segment and preventing use-after-free. + final V record = storageEngineReader.getRecord(recordKey, indexType, index); + if (record instanceof FlyweightNode fn && fn.isBound()) { + fn.unbind(); + } + return record; } else { DataRecord node = getRecordForWriteAccess(((KeyValueLeafPage) pageCont.getModified()), recordKey); if (node == null) { @@ -709,29 +575,6 @@ private DataRecord getRecordForWriteAccess(final KeyValuePage 0 check) storageEngineReader.closeCurrentPageGuard(); - if (DEBUG_MEMORY_LEAKS) { - LOGGER.debug("[WRITER-COMMIT] Clearing TIL with {} entries before commit", log.size()); - } - // Clear TransactionIntentLog - closes all modified pages log.clear(); @@ -980,10 +809,6 @@ private void setUserIfPresent() { public UberPage rollback() { storageEngineReader.assertNotClosed(); - if (DEBUG_MEMORY_LEAKS) { - LOGGER.debug("[WRITER-ROLLBACK] Rolling back transaction with {} TIL entries", log.size()); - } - // CRITICAL: Release current page guard BEFORE TIL.clear() // If guard is on a TIL page, the page won't close (guardCount > 0 check) storageEngineReader.closeCurrentPageGuard(); @@ -1016,12 +841,6 @@ public void close() { pendingFsync = null; } - // Release the demotion buffer's off-heap memory. - if (demotionBuffer != null) { - demotionBuffer.close(); - demotionBuffer = null; - } - // Don't clear the cached containers here - they've either been: // 1. Already cleared and returned to pool during commit(), or // 2. Will be cleared and returned to pool by log.close() below @@ -1230,11 +1049,6 @@ private PageContainer prepareRecordPageViaKeyedTrie(final @NonNegative long reco false // Memory from allocator - release on close() ); - if (DEBUG_MEMORY_LEAKS && recordPageKey == 0) { - LOGGER.debug("[WRITER-CREATE] Created Page 0 pair: indexType={}, rev={}, complete={}, modify={}", indexType, - storageEngineReader.getRevisionNumber(), System.identityHashCode(completePage), System.identityHashCode(modifyPage)); - } - pageContainer = PageContainer.getInstance(completePage, modifyPage); appendLogRecord(reference, pageContainer); return pageContainer; @@ -1480,6 +1294,19 @@ public StorageEngineReader getStorageEngineReader() { return storageEngineReader; } + @Override + public KeyValueLeafPage getModifiedPageForRead(final long recordPageKey, + @NonNull final IndexType indexType, final int index) { + final PageContainer pc = getPageContainer(recordPageKey, index, indexType); + if (pc != null) { + final var modified = pc.getModified(); + if (modified instanceof KeyValueLeafPage kvl && !kvl.isClosed()) { + return kvl; + } + } + return null; + } + @Override public StorageEngineWriter appendLogRecord(@NonNull final PageReference reference, @NonNull final PageContainer pageContainer) { @@ -1513,10 +1340,9 @@ public CommitCredentials getCommitCredentials() { @Override @SuppressWarnings("deprecation") protected void finalize() { - // DIAGNOSTIC: Detect if NodeStorageEngineWriter is GC'd without being closed - if (!isClosed && KeyValueLeafPage.DEBUG_MEMORY_LEAKS) { + if (!isClosed) { LOGGER.warn( - "⚠️ NodeStorageEngineWriter FINALIZED WITHOUT CLOSE: trxId={} instance={} TIL={} with {} containers in TIL", + "NodeStorageEngineWriter FINALIZED WITHOUT CLOSE: trxId={} instance={} TIL={} with {} containers in TIL", storageEngineReader.getTrxId(), System.identityHashCode(this), System.identityHashCode(log), log.getList().size()); } } @@ -1538,137 +1364,6 @@ public PageGuard acquireGuardForCurrentNode() { return new PageGuard(currentPage); } - private static final QNm EMPTY_QNM = new QNm(""); - - /** - * Get or lazily create a write-path singleton for the given node kind. Returns null for unsupported - * kinds (non-XML/JSON). - */ - private DataRecord getOrCreateWriteSingleton(final NodeKind kind) { - final var hashFunction = storageEngineReader.getResourceSession().getResourceConfig().nodeHashFunction; - return switch (kind) { - case OBJECT -> { - if (writeObjectSingleton == null) { - writeObjectSingleton = new ObjectNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeObjectSingleton; - } - case ARRAY -> { - if (writeArraySingleton == null) { - writeArraySingleton = new ArrayNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeArraySingleton; - } - case OBJECT_KEY -> { - if (writeObjectKeySingleton == null) { - writeObjectKeySingleton = new ObjectKeyNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeObjectKeySingleton; - } - case STRING_VALUE -> { - if (writeStringSingleton == null) { - writeStringSingleton = new StringNode(0, 0, 0, 0, 0, 0, 0, null, hashFunction, (byte[]) null); - } - yield writeStringSingleton; - } - case NUMBER_VALUE -> { - if (writeNumberSingleton == null) { - writeNumberSingleton = new NumberNode(0, 0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeNumberSingleton; - } - case BOOLEAN_VALUE -> { - if (writeBooleanSingleton == null) { - writeBooleanSingleton = new BooleanNode(0, 0, 0, 0, 0, 0, 0, false, hashFunction, (byte[]) null); - } - yield writeBooleanSingleton; - } - case NULL_VALUE -> { - if (writeNullSingleton == null) { - writeNullSingleton = new NullNode(0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeNullSingleton; - } - case OBJECT_STRING_VALUE -> { - if (writeObjectStringSingleton == null) { - writeObjectStringSingleton = new ObjectStringNode(0, 0, 0, 0, 0, null, hashFunction, (byte[]) null); - } - yield writeObjectStringSingleton; - } - case OBJECT_NUMBER_VALUE -> { - if (writeObjectNumberSingleton == null) { - writeObjectNumberSingleton = new ObjectNumberNode(0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeObjectNumberSingleton; - } - case OBJECT_BOOLEAN_VALUE -> { - if (writeObjectBooleanSingleton == null) { - writeObjectBooleanSingleton = new ObjectBooleanNode(0, 0, 0, 0, 0, false, hashFunction, (byte[]) null); - } - yield writeObjectBooleanSingleton; - } - case OBJECT_NULL_VALUE -> { - if (writeObjectNullSingleton == null) { - writeObjectNullSingleton = new ObjectNullNode(0, 0, 0, 0, 0, hashFunction, (byte[]) null); - } - yield writeObjectNullSingleton; - } - case JSON_DOCUMENT -> { - if (writeJsonDocumentSingleton == null) { - writeJsonDocumentSingleton = new JsonDocumentRootNode(0, 0, 0, 0, 0, hashFunction); - } - yield writeJsonDocumentSingleton; - } - case XML_DOCUMENT -> { - if (writeXmlDocumentSingleton == null) { - writeXmlDocumentSingleton = new XmlDocumentRootNode(0, 0, 0, 0, 0, hashFunction); - } - yield writeXmlDocumentSingleton; - } - case ELEMENT -> { - if (writeElementSingleton == null) { - writeElementSingleton = new ElementNode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, - (byte[]) null, null, null, EMPTY_QNM); - } - yield writeElementSingleton; - } - case TEXT -> { - if (writeTextSingleton == null) { - writeTextSingleton = new TextNode(0, 0, 0, 0, 0, 0, 0, new byte[0], false, hashFunction, (byte[]) null); - } - yield writeTextSingleton; - } - case COMMENT -> { - if (writeCommentSingleton == null) { - writeCommentSingleton = new CommentNode(0, 0, 0, 0, 0, 0, 0, new byte[0], false, hashFunction, (byte[]) null); - } - yield writeCommentSingleton; - } - case ATTRIBUTE -> { - if (writeAttributeSingleton == null) { - writeAttributeSingleton = - new AttributeNode(0, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], hashFunction, (byte[]) null, EMPTY_QNM); - } - yield writeAttributeSingleton; - } - case NAMESPACE -> { - if (writeNamespaceSingleton == null) { - writeNamespaceSingleton = - new NamespaceNode(0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, (byte[]) null, EMPTY_QNM); - } - yield writeNamespaceSingleton; - } - case PROCESSING_INSTRUCTION -> { - if (writePISingleton == null) { - writePISingleton = new PINode(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], false, hashFunction, - (byte[]) null, EMPTY_QNM); - } - yield writePISingleton; - } - default -> null; - }; - } - /** * Package-private helper class for managing the trie structure of IndirectPages. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/api/StorageEngineWriter.java b/bundles/sirix-core/src/main/java/io/sirix/api/StorageEngineWriter.java index 721f9c66d..664fb425e 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/api/StorageEngineWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/api/StorageEngineWriter.java @@ -9,6 +9,7 @@ import io.sirix.node.BytesOut; import io.sirix.node.NodeKind; import io.sirix.node.interfaces.DataRecord; +import io.sirix.page.KeyValueLeafPage; import io.sirix.page.PageReference; import io.sirix.page.UberPage; import org.checkerframework.checker.index.qual.NonNegative; @@ -85,18 +86,18 @@ public interface StorageEngineWriter extends StorageEngineReader { V prepareRecordForModification(@NonNegative long key, @NonNull IndexType indexType, int index); /** - * Persist a modified record directly back into its record-page slot. + * Persist a mutated record into the TIL's modified page. + * Ensures the record's page is prepared for modification in the TIL + * and stores the record in the modified page's records[]. * - *

- * This is the hot update path used to keep page slot memory in sync with in-memory mutable records - * without waiting for commit-time materialization. - *

+ *

This is used by mutation operations (setName, setValue, hash updates) that + * mutate the current node directly without going through prepareRecordForModification.

* - * @param record modified record to persist + * @param record the mutated record to persist * @param indexType the index type - * @param index the index number + * @param index the index number */ - void updateRecordSlot(@NonNull DataRecord record, @NonNull IndexType indexType, int index); + void persistRecord(@NonNull DataRecord record, @NonNull IndexType indexType, int index); /** * Remove an entry from the storage. @@ -181,6 +182,56 @@ default UberPage commit(@Nullable String commitMessage, @Nullable Instant commit PageContainer dereferenceRecordPageForModification(PageReference reference); + /** + * Functional interface for binding a write-path singleton to a slotted page slot. + * Set by the node transaction to enable zero-allocation write path. + */ + @FunctionalInterface + interface WriteSingletonBinder { + DataRecord bind(KeyValueLeafPage page, int offset, long nodeKey); + } + + /** + * Set the write singleton binder for zero-allocation write path. + * When set, prepareRecordForModification rebinds factory singletons instead of allocating. + * + * @param binder the write singleton binder from the node factory + */ + default void setWriteSingletonBinder(final WriteSingletonBinder binder) { + // Default no-op; NodeStorageEngineWriter overrides + } + + /** + * Allocate a record key and resolve the KVL page for direct-to-heap creation. + * After this call, read results from {@link #getAllocKvl()}, {@link #getAllocSlotOffset()}, + * {@link #getAllocNodeKey()}. + *

Only supports DOCUMENT index (the hot path). Other index types use createRecord().

+ */ + default void allocateForDocumentCreation() { + throw new UnsupportedOperationException(); + } + + /** + * Get the KVL page from the last {@link #allocateForDocumentCreation()} call. + */ + default KeyValueLeafPage getAllocKvl() { + throw new UnsupportedOperationException(); + } + + /** + * Get the slot offset from the last {@link #allocateForDocumentCreation()} call. + */ + default int getAllocSlotOffset() { + throw new UnsupportedOperationException(); + } + + /** + * Get the node key from the last {@link #allocateForDocumentCreation()} call. + */ + default long getAllocNodeKey() { + throw new UnsupportedOperationException(); + } + /** * Get the underlying {@link StorageEngineReader}. * @@ -225,4 +276,17 @@ default UberPage commit(@Nullable String commitMessage, @Nullable Instant commit * @return a PageGuard that must be closed when done with the node */ PageGuard acquireGuardForCurrentNode(); + + /** + * Get the TIL's modified {@link KeyValueLeafPage} for a given record page key, or null if not in TIL. + * Used by the singleton moveTo path to read from the correct (modified) page during write transactions. + * + * @param recordPageKey the record page key + * @param indexType the index type + * @param index the index number + * @return the modified page if in TIL, null otherwise + */ + default @Nullable KeyValueLeafPage getModifiedPageForRead(long recordPageKey, @NonNull IndexType indexType, int index) { + return null; + } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/index/path/PathIndex.java b/bundles/sirix-core/src/main/java/io/sirix/index/path/PathIndex.java index c6fc26e61..6a235ab54 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/index/path/PathIndex.java +++ b/bundles/sirix-core/src/main/java/io/sirix/index/path/PathIndex.java @@ -30,16 +30,16 @@ public interface PathIndex { default Iterator openIndex(final StorageEngineReader storageEngineReader, final IndexDef indexDef, final PathFilter filter) { - + // Check if HOT is enabled (system property takes precedence, then resource config) if (isHOTEnabled(storageEngineReader)) { return openHOTIndex(storageEngineReader, indexDef, filter); } - + // Use RBTree (default) return openRBTreeIndex(storageEngineReader, indexDef, filter); } - + /** * Checks if HOT indexes should be used for reading. */ @@ -49,12 +49,12 @@ private static boolean isHOTEnabled(final StorageEngineReader storageEngineReade if (sysProp != null) { return Boolean.parseBoolean(sysProp); } - + // Fall back to resource configuration final var resourceConfig = storageEngineReader.getResourceSession().getResourceConfig(); return resourceConfig.indexBackendType == IndexBackendType.HOT; } - + /** * Open HOT-based path index. */ @@ -72,14 +72,12 @@ private Iterator openHOTIndex(final StorageEngineReader storageE return Collections.emptyIterator(); } else { // Iterate over all entries and apply filter - final Set pcrsRequested = filter != null - ? filter.getPCRs() - : Set.of(); + final Set pcrsRequested = filter != null ? filter.getPCRs() : Set.of(); final Iterator> entryIterator = reader.iterator(); - + return new Iterator<>() { private NodeReferences next = null; - + @Override public boolean hasNext() { if (next != null) { @@ -94,7 +92,7 @@ public boolean hasNext() { } return false; } - + @Override public NodeReferences next() { if (!hasNext()) { @@ -107,23 +105,26 @@ public NodeReferences next() { }; } } - + /** * Open RBTree-based path index (default). */ private Iterator openRBTreeIndex(final StorageEngineReader storageEngineReader, final IndexDef indexDef, final PathFilter filter) { - final RBTreeReader reader = RBTreeReader.getInstance( - storageEngineReader.getResourceSession().getIndexCache(), storageEngineReader, indexDef.getType(), indexDef.getID()); + final RBTreeReader reader = + RBTreeReader.getInstance(storageEngineReader.getResourceSession().getIndexCache(), + storageEngineReader, + indexDef.getType(), + indexDef.getID()); if (filter != null && filter.getPCRs().size() == 1) { - final var optionalNodeReferences = reader.get(filter.getPCRs().iterator().next(), SearchMode.EQUAL); + final var optionalNodeReferences = + reader.get(filter.getPCRs().iterator().next(), SearchMode.EQUAL); return Iterators.forArray(optionalNodeReferences.orElse(new NodeReferences())); } else { - final Iterator> iter = reader.new RBNodeIterator(Fixed.DOCUMENT_NODE_KEY.getStandardProperty()); - final Set setFilter = filter == null - ? ImmutableSet.of() - : ImmutableSet.of(filter); + final Iterator> iter = + reader.new RBNodeIterator(Fixed.DOCUMENT_NODE_KEY.getStandardProperty()); + final Set setFilter = filter == null ? ImmutableSet.of() : ImmutableSet.of(filter); return new IndexFilterAxis<>(reader, iter, setFilter); } diff --git a/bundles/sirix-core/src/main/java/io/sirix/index/path/summary/PathSummaryWriter.java b/bundles/sirix-core/src/main/java/io/sirix/index/path/summary/PathSummaryWriter.java index de35cc719..f320a40f2 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/index/path/summary/PathSummaryWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/index/path/summary/PathSummaryWriter.java @@ -187,7 +187,11 @@ private void movePathSummary() { } else if (nodeRtx.getNode() instanceof ImmutableNameNode) { pathSummaryReader.moveTo(((ImmutableNameNode) nodeRtx.getNode()).getPathNodeKey()); } else { - throw new IllegalStateException(); + final var node = nodeRtx.getNode(); + throw new IllegalStateException("movePathSummary: unexpected node kind=" + nodeRtx.getKind() + + " nodeClass=" + (node != null ? node.getClass().getName() : "null") + + " nodeKey=" + nodeRtx.getNodeKey() + + " instanceOfImmutableNameNode=" + (node instanceof ImmutableNameNode)); } } @@ -750,11 +754,13 @@ public void remove(final ImmutableNameNode node) { } private void persistDocumentRecord(final DataRecord record) { - storageEngineWriter.updateRecordSlot(record, IndexType.DOCUMENT, -1); + // No-op: records are mutated in-place via prepareRecordForModification() + // and serialized at commit time via processEntries(). No slot sync needed. } private void persistPathSummaryRecord(final DataRecord record) { - storageEngineWriter.updateRecordSlot(record, IndexType.PATH_SUMMARY, 0); + // No-op: records are mutated in-place via prepareRecordForModification() + // and serialized at commit time via processEntries(). No slot sync needed. } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/BytesUtils.java b/bundles/sirix-core/src/main/java/io/sirix/io/BytesUtils.java deleted file mode 100644 index 8c62e20c8..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/io/BytesUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.sirix.io; - -import io.sirix.node.BytesOut; - -import java.nio.ByteBuffer; - -public final class BytesUtils { - private BytesUtils() { - throw new AssertionError(); - } - - public static void doWrite(BytesOut bytes, ByteBuffer toWrite) { - // Write ByteBuffer content to BytesOut - byte[] buffer = new byte[toWrite.remaining()]; - toWrite.get(buffer); - bytes.write(buffer); - } - - public static void doWrite(BytesOut bytes, byte[] toWrite) { - bytes.write(toWrite); - } - - public static ByteBuffer doRead(BytesOut bytes) { - // Get underlying destination from BytesOut - Object destination = bytes.getDestination(); - if (destination instanceof ByteBuffer) { - return (ByteBuffer) destination; - } - // Fallback for other destination types - throw new UnsupportedOperationException("Unsupported destination type: " + destination.getClass()); - } - - /** - * Convert the byte[] into a String to be used for logging and debugging. - * - * @param bytes the byte[] to be dumped - * @return the String representation - */ - public static String dumpBytes(byte[] bytes) { - StringBuilder buffer = new StringBuilder("byte["); - buffer.append(bytes.length); - buffer.append("]: ["); - for (byte aByte : bytes) { - buffer.append(aByte); - buffer.append(" "); - } - buffer.append("]"); - return buffer.toString(); - } - - /** - * Convert the byteBuffer into a String to be used for logging and debugging. - * - * @param byteBuffer the byteBuffer to be dumped - * @return the String representation - */ - public static String dumpBytes(ByteBuffer byteBuffer) { - byteBuffer.mark(); - int length = byteBuffer.limit() - byteBuffer.position(); - byte[] dst = new byte[length]; - byteBuffer.get(dst); - byteBuffer.reset(); - return dumpBytes(dst); - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/Writer.java b/bundles/sirix-core/src/main/java/io/sirix/io/Writer.java index f189a5a12..3a66f8c93 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/io/Writer.java +++ b/bundles/sirix-core/src/main/java/io/sirix/io/Writer.java @@ -28,8 +28,6 @@ import io.sirix.page.interfaces.Page; import io.sirix.node.BytesOut; -import java.nio.ByteBuffer; - /** * Interface to provide the abstract layer related to write access of the Sirix-backend. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/bytepipe/FFILz4Compressor.java b/bundles/sirix-core/src/main/java/io/sirix/io/bytepipe/FFILz4Compressor.java index df530d523..5c83c3798 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/io/bytepipe/FFILz4Compressor.java +++ b/bundles/sirix-core/src/main/java/io/sirix/io/bytepipe/FFILz4Compressor.java @@ -538,7 +538,7 @@ public MemorySegment decompress(MemorySegment compressed) { } /** - * Decompress data using the unified allocator for zero-copy page support. + * Decompress data using the page allocator for zero-copy page support. * *

* Unlike the pool-based approach, this allocates from the MemorySegmentAllocator which allows @@ -585,7 +585,7 @@ public DecompressionResult decompressScoped(MemorySegment compressed) { int decompressedSize = sizeHeader; - // Use unified allocator - buffer lifetime matches page lifetime for zero-copy + // Use page allocator - buffer lifetime matches page lifetime for zero-copy // This replaces the pool-based approach to enable ownership transfer MemorySegment buffer = ALLOCATOR.allocate(decompressedSize); diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/file/FileReader.java b/bundles/sirix-core/src/main/java/io/sirix/io/file/FileReader.java index a1c52aae1..caf9a5341 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/io/file/FileReader.java +++ b/bundles/sirix-core/src/main/java/io/sirix/io/file/FileReader.java @@ -49,7 +49,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.RandomAccessFile; -import java.nio.ByteBuffer; import java.time.Instant; import static java.util.Objects.requireNonNull; @@ -211,7 +210,7 @@ public RevisionRootPage readRevisionRootPage(final int revision, final ResourceC dataFile.read(page); // Perform byte operations. - final BytesIn input = Bytes.wrapForRead(ByteBuffer.wrap(page)); // byteHandler.deserialize(Bytes.wrapForRead(ByteBuffer.wrap(page))); + final BytesIn input = Bytes.wrapForRead(page); // Return reader required to instantiate and deserialize page. return (RevisionRootPage) pagePersiter.deserializePage(resourceConfiguration, input, serializationType); diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/filechannel/FileChannelWriter.java b/bundles/sirix-core/src/main/java/io/sirix/io/filechannel/FileChannelWriter.java index 289b9056d..6ec3ae3f7 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/io/filechannel/FileChannelWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/io/filechannel/FileChannelWriter.java @@ -181,13 +181,21 @@ private byte[] buildSerializedPage(final ResourceConfiguration resourceConfigura throws IOException { final BytesIn uncompressedBytes = byteBufferBytes.bytesForRead(); - if (page instanceof KeyValueLeafPage keyValueLeafPage && keyValueLeafPage.getBytes() != null) { + if (page instanceof KeyValueLeafPage keyValueLeafPage) { + // Check compressed MemorySegment cache first (slotted page format path) + final MemorySegment cachedSegment = keyValueLeafPage.getCompressedSegment(); + if (cachedSegment != null) { + return segmentToByteArray(cachedSegment); + } + // Check legacy byte[] cache final var cached = keyValueLeafPage.getBytes(); - if (cached instanceof MemorySegmentBytesOut msOut) { - MemorySegment segment = msOut.getDestination(); - return segmentToByteArray(segment); + if (cached != null) { + if (cached instanceof MemorySegmentBytesOut msOut) { + MemorySegment segment = msOut.getDestination(); + return segmentToByteArray(segment); + } + return cached.toByteArray(); } - return cached.toByteArray(); } final var pipeline = resourceConfiguration.byteHandlePipeline; @@ -268,11 +276,12 @@ private FileChannelWriter writePageReference(final ResourceConfiguration resourc buffer.position(8); buffer.putLong(revisionRootPage.getRevisionTimestamp()); buffer.position(0); + final long revisionsFileSize = revisionsFileChannel.size(); final long revisionsFileOffset; if (revisionRootPage.getRevision() == 0) { - revisionsFileOffset = revisionsFileChannel.size() + IOStorage.FIRST_BEACON; + revisionsFileOffset = revisionsFileSize + IOStorage.FIRST_BEACON; } else { - revisionsFileOffset = revisionsFileChannel.size(); + revisionsFileOffset = revisionsFileSize; } revisionsFileChannel.write(buffer, revisionsFileOffset); final long currOffset = offset; @@ -334,9 +343,8 @@ public Writer writeUberPageReference(final ResourceConfiguration resourceConfigu isFirstUberPage = false; writePageReference(resourceConfiguration, pageReference, page, bufferedBytes, IOStorage.FIRST_BEACON >> 1); - @SuppressWarnings("DataFlowIssue") - final var buffer = ((ByteBuffer) bufferedBytes.underlyingObject()).rewind(); - buffer.limit((int) bufferedBytes.readLimit()); + final var segment = (MemorySegment) bufferedBytes.underlyingObject(); + final var buffer = segment.asByteBuffer(); dataFileChannel.write(buffer, 0L); // NOTE: force() removed here - now called via forceAll() at end of commit for single barrier bufferedBytes.clear(); @@ -372,9 +380,8 @@ private void flushBuffer(BytesOut bufferedBytes) throws IOException { offset = fileSize; } - @SuppressWarnings("DataFlowIssue") - final var buffer = ((ByteBuffer) bufferedBytes.underlyingObject()).rewind(); - buffer.limit((int) bufferedBytes.readLimit()); + final var segment = (MemorySegment) bufferedBytes.underlyingObject(); + final var buffer = segment.asByteBuffer(); dataFileChannel.write(buffer, offset); bufferedBytes.clear(); } diff --git a/bundles/sirix-core/src/main/java/io/sirix/io/memorymapped/MMFileWriter.java b/bundles/sirix-core/src/main/java/io/sirix/io/memorymapped/MMFileWriter.java index c71af3bf1..3ee36270c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/io/memorymapped/MMFileWriter.java +++ b/bundles/sirix-core/src/main/java/io/sirix/io/memorymapped/MMFileWriter.java @@ -260,13 +260,21 @@ private byte[] buildSerializedPage(final ResourceConfiguration resourceConfigura throws IOException { final BytesIn uncompressedBytes = byteBufferBytes.bytesForRead(); - if (page instanceof KeyValueLeafPage keyValueLeafPage && keyValueLeafPage.getBytes() != null) { + if (page instanceof KeyValueLeafPage keyValueLeafPage) { + // Check compressed MemorySegment cache first (slotted page format path) + final MemorySegment cachedSegment = keyValueLeafPage.getCompressedSegment(); + if (cachedSegment != null) { + return cachedSegment.toArray(ValueLayout.JAVA_BYTE); + } + // Check legacy byte[] cache final var cached = keyValueLeafPage.getBytes(); - if (cached instanceof MemorySegmentBytesOut msOut) { - MemorySegment segment = msOut.getDestination(); - return segment.toArray(ValueLayout.JAVA_BYTE); + if (cached != null) { + if (cached instanceof MemorySegmentBytesOut msOut) { + MemorySegment segment = msOut.getDestination(); + return segment.toArray(ValueLayout.JAVA_BYTE); + } + return cached.toByteArray(); } - return cached.toByteArray(); } final var pipeline = resourceConfiguration.byteHandlePipeline; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/AbstractForwardingNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/AbstractForwardingNode.java index 19f84df1a..ebf818d47 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/AbstractForwardingNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/AbstractForwardingNode.java @@ -7,7 +7,6 @@ import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.interfaces.Node; -import java.nio.ByteBuffer; /** * Skeletal implementation of {@link Node} interface. diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/Bytes.java b/bundles/sirix-core/src/main/java/io/sirix/node/Bytes.java index 9bc251a45..6bdc61a01 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/Bytes.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/Bytes.java @@ -1,7 +1,6 @@ package io.sirix.node; import java.lang.foreign.MemorySegment; -import java.nio.ByteBuffer; /** * Utility class to replace Chronicle Bytes factory methods. Provides factory methods for creating @@ -102,28 +101,6 @@ public static BytesIn wrapForRead(MemorySegment segment) { return new MemorySegmentBytesIn(segment); } - /** - * Factory method to wrap a ByteBuffer for reading. If the buffer has a backing array, uses - * zero-copy wrapping. Otherwise, copies the data to a new array. - * - * @param buffer the ByteBuffer to wrap - * @return a BytesIn instance for reading - */ - public static BytesIn wrapForRead(ByteBuffer buffer) { - if (buffer.hasArray()) { - // Zero-copy path for heap buffers with backing array - int offset = buffer.arrayOffset() + buffer.position(); - int length = buffer.remaining(); - MemorySegment segment = MemorySegment.ofArray(buffer.array()).asSlice(offset, length); - return new MemorySegmentBytesIn(segment); - } else { - // Fallback: copy data for direct buffers without backing array - byte[] data = new byte[buffer.remaining()]; - buffer.get(data); - return wrapForRead(data); - } - } - /** * Factory method to create an elastic heap BytesOut with initial capacity. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/BytesOut.java b/bundles/sirix-core/src/main/java/io/sirix/node/BytesOut.java index 59f11a49a..f954a02c1 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/BytesOut.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/BytesOut.java @@ -2,7 +2,6 @@ import java.lang.foreign.MemorySegment; import java.math.BigInteger; -import java.nio.ByteBuffer; import net.openhft.hashing.LongHashFunction; /** @@ -149,17 +148,6 @@ default BytesOut writeSegment(MemorySegment source, long sourceOffset, long l return write(temp); } - /** - * Write from a ByteBuffer at a specific position. - * - * @param position the position to write at - * @param buffer the buffer to write from - * @param bufferPosition the position in the buffer - * @param length the number of bytes to write - * @return this BytesOut for method chaining - */ - BytesOut write(long position, ByteBuffer buffer, int bufferPosition, int length); - /** * Get the current write position. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/DeltaVarIntCodec.java b/bundles/sirix-core/src/main/java/io/sirix/node/DeltaVarIntCodec.java index c1cbf77d8..f65fdab80 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/DeltaVarIntCodec.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/DeltaVarIntCodec.java @@ -36,22 +36,19 @@ /** * High-performance codec for delta-encoded node keys using zigzag + varint encoding. * - *

- * Inspired by Umbra-style storage optimization: instead of storing absolute 8-byte keys, we store - * deltas relative to the current nodeKey. Since siblings are usually adjacent (delta = ±1) and - * parents are nearby, deltas are small and compress well. + *

Inspired by Umbra-style storage optimization: instead of storing absolute 8-byte keys, + * we store deltas relative to the current nodeKey. Since siblings are usually adjacent + * (delta = ±1) and parents are nearby, deltas are small and compress well. * *

Encoding Strategy

*
    - *
  • NULL keys: Encoded as a single byte 0x00 (zigzag of 0 with special meaning)
  • - *
  • Delta encoding: Store (targetKey - baseKey) instead of absolute key
  • - *
  • Zigzag encoding: Maps signed to unsigned (0→0, -1→1, 1→2, -2→3, 2→4, ...)
  • - *
  • Varint encoding: Variable-length unsigned integer (7 bits per byte, MSB = - * continue)
  • + *
  • NULL keys: Encoded as a single byte 0x00 (zigzag of 0 with special meaning)
  • + *
  • Delta encoding: Store (targetKey - baseKey) instead of absolute key
  • + *
  • Zigzag encoding: Maps signed to unsigned (0→0, -1→1, 1→2, -2→3, 2→4, ...)
  • + *
  • Varint encoding: Variable-length unsigned integer (7 bits per byte, MSB = continue)
  • *
* *

Space Savings Example

- * *
  * Absolute key 1000000 (8 bytes) → Delta +1 → Zigzag 2 → Varint 1 byte
  * Absolute key -1 (NULL, 8 bytes) → Special marker → 1 byte
@@ -59,41 +56,40 @@
  * 
  * 

Performance

*
    - *
  • No allocations during encode/decode
  • - *
  • Branchless zigzag encoding
  • - *
  • Unrolled varint for common cases (1-2 bytes)
  • + *
  • No allocations during encode/decode
  • + *
  • Branchless zigzag encoding
  • + *
  • Unrolled varint for common cases (1-2 bytes)
  • *
* * @author Johannes Lichtenberger */ public final class DeltaVarIntCodec { - + /** - * Sentinel value for NULL node keys. We use 0 in zigzag encoding as the NULL marker since delta=0 - * (self-reference) is meaningless. + * Sentinel value for NULL node keys. + * We use 0 in zigzag encoding as the NULL marker since delta=0 (self-reference) is meaningless. */ private static final int NULL_MARKER = 0; - + /** * The actual NULL key value from the Fixed enum. */ private static final long NULL_KEY = Fixed.NULL_NODE_KEY.getStandardProperty(); - + private DeltaVarIntCodec() { // Utility class } - + // ==================== ENCODING ==================== - + /** * Encode a node key as a delta from a base key. * - *

- * For structural pointers (siblings, children), the base is typically the current nodeKey. For - * parent pointers, the base is also the current nodeKey. + *

For structural pointers (siblings, children), the base is typically the current nodeKey. + * For parent pointers, the base is also the current nodeKey. * - * @param sink output sink to write to - * @param key the key to encode (may be NULL_NODE_KEY) + * @param sink output sink to write to + * @param key the key to encode (may be NULL_NODE_KEY) * @param baseKey the reference key for delta calculation */ public static void encodeDelta(BytesOut sink, long key, long baseKey) { @@ -102,26 +98,26 @@ public static void encodeDelta(BytesOut sink, long key, long baseKey) { sink.writeByte((byte) NULL_MARKER); return; } - + // Calculate delta long delta = key - baseKey; - + // Zigzag encode (signed → unsigned) // This maps small positive/negative values to small unsigned values: // 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ... // But we reserve 0 for NULL, so we add 1 to shift everything up long zigzag = zigzagEncode(delta) + 1; - + // Varint encode writeVarLong(sink, zigzag); } - + /** - * Encode an absolute key (no delta, used for nodeKey itself). Uses varint encoding directly for - * unsigned values. + * Encode an absolute key (no delta, used for nodeKey itself). + * Uses varint encoding directly for unsigned values. * * @param sink output sink - * @param key the absolute key (must be non-negative) + * @param key the absolute key (must be non-negative) */ public static void encodeAbsolute(BytesOut sink, long key) { if (key < 0) { @@ -129,10 +125,10 @@ public static void encodeAbsolute(BytesOut sink, long key) { } writeVarLong(sink, key); } - + /** - * Encode a revision number which can be -1 (NULL_REVISION_NUMBER). Uses zigzag encoding to handle - * the -1 case efficiently. + * Encode a revision number which can be -1 (NULL_REVISION_NUMBER). + * Uses zigzag encoding to handle the -1 case efficiently. * * @param sink output sink * @param revision the revision number (can be -1) @@ -143,7 +139,7 @@ public static void encodeRevision(BytesOut sink, int revision) { long zigzag = zigzagEncode(revision); writeVarLong(sink, zigzag); } - + /** * Decode a revision number which can be -1 (NULL_REVISION_NUMBER). * @@ -154,10 +150,10 @@ public static int decodeRevision(BytesIn source) { long zigzag = readVarLong(source); return (int) zigzagDecode(zigzag); } - + /** - * Encode a signed integer (like nameKey which is a hash and can be negative). Uses zigzag encoding - * to handle negative values efficiently. + * Encode a signed integer (like nameKey which is a hash and can be negative). + * Uses zigzag encoding to handle negative values efficiently. * * @param sink output sink * @param value the signed integer value @@ -166,7 +162,7 @@ public static void encodeSigned(BytesOut sink, int value) { long zigzag = zigzagEncode(value); writeVarLong(sink, zigzag); } - + /** * Decode a signed integer. * @@ -177,13 +173,12 @@ public static int decodeSigned(BytesIn source) { long zigzag = readVarLong(source); return (int) zigzagDecode(zigzag); } - + /** - * Encode a signed long value using zigzag + varint encoding. Optimized for small values: values -64 - * to 63 use 1 byte, -8192 to 8191 use 2 bytes. + * Encode a signed long value using zigzag + varint encoding. + * Optimized for small values: values -64 to 63 use 1 byte, -8192 to 8191 use 2 bytes. * - *

- * This is ideal for JSON numbers which are often small integers stored as long. + *

This is ideal for JSON numbers which are often small integers stored as long. * * @param sink output sink * @param value the signed long value @@ -192,7 +187,7 @@ public static void encodeSignedLong(BytesOut sink, long value) { long zigzag = zigzagEncode(value); writeVarLong(sink, zigzag); } - + /** * Decode a signed long value from zigzag + varint encoding. * @@ -203,10 +198,10 @@ public static long decodeSignedLong(BytesIn source) { long zigzag = readVarLong(source); return zigzagDecode(zigzag); } - + /** - * Encode an unsigned long value using varint encoding. Optimized for small values: values 0-127 use - * 1 byte, 128-16383 use 2 bytes. + * Encode an unsigned long value using varint encoding. + * Optimized for small values: values 0-127 use 1 byte, 128-16383 use 2 bytes. * * @param sink output sink * @param value the unsigned long value (must be non-negative) @@ -214,7 +209,7 @@ public static long decodeSignedLong(BytesIn source) { public static void encodeUnsignedLong(BytesOut sink, long value) { writeVarLong(sink, value); } - + /** * Decode an unsigned long value from varint encoding. * @@ -224,29 +219,29 @@ public static void encodeUnsignedLong(BytesOut sink, long value) { public static long decodeUnsignedLong(BytesIn source) { return readVarLong(source); } - + // ==================== DECODING ==================== - + /** * Decode a delta-encoded node key. * - * @param source input source to read from + * @param source input source to read from * @param baseKey the reference key for delta calculation * @return the decoded absolute key, or NULL_NODE_KEY if null marker */ public static long decodeDelta(BytesIn source, long baseKey) { long zigzag = readVarLong(source); - + if (zigzag == NULL_MARKER) { return NULL_KEY; } - + // Undo the +1 shift and zigzag decode long delta = zigzagDecode(zigzag - 1); - + return baseKey + delta; } - + /** * Decode an absolute key (no delta). * @@ -256,33 +251,34 @@ public static long decodeDelta(BytesIn source, long baseKey) { public static long decodeAbsolute(BytesIn source) { return readVarLong(source); } - + // ==================== ZIGZAG ENCODING ==================== - + /** - * Zigzag encode a signed long to unsigned. Maps: 0→0, -1→1, 1→2, -2→3, 2→4, ... + * Zigzag encode a signed long to unsigned. + * Maps: 0→0, -1→1, 1→2, -2→3, 2→4, ... * - *

- * This is branchless and uses arithmetic shift. + *

This is branchless and uses arithmetic shift. */ private static long zigzagEncode(long value) { return (value << 1) ^ (value >> 63); } - + /** - * Zigzag decode an unsigned long to signed. Inverse of zigzagEncode. + * Zigzag decode an unsigned long to signed. + * Inverse of zigzagEncode. */ private static long zigzagDecode(long encoded) { return (encoded >>> 1) ^ -(encoded & 1); } - + // ==================== VARINT ENCODING ==================== - + /** - * Write a variable-length unsigned long. Uses 7 bits per byte, MSB indicates continuation. + * Write a variable-length unsigned long. + * Uses 7 bits per byte, MSB indicates continuation. * - *

- * Optimized for small values (1-2 bytes for values 0-16383). + *

Optimized for small values (1-2 bytes for values 0-16383). */ private static void writeVarLong(BytesOut sink, long value) { // Fast path for small values (fits in 1 byte: 0-127) @@ -290,14 +286,14 @@ private static void writeVarLong(BytesOut sink, long value) { sink.writeByte((byte) value); return; } - + // Fast path for 2-byte values (128-16383) if ((value & ~0x3FFFL) == 0) { sink.writeByte((byte) ((value & 0x7F) | 0x80)); sink.writeByte((byte) (value >>> 7)); return; } - + // General case for larger values while ((value & ~0x7FL) != 0) { sink.writeByte((byte) ((value & 0x7F) | 0x80)); @@ -305,68 +301,46 @@ private static void writeVarLong(BytesOut sink, long value) { } sink.writeByte((byte) value); } - + /** * Read a variable-length unsigned long. * - *

- * Optimized with fast paths for common 1-byte and 2-byte cases. Most delta-encoded values (siblings - * ±1, nearby parents) fit in 1-2 bytes. + *

Optimized with fast paths for common 1-byte and 2-byte cases. + * Most delta-encoded values (siblings ±1, nearby parents) fit in 1-2 bytes. */ private static long readVarLong(BytesIn source) { byte b = source.readByte(); - + // Fast path: 1 byte (0-127) - most common case for small deltas if ((b & 0x80) == 0) { return b; } - + long result = b & 0x7F; b = source.readByte(); - + // Fast path: 2 bytes (128-16383) - second most common if ((b & 0x80) == 0) { return result | ((long) b << 7); } - + // General case for larger values (rare for structural keys) result |= (long) (b & 0x7F) << 7; int shift = 14; - while (true) { - if (shift >= Long.SIZE) { - throw new IllegalStateException("Varint too long (more than 10 bytes)"); - } - + do { b = source.readByte(); - if (shift == Long.SIZE - 1 && (b & 0x7E) != 0) { - throw new IllegalStateException("Varint exceeds 64-bit range"); - } result |= (long) (b & 0x7F) << shift; - if ((b & 0x80) == 0) { - return result; - } shift += 7; - } - } - - // ==================== SIZE ESTIMATION ==================== - /** - * Estimate the encoded size of a delta-encoded key (for buffer sizing). - * - * @param key the key to encode - * @param baseKey the reference key - * @return estimated bytes needed - */ - public static int estimateEncodedSize(long key, long baseKey) { - if (key == NULL_KEY) { - return 1; - } + if (shift > 70) { + throw new IllegalStateException("Varint too long (more than 10 bytes)"); + } + } while ((b & 0x80) != 0); - long delta = key - baseKey; - long zigzag = zigzagEncode(delta) + 1; - return varLongSize(zigzag); + return result; } + + // ==================== SIZE ESTIMATION ==================== /** * Calculate the number of bytes needed for a varint. @@ -379,16 +353,16 @@ private static int varLongSize(long value) { } return size; } - + // ==================== DIRECT SEGMENT WRITE METHODS ==================== // These methods eliminate per-byte ensureCapacity() overhead by using direct segment writes - + /** - * Encode a node key as a delta from a base key, writing directly to a GrowingMemorySegment. This is - * the high-performance variant that eliminates per-byte capacity checks. + * Encode a node key as a delta from a base key, writing directly to a GrowingMemorySegment. + * This is the high-performance variant that eliminates per-byte capacity checks. * - * @param seg the segment to write to - * @param key the key to encode (may be NULL_NODE_KEY) + * @param seg the segment to write to + * @param key the key to encode (may be NULL_NODE_KEY) * @param baseKey the reference key for delta calculation */ public static void encodeDelta(GrowingMemorySegment seg, long key, long baseKey) { @@ -397,21 +371,21 @@ public static void encodeDelta(GrowingMemorySegment seg, long key, long baseKey) seg.writeByte((byte) NULL_MARKER); return; } - + // Calculate delta and zigzag encode (add 1 to reserve 0 for NULL) long delta = key - baseKey; long zigzag = zigzagEncode(delta) + 1; - + // Use direct varint write (single ensureCapacity call) seg.writeVarLong(zigzag); } - + /** - * Encode a node key as a delta from a base key, writing directly to a PooledGrowingSegment. This is - * the high-performance variant that eliminates per-byte capacity checks. + * Encode a node key as a delta from a base key, writing directly to a PooledGrowingSegment. + * This is the high-performance variant that eliminates per-byte capacity checks. * - * @param seg the segment to write to - * @param key the key to encode (may be NULL_NODE_KEY) + * @param seg the segment to write to + * @param key the key to encode (may be NULL_NODE_KEY) * @param baseKey the reference key for delta calculation */ public static void encodeDelta(PooledGrowingSegment seg, long key, long baseKey) { @@ -420,15 +394,15 @@ public static void encodeDelta(PooledGrowingSegment seg, long key, long baseKey) seg.writeByte((byte) NULL_MARKER); return; } - + // Calculate delta and zigzag encode (add 1 to reserve 0 for NULL) long delta = key - baseKey; long zigzag = zigzagEncode(delta) + 1; - + // Use direct varint write (single ensureCapacity call) seg.writeVarLong(zigzag); } - + /** * Encode an absolute key directly to a GrowingMemorySegment. * @@ -441,7 +415,7 @@ public static void encodeAbsolute(GrowingMemorySegment seg, long key) { } seg.writeVarLong(key); } - + /** * Encode an absolute key directly to a PooledGrowingSegment. * @@ -454,149 +428,147 @@ public static void encodeAbsolute(PooledGrowingSegment seg, long key) { } seg.writeVarLong(key); } - + /** * Encode a signed long value using zigzag + varint directly to a GrowingMemorySegment. * - * @param seg the segment to write to + * @param seg the segment to write to * @param value the signed long value */ public static void encodeSignedLong(GrowingMemorySegment seg, long value) { long zigzag = zigzagEncode(value); seg.writeVarLong(zigzag); } - + /** * Encode a signed long value using zigzag + varint directly to a PooledGrowingSegment. * - * @param seg the segment to write to + * @param seg the segment to write to * @param value the signed long value */ public static void encodeSignedLong(PooledGrowingSegment seg, long value) { long zigzag = zigzagEncode(value); seg.writeVarLong(zigzag); } - + /** * Encode an unsigned long value using varint directly to a GrowingMemorySegment. * - * @param seg the segment to write to + * @param seg the segment to write to * @param value the unsigned long value (must be non-negative) */ public static void encodeUnsignedLong(GrowingMemorySegment seg, long value) { seg.writeVarLong(value); } - + /** * Encode an unsigned long value using varint directly to a PooledGrowingSegment. * - * @param seg the segment to write to + * @param seg the segment to write to * @param value the unsigned long value (must be non-negative) */ public static void encodeUnsignedLong(PooledGrowingSegment seg, long value) { seg.writeVarLong(value); } - + // ==================== MEMORY SEGMENT SUPPORT ==================== // These methods support zero-allocation access by reading directly from MemorySegment - + /** - * Decode a delta-encoded key directly from MemorySegment. ZERO ALLOCATION - reads directly from - * memory. + * Decode a delta-encoded key directly from MemorySegment. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @param baseKey the reference key for delta calculation * @return the decoded key */ public static long decodeDeltaFromSegment(MemorySegment segment, int offset, long baseKey) { long zigzag = readVarLongFromSegment(segment, offset); - + if (zigzag == NULL_MARKER) { return NULL_KEY; } - + // Undo the +1 shift and zigzag decode long delta = zigzagDecode(zigzag - 1); return baseKey + delta; } - + /** - * Read unsigned varint from MemorySegment. ZERO ALLOCATION - reads directly from memory. + * Read unsigned varint from MemorySegment. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the decoded unsigned long value */ public static long readVarLongFromSegment(MemorySegment segment, int offset) { byte b = segment.get(ValueLayout.JAVA_BYTE, offset); - + // Fast path: 1 byte (0-127) - most common case for small deltas if ((b & 0x80) == 0) { return b; } - + long result = b & 0x7F; b = segment.get(ValueLayout.JAVA_BYTE, offset + 1); - + // Fast path: 2 bytes (128-16383) - second most common if ((b & 0x80) == 0) { return result | ((long) b << 7); } - + // General case for larger values (rare for structural keys) result |= (long) (b & 0x7F) << 7; int pos = offset + 2; int shift = 14; - while (true) { - if (shift >= Long.SIZE) { - throw new IllegalStateException("Varint too long (more than 10 bytes)"); - } - + do { b = segment.get(ValueLayout.JAVA_BYTE, pos++); - if (shift == Long.SIZE - 1 && (b & 0x7E) != 0) { - throw new IllegalStateException("Varint exceeds 64-bit range"); - } result |= (long) (b & 0x7F) << shift; - if ((b & 0x80) == 0) { - return result; - } shift += 7; - } - } + if (shift > 70) { + throw new IllegalStateException("Varint too long (more than 10 bytes)"); + } + } while ((b & 0x80) != 0); + + return result; + } + /** - * Read signed varint (zigzag decoded) from MemorySegment. ZERO ALLOCATION - reads directly from - * memory. + * Read signed varint (zigzag decoded) from MemorySegment. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the decoded signed integer value */ public static int decodeSignedFromSegment(MemorySegment segment, int offset) { long zigzag = readVarLongFromSegment(segment, offset); return (int) zigzagDecode(zigzag); } - + /** - * Read signed long (zigzag decoded) from MemorySegment. ZERO ALLOCATION - reads directly from - * memory. + * Read signed long (zigzag decoded) from MemorySegment. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the decoded signed long value */ public static long decodeSignedLongFromSegment(MemorySegment segment, int offset) { long zigzag = readVarLongFromSegment(segment, offset); return zigzagDecode(zigzag); } - + /** - * Get the byte length of a varint at the given offset. Used for computing field offsets during - * flyweight cursor parsing. ZERO ALLOCATION - reads directly from memory. + * Get the byte length of a varint at the given offset. + * Used for computing field offsets during flyweight cursor parsing. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the number of bytes this varint occupies */ public static int varintLength(MemorySegment segment, int offset) { @@ -606,32 +578,413 @@ public static int varintLength(MemorySegment segment, int offset) { } return len + 1; } - + /** - * Get length of a delta-encoded value (includes NULL check). ZERO ALLOCATION - reads directly from - * memory. + * Get length of a delta-encoded value (includes NULL check). + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the number of bytes this delta-encoded value occupies */ public static int deltaLength(MemorySegment segment, int offset) { byte first = segment.get(ValueLayout.JAVA_BYTE, offset); if (first == NULL_MARKER) { - return 1; // NULL is single byte + return 1; // NULL is single byte } return varintLength(segment, offset); } - + /** - * Read a fixed 8-byte long value from MemorySegment. Used for hash values which are stored as - * fixed-length. ZERO ALLOCATION - reads directly from memory. + * Read a fixed 8-byte long value from MemorySegment. + * Used for hash values which are stored as fixed-length. + * ZERO ALLOCATION - reads directly from memory. * * @param segment the MemorySegment containing the data - * @param offset the byte offset to start reading from + * @param offset the byte offset to start reading from * @return the 8-byte long value */ public static long readLongFromSegment(MemorySegment segment, int offset) { return segment.get(ValueLayout.JAVA_LONG_UNALIGNED, offset); } + + // ==================== DIRECT SEGMENT WRITE METHODS (for in-place mutation) ==================== + + /** + * Write a fixed 8-byte long value directly to a MemorySegment. + * Used for hash values which are always fixed-width, enabling in-place mutation. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment to write to + * @param offset the byte offset to write at + * @param value the 8-byte long value + */ + public static void writeLongToSegment(MemorySegment segment, long offset, long value) { + segment.set(ValueLayout.JAVA_LONG_UNALIGNED, offset, value); + } + + /** + * Compute the encoded byte width of a delta-encoded key. + * This is critical for in-place mutation: if the new width matches the old width, + * we can overwrite in-place without re-serializing the record. + * + * @param key the key to encode + * @param baseKey the reference key for delta calculation + * @return the number of bytes this delta would occupy + */ + public static int computeDeltaEncodedWidth(long key, long baseKey) { + if (key == NULL_KEY) { + return 1; + } + long delta = key - baseKey; + long zigzag = zigzagEncode(delta) + 1; + return varLongSize(zigzag); + } + + /** + * Compute the encoded byte width of a signed integer (zigzag + varint). + * + * @param value the signed integer value + * @return the number of bytes this value would occupy + */ + public static int computeSignedEncodedWidth(int value) { + long zigzag = zigzagEncode(value); + return varLongSize(zigzag); + } + + /** + * Compute the encoded byte width of a signed long (zigzag + varint). + * + * @param value the signed long value + * @return the number of bytes this value would occupy + */ + public static int computeSignedLongEncodedWidth(long value) { + long zigzag = zigzagEncode(value); + return varLongSize(zigzag); + } + + /** + * Write a delta-encoded key directly to a MemorySegment at the given offset. + * ZERO ALLOCATION - writes directly to page memory. + * + * @param segment the MemorySegment to write to + * @param offset the byte offset to start writing at + * @param key the key to encode (may be NULL_NODE_KEY) + * @param baseKey the reference key for delta calculation + * @return the number of bytes written + */ + public static int writeDeltaToSegment(MemorySegment segment, long offset, long key, long baseKey) { + if (key == NULL_KEY) { + segment.set(ValueLayout.JAVA_BYTE, offset, (byte) NULL_MARKER); + return 1; + } + + long delta = key - baseKey; + long zigzag = zigzagEncode(delta) + 1; + return writeVarLongToSegment(segment, offset, zigzag); + } + + /** + * Write a signed integer (zigzag + varint) directly to a MemorySegment. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment to write to + * @param offset the byte offset to start writing at + * @param value the signed integer value + * @return the number of bytes written + */ + public static int writeSignedToSegment(MemorySegment segment, long offset, int value) { + long zigzag = zigzagEncode(value); + return writeVarLongToSegment(segment, offset, zigzag); + } + + /** + * Write a signed long (zigzag + varint) directly to a MemorySegment. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment to write to + * @param offset the byte offset to start writing at + * @param value the signed long value + * @return the number of bytes written + */ + public static int writeSignedLongToSegment(MemorySegment segment, long offset, long value) { + long zigzag = zigzagEncode(value); + return writeVarLongToSegment(segment, offset, zigzag); + } + + /** + * Write an unsigned varint directly to a MemorySegment. + * ZERO ALLOCATION - optimized with fast paths for 1-2 byte values. + * + * @param segment the MemorySegment to write to + * @param offset the byte offset to start writing at + * @param value the unsigned long value + * @return the number of bytes written + */ + public static int writeVarLongToSegment(MemorySegment segment, long offset, long value) { + // Fast path for 1-byte values (0-127) + if ((value & ~0x7FL) == 0) { + segment.set(ValueLayout.JAVA_BYTE, offset, (byte) value); + return 1; + } + + // Fast path for 2-byte values (128-16383) + if ((value & ~0x3FFFL) == 0) { + segment.set(ValueLayout.JAVA_BYTE, offset, (byte) ((value & 0x7F) | 0x80)); + segment.set(ValueLayout.JAVA_BYTE, offset + 1, (byte) (value >>> 7)); + return 2; + } + + // General case + int bytesWritten = 0; + while ((value & ~0x7FL) != 0) { + segment.set(ValueLayout.JAVA_BYTE, offset + bytesWritten, (byte) ((value & 0x7F) | 0x80)); + value >>>= 7; + bytesWritten++; + } + segment.set(ValueLayout.JAVA_BYTE, offset + bytesWritten, (byte) value); + return bytesWritten + 1; + } + + /** + * Read the encoded width (number of bytes) of a delta-encoded value + * at the given offset in a MemorySegment. + * Equivalent to {@link #deltaLength} but uses long offset for large segments. + * + * @param segment the MemorySegment + * @param offset the byte offset + * @return the number of bytes this encoded value occupies + */ + public static int readDeltaEncodedWidth(MemorySegment segment, long offset) { + byte first = segment.get(ValueLayout.JAVA_BYTE, offset); + if (first == NULL_MARKER) { + return 1; + } + return readVarintWidth(segment, offset); + } + + /** + * Read the byte width of a varint at the given offset. + * Scans continuation bits without decoding the value. + * + * @param segment the MemorySegment + * @param offset the byte offset + * @return number of bytes the varint occupies + */ + public static int readVarintWidth(MemorySegment segment, long offset) { + int len = 0; + while ((segment.get(ValueLayout.JAVA_BYTE, offset + len) & 0x80) != 0) { + len++; + } + return len + 1; + } + + /** + * Read the byte width of a signed varint at the given offset. + * Signed values use zigzag encoding, but the byte-level representation + * is the same as unsigned varint. + * + * @param segment the MemorySegment + * @param offset the byte offset + * @return number of bytes the signed varint occupies + */ + public static int readSignedVarintWidth(MemorySegment segment, long offset) { + return readVarintWidth(segment, offset); + } + + // ==================== RAW-COPY FIELD RESIZE ==================== + + /** + * Encoder for a single field during raw-copy resize. + * Called to write the new value of the changed field into the target segment. + */ + @FunctionalInterface + public interface FieldEncoder { + /** + * Encode a field value into the target segment at the given absolute offset. + * + * @param target the target MemorySegment + * @param offset absolute byte offset to write at + * @return number of bytes written + */ + int encode(MemorySegment target, long offset); + } + + /** + * Resize a single field in a record by raw-copying unchanged fields and re-encoding + * only the changed field. This avoids the full unbind→re-serialize round-trip. + * + *

Uses three bulk {@link MemorySegment#copy} calls (AVX/SSE intrinsics on x86) + * for unchanged regions, plus one {@link FieldEncoder#encode} call for the changed field. + * The offset table is patched in-place to reflect the new field sizes. + * + *

Record Layout Assumed

+ *
+   * [nodeKind: 1 byte][offsetTable: fieldCount × 1 byte][data region: varint fields]
+   * 
+ * + * @param srcPage source page MemorySegment + * @param srcRecordBase absolute offset of the source record start + * @param srcRecordLen byte length of the source record (excluding DeweyID trailer) + * @param fieldCount number of fields in the offset table + * @param fieldIndex index of the field to change (0 to fieldCount-1) + * @param dstPage destination page MemorySegment (may be same as srcPage for different offset) + * @param dstRecordBase absolute offset to write the new record at + * @param encoder encodes the new field value + * @return total bytes written for the new record (excluding DeweyID trailer) + */ + public static int resizeField( + final MemorySegment srcPage, final long srcRecordBase, final int srcRecordLen, + final int fieldCount, final int fieldIndex, + final MemorySegment dstPage, final long dstRecordBase, + final FieldEncoder encoder) { + + // Offset table starts at recordBase + 1 (after nodeKind byte) + final long srcOffsetTable = srcRecordBase + 1; + final long srcDataRegion = srcRecordBase + 1 + fieldCount; + + // Read old field offsets from source offset table + final int oldFieldStart = srcPage.get(ValueLayout.JAVA_BYTE, srcOffsetTable + fieldIndex) & 0xFF; + final int oldFieldEnd; + if (fieldIndex + 1 < fieldCount) { + oldFieldEnd = srcPage.get(ValueLayout.JAVA_BYTE, srcOffsetTable + fieldIndex + 1) & 0xFF; + } else { + // Last field: ends at record boundary + oldFieldEnd = srcRecordLen - 1 - fieldCount; // subtract header (1) + offset table (fieldCount) + } + final int oldFieldLen = oldFieldEnd - oldFieldStart; + + // --- Step 1: Copy nodeKind byte --- + dstPage.set(ValueLayout.JAVA_BYTE, dstRecordBase, + srcPage.get(ValueLayout.JAVA_BYTE, srcRecordBase)); + + // --- Step 2: Reserve space for offset table (written after field data) --- + final long dstOffsetTable = dstRecordBase + 1; + final long dstDataRegion = dstRecordBase + 1 + fieldCount; + + // --- Step 3: Copy data before changed field --- + long dstPos = dstDataRegion; + if (oldFieldStart > 0) { + MemorySegment.copy(srcPage, srcDataRegion, dstPage, dstDataRegion, oldFieldStart); + dstPos += oldFieldStart; + } + + // --- Step 4: Encode the changed field --- + final int newFieldLen = encoder.encode(dstPage, dstPos); + dstPos += newFieldLen; + + // --- Step 5: Copy data after changed field --- + final int dataRegionLen = srcRecordLen - 1 - fieldCount; + final int tailLen = dataRegionLen - oldFieldEnd; + if (tailLen > 0) { + MemorySegment.copy(srcPage, srcDataRegion + oldFieldEnd, dstPage, dstPos, tailLen); + dstPos += tailLen; + } + + // --- Step 6: Patch offset table --- + final int widthDelta = newFieldLen - oldFieldLen; + + // Copy offsets for fields before the changed field (unchanged) + for (int i = 0; i < fieldIndex; i++) { + dstPage.set(ValueLayout.JAVA_BYTE, dstOffsetTable + i, + srcPage.get(ValueLayout.JAVA_BYTE, srcOffsetTable + i)); + } + + // Changed field offset is the same as old (data before it hasn't moved) + dstPage.set(ValueLayout.JAVA_BYTE, dstOffsetTable + fieldIndex, (byte) oldFieldStart); + + // Shift offsets for fields after the changed field + for (int i = fieldIndex + 1; i < fieldCount; i++) { + final int oldOff = srcPage.get(ValueLayout.JAVA_BYTE, srcOffsetTable + i) & 0xFF; + dstPage.set(ValueLayout.JAVA_BYTE, dstOffsetTable + i, (byte) (oldOff + widthDelta)); + } + + return (int) (dstPos - dstRecordBase); + } + + // ==================== LONG-OFFSET SEGMENT DECODE METHODS ==================== + + /** + * Decode a delta-encoded key from MemorySegment using long offset. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment + * @param offset the byte offset (long for large pages) + * @param baseKey the reference key for delta calculation + * @return the decoded absolute key + */ + public static long decodeDeltaFromSegment(MemorySegment segment, long offset, long baseKey) { + long zigzag = readVarLongFromSegment(segment, offset); + + if (zigzag == NULL_MARKER) { + return NULL_KEY; + } + + long delta = zigzagDecode(zigzag - 1); + return baseKey + delta; + } + + /** + * Read unsigned varint from MemorySegment using long offset. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment + * @param offset the byte offset (long) + * @return the decoded unsigned long value + */ + public static long readVarLongFromSegment(MemorySegment segment, long offset) { + byte b = segment.get(ValueLayout.JAVA_BYTE, offset); + + if ((b & 0x80) == 0) { + return b; + } + + long result = b & 0x7F; + b = segment.get(ValueLayout.JAVA_BYTE, offset + 1); + + if ((b & 0x80) == 0) { + return result | ((long) b << 7); + } + + result |= (long) (b & 0x7F) << 7; + long pos = offset + 2; + int shift = 14; + do { + b = segment.get(ValueLayout.JAVA_BYTE, pos++); + result |= (long) (b & 0x7F) << shift; + shift += 7; + + if (shift > 70) { + throw new IllegalStateException("Varint too long (more than 10 bytes)"); + } + } while ((b & 0x80) != 0); + + return result; + } + + /** + * Read signed integer (zigzag decoded) from MemorySegment using long offset. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment + * @param offset the byte offset (long) + * @return the decoded signed integer value + */ + public static int decodeSignedFromSegment(MemorySegment segment, long offset) { + long zigzag = readVarLongFromSegment(segment, offset); + return (int) zigzagDecode(zigzag); + } + + /** + * Read signed long (zigzag decoded) from MemorySegment using long offset. + * ZERO ALLOCATION. + * + * @param segment the MemorySegment + * @param offset the byte offset (long) + * @return the decoded signed long value + */ + public static long decodeSignedLongFromSegment(MemorySegment segment, long offset) { + long zigzag = readVarLongFromSegment(segment, offset); + return zigzagDecode(zigzag); + } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/MemorySegmentBytesOut.java b/bundles/sirix-core/src/main/java/io/sirix/node/MemorySegmentBytesOut.java index a5c5fbc01..4d8bc728c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/MemorySegmentBytesOut.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/MemorySegmentBytesOut.java @@ -4,8 +4,6 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import net.openhft.hashing.LongHashFunction; /** @@ -172,22 +170,6 @@ public BytesOut writeSegment(MemorySegment source, long sourceOff return this; } - @Override - public BytesOut write(long position, ByteBuffer buffer, int bufferPosition, int length) { - long oldPos = growingSegment.position(); - growingSegment.setPosition(position); - - byte[] temp = new byte[length]; - int oldBufferPos = buffer.position(); - buffer.position(bufferPosition); - buffer.get(temp); - buffer.position(oldBufferPos); - - write(temp); - growingSegment.setPosition(oldPos); - return this; - } - @Override public long position() { return growingSegment.position(); @@ -225,7 +207,9 @@ public long hashDirect(LongHashFunction hashFunction) { if (backingArray != null) { return hashFunction.hashBytes(backingArray, 0, growingSegment.getUsedSize()); } - return hashFunction.hashBytes(growingSegment.getUsedSegment().asByteBuffer()); + // Off-heap: hash directly from native address — no legacy ByteBuffer + final MemorySegment seg = growingSegment.getUsedSegment(); + return hashFunction.hashMemory(seg.address(), seg.byteSize()); } @Override @@ -246,9 +230,7 @@ public BytesOut clear() { @Override public Object underlyingObject() { - // Return a ByteBuffer view for compatibility with FileChannelWriter - // Set native byte order to match FileChannelWriter expectations - return growingSegment.getUsedSegment().asByteBuffer().order(ByteOrder.nativeOrder()); + return growingSegment.getUsedSegment(); } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/NodeKind.java b/bundles/sirix-core/src/main/java/io/sirix/node/NodeKind.java index 91030adb5..cf539bb73 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/NodeKind.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/NodeKind.java @@ -44,9 +44,6 @@ import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.delegates.StructNodeDelegate; import io.sirix.node.delegates.ValueNodeDelegate; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.NodeKindLayouts; -import io.sirix.node.layout.StructuralField; import io.sirix.node.interfaces.DataRecord; import io.sirix.node.interfaces.DeweyIdSerializer; import io.sirix.node.interfaces.StructNode; @@ -357,13 +354,10 @@ public void serialize(final BytesOut sink, final DataRecord record, final long childCount = config.storeChildCount() ? DeltaVarIntCodec.decodeSigned(source) : 0; - final long hash; final long descendantCount; if (config.hashType != HashType.NONE) { - hash = source.readLong(); descendantCount = DeltaVarIntCodec.decodeSigned(source); } else { - hash = 0; descendantCount = 0; } @@ -372,7 +366,7 @@ public void serialize(final BytesOut sink, final DataRecord record, source.read(value, 0, value.length); return new PINode(recordID, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - firstChildKey, lastChildKey, childCount, descendantCount, hash, pathNodeKey, prefixKey, localNameKey, uriKey, + firstChildKey, lastChildKey, childCount, descendantCount, 0, pathNodeKey, prefixKey, localNameKey, uriKey, value, isCompressed, resourceConfiguration.nodeHashFunction, deweyID, new QNm("")); } @@ -399,7 +393,6 @@ public void serialize(final BytesOut sink, final DataRecord record, DeltaVarIntCodec.encodeSigned(sink, (int) node.getChildCount()); } if (config.hashType != HashType.NONE) { - writeHash(sink, node.getHash()); DeltaVarIntCodec.encodeSigned(sink, (int) node.getDescendantCount()); } @@ -1104,16 +1097,13 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata + value) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; // Compression flag (1 byte: 0 = none, 1 = FSST) boolean isCompressed = source.readByte() == 1; int length = DeltaVarIntCodec.decodeSigned(source); byte[] value = new byte[length]; source.read(value); // Note: fsstSymbolTable will be set by the page after deserialization if needed - return new ObjectStringNode(recordID, parentKey, prevRev, lastModRev, hash, value, + return new ObjectStringNode(recordID, parentKey, prevRev, lastModRev, 0, value, resourceConfiguration.nodeHashFunction, deweyID, isCompressed, null); } @@ -1127,9 +1117,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata + value) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } // Compression flag (1 byte: 0 = none, 1 = FSST) sink.writeByte(node.isCompressed() ? (byte) 1 @@ -1165,10 +1152,7 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); boolean value = source.readBoolean(); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; - return new ObjectBooleanNode(recordID, parentKey, prevRev, lastModRev, hash, value, + return new ObjectBooleanNode(recordID, parentKey, prevRev, lastModRev, 0, value, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1183,9 +1167,6 @@ public void serialize(final BytesOut sink, final DataRecord record, DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); sink.writeBoolean(node.getValue()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } } @Override @@ -1212,11 +1193,8 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata + value) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; Number value = deserializeNumber(source); - return new ObjectNumberNode(recordID, parentKey, prevRev, lastModRev, hash, value, + return new ObjectNumberNode(recordID, parentKey, prevRev, lastModRev, 0, value, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1230,9 +1208,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata + value) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } serializeNumber(node.getValue(), sink); } @@ -1259,10 +1234,7 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; - return new ObjectNullNode(recordID, parentKey, prevRev, lastModRev, hash, resourceConfiguration.nodeHashFunction, + return new ObjectNullNode(recordID, parentKey, prevRev, lastModRev, 0, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1276,9 +1248,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } } @Override @@ -1307,16 +1276,13 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata + value) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; // Compression flag (1 byte: 0 = none, 1 = FSST) boolean isCompressed = source.readByte() == 1; int length = DeltaVarIntCodec.decodeSigned(source); byte[] value = new byte[length]; source.read(value); // Note: fsstSymbolTable will be set by the page after deserialization if needed - return new StringNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, hash, value, + return new StringNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, 0, value, resourceConfiguration.nodeHashFunction, deweyID, isCompressed, null); } @@ -1332,9 +1298,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata + value) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } // Compression flag (1 byte: 0 = none, 1 = FSST) sink.writeByte(node.isCompressed() ? (byte) 1 @@ -1372,10 +1335,7 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); boolean value = source.readBoolean(); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; - return new BooleanNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, hash, value, + return new BooleanNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, 0, value, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1392,9 +1352,6 @@ public void serialize(final BytesOut sink, final DataRecord record, DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); sink.writeBoolean(node.getValue()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } } @Override @@ -1423,11 +1380,8 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata + value) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; Number value = deserializeNumber(source); - return new NumberNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, hash, value, + return new NumberNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, 0, value, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1443,9 +1397,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata + value) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } serializeNumber(node.getValue(), sink); } @@ -1474,10 +1425,7 @@ public void serializeDeweyID(BytesOut sink, byte[] deweyID, byte[] nextDeweyI // LAZY FIELDS (metadata) int prevRev = DeltaVarIntCodec.decodeSigned(source); int lastModRev = DeltaVarIntCodec.decodeSigned(source); - long hash = resourceConfiguration.hashType != HashType.NONE - ? source.readLong() - : 0; - return new NullNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, hash, + return new NullNode(recordID, parentKey, prevRev, lastModRev, rightSiblingKey, leftSiblingKey, 0, resourceConfiguration.nodeHashFunction, deweyID); } @@ -1493,9 +1441,6 @@ public void serialize(final BytesOut sink, final DataRecord record, // LAZY FIELDS (metadata) DeltaVarIntCodec.encodeSigned(sink, node.getPreviousRevisionNumber()); DeltaVarIntCodec.encodeSigned(sink, node.getLastModifiedRevisionNumber()); - if (resourceConfiguration.hashType != HashType.NONE) { - sink.writeLong(node.getHash()); - } } @Override @@ -1720,25 +1665,6 @@ public byte getId() { return id; } - /** - * Fixed-slot layout contract for this node kind. - */ - public NodeKindLayout layoutDescriptor() { - return NodeKindLayouts.layoutFor(this); - } - - public boolean hasFixedSlotLayout() { - return layoutDescriptor().isFixedSlotSupported(); - } - - public int fixedSlotSizeInBytes() { - return layoutDescriptor().fixedSlotSizeInBytes(); - } - - public int offsetOfOrMinusOne(final StructuralField field) { - return layoutDescriptor().offsetOfOrMinusOne(field); - } - /** * Public method to get the related node based on the identifier. * diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/NullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/NullNode.java index a07aa6017..320fbc92d 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/NullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/NullNode.java @@ -29,7 +29,6 @@ import io.sirix.node.interfaces.immutable.ImmutableNode; import io.sirix.settings.Fixed; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/PooledBytesOut.java b/bundles/sirix-core/src/main/java/io/sirix/node/PooledBytesOut.java index 5500f9cef..7d871b995 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/PooledBytesOut.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/PooledBytesOut.java @@ -5,7 +5,7 @@ import java.lang.foreign.ValueLayout; import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.ByteBuffer; +import net.openhft.hashing.LongHashFunction; /** * A BytesOut implementation backed by a PooledGrowingSegment. @@ -172,22 +172,6 @@ public BytesOut writeSegment(MemorySegment source, long sourceOff return this; } - @Override - public BytesOut write(long position, ByteBuffer buffer, int bufferPosition, int length) { - long oldPos = segment.position(); - segment.position(position); - - byte[] temp = new byte[length]; - int oldBufferPos = buffer.position(); - buffer.position(bufferPosition); - buffer.get(temp); - buffer.position(oldBufferPos); - - write(temp); - segment.position(oldPos); - return this; - } - @Override public long position() { return segment.position(); @@ -226,6 +210,22 @@ public byte[] toByteArray() { return result; } + @Override + public long hashDirect(LongHashFunction hashFunction) { + final long len = segment.position(); + if (len == 0) { + return hashFunction.hashBytes(new byte[0]); + } + final MemorySegment seg = segment.getCurrentSegment(); + final Object heapBase = seg.heapBase().orElse(null); + if (heapBase instanceof byte[] backingArray) { + // Heap-backed: hash directly from backing array — zero allocation + return hashFunction.hashBytes(backingArray, 0, (int) len); + } + // Native segment: hash directly from native address — zero allocation + return hashFunction.hashMemory(seg.address(), len); + } + @Override public BytesIn bytesForRead() { return new MemorySegmentBytesIn(segment.getWrittenSlice()); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/delegates/NodeDelegate.java b/bundles/sirix-core/src/main/java/io/sirix/node/delegates/NodeDelegate.java index 89571a246..d8f5cc905 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/delegates/NodeDelegate.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/delegates/NodeDelegate.java @@ -246,6 +246,13 @@ public void setLastModifiedRevision(int lastModifiedRevision) { @Override public void setDeweyID(final SirixDeweyID id) { sirixDeweyID = id; + deweyIDData = null; + } + + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDData = bytes; + this.sirixDeweyID = null; } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableArrayNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableArrayNode.java index aa2205258..8a77c2497 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableArrayNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableArrayNode.java @@ -8,7 +8,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.ArrayNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableBooleanNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableBooleanNode.java index 9d215e06f..d2b664ac8 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableBooleanNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableBooleanNode.java @@ -9,7 +9,6 @@ import io.sirix.node.json.StringNode; import io.sirix.node.xml.TextNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableJsonDocumentRootNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableJsonDocumentRootNode.java index f4fee0020..716b3e676 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableJsonDocumentRootNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableJsonDocumentRootNode.java @@ -7,7 +7,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.JsonDocumentRootNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNullNode.java index 41564abd4..462578428 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNullNode.java @@ -8,7 +8,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.NullNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNumberNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNumberNode.java index caaa2dc7c..ded653ba2 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNumberNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableNumberNode.java @@ -7,7 +7,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.NumberNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectBooleanNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectBooleanNode.java index e59558f23..292bfeec9 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectBooleanNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectBooleanNode.java @@ -9,7 +9,6 @@ import io.sirix.node.json.StringNode; import io.sirix.node.xml.TextNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectKeyNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectKeyNode.java index 7f25e5948..d83c4c582 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectKeyNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectKeyNode.java @@ -10,7 +10,6 @@ import io.sirix.node.json.ObjectNode; import io.sirix.node.BytesOut; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNode.java index a1d098866..02789dd86 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNode.java @@ -7,7 +7,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.ObjectNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNullNode.java index bc9c50eea..63712c80f 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNullNode.java @@ -8,7 +8,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.json.ObjectNullNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNumberNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNumberNode.java index 4884471b0..1b80730c8 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNumberNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectNumberNode.java @@ -8,7 +8,6 @@ import io.sirix.node.json.NumberNode; import io.sirix.node.json.ObjectNumberNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectStringNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectStringNode.java index faa3fff3e..e422eb27b 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectStringNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableObjectStringNode.java @@ -11,7 +11,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import io.sirix.node.json.ObjectStringNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableStringNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableStringNode.java index f2f6da70c..c79f9a726 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableStringNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/json/ImmutableStringNode.java @@ -12,7 +12,6 @@ import io.sirix.node.json.StringNode; import io.sirix.node.xml.TextNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableAttributeNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableAttributeNode.java index 839b418d4..9dd14bd4c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableAttributeNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableAttributeNode.java @@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import io.sirix.node.xml.AttributeNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableComment.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableComment.java index 546b39099..483c844ad 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableComment.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableComment.java @@ -12,7 +12,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import io.sirix.node.xml.CommentNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableElement.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableElement.java index ac1f32aaf..406cb2c7d 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableElement.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableElement.java @@ -13,7 +13,6 @@ import io.brackit.query.atomic.QNm; import org.checkerframework.checker.nullness.qual.Nullable; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableNamespace.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableNamespace.java index b85c6970a..332e6fb7e 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableNamespace.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableNamespace.java @@ -12,7 +12,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import io.sirix.node.xml.NamespaceNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutablePI.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutablePI.java index 9e14d90a2..2ab706997 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutablePI.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutablePI.java @@ -14,7 +14,6 @@ import io.brackit.query.atomic.QNm; import org.checkerframework.checker.nullness.qual.Nullable; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableText.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableText.java index e0b5c7645..e87755628 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableText.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableText.java @@ -12,7 +12,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import io.sirix.node.xml.TextNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableXmlDocumentRootNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableXmlDocumentRootNode.java index c8076dd5c..60ae4395d 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableXmlDocumentRootNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/immutable/xml/ImmutableXmlDocumentRootNode.java @@ -11,7 +11,6 @@ import io.sirix.node.BytesOut; import io.sirix.node.xml.XmlDocumentRootNode; -import java.nio.ByteBuffer; import static java.util.Objects.requireNonNull; diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/FlyweightNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/FlyweightNode.java new file mode 100644 index 000000000..32d52ef04 --- /dev/null +++ b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/FlyweightNode.java @@ -0,0 +1,140 @@ +package io.sirix.node.interfaces; + +import java.lang.foreign.MemorySegment; + +import io.sirix.page.KeyValueLeafPage; + +/** + * Interface for nodes that support LeanStore-style flyweight binding to a slotted page MemorySegment. + * + *

Flyweight nodes can serialize themselves directly to a page heap (with per-record offset tables) + * and bind to an existing record in the heap for direct in-place reads/writes without Java object + * intermediation.

+ * + *

When bound, all getters/setters operate directly on the page MemorySegment via the offset table. + * When unbound, they operate on Java primitive fields (normal mode).

+ */ +public interface FlyweightNode extends DataRecord { + + /** + * Serialize this node to the target MemorySegment in the slotted page heap format: + * {@code [nodeKind:1][fieldOffsets:N×1byte][varint fields + hash + payload]}. + * + *

All Java primitive fields must be materialized before calling this method + * (i.e., if the node has lazy fields, they must be parsed first).

+ * + * @param target the target MemorySegment to write to + * @param offset the absolute byte offset to start writing at + * @return the total number of bytes written + */ + int serializeToHeap(MemorySegment target, long offset); + + /** + * Bind this node as a flyweight to a page MemorySegment. + * After binding, all getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + void bind(MemorySegment page, long recordBase, long nodeKey, int slotIndex); + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + void unbind(); + + /** + * Clear the page binding without materializing fields. + * Use this instead of {@link #unbind()} when all Java fields will be overwritten immediately + * after clearing (e.g., in factory bind methods that set all fields via setters). + * This avoids the cost of reading each field from the MemorySegment back to Java primitives + * only to have them overwritten a moment later. + */ + void clearBinding(); + + /** + * Check if this node is currently bound to a page MemorySegment. + * + * @return true if bound (flyweight mode), false if operating on Java primitives + */ + boolean isBound(); + + /** + * Check if this node is bound to a specific page MemorySegment. + * Used to detect cross-page bindings (e.g., bound to complete page but need to rebind to modified page). + * + * @param page the page MemorySegment to check against + * @return true if bound to the specified page + */ + boolean isBoundTo(MemorySegment page); + + /** + * Get the slot index this node is currently bound to. + * Only valid when {@link #isBound()} is true. + * + * @return the slot index in the page directory + */ + int getSlotIndex(); + + /** + * Estimate the serialized size of this record in bytes. + * Used to ensure the slotted page has enough space before serialization. + * Structural nodes return a small constant; value nodes add their payload size. + * + * @return conservative upper bound on serialized byte count + */ + default int estimateSerializedSize() { + return 256; + } + + /** + * Check if this node is a write-path singleton managed by a node factory. + * Write singletons are rebound per-access and must NOT be stored in records[]. + * + * @return true if this is a factory-managed write singleton + */ + default boolean isWriteSingleton() { + return false; + } + + /** + * Mark this node as a write-path singleton (or clear the mark). + * + * @param writeSingleton true to mark as write singleton + */ + default void setWriteSingleton(boolean writeSingleton) { + // Default no-op; concrete types override + } + + /** + * Get the owning KeyValueLeafPage for resize-in-place operations. + * Only valid when bound ({@link #isBound()} is true). + * + * @return the owner page, or null if not set + */ + default KeyValueLeafPage getOwnerPage() { + return null; + } + + /** + * Set the owning KeyValueLeafPage. + * Called after bind/serializeToHeap so resize-in-place can re-serialize on width changes. + * + * @param ownerPage the owner page + */ + default void setOwnerPage(KeyValueLeafPage ownerPage) { + // Default no-op; concrete write-singleton types override + } + + /** + * Create an independent snapshot copy of this node with all fields materialized. + * The snapshot is a non-singleton DataRecord that can be safely stored in records[] + * without singleton aliasing issues. + * + * @return a new DataRecord copy with all fields set + */ + DataRecord toSnapshot(); +} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/Node.java b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/Node.java index 3b2378ea6..d85de2056 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/Node.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/Node.java @@ -88,4 +88,17 @@ public interface Node extends ImmutableNode { * @param nodeKey the new node key */ void setNodeKey(long nodeKey); + + /** + * Set the raw DeweyID bytes without parsing. The full {@link SirixDeweyID} is reconstructed + * lazily on the first {@link io.sirix.node.interfaces.immutable.ImmutableNode#getDeweyID()} call. + * + *

Unlike {@link #setDeweyID(SirixDeweyID)}, this method does NOT trigger resize on bound + * flyweight nodes — it's used during binding when the bytes are already in the page trailer.

+ * + * @param deweyIdBytes the raw DeweyID bytes, or null to clear + */ + default void setDeweyIDBytes(final byte[] deweyIdBytes) { + setDeweyID(deweyIdBytes != null ? new SirixDeweyID(deweyIdBytes) : null); + } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/ReusableNodeProxy.java b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/ReusableNodeProxy.java deleted file mode 100644 index 83f3fa959..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/ReusableNodeProxy.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.sirix.node.interfaces; - -/** - * Marker for transaction-local node proxy instances that may be rebound and reused by node - * factories. - * - *

- * Records implementing this marker must never be retained as authoritative in-memory page state. - * Writers persist them directly to slot storage and avoid keeping object references in record - * arrays. - *

- */ -public interface ReusableNodeProxy { -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/immutable/ImmutableNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/immutable/ImmutableNode.java index 369701926..e04acde34 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/immutable/ImmutableNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/interfaces/immutable/ImmutableNode.java @@ -6,7 +6,6 @@ import io.sirix.node.BytesOut; import org.checkerframework.checker.nullness.qual.Nullable; -import java.nio.ByteBuffer; /** * An immutable node. diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractBooleanNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractBooleanNode.java index bbfa69cb0..a98ee6a6b 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractBooleanNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractBooleanNode.java @@ -74,16 +74,6 @@ public long computeHash(BytesOut bytes) { .writeLong(nodeDelegate.getParentKey()) .writeByte(nodeDelegate.getKind().getId()); - bytes.writeLong(structNodeDelegate.getChildCount()) - .writeLong(structNodeDelegate.getDescendantCount()) - .writeLong(structNodeDelegate.getLeftSiblingKey()) - .writeLong(structNodeDelegate.getRightSiblingKey()) - .writeLong(structNodeDelegate.getFirstChildKey()); - - if (structNodeDelegate.getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(structNodeDelegate.getLastChildKey()); - } - bytes.writeBoolean(boolValue); return bytes.hashDirect(nodeDelegate.getHashFunction()); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNullNode.java index 3124b72fa..0b50b36f3 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNullNode.java @@ -31,13 +31,11 @@ import io.sirix.node.delegates.NodeDelegate; import io.sirix.node.delegates.StructNodeDelegate; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; -import io.sirix.settings.Fixed; import io.sirix.node.Bytes; import io.sirix.node.BytesOut; import io.sirix.node.xml.AbstractStructForwardingNode; import org.checkerframework.checker.nullness.qual.NonNull; -import java.nio.ByteBuffer; public abstract class AbstractNullNode extends AbstractStructForwardingNode implements ImmutableJsonNode { private final StructNodeDelegate structNodeDelegate; @@ -58,16 +56,6 @@ public long computeHash(final BytesOut bytes) { .writeLong(nodeDelegate.getParentKey()) .writeByte(nodeDelegate.getKind().getId()); - bytes.writeLong(structNodeDelegate.getChildCount()) - .writeLong(structNodeDelegate.getDescendantCount()) - .writeLong(structNodeDelegate.getLeftSiblingKey()) - .writeLong(structNodeDelegate.getRightSiblingKey()) - .writeLong(structNodeDelegate.getFirstChildKey()); - - if (structNodeDelegate.getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(structNodeDelegate.getLastChildKey()); - } - return bytes.hashDirect(nodeDelegate.getHashFunction()); } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNumberNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNumberNode.java index de9c9beff..429d2175b 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNumberNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractNumberNode.java @@ -75,16 +75,6 @@ public long computeHash(final BytesOut bytes) { .writeLong(nodeDelegate.getParentKey()) .writeByte(nodeDelegate.getKind().getId()); - bytes.writeLong(structNodeDelegate.getChildCount()) - .writeLong(structNodeDelegate.getDescendantCount()) - .writeLong(structNodeDelegate.getLeftSiblingKey()) - .writeLong(structNodeDelegate.getRightSiblingKey()) - .writeLong(structNodeDelegate.getFirstChildKey()); - - if (structNodeDelegate.getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(structNodeDelegate.getLastChildKey()); - } - switch (number) { case Float floatVal -> bytes.writeFloat(floatVal); case Double doubleVal -> bytes.writeDouble(doubleVal); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractStringNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractStringNode.java index 011e2a6c7..f439c2f27 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractStringNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/AbstractStringNode.java @@ -39,7 +39,6 @@ import io.sirix.node.xml.AbstractStructForwardingNode; import org.checkerframework.checker.nullness.qual.NonNull; -import java.nio.ByteBuffer; public abstract class AbstractStringNode extends AbstractStructForwardingNode implements ValueNode, ImmutableJsonNode { @@ -64,8 +63,6 @@ public long computeHash(final BytesOut bytes) { .writeLong(nodeDelegate.getParentKey()) .writeByte(nodeDelegate.getKind().getId()); - bytes.writeLong(structNodeDelegate.getLeftSiblingKey()).writeLong(structNodeDelegate.getRightSiblingKey()); - bytes.writeUtf8(new String(valueNodeDelegate.getRawValue(), Constants.DEFAULT_ENCODING)); return bytes.hashDirect(nodeDelegate.getHashFunction()); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ArrayNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ArrayNode.java index bb254cf44..bcf977660 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ArrayNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ArrayNode.java @@ -43,14 +43,15 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableArrayNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; +import io.sirix.page.PageLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.index.qual.NonNegative; @@ -60,18 +61,16 @@ /** * JSON Array node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. This eliminates - * MemorySegment/VarHandle overhead and enables compact serialization. - *

+ *

Uses primitive fields for efficient storage with delta+varint encoding. + * This eliminates MemorySegment/VarHandle overhead and enables compact serialization.

* * @author Johannes Lichtenberger */ -public final class ArrayNode implements StructNode, ImmutableJsonNode, ReusableNodeProxy { +public final class ArrayNode implements StructNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; - + // Mutable structural fields (updated during tree modifications) private long parentKey; private long pathNodeKey; @@ -79,43 +78,78 @@ public final class ArrayNode implements StructNode, ImmutableJsonNode, ReusableN private long leftSiblingKey; private long firstChildKey; private long lastChildKey; - + // Mutable revision tracking private int previousRevision; private int lastModifiedRevision; - + // Mutable counters private long childCount; private long descendantCount; - + // Mutable hash private long hash; - + // Hash function for computing node hashes (mutable for singleton reuse) private LongHashFunction hashFunction; - + // DeweyID support (lazily parsed) private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; // Lazy parsing state private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; private boolean hasHash; private boolean storeChildCount; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.ARRAY_FIELD_COUNT; /** - * Primary constructor with all primitive fields. Used by deserialization - * (NodeKind.ARRAY.deserialize). + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config */ - public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, int lastModifiedRevision, - long rightSiblingKey, long leftSiblingKey, long firstChildKey, long lastChildKey, long childCount, - long descendantCount, long hash, LongHashFunction hashFunction, byte[] deweyID) { + public ArrayNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + + /** + * Primary constructor with all primitive fields. + * Used by deserialization (NodeKind.ARRAY.deserialize). + */ + public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + long lastChildKey, long childCount, long descendantCount, long hash, + LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.pathNodeKey = pathNodeKey; @@ -131,15 +165,17 @@ public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRev this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** - * Constructor with SirixDeweyID instead of byte array. Used by factory methods when creating new - * nodes. + * Constructor with SirixDeweyID instead of byte array. + * Used by factory methods when creating new nodes. */ - public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, int lastModifiedRevision, - long rightSiblingKey, long leftSiblingKey, long firstChildKey, long lastChildKey, long childCount, - long descendantCount, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + long lastChildKey, long childCount, long descendantCount, long hash, + LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.pathNodeKey = pathNodeKey; @@ -155,6 +191,234 @@ public ArrayNode(long nodeKey, long parentKey, long pathNodeKey, int previousRev this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.ARRAY_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.ARRAY_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.ARRAY_LEFT_SIB_KEY, nk); + this.firstChildKey = readDeltaField(NodeFieldLayout.ARRAY_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.ARRAY_LAST_CHILD_KEY, nk); + this.pathNodeKey = readDeltaField(NodeFieldLayout.ARRAY_PATH_NODE_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.ARRAY_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.ARRAY_LAST_MOD_REVISION); + this.hash = readLongField(NodeFieldLayout.ARRAY_HASH); + this.childCount = readSignedLongField(NodeFieldLayout.ARRAY_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.ARRAY_DESCENDANT_COUNT); + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode an ArrayNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param pathNodeKey the path node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param hash the hash value + * @param childCount the child count + * @param descendantCount the descendant count + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final long firstChildKey, final long lastChildKey, final long pathNodeKey, + final int prevRev, final int lastModRev, final long hash, + final long childCount, final long descendantCount) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.ARRAY.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: lastChildKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 5: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.ARRAY_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 6: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.ARRAY_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 7: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.ARRAY_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 8: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.ARRAY_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Field 9: childCount (signed long varint) + heapOffsets[NodeFieldLayout.ARRAY_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 10: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.ARRAY_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + firstChildKey, lastChildKey, pathNodeKey, + previousRevision, lastModifiedRevision, + hash, childCount, descendantCount); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -169,16 +433,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_PARENT_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -193,27 +479,79 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_PREV_REVISION, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_LAST_MOD_REVISION, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { + if (page != null) { + return readLongField(NodeFieldLayout.ARRAY_HASH); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -222,13 +560,22 @@ public long getHash() { @Override public void setHash(final long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); bytes.writeLong(getChildCount()) .writeLong(getDescendantCount()) @@ -247,45 +594,133 @@ public long computeHash(final BytesOut bytes) { @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_RIGHT_SIB_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_LEFT_SIB_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } public void setFirstChildKey(final long firstChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(firstChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, firstChild, nodeKey); + return; + } + resizeFirstChildKey(firstChild); + return; + } this.firstChildKey = firstChild; } + private void resizeFirstChildKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_FIRST_CHILD_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getLastChildKey() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_LAST_CHILD_KEY, nodeKey); } return lastChildKey; } public void setLastChildKey(final long lastChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(lastChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, lastChild, nodeKey); + return; + } + resizeLastChildKey(lastChild); + return; + } this.lastChildKey = lastChild; } + private void resizeLastChildKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_LAST_CHILD_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getChildCount() { + if (page != null) { + return readSignedLongField(NodeFieldLayout.ARRAY_CHILD_COUNT); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -293,11 +728,33 @@ public long getChildCount() { } public void setChildCount(final long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_CHILD_COUNT, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, value)); + } + @Override public long getDescendantCount() { + if (page != null) { + return readSignedLongField(NodeFieldLayout.ARRAY_DESCENDANT_COUNT); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -305,75 +762,104 @@ public long getDescendantCount() { } public void setDescendantCount(final long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_DESCENDANT_COUNT, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, value)); + } + public long getPathNodeKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ARRAY_PATH_NODE_KEY, nodeKey); + } return pathNodeKey; } public ArrayNode setPathNodeKey(final @NonNegative long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ARRAY_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return this; + } + resizePathNodeKey(pathNodeKey); + return this; + } this.pathNodeKey = pathNodeKey; return this; } + private void resizePathNodeKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ARRAY_PATH_NODE_KEY, NodeFieldLayout.ARRAY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasLastChild() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public void incrementChildCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - childCount--; + setChildCount(getChildCount() - 1); } @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.ARRAY_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -382,6 +868,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.ARRAY_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -398,11 +887,13 @@ public void setNodeKey(final long nodeKey) { } /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural fields immediately (NEW ORDER). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately (NEW ORDER). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -422,46 +913,24 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazyFieldsParsed = false; this.hasHash = config.hashType != HashType.NONE; this.storeChildCount = config.storeChildCount(); - + this.previousRevision = 0; this.lastModifiedRevision = 0; this.childCount = 0; this.hash = 0; this.descendantCount = 0; } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - + private void parseLazyFields() { if (lazyFieldsParsed) { return; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.lastChildKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.LAST_CHILD_KEY); - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - + if (lazySource == null) { lazyFieldsParsed = true; return; } - + BytesIn bytesIn; if (lazySource instanceof MemorySegment segment) { bytesIn = new MemorySegmentBytesIn(segment); @@ -472,12 +941,10 @@ private void parseLazyFields() { } else { throw new IllegalStateException("Unknown lazy source type: " + lazySource.getClass()); } - + this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - this.childCount = storeChildCount - ? DeltaVarIntCodec.decodeSigned(bytesIn) - : 0; + this.childCount = storeChildCount ? DeltaVarIntCodec.decodeSigned(bytesIn) : 0; if (hasHash) { this.hash = bytesIn.readLong(); this.descendantCount = DeltaVarIntCodec.decodeSigned(bytesIn); @@ -486,18 +953,44 @@ private void parseLazyFields() { } /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public ArrayNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new ArrayNode(nodeKey, + readDeltaField(NodeFieldLayout.ARRAY_PARENT_KEY, nodeKey), + readDeltaField(NodeFieldLayout.ARRAY_PATH_NODE_KEY, nodeKey), + readSignedField(NodeFieldLayout.ARRAY_PREV_REVISION), + readSignedField(NodeFieldLayout.ARRAY_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.ARRAY_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.ARRAY_LEFT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.ARRAY_FIRST_CHILD_KEY, nodeKey), + readDeltaField(NodeFieldLayout.ARRAY_LAST_CHILD_KEY, nodeKey), + readSignedLongField(NodeFieldLayout.ARRAY_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.ARRAY_DESCENDANT_COUNT), + readLongField(NodeFieldLayout.ARRAY_HASH), + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new ArrayNode(nodeKey, parentKey, pathNodeKey, previousRevision, lastModifiedRevision, rightSiblingKey, - leftSiblingKey, firstChildKey, lastChildKey, childCount, descendantCount, hash, hashFunction, - deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ArrayNode(nodeKey, parentKey, pathNodeKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, firstChildKey, lastChildKey, childCount, + descendantCount, hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -549,6 +1042,7 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ArrayNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey; + return nodeKey == other.nodeKey + && parentKey == other.parentKey; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/BooleanNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/BooleanNode.java index 15d42dcc1..3dbbf005a 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/BooleanNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/BooleanNode.java @@ -31,9 +31,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -43,15 +43,15 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableBooleanNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; import io.sirix.node.interfaces.BooleanValueNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -60,13 +60,11 @@ /** * JSON Boolean node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding.

+ * * @author Johannes Lichtenberger */ -public final class BooleanNode implements StructNode, ImmutableJsonNode, BooleanValueNode, ReusableNodeProxy { +public final class BooleanNode implements StructNode, ImmutableJsonNode, BooleanValueNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -95,19 +93,52 @@ public final class BooleanNode implements StructNode, ImmutableJsonNode, Boolean // Lazy parsing state (single-stage since boolean value is cheap) private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; - private boolean hasHash; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + private static final int FIELD_COUNT = NodeFieldLayout.BOOLEAN_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public BooleanNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public BooleanNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, boolean value, LongHashFunction hashFunction, byte[] deweyID) { + public BooleanNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + boolean value, LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -119,13 +150,15 @@ public BooleanNode(long nodeKey, long parentKey, int previousRevision, int lastM this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public BooleanNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, boolean value, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public BooleanNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + boolean value, LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -137,6 +170,208 @@ public BooleanNode(long nodeKey, long parentKey, int previousRevision, int lastM this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.BOOLVAL_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.BOOLVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION); + this.value = readByteField(NodeFieldLayout.BOOLVAL_VALUE) != 0; + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private byte readByteField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return page.get(ValueLayout.JAVA_BYTE, dataRegionStart + fieldOff); + } + + /** + * Resize a single field via raw-copy on the owning slotted page. + * Avoids the full unbind-to-primitives + re-serialize round-trip. + * + * @param fieldIndex the field index in the offset table (e.g. {@code BOOLVAL_PARENT_KEY}) + * @param encoder writes the new field value at the target offset + */ + private void resizeRecordField(final int fieldIndex, + final DeltaVarIntCodec.FieldEncoder encoder) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, fieldIndex, FIELD_COUNT, encoder); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a BooleanNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param boolValue the boolean value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev, final boolean boolValue) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.BOOLEAN_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.BOOLVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.BOOLVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 5: value (1 byte boolean) + heapOffsets[NodeFieldLayout.BOOLVAL_VALUE] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, (byte) (boolValue ? 1 : 0)); + pos += 1; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision, value); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -151,16 +386,33 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.BOOLVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.BOOLVAL_PARENT_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + return; + } this.parentKey = parentKey; } @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -175,31 +427,73 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.BOOLVAL_PREV_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.previousRevision = revision; } @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.lastModifiedRevision = revision; } @Override public long getHash() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -210,17 +504,9 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); - - bytes.writeLong(getChildCount()) - .writeLong(getDescendantCount()) - .writeLong(getLeftSiblingKey()) - .writeLong(getRightSiblingKey()) - .writeLong(getFirstChildKey()); - - if (getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(getLastChildKey()); - } + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); bytes.writeBoolean(getValue()); @@ -229,19 +515,53 @@ public long computeHash(final BytesOut bytes) { @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, rightSibling, nodeKey)); + return; + } this.rightSiblingKey = rightSibling; } @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, leftSibling, nodeKey)); + return; + } this.leftSiblingKey = leftSibling; } @@ -282,6 +602,9 @@ public void setDescendantCount(final long descendantCount) { } public boolean getValue() { + if (page != null) { + return readByteField(NodeFieldLayout.BOOLVAL_VALUE) != 0; + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -289,6 +612,13 @@ public boolean getValue() { } public void setValue(final boolean value) { + if (page != null) { + // Boolean is always 1 byte, always fits in-place + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.BOOLVAL_VALUE) & 0xFF; + page.set(ValueLayout.JAVA_BYTE, dataRegionStart + fieldOff, (byte) (value ? 1 : 0)); + return; + } this.value = value; } @@ -324,16 +654,19 @@ public void decrementDescendantCount() { @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.BOOLVAL_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -342,6 +675,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -358,7 +694,9 @@ public void setNodeKey(final long nodeKey) { } public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -373,38 +711,17 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.lazyFieldsParsed = false; - this.hasHash = config.hashType != HashType.NONE; - this.previousRevision = 0; this.lastModifiedRevision = 0; this.value = false; this.hash = 0; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - private void parseLazyFields() { if (lazyFieldsParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - if (lazySource == null) { lazyFieldsParsed = true; return; @@ -424,20 +741,39 @@ private void parseLazyFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.value = bytesIn.readBoolean(); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.lazyFieldsParsed = true; } public BooleanNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new BooleanNode(nodeKey, + readDeltaField(NodeFieldLayout.BOOLVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.BOOLVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.BOOLVAL_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.BOOLVAL_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.BOOLVAL_LEFT_SIB_KEY, nodeKey), + hash, + readByteField(NodeFieldLayout.BOOLVAL_VALUE) != 0, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new BooleanNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, value, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new BooleanNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, value, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -484,6 +820,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final BooleanNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && value == other.value; + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && value == other.value; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/JsonDocumentRootNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/JsonDocumentRootNode.java index 9a825d6a0..1ceac4bc8 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/JsonDocumentRootNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/JsonDocumentRootNode.java @@ -32,15 +32,15 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.json.ImmutableJsonDocumentRootNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; - import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -49,12 +49,12 @@ * Node representing the root of a JSON document. This node is guaranteed to exist in revision 0 and * cannot be removed. * - *

- * Uses primitive fields for efficient storage following the ObjectNode pattern. Document root has - * fixed values for nodeKey (0), parentKey (-1), and no siblings. - *

+ *

Uses primitive fields for efficient storage following the ObjectNode pattern. + * Document root has fixed values for nodeKey (0), parentKey (-1), and no siblings.

+ * + *

Supports flyweight binding to a page MemorySegment for zero-copy field access.

*/ -public final class JsonDocumentRootNode implements StructNode, ImmutableJsonNode { +public final class JsonDocumentRootNode implements StructNode, ImmutableJsonNode, FlyweightNode { // === STRUCTURAL FIELDS (immediate) === @@ -89,15 +89,47 @@ public final class JsonDocumentRootNode implements StructNode, ImmutableJsonNode /** DeweyID as bytes. */ private byte[] deweyIDBytes; - // Fixed-slot lazy support - private Object lazySource; - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean lazyFieldsParsed = true; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place operations. */ + private KeyValueLeafPage ownerPage; + + /** Reusable offset array for serializeToHeap (avoids allocation). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.JSON_DOCUMENT_ROOT_FIELD_COUNT; /** - * Primary constructor with all primitive fields. Used by deserialization - * (NodeKind.JSON_DOCUMENT.deserialize). + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public JsonDocumentRootNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + + /** + * Primary constructor with all primitive fields. + * Used by deserialization (NodeKind.JSON_DOCUMENT.deserialize). * * @param nodeKey the node key (always 0 for document root) * @param firstChildKey the first child key @@ -106,8 +138,8 @@ public final class JsonDocumentRootNode implements StructNode, ImmutableJsonNode * @param descendantCount the descendant count * @param hashFunction the hash function */ - public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, long childCount, - long descendantCount, LongHashFunction hashFunction) { + public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, + long childCount, long descendantCount, LongHashFunction hashFunction) { this.nodeKey = nodeKey; this.firstChildKey = firstChildKey; this.lastChildKey = lastChildKey; @@ -115,6 +147,7 @@ public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, this.descendantCount = descendantCount; this.hashFunction = hashFunction; this.deweyIDBytes = SirixDeweyID.newRootID().toBytes(); + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -128,8 +161,9 @@ public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, * @param hashFunction the hash function * @param deweyID the DeweyID */ - public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, long childCount, - long descendantCount, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, + long childCount, long descendantCount, LongHashFunction hashFunction, + SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.firstChildKey = firstChildKey; this.lastChildKey = lastChildKey; @@ -137,6 +171,184 @@ public JsonDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, this.descendantCount = descendantCount; this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.firstChildKey = readDeltaField(NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY, nk); + this.childCount = readSignedLongField(NodeFieldLayout.JDOCROOT_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT); + this.hash = readLongField(NodeFieldLayout.JDOCROOT_HASH); + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a JsonDocumentRootNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param childCount the child count + * @param descendantCount the descendant count + * @param hash the hash value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long firstChildKey, final long lastChildKey, + final long childCount, final long descendantCount, final long hash) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.JSON_DOCUMENT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: firstChildKey (delta-varint from nodeKey) + heapOffsets[NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 1: lastChildKey (delta-varint from nodeKey) + heapOffsets[NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 2: childCount (signed long varint) + heapOffsets[NodeFieldLayout.JDOCROOT_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 3: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Field 4: previousRevision (signed varint) -- always 0 for document root + heapOffsets[NodeFieldLayout.JDOCROOT_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); + + // Field 5: lastModifiedRevision (signed varint) -- always 0 for document root + heapOffsets[NodeFieldLayout.JDOCROOT_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); + + // Field 6: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.JDOCROOT_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + return writeNewRecord(target, offset, heapOffsets, nodeKey, + firstChildKey, lastChildKey, childCount, descendantCount, hash); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -201,83 +413,155 @@ public boolean hasLeftSibling() { @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } @Override public void setFirstChildKey(long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeFirstChildKey(key); + return; + } this.firstChildKey = key; } + private void resizeFirstChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLastChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY, nodeKey); + } return lastChildKey; } @Override public void setLastChildKey(long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLastChildKey(key); + return; + } this.lastChildKey = key; } + private void resizeLastChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLastChild() { - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.JDOCROOT_CHILD_COUNT); + } return childCount; } public void setChildCount(long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long childCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_CHILD_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, childCount)); + } + @Override public void incrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount--; + setChildCount(getChildCount() - 1); } @Override public long getDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT); + } return descendantCount; } @Override public void setDescendantCount(long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long descendantCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, descendantCount)); + } + @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override @@ -287,54 +571,103 @@ public long computeHash(BytesOut bytes) { } bytes.clear(); - bytes.writeLong(nodeKey).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(nodeKey) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); - bytes.writeLong(childCount) - .writeLong(descendantCount) + bytes.writeLong(getChildCount()) + .writeLong(getDescendantCount()) .writeLong(getLeftSiblingKey()) .writeLong(getRightSiblingKey()) - .writeLong(firstChildKey); + .writeLong(getFirstChildKey()); - if (lastChildKey != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(lastChildKey); + if (getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { + bytes.writeLong(getLastChildKey()); } - final var buffer = ((java.nio.ByteBuffer) bytes.underlyingObject()).rewind(); - buffer.limit((int) bytes.readLimit()); - - return hashFunction.hashBytes(buffer); + return bytes.hashDirect(hashFunction); } @Override public void setHash(long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.JDOCROOT_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public long getHash() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readLongField(NodeFieldLayout.JDOCROOT_HASH); + } return hash; } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.JDOCROOT_PREV_REVISION); + } return 0; // Document root is always in revision 0 } @Override public void setPreviousRevision(int revision) { - // Document root doesn't track previous revision + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } + // Document root doesn't track previous revision in primitive mode + } + + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_PREV_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); } @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.JDOCROOT_LAST_MOD_REVISION); + } return 0; } @Override public void setLastModifiedRevision(int revision) { - // Document root doesn't track last modified revision + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.JDOCROOT_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } + // Document root doesn't track last modified revision in primitive mode + } + + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.JDOCROOT_LAST_MOD_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); } @Override @@ -347,6 +680,19 @@ public void setTypeKey(int typeKey) { // Not supported for JSON nodes } + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; + } + + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + @Override public SirixDeweyID getDeweyID() { if (sirixDeweyID == null && deweyIDBytes != null) { @@ -371,12 +717,23 @@ public byte[] getDeweyIDAsBytes() { @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @@ -394,46 +751,19 @@ public LongHashFunction getHashFunction() { */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { - final long firstChildKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; + final long firstChildKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); this.nodeKey = Fixed.DOCUMENT_NODE_KEY.getStandardProperty(); this.firstChildKey = firstChildKey; - this.lastChildKey = firstChildKey; - this.childCount = firstChildKey == Fixed.NULL_NODE_KEY.getStandardProperty() - ? 0L - : 1L; + this.lastChildKey = firstChildKey; // lastChildKey same as firstChildKey for document root + this.childCount = firstChildKey == Fixed.NULL_NODE_KEY.getStandardProperty() ? 0L : 1L; this.descendantCount = DeltaVarIntCodec.decodeSignedLong(source); + this.hash = 0L; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; this.sirixDeweyID = null; - this.hash = 0L; - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - - private void parseLazyFields() { - if (lazyFieldsParsed) { - return; - } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - - this.lazyFieldsParsed = true; } /** @@ -442,10 +772,27 @@ private void parseLazyFields() { * @return a new instance with copied values */ public JsonDocumentRootNode toSnapshot() { - if (!lazyFieldsParsed) - parseLazyFields(); - final JsonDocumentRootNode snapshot = - new JsonDocumentRootNode(nodeKey, firstChildKey, lastChildKey, childCount, descendantCount, hashFunction); + if (page != null) { + // Bound mode: read all fields from page + final long nk = this.nodeKey; + final JsonDocumentRootNode snapshot = new JsonDocumentRootNode( + nk, + readDeltaField(NodeFieldLayout.JDOCROOT_FIRST_CHILD_KEY, nk), + readDeltaField(NodeFieldLayout.JDOCROOT_LAST_CHILD_KEY, nk), + readSignedLongField(NodeFieldLayout.JDOCROOT_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.JDOCROOT_DESCENDANT_COUNT), + hashFunction); + snapshot.hash = readLongField(NodeFieldLayout.JDOCROOT_HASH); + if (deweyIDBytes != null) { + snapshot.deweyIDBytes = deweyIDBytes.clone(); + } + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; + } + final JsonDocumentRootNode snapshot = new JsonDocumentRootNode( + nodeKey, firstChildKey, lastChildKey, childCount, descendantCount, hashFunction); snapshot.hash = this.hash; if (deweyIDBytes != null) { snapshot.deweyIDBytes = deweyIDBytes.clone(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/NullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/NullNode.java index de0ab91fd..a946cab2b 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/NullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/NullNode.java @@ -31,9 +31,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -43,14 +43,14 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableNullNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -59,13 +59,11 @@ /** * JSON Null node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding.

+ * * @author Johannes Lichtenberger */ -public final class NullNode implements StructNode, ImmutableJsonNode, ReusableNodeProxy { +public final class NullNode implements StructNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -91,19 +89,52 @@ public final class NullNode implements StructNode, ImmutableJsonNode, ReusableNo // Lazy parsing state private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; - private boolean hasHash; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + private static final int FIELD_COUNT = NodeFieldLayout.NULL_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public NullNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public NullNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, LongHashFunction hashFunction, byte[] deweyID) { + public NullNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -114,13 +145,15 @@ public NullNode(long nodeKey, long parentKey, int previousRevision, int lastModi this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public NullNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public NullNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -131,6 +164,184 @@ public NullNode(long nodeKey, long parentKey, int previousRevision, int lastModi this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.NULLVAL_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.NULLVAL_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.NULLVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.NULLVAL_LAST_MOD_REVISION); + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a NullNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.NULL_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.NULLVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.NULLVAL_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.NULLVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.NULLVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -145,16 +356,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NULLVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NULLVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NULLVAL_PARENT_KEY, NodeFieldLayout.NULL_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -169,31 +402,83 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NULLVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NULLVAL_PREV_REVISION, NodeFieldLayout.NULL_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NULLVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NULLVAL_LAST_MOD_REVISION, NodeFieldLayout.NULL_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (hash != 0L) { + return hash; + } + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); } - return hash; + return 0L; } @Override @@ -204,39 +489,75 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); - - bytes.writeLong(getChildCount()) - .writeLong(getDescendantCount()) - .writeLong(getLeftSiblingKey()) - .writeLong(getRightSiblingKey()) - .writeLong(getFirstChildKey()); - - if (getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(getLastChildKey()); - } + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); return bytes.hashDirect(hashFunction); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY, NodeFieldLayout.NULL_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NULLVAL_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NULLVAL_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NULLVAL_LEFT_SIB_KEY, NodeFieldLayout.NULL_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getFirstChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); @@ -305,16 +626,19 @@ public void decrementDescendantCount() { @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.NULLVAL_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -323,6 +647,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.NULLVAL_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -339,7 +666,9 @@ public void setNodeKey(final long nodeKey) { } public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -354,37 +683,16 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.lazyFieldsParsed = false; - this.hasHash = config.hashType != HashType.NONE; - this.previousRevision = 0; this.lastModifiedRevision = 0; this.hash = 0; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - private void parseLazyFields() { if (lazyFieldsParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - if (lazySource == null) { lazyFieldsParsed = true; return; @@ -403,20 +711,38 @@ private void parseLazyFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.lazyFieldsParsed = true; } public NullNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new NullNode(nodeKey, + readDeltaField(NodeFieldLayout.NULLVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.NULLVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.NULLVAL_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.NULLVAL_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.NULLVAL_LEFT_SIB_KEY, nodeKey), + hash, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new NullNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new NullNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -462,6 +788,7 @@ public boolean equals(final Object obj) { if (!(obj instanceof final NullNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey; + return nodeKey == other.nodeKey + && parentKey == other.parentKey; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/NumberNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/NumberNode.java index a2e7c8243..6d49c79f3 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/NumberNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/NumberNode.java @@ -34,6 +34,7 @@ import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -43,15 +44,15 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableNumberNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; import io.sirix.node.interfaces.NumericValueNode; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -63,62 +64,83 @@ /** * JSON Number node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

+ *

Uses primitive fields for efficient storage with delta+varint encoding.

* * @author Johannes Lichtenberger */ -public final class NumberNode implements StructNode, ImmutableJsonNode, NumericValueNode, ReusableNodeProxy { +public final class NumberNode implements StructNode, ImmutableJsonNode, NumericValueNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; - + // Mutable structural fields private long parentKey; private long rightSiblingKey; private long leftSiblingKey; - + // Mutable revision tracking private int previousRevision; private int lastModifiedRevision; - + // Mutable hash (computed on demand for value nodes) private long hash; - + // Number value private Number value; - + // Hash function for computing node hashes (mutable for singleton reuse) private LongHashFunction hashFunction; - + // DeweyID support (lazily parsed) private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; // Lazy parsing state (for singleton reuse optimization) // Two-stage lazy parsing: metadata (cheap) vs value (expensive Number allocation) - private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) - private long lazyOffset; // Offset where lazy metadata fields start - private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed - private boolean valueParsed; // Whether Number value is parsed - private boolean hasHash; // Whether hash is stored (from config) - private long valueOffset; // Offset where value starts (after metadata) + private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) + private long lazyOffset; // Offset where lazy metadata fields start + private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed + private boolean valueParsed; // Whether Number value is parsed + private boolean hasHash; // Whether hash is stored (from config) + private long valueOffset; // Offset where value starts (after metadata) - // Fixed-slot value encoding state (for read path via populateSingletonFromFixedSlot) - private boolean fixedValueEncoding; // Whether value comes from fixed-slot inline payload - private int fixedValueLength; // Length of inline payload bytes + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; - // Fixed-slot lazy metadata support - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.NUMBER_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public NumberNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** - * Primary constructor with all primitive fields. All fields are already parsed - no lazy loading - * needed. + * Primary constructor with all primitive fields. + * All fields are already parsed - no lazy loading needed. */ - public NumberNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, Number value, LongHashFunction hashFunction, byte[] deweyID) { + public NumberNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + Number value, LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -132,14 +154,16 @@ public NumberNode(long nodeKey, long parentKey, int previousRevision, int lastMo // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** - * Constructor with SirixDeweyID instead of byte array. All fields are already parsed - no lazy - * loading needed. + * Constructor with SirixDeweyID instead of byte array. + * All fields are already parsed - no lazy loading needed. */ - public NumberNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, Number value, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public NumberNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + Number value, LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -153,6 +177,299 @@ public NumberNode(long nodeKey, long parentKey, int previousRevision, int lastMo // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.metadataParsed = true; + this.valueParsed = false; + this.lazySource = null; + } + + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.NUMVAL_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.NUMVAL_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.NUMVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.NUMVAL_LAST_MOD_REVISION); + if (!valueParsed) { + readPayloadFromPage(); + } + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + public boolean isBound() { return page != null; } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override + public int estimateSerializedSize() { + // 1 (nodeKind) + 6 (offset table) + ~30 (varint fields avg) + ~10 (number payload) = ~47 + // Conservative upper bound without hash (was 56, minus 9 for removed 8-byte hash + 1-byte offset entry) + return 47; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + /** + * Read the Number payload from page memory when bound. + * Uses the flyweight format written by {@link #serializeNumberToSegment}. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + this.value = deserializeNumberFromSegment(page, payloadStart); + this.valueParsed = true; + } + + /** + * Deserialize a Number value directly from a MemorySegment. + * Format must match {@link #serializeNumberToSegment} exactly: + *
+   *   Type 0 = Integer (zigzag varint)
+   *   Type 1 = Long (zigzag varlong)
+   *   Type 2 = Float (4 bytes, native endian raw bits)
+   *   Type 3 = Double (8 bytes, native endian raw bits)
+   *   Type 4 = BigDecimal (varint scale + varint byte-length + bytes)
+   *   Type 5 = BigInteger (varint byte-length + bytes)
+   * 
+ * + * @param segment the MemorySegment containing the serialized number + * @param offset the byte offset of the number type byte + * @return the deserialized Number + */ + static Number deserializeNumberFromSegment(final MemorySegment segment, final long offset) { + final byte valueType = segment.get(ValueLayout.JAVA_BYTE, offset); + long pos = offset + 1; + + return switch (valueType) { + case 0 -> // Integer (zigzag varint) + DeltaVarIntCodec.decodeSignedFromSegment(segment, pos); + case 1 -> // Long (zigzag varlong) + DeltaVarIntCodec.decodeSignedLongFromSegment(segment, pos); + case 2 -> // Float (4 bytes raw bits, native endian) + Float.intBitsToFloat(segment.get(ValueLayout.JAVA_INT_UNALIGNED, pos)); + case 3 -> // Double (8 bytes raw bits, native endian) + Double.longBitsToDouble(segment.get(ValueLayout.JAVA_LONG_UNALIGNED, pos)); + case 4 -> { // BigDecimal (varint scale + varint byte-length + bytes) + final int scale = DeltaVarIntCodec.decodeSignedFromSegment(segment, pos); + final int scaleWidth = DeltaVarIntCodec.readSignedVarintWidth(segment, pos); + pos += scaleWidth; + final int bytesLen = DeltaVarIntCodec.decodeSignedFromSegment(segment, pos); + final int bytesLenWidth = DeltaVarIntCodec.readSignedVarintWidth(segment, pos); + pos += bytesLenWidth; + final byte[] bytes = new byte[bytesLen]; + MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, pos, bytes, 0, bytesLen); + yield new BigDecimal(new BigInteger(bytes), scale); + } + case 5 -> { // BigInteger (varint byte-length + bytes) + final int bytesLen = DeltaVarIntCodec.decodeSignedFromSegment(segment, pos); + final int bytesLenWidth = DeltaVarIntCodec.readSignedVarintWidth(segment, pos); + pos += bytesLenWidth; + final byte[] bytes = new byte[bytesLen]; + MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, pos, bytes, 0, bytesLen); + yield new BigInteger(bytes); + } + default -> throw new IllegalStateException("Unknown flyweight number type: " + valueType); + }; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a NumberNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param value the Number value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev, final Number value) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.NUMBER_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.NUMVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.NUMVAL_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.NUMVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.NUMVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 5: payload (Number type dispatch) + heapOffsets[NodeFieldLayout.NUMVAL_PAYLOAD] = (int) (pos - dataStart); + pos += serializeNumberToSegment(target, pos, value); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!metadataParsed) parseMetadataFields(); + if (!valueParsed) parseValueField(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision, value); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + /** + * Serialize a Number value directly to a MemorySegment. + * Format: [numberType:1][numberData:variable] + */ + private static int serializeNumberToSegment(final MemorySegment target, final long offset, + final Number number) { + long pos = offset; + switch (number) { + case Integer intVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 0); + pos++; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, intVal); + } + case Long longVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 1); + pos++; + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, longVal); + } + case Float floatVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 2); + pos++; + target.set(ValueLayout.JAVA_INT_UNALIGNED, pos, Float.floatToRawIntBits(floatVal)); + pos += Float.BYTES; + } + case Double doubleVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 3); + pos++; + target.set(ValueLayout.JAVA_LONG_UNALIGNED, pos, Double.doubleToRawLongBits(doubleVal)); + pos += Double.BYTES; + } + case BigDecimal bigDecimalVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 4); + pos++; + final byte[] bytes = bigDecimalVal.unscaledValue().toByteArray(); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, bigDecimalVal.scale()); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, bytes.length); + MemorySegment.copy(bytes, 0, target, ValueLayout.JAVA_BYTE, pos, bytes.length); + pos += bytes.length; + } + case BigInteger bigIntegerVal -> { + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 5); + pos++; + final byte[] bytes = bigIntegerVal.toByteArray(); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, bytes.length); + MemorySegment.copy(bytes, 0, target, ValueLayout.JAVA_BYTE, pos, bytes.length); + pos += bytes.length; + } + default -> throw new IllegalStateException("Unexpected number type: " + number.getClass()); + } + return (int) (pos - offset); } @Override @@ -167,16 +484,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NUMVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NUMVAL_PARENT_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -191,26 +530,83 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; + this.sirixDeweyID = null; + } + @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NUMVAL_PREV_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NUMVAL_LAST_MOD_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!metadataParsed) { - parseMetadataFields(); + if (hash != 0L) { + return hash; + } + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); } - return hash; + return 0L; } @Override @@ -221,17 +617,9 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); - - bytes.writeLong(getChildCount()) - .writeLong(getDescendantCount()) - .writeLong(getLeftSiblingKey()) - .writeLong(getRightSiblingKey()) - .writeLong(getFirstChildKey()); - - if (getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(getLastChildKey()); - } + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); final Number number = getValue(); switch (number) { @@ -249,27 +637,71 @@ public long computeHash(final BytesOut bytes) { @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long rightSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, rightSibling, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NUMVAL_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NUMVAL_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long leftSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NUMVAL_LEFT_SIB_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, leftSibling, nodeKey)); + } + @Override public long getFirstChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setFirstChildKey(final long firstChild) { // Value nodes are leaf nodes - no-op } @@ -278,7 +710,7 @@ public void setFirstChildKey(final long firstChild) { public long getLastChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setLastChildKey(final long lastChild) { // Value nodes are leaf nodes - no-op } @@ -287,7 +719,7 @@ public void setLastChildKey(final long lastChild) { public long getChildCount() { return 0; } - + public void setChildCount(final long childCount) { // Value nodes are leaf nodes - no-op } @@ -296,22 +728,32 @@ public void setChildCount(final long childCount) { public long getDescendantCount() { return 0; } - + public void setDescendantCount(final long descendantCount) { // Value nodes are leaf nodes - no-op } public Number getValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } return value; } public void setValue(final Number value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; - this.fixedValueEncoding = false; - this.valueParsed = true; } @Override @@ -346,16 +788,19 @@ public void decrementDescendantCount() { @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.NUMVAL_PREV_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -364,6 +809,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.NUMVAL_LAST_MOD_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -379,18 +827,15 @@ public void setNodeKey(final long nodeKey) { this.nodeKey = nodeKey; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; - this.sirixDeweyID = null; - } - /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural fields immediately. Two-stage lazy parsing: metadata (cheap) vs value (expensive - * Number allocation). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately. + * Two-stage lazy parsing: metadata (cheap) vs value (expensive Number allocation). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -408,73 +853,36 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.valueParsed = false; this.hasHash = config.hashType != HashType.NONE; this.valueOffset = 0; - + // Initialize lazy fields to defaults (will be populated on demand) this.previousRevision = 0; this.lastModifiedRevision = 0; this.hash = 0; this.value = null; } - + /** - * Populate this singleton from fixed-slot inline payload (zero allocation). Sets up lazy value - * parsing from the fixed-slot MemorySegment. CRITICAL: Resets hash to 0 — caller MUST call - * setHash() AFTER this method. - * - * @param source the slot data (MemorySegment) containing inline payload - * @param valueOffset byte offset within source where payload bytes start - * @param valueLength length of payload bytes + * Parse metadata fields on demand (cheap - just varints and optionally a long). + * Called by getters that access prevRev, lastModRev, or hash. */ - public void setLazyNumberValue(final Object source, final long valueOffset, final int valueLength) { - this.lazySource = source; - this.valueOffset = valueOffset; - this.metadataParsed = true; - this.valueParsed = false; - this.fixedValueEncoding = true; - this.fixedValueLength = valueLength; - this.value = null; - this.hash = 0L; - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - } - private void parseMetadataFields() { if (metadataParsed) { return; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - + if (lazySource == null) { metadataParsed = true; return; } - + BytesIn bytesIn = createBytesIn(lazyOffset); - + this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.valueOffset = bytesIn.position(); this.metadataParsed = true; } - + /** * Parse value field on demand (expensive - may allocate BigDecimal/BigInteger). */ @@ -482,33 +890,21 @@ private void parseValueField() { if (valueParsed) { return; } - - // Fixed-slot inline payload path (from setLazyNumberValue) - if (fixedValueEncoding) { - if (fixedValueLength > 0) { - final BytesIn bytesIn = createBytesIn(valueOffset); - this.value = NodeKind.deserializeNumber(bytesIn); - } else { - this.value = 0; - } - this.valueParsed = true; - return; - } - + if (!metadataParsed) { parseMetadataFields(); } - + if (lazySource == null) { valueParsed = true; return; } - - final BytesIn bytesIn = createBytesIn(valueOffset); + + BytesIn bytesIn = createBytesIn(valueOffset); this.value = NodeKind.deserializeNumber(bytesIn); this.valueParsed = true; } - + private BytesIn createBytesIn(long offset) { if (lazySource instanceof MemorySegment segment) { var bytesIn = new MemorySegmentBytesIn(segment); @@ -524,10 +920,25 @@ private BytesIn createBytesIn(long offset) { } /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public NumberNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new NumberNode(nodeKey, + readDeltaField(NodeFieldLayout.NUMVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.NUMVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.NUMVAL_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.NUMVAL_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.NUMVAL_LEFT_SIB_KEY, nodeKey), + hash, + value, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } // Force parse all lazy fields for snapshot (must be complete and independent) if (!metadataParsed) { parseMetadataFields(); @@ -535,10 +946,19 @@ public NumberNode toSnapshot() { if (!valueParsed) { parseValueField(); } - return new NumberNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, value, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new NumberNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, value, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -585,6 +1005,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final NumberNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && Objects.equal(value, other.value); + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && Objects.equal(value, other.value); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectBooleanNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectBooleanNode.java index 5e473dce3..94ae290c8 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectBooleanNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectBooleanNode.java @@ -31,9 +31,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -43,15 +43,15 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableObjectBooleanNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; import io.sirix.node.interfaces.BooleanValueNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; +import io.sirix.page.KeyValueLeafPage; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -60,13 +60,11 @@ /** * JSON Object Boolean node (direct child of ObjectKeyNode, no siblings). * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding.

+ * * @author Johannes Lichtenberger */ -public final class ObjectBooleanNode implements StructNode, ImmutableJsonNode, BooleanValueNode, ReusableNodeProxy { +public final class ObjectBooleanNode implements StructNode, ImmutableJsonNode, BooleanValueNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -93,19 +91,54 @@ public final class ObjectBooleanNode implements StructNode, ImmutableJsonNode, B // Lazy parsing state (single-stage since boolean value is cheap) private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; - private boolean hasHash; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_BOOLEAN_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ObjectBooleanNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - boolean value, LongHashFunction hashFunction, byte[] deweyID) { + public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, boolean value, + LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -115,13 +148,15 @@ public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, int this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - boolean value, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, boolean value, + LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -131,6 +166,201 @@ public ObjectBooleanNode(long nodeKey, long parentKey, int previousRevision, int this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== STATIC WRITE / HEAP OFFSETS / DEWEYID ==================== + + /** + * Encode an ObjectBooleanNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param boolValue the boolean value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final int prevRev, final int lastModRev, + final boolean boolValue) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT_BOOLEAN_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJBOOLVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJBOOLVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 2: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 3: value (1 byte boolean) + heapOffsets[NodeFieldLayout.OBJBOOLVAL_VALUE] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, (byte) (boolValue ? 1 : 0)); + pos += 1; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJBOOLVAL_PARENT_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJBOOLVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION); + this.value = readByteField(NodeFieldLayout.OBJBOOLVAL_VALUE) != 0; + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private byte readByteField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return page.get(ValueLayout.JAVA_BYTE, dataRegionStart + fieldOff); + } + + /** + * Resize a single field via raw-copy on the owning slotted page. + * Avoids the full unbind-to-primitives + re-serialize round-trip. + * + * @param fieldIndex the field index in the offset table (e.g. {@code OBJBOOLVAL_PARENT_KEY}) + * @param encoder writes the new field value at the target offset + */ + private void resizeRecordField(final int fieldIndex, + final DeltaVarIntCodec.FieldEncoder encoder) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, fieldIndex, FIELD_COUNT, encoder); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + * + * @param target the target MemorySegment + * @param offset the absolute byte offset to write at + * @return the total number of bytes written + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, previousRevision, lastModifiedRevision, value); } @Override @@ -145,16 +375,33 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJBOOLVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJBOOLVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.OBJBOOLVAL_PARENT_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + return; + } this.parentKey = parentKey; } @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -169,31 +416,73 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJBOOLVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJBOOLVAL_PREV_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.previousRevision = revision; } @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.lastModifiedRevision = revision; } @Override public long getHash() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -204,7 +493,9 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); bytes.writeLong(getChildCount()) .writeLong(getDescendantCount()) @@ -276,6 +567,9 @@ public void setDescendantCount(final long descendantCount) { } public boolean getValue() { + if (page != null) { + return readByteField(NodeFieldLayout.OBJBOOLVAL_VALUE) != 0; + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -283,6 +577,13 @@ public boolean getValue() { } public void setValue(final boolean value) { + if (page != null) { + // Boolean is always 1 byte, always fits in-place + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJBOOLVAL_VALUE) & 0xFF; + page.set(ValueLayout.JAVA_BYTE, dataRegionStart + fieldOff, (byte) (value ? 1 : 0)); + return; + } this.value = value; } @@ -320,6 +621,9 @@ public boolean hasRightSibling() { @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJBOOLVAL_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -328,6 +632,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -344,7 +651,9 @@ public void setNodeKey(final long nodeKey) { } public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -357,38 +666,17 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.lazyFieldsParsed = false; - this.hasHash = config.hashType != HashType.NONE; - this.previousRevision = 0; this.lastModifiedRevision = 0; this.value = false; this.hash = 0; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - private void parseLazyFields() { if (lazyFieldsParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - if (lazySource == null) { lazyFieldsParsed = true; return; @@ -408,20 +696,37 @@ private void parseLazyFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.value = bytesIn.readBoolean(); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.lazyFieldsParsed = true; } public ObjectBooleanNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new ObjectBooleanNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJBOOLVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJBOOLVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJBOOLVAL_LAST_MOD_REVISION), + hash, + readByteField(NodeFieldLayout.OBJBOOLVAL_VALUE) != 0, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new ObjectBooleanNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, hashFunction, - deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ObjectBooleanNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + hash, value, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -466,6 +771,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ObjectBooleanNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && value == other.value; + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && value == other.value; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectKeyNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectKeyNode.java index 5cdcb5737..f9e8de1c6 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectKeyNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectKeyNode.java @@ -43,15 +43,16 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableObjectKeyNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; +import io.sirix.page.PageLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.index.qual.NonNegative; @@ -60,58 +61,91 @@ /** * Node representing an object key/field. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

+ *

Uses primitive fields for efficient storage with delta+varint encoding.

*/ -public final class ObjectKeyNode implements StructNode, NameNode, ImmutableJsonNode, ReusableNodeProxy { +public final class ObjectKeyNode implements StructNode, NameNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; - + // Mutable structural fields private long parentKey; private long pathNodeKey; private long rightSiblingKey; private long leftSiblingKey; private long firstChildKey; - + // Name key (hash of the name string) private int nameKey; - + // Mutable revision tracking private int previousRevision; private int lastModifiedRevision; - + // Mutable hash and descendant count private long hash; private long descendantCount; - + // Hash function for computing node hashes (mutable for singleton reuse) private LongHashFunction hashFunction; - + // DeweyID support (lazily parsed) private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; - + // Cache for name (not serialized, only nameKey is) private QNm cachedName; // Lazy parsing state private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; private boolean hasHash; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_KEY_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ObjectKeyNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, int lastModifiedRevision, - long rightSiblingKey, long leftSiblingKey, long firstChildKey, int nameKey, long descendantCount, long hash, + public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + int nameKey, long descendantCount, long hash, LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -127,13 +161,15 @@ public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previou this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, int lastModifiedRevision, - long rightSiblingKey, long leftSiblingKey, long firstChildKey, int nameKey, long descendantCount, long hash, + public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + int nameKey, long descendantCount, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -149,6 +185,228 @@ public ObjectKeyNode(long nodeKey, long parentKey, long pathNodeKey, int previou this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJKEY_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.OBJKEY_LEFT_SIB_KEY, nk); + this.firstChildKey = readDeltaField(NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY, nk); + this.nameKey = readSignedField(NodeFieldLayout.OBJKEY_NAME_KEY); + this.pathNodeKey = readDeltaField(NodeFieldLayout.OBJKEY_PATH_NODE_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJKEY_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJKEY_LAST_MOD_REVISION); + this.hash = readLongField(NodeFieldLayout.OBJKEY_HASH); + this.descendantCount = readSignedLongField(NodeFieldLayout.OBJKEY_DESCENDANT_COUNT); + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode an ObjectKeyNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param firstChildKey the first child key + * @param nameKey the name key (hash of the name string) + * @param pathNodeKey the path node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param hash the hash value + * @param descendantCount the descendant count + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final long firstChildKey, final int nameKey, final long pathNodeKey, + final int prevRev, final int lastModRev, final long hash, + final long descendantCount) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT_KEY.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJKEY_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJKEY_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: nameKey (signed varint) + heapOffsets[NodeFieldLayout.OBJKEY_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, nameKey); + + // Field 5: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJKEY_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 6: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJKEY_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 7: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJKEY_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 8: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.OBJKEY_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Field 9: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.OBJKEY_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + firstChildKey, nameKey, pathNodeKey, + previousRevision, lastModifiedRevision, + hash, descendantCount); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -163,16 +421,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJKEY_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_PARENT_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -187,27 +467,79 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_PREV_REVISION, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_LAST_MOD_REVISION, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { + if (page != null) { + return readLongField(NodeFieldLayout.OBJKEY_HASH); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -216,13 +548,22 @@ public long getHash() { @Override public void setHash(final long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public long computeHash(BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); bytes.writeLong(getDescendantCount()) .writeLong(getLeftSiblingKey()) @@ -239,6 +580,9 @@ public long computeHash(BytesOut bytes) { } public int getNameKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJKEY_NAME_KEY); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -246,9 +590,28 @@ public int getNameKey() { } public void setNameKey(final int nameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(nameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, nameKey); + return; + } + resizeNameKey(nameKey); + return; + } this.nameKey = nameKey; } + private void resizeNameKey(final int nameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_NAME_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, nameKey)); + } + public QNm getName() { return cachedName; } @@ -257,13 +620,9 @@ public void setName(final String name) { this.cachedName = new QNm(name); } - public void clearCachedName() { - this.cachedName = null; - } - // NameNode interface methods public int getLocalNameKey() { - return nameKey; + return getNameKey(); } public int getPrefixKey() { @@ -283,10 +642,32 @@ public void setURIKey(final int uriKey) { } public void setLocalNameKey(final int localNameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(localNameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, localNameKey); + return; + } + resizeLocalNameKey(localNameKey); + return; + } this.nameKey = localNameKey; } + private void resizeLocalNameKey(final int localNameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_NAME_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, localNameKey)); + } + public long getPathNodeKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJKEY_PATH_NODE_KEY, nodeKey); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -294,40 +675,125 @@ public long getPathNodeKey() { } public void setPathNodeKey(final @NonNegative long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return; + } + resizePathNodeKey(pathNodeKey); + return; + } this.pathNodeKey = pathNodeKey; } + private void resizePathNodeKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_PATH_NODE_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJKEY_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_LEFT_SIB_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } public void setFirstChildKey(final long firstChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(firstChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, firstChild, nodeKey); + return; + } + resizeFirstChildKey(firstChild); + return; + } this.firstChildKey = firstChild; } + private void resizeFirstChildKey(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, value, nodeKey)); + } + @Override public long getLastChildKey() { // ObjectKeyNode only has one child (the value), so first == last - return firstChildKey; + return getFirstChildKey(); } public void setLastChildKey(final long lastChild) { @@ -356,6 +822,9 @@ public void decrementChildCount() { @Override public long getDescendantCount() { + if (page != null) { + return readSignedLongField(NodeFieldLayout.OBJKEY_DESCENDANT_COUNT); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -364,27 +833,43 @@ public long getDescendantCount() { @Override public void setDescendantCount(final long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJKEY_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long value) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJKEY_DESCENDANT_COUNT, NodeFieldLayout.OBJECT_KEY_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, value)); + } + @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJKEY_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -393,6 +878,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJKEY_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -405,22 +893,32 @@ public int getTypeKey() { @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasLastChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -454,11 +952,13 @@ public void setNodeKey(final long nodeKey) { } /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural fields immediately (NEW ORDER). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately (NEW ORDER). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -470,13 +970,13 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.rightSiblingKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); this.leftSiblingKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); this.firstChildKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); - + // Store for lazy parsing this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.lazyFieldsParsed = false; this.hasHash = config.hashType != HashType.NONE; - + this.nameKey = 0; this.pathNodeKey = 0; this.previousRevision = 0; @@ -484,38 +984,17 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.hash = 0; this.descendantCount = 0; } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - + private void parseLazyFields() { if (lazyFieldsParsed) { return; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.pathNodeKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.PATH_NODE_KEY); - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - + if (lazySource == null) { lazyFieldsParsed = true; return; } - + BytesIn bytesIn; if (lazySource instanceof MemorySegment segment) { bytesIn = new MemorySegmentBytesIn(segment); @@ -526,7 +1005,7 @@ private void parseLazyFields() { } else { throw new IllegalStateException("Unknown lazy source type: " + lazySource.getClass()); } - + this.nameKey = DeltaVarIntCodec.decodeSigned(bytesIn); this.pathNodeKey = DeltaVarIntCodec.decodeDelta(bytesIn, nodeKey); this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); @@ -539,23 +1018,44 @@ private void parseLazyFields() { } /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public ObjectKeyNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new ObjectKeyNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJKEY_PARENT_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJKEY_PATH_NODE_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJKEY_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJKEY_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.OBJKEY_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJKEY_LEFT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJKEY_FIRST_CHILD_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJKEY_NAME_KEY), + readSignedLongField(NodeFieldLayout.OBJKEY_DESCENDANT_COUNT), + readLongField(NodeFieldLayout.OBJKEY_HASH), + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new ObjectKeyNode(nodeKey, parentKey, pathNodeKey, previousRevision, lastModifiedRevision, rightSiblingKey, - leftSiblingKey, firstChildKey, nameKey, descendantCount, hash, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ObjectKeyNode(nodeKey, parentKey, pathNodeKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, firstChildKey, nameKey, descendantCount, hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); } public String toString() { - return "ObjectKeyNode{" + "nodeKey=" + nodeKey + ", parentKey=" + parentKey + ", nameKey=" + nameKey - + ", pathNodeKey=" + pathNodeKey + ", rightSiblingKey=" + rightSiblingKey + ", leftSiblingKey=" + leftSiblingKey - + ", firstChildKey=" + firstChildKey + '}'; + return "ObjectKeyNode{" + + "nodeKey=" + nodeKey + + ", parentKey=" + parentKey + + ", nameKey=" + nameKey + + ", pathNodeKey=" + pathNodeKey + + ", rightSiblingKey=" + rightSiblingKey + + ", leftSiblingKey=" + leftSiblingKey + + ", firstChildKey=" + firstChildKey + + '}'; } public static Funnel getFunnel() { diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNode.java index 9d8e0c91d..5fa13619e 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNode.java @@ -43,14 +43,14 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableObjectNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -59,14 +59,12 @@ /** * JSON Object node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. This eliminates - * MemorySegment/VarHandle overhead and enables compact serialization. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding. + * This eliminates MemorySegment/VarHandle overhead and enables compact serialization.

+ * * @author Johannes Lichtenberger */ -public final class ObjectNode implements StructNode, ImmutableJsonNode, ReusableNodeProxy { +public final class ObjectNode implements StructNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -98,21 +96,56 @@ public final class ObjectNode implements StructNode, ImmutableJsonNode, Reusable // Lazy parsing state (single-stage for metadata) private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; private boolean hasHash; private boolean storeChildCount; - // Fixed-slot lazy support (non-null = use SlotLayoutAccessors for cold fields) - private NodeKindLayout fixedSlotLayout; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_FIELD_COUNT; /** - * Primary constructor with all primitive fields. Used by deserialization - * (NodeKind.OBJECT.deserialize). + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config */ - public ObjectNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long firstChildKey, long lastChildKey, long childCount, long descendantCount, long hash, + public ObjectNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + + /** + * Primary constructor with all primitive fields. + * Used by deserialization (NodeKind.OBJECT.deserialize). + */ + public ObjectNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + long lastChildKey, long childCount, long descendantCount, long hash, LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -128,14 +161,16 @@ public ObjectNode(long nodeKey, long parentKey, int previousRevision, int lastMo this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** - * Constructor with SirixDeweyID instead of byte array. Used by factory methods when creating new - * nodes. + * Constructor with SirixDeweyID instead of byte array. + * Used by factory methods when creating new nodes. */ - public ObjectNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long firstChildKey, long lastChildKey, long childCount, long descendantCount, long hash, + public ObjectNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long firstChildKey, + long lastChildKey, long childCount, long descendantCount, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -151,6 +186,227 @@ public ObjectNode(long nodeKey, long parentKey, int previousRevision, int lastMo this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJECT_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.OBJECT_LEFT_SIB_KEY, nk); + this.firstChildKey = readDeltaField(NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.OBJECT_LAST_CHILD_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJECT_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJECT_LAST_MOD_REVISION); + this.hash = readLongField(NodeFieldLayout.OBJECT_HASH); + this.childCount = readSignedLongField(NodeFieldLayout.OBJECT_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.OBJECT_DESCENDANT_COUNT); + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode an ObjectNode record directly to a MemorySegment from parameter values. + * Static — reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param hash the hash value + * @param childCount the child count + * @param descendantCount the descendant count + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final long firstChildKey, final long lastChildKey, + final int prevRev, final int lastModRev, final long hash, + final long childCount, final long descendantCount) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJECT_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJECT_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJECT_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJECT_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: lastChildKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJECT_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 5: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJECT_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 6: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJECT_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 7: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.OBJECT_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Field 8: childCount (signed long varint) + heapOffsets[NodeFieldLayout.OBJECT_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 9: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.OBJECT_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + firstChildKey, lastChildKey, previousRevision, lastModifiedRevision, + hash, childCount, descendantCount); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer — this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -165,16 +421,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJECT_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_PARENT_KEY, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -189,27 +467,79 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; - this.deweyIDBytes = null; // Clear cached bytes + this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_PREV_REVISION, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_LAST_MOD_REVISION, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { + if (page != null) { + return readLongField(NodeFieldLayout.OBJECT_HASH); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -218,13 +548,22 @@ public long getHash() { @Override public void setHash(final long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); bytes.writeLong(getChildCount()) .writeLong(getDescendantCount()) @@ -241,45 +580,133 @@ public long computeHash(final BytesOut bytes) { @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long rightSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, rightSibling, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJECT_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long leftSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_LEFT_SIB_KEY, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, leftSibling, nodeKey)); + } + @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } public void setFirstChildKey(final long firstChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(firstChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, firstChild, nodeKey); + return; + } + resizeFirstChildKey(firstChild); + return; + } this.firstChildKey = firstChild; } + private void resizeFirstChildKey(final long firstChild) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, firstChild, nodeKey)); + } + @Override public long getLastChildKey() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJECT_LAST_CHILD_KEY, nodeKey); } return lastChildKey; } public void setLastChildKey(final long lastChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(lastChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, lastChild, nodeKey); + return; + } + resizeLastChildKey(lastChild); + return; + } this.lastChildKey = lastChild; } + private void resizeLastChildKey(final long lastChild) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_LAST_CHILD_KEY, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, lastChild, nodeKey)); + } + @Override public long getChildCount() { + if (page != null) { + return readSignedLongField(NodeFieldLayout.OBJECT_CHILD_COUNT); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -287,11 +714,33 @@ public long getChildCount() { } public void setChildCount(final long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long childCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_CHILD_COUNT, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, childCount)); + } + @Override public long getDescendantCount() { + if (page != null) { + return readSignedLongField(NodeFieldLayout.OBJECT_DESCENDANT_COUNT); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -299,66 +748,73 @@ public long getDescendantCount() { } public void setDescendantCount(final long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJECT_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long descendantCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJECT_DESCENDANT_COUNT, NodeFieldLayout.OBJECT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, descendantCount)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasLastChild() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public void incrementChildCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - childCount--; + setChildCount(getChildCount() - 1); } @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) { - parseLazyFields(); - } - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJECT_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -367,6 +823,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJECT_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -383,11 +842,13 @@ public void setNodeKey(final long nodeKey) { } /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural fields immediately (NEW ORDER). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately (NEW ORDER). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -415,19 +876,20 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d } /** - * Populate this node directly from a MemorySegment, bypassing BytesIn overhead. ZERO ALLOCATION - - * reads directly from memory segment. - * - * @param segment the MemorySegment containing the serialized node data (after kind byte) + * Populate this node directly from a MemorySegment, bypassing BytesIn overhead. + * ZERO ALLOCATION - reads directly from memory segment. + * + * @param segment the MemorySegment containing the serialized node data (after kind byte) * @param startOffset the byte offset within the segment to start reading - * @param nodeKey the node key - * @param deweyId the DeweyID bytes (may be null) + * @param nodeKey the node key + * @param deweyId the DeweyID bytes (may be null) * @param hashFunction the hash function - * @param config the resource configuration + * @param config the resource configuration * @return the byte offset after reading all structural fields (for lazy field position) */ public int readFromSegment(final MemorySegment segment, final int startOffset, final long nodeKey, - final byte[] deweyId, final LongHashFunction hashFunction, final ResourceConfiguration config) { + final byte[] deweyId, final LongHashFunction hashFunction, + final ResourceConfiguration config) { this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -467,38 +929,11 @@ public int readFromSegment(final MemorySegment segment, final int startOffset, f return offset; } - /** - * Bind this singleton for fixed-slot lazy cold-field reading. Hot fields (parentKey, siblings, - * firstChildKey) must already be set. Cold fields (hash, childCount, descendantCount, lastChildKey, - * prevRev, lastModRev) will be read on demand via parseLazyFields(). - */ - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - private void parseLazyFields() { if (lazyFieldsParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.lastChildKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.LAST_CHILD_KEY); - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - if (lazySource == null) { lazyFieldsParsed = true; return; @@ -517,9 +952,7 @@ private void parseLazyFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - this.childCount = storeChildCount - ? DeltaVarIntCodec.decodeSigned(bytesIn) - : 0; + this.childCount = storeChildCount ? DeltaVarIntCodec.decodeSigned(bytesIn) : 0; if (hasHash) { this.hash = bytesIn.readLong(); this.descendantCount = DeltaVarIntCodec.decodeSigned(bytesIn); @@ -528,17 +961,50 @@ private void parseLazyFields() { } /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public ObjectNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new ObjectNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJECT_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJECT_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJECT_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJECT_LEFT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, nodeKey), + readDeltaField(NodeFieldLayout.OBJECT_LAST_CHILD_KEY, nodeKey), + readSignedLongField(NodeFieldLayout.OBJECT_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.OBJECT_DESCENDANT_COUNT), + readLongField(NodeFieldLayout.OBJECT_HASH), + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new ObjectNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - firstChildKey, lastChildKey, childCount, descendantCount, hash, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ObjectNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, firstChildKey, lastChildKey, childCount, + descendantCount, hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public int estimateSerializedSize() { + // 1 (nodeKind) + 10 (offset table) + ~20 (varint fields avg) + 8 (hash) = ~39 + // Use conservative upper bound + return 1 + FIELD_COUNT + 10 * 2 + 8 + 2 * 2; + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -589,6 +1055,7 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ObjectNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey; + return nodeKey == other.nodeKey + && parentKey == other.parentKey; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNullNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNullNode.java index ef1391451..7684db2bd 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNullNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNullNode.java @@ -31,9 +31,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -41,31 +41,29 @@ import io.sirix.node.MemorySegmentBytesIn; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; - -import java.lang.foreign.MemorySegment; import io.sirix.node.immutable.json.ImmutableObjectNullNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + /** * JSON Object Null node (direct child of ObjectKeyNode, no siblings). * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding.

+ * * @author Johannes Lichtenberger */ -public final class ObjectNullNode implements StructNode, ImmutableJsonNode, ReusableNodeProxy { +public final class ObjectNullNode implements StructNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -89,18 +87,53 @@ public final class ObjectNullNode implements StructNode, ImmutableJsonNode, Reus // Lazy parsing state (single-stage since there's no value to parse) private Object lazySource; - private long lazyBaseOffset; private long lazyOffset; private boolean lazyFieldsParsed; - private boolean hasHash; - // Fixed-slot lazy support - private NodeKindLayout fixedSlotLayout; + + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_NULL_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ObjectNullNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, + public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -110,12 +143,14 @@ public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, int la this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, + public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -125,6 +160,182 @@ public ObjectNullNode(long nodeKey, long parentKey, int previousRevision, int la this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJNULLVAL_PARENT_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJNULLVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION); + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Resize a single field via raw-copy on the owning slotted page. + * Avoids the full unbind-to-primitives + re-serialize round-trip. + * + * @param fieldIndex the field index in the offset table (e.g. {@code OBJNULLVAL_PARENT_KEY}) + * @param encoder writes the new field value at the target offset + */ + private void resizeRecordField(final int fieldIndex, + final DeltaVarIntCodec.FieldEncoder encoder) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, fieldIndex, FIELD_COUNT, encoder); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode an ObjectNullNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final int prevRev, final int lastModRev) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT_NULL_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJNULLVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJNULLVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 2: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!lazyFieldsParsed) { + parseLazyFields(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, previousRevision, lastModifiedRevision); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -139,16 +350,33 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJNULLVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNULLVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.OBJNULLVAL_PARENT_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + return; + } this.parentKey = parentKey; } @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -161,31 +389,73 @@ public void setTypeKey(final int typeKey) {} @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; this.sirixDeweyID = null; } @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNULLVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJNULLVAL_PREV_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.previousRevision = revision; } @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.lastModifiedRevision = revision; } @Override public long getHash() { - if (!lazyFieldsParsed) { - parseLazyFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -196,7 +466,9 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); return bytes.hashDirect(hashFunction); } @@ -276,6 +548,9 @@ public boolean hasRightSibling() { @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJNULLVAL_PREV_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -284,6 +559,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION); + } if (!lazyFieldsParsed) { parseLazyFields(); } @@ -300,7 +578,9 @@ public void setNodeKey(final long nodeKey) { } public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -313,37 +593,17 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.lazyFieldsParsed = false; - this.hasHash = config.hashType != HashType.NONE; this.previousRevision = 0; this.lastModifiedRevision = 0; this.hash = 0; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - private void parseLazyFields() { if (lazyFieldsParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - if (lazySource == null) { lazyFieldsParsed = true; return; @@ -362,20 +622,36 @@ private void parseLazyFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.lazyFieldsParsed = true; } public ObjectNullNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new ObjectNullNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJNULLVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJNULLVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJNULLVAL_LAST_MOD_REVISION), + hash, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!lazyFieldsParsed) { parseLazyFields(); } - return new ObjectNullNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, hashFunction, - deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ObjectNullNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -419,6 +695,7 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ObjectNullNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey; + return nodeKey == other.nodeKey + && parentKey == other.parentKey; } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNumberNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNumberNode.java index 3bd0a0497..337a9dde5 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNumberNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectNumberNode.java @@ -31,27 +31,29 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; import io.sirix.node.DeltaVarIntCodec; import io.sirix.node.MemorySegmentBytesIn; +import io.sirix.node.MemorySegmentBytesOut; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableObjectNumberNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; import io.sirix.node.interfaces.NumericValueNode; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; +import io.sirix.page.PageLayout; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -63,13 +65,11 @@ /** * JSON Object Number node (direct child of ObjectKeyNode, no siblings). * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

- * + *

Uses primitive fields for efficient storage with delta+varint encoding.

+ * * @author Johannes Lichtenberger */ -public final class ObjectNumberNode implements StructNode, ImmutableJsonNode, NumericValueNode, ReusableNodeProxy { +public final class ObjectNumberNode implements StructNode, ImmutableJsonNode, NumericValueNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; @@ -99,22 +99,56 @@ public final class ObjectNumberNode implements StructNode, ImmutableJsonNode, Nu private long lazyOffset; private boolean metadataParsed; private boolean valueParsed; - private boolean hasHash; private long valueOffset; - // Fixed-slot value encoding state (for read path via populateSingletonFromFixedSlot) - private boolean fixedValueEncoding; // Whether value comes from fixed-slot inline payload - private int fixedValueLength; // Length of inline payload bytes + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_NUMBER_VALUE_FIELD_COUNT; + + /** Thread-local buffer for serializing Number payloads during serializeToHeap. */ + private static final ThreadLocal TL_NUMBER_BUFFER = + ThreadLocal.withInitial(() -> new MemorySegmentBytesOut(64)); - // Fixed-slot lazy metadata support - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ObjectNumberNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. */ - public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - Number value, LongHashFunction hashFunction, byte[] deweyID) { + public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, Number value, + LongHashFunction hashFunction, byte[] deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -125,13 +159,15 @@ public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, int this.deweyIDBytes = deweyID; this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID instead of byte array. */ - public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - Number value, LongHashFunction hashFunction, SirixDeweyID deweyID) { + public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, Number value, + LongHashFunction hashFunction, SirixDeweyID deweyID) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -142,6 +178,223 @@ public ObjectNumberNode(long nodeKey, long parentKey, int previousRevision, int this.sirixDeweyID = deweyID; this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== STATIC WRITE / HEAP OFFSETS / DEWEYID ==================== + + /** + * Encode an ObjectNumberNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param value the Number value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final int prevRev, final int lastModRev, + final Number value) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT_NUMBER_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJNUMVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJNUMVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 2: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 3: payload (number: [numberType:1][numberData:variable]) + heapOffsets[NodeFieldLayout.OBJNUMVAL_PAYLOAD] = (int) (pos - dataStart); + final MemorySegmentBytesOut numBuf = TL_NUMBER_BUFFER.get(); + numBuf.clear(); + NodeKind.serializeNumber(value, numBuf); + final int numBytes = (int) numBuf.position(); + final MemorySegment numSegment = numBuf.getDestination(); + MemorySegment.copy(numSegment, 0, target, pos, numBytes); + pos += numBytes; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.metadataParsed = true; // No lazy state when bound + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazySource = null; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJNUMVAL_PARENT_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJNUMVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION); + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Resize a single field via raw-copy on the owning slotted page. + * Avoids the full unbind-to-primitives + re-serialize round-trip. + * + * @param fieldIndex the field index in the offset table (e.g. {@code OBJNUMVAL_PARENT_KEY}) + * @param encoder writes the new field value at the target offset + */ + private void resizeRecordField(final int fieldIndex, + final DeltaVarIntCodec.FieldEncoder encoder) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, fieldIndex, FIELD_COUNT, encoder); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + /** + * Read the Number payload directly from page memory when bound. + * Uses a MemorySegmentBytesIn wrapper to invoke NodeKind.deserializeNumber. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNUMVAL_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + final MemorySegmentBytesIn bytesIn = new MemorySegmentBytesIn(page); + bytesIn.position(payloadStart); + this.value = NodeKind.deserializeNumber(bytesIn); + this.valueParsed = true; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + * + * @param target the target MemorySegment + * @param offset the absolute byte offset to write at + * @return the total number of bytes written + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!metadataParsed) { + parseMetadataFields(); + } + if (!valueParsed) { + parseValueField(); + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, previousRevision, lastModifiedRevision, value); } @Override @@ -156,16 +409,33 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJNUMVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNUMVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeRecordField(NodeFieldLayout.OBJNUMVAL_PARENT_KEY, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + return; + } this.parentKey = parentKey; } @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -178,26 +448,73 @@ public void setTypeKey(final int typeKey) {} @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; + this.sirixDeweyID = null; + } + @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNUMVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJNUMVAL_PREV_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.previousRevision = revision; } @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeRecordField(NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.lastModifiedRevision = revision; } @Override public long getHash() { - if (!metadataParsed) { - parseMetadataFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -208,7 +525,9 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); final Number number = getValue(); switch (number) { @@ -267,15 +586,27 @@ public long getDescendantCount() { public void setDescendantCount(final long descendantCount) {} public Number getValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } return value; } public void setValue(final Number value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.valueParsed = true; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; - this.fixedValueEncoding = false; this.valueParsed = true; } @@ -313,6 +644,9 @@ public boolean hasRightSibling() { @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJNUMVAL_PREV_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -321,6 +655,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -336,13 +673,10 @@ public void setNodeKey(final long nodeKey) { this.nodeKey = nodeKey; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; - this.sirixDeweyID = null; - } - public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -356,7 +690,6 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.lazyOffset = source.position(); this.metadataParsed = false; this.valueParsed = false; - this.hasHash = config.hashType != HashType.NONE; this.valueOffset = 0; this.previousRevision = 0; @@ -365,61 +698,20 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.value = null; } - /** - * Populate this singleton from fixed-slot inline payload (zero allocation). Sets up lazy value - * parsing from the fixed-slot MemorySegment. CRITICAL: Resets hash to 0 — caller MUST call - * setHash() AFTER this method. - * - * @param source the slot data (MemorySegment) containing inline payload - * @param valueOffset byte offset within source where payload bytes start - * @param valueLength length of payload bytes - */ - public void setLazyNumberValue(final Object source, final long valueOffset, final int valueLength) { - this.lazySource = source; - this.valueOffset = valueOffset; - this.metadataParsed = true; - this.valueParsed = false; - this.fixedValueEncoding = true; - this.fixedValueLength = valueLength; - this.value = null; - this.hash = 0L; - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - } - private void parseMetadataFields() { if (metadataParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - if (lazySource == null) { metadataParsed = true; return; } - final BytesIn bytesIn = createBytesIn(lazyOffset); + BytesIn bytesIn = createBytesIn(lazyOffset); this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.valueOffset = bytesIn.position(); this.metadataParsed = true; } @@ -429,18 +721,6 @@ private void parseValueField() { return; } - // Fixed-slot inline payload path (from setLazyNumberValue) - if (fixedValueEncoding) { - if (fixedValueLength > 0) { - final BytesIn bytesIn = createBytesIn(valueOffset); - this.value = NodeKind.deserializeNumber(bytesIn); - } else { - this.value = 0; - } - this.valueParsed = true; - return; - } - if (!metadataParsed) { parseMetadataFields(); } @@ -450,7 +730,7 @@ private void parseValueField() { return; } - final BytesIn bytesIn = createBytesIn(valueOffset); + BytesIn bytesIn = createBytesIn(valueOffset); this.value = NodeKind.deserializeNumber(bytesIn); this.valueParsed = true; } @@ -469,17 +749,44 @@ private BytesIn createBytesIn(long offset) { } } + /** + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. + */ public ObjectNumberNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new ObjectNumberNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJNUMVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJNUMVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJNUMVAL_LAST_MOD_REVISION), + hash, + value, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } if (!metadataParsed) { parseMetadataFields(); } if (!valueParsed) { parseValueField(); } - return new ObjectNumberNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, hashFunction, - deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return new ObjectNumberNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + hash, value, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -524,6 +831,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ObjectNumberNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && Objects.equal(value, other.value); + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && Objects.equal(value, other.value); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectStringNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectStringNode.java index e1b224d99..9b777d98a 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectStringNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/ObjectStringNode.java @@ -31,9 +31,9 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -43,17 +43,19 @@ import io.sirix.node.SirixDeweyID; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import io.sirix.node.immutable.json.ImmutableObjectStringNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; +import io.sirix.page.PageLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; +import io.sirix.settings.StringCompressionType; import io.sirix.utils.FSSTCompressor; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; @@ -62,33 +64,31 @@ /** * JSON Object String node (direct child of ObjectKeyNode, no siblings). * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

+ *

Uses primitive fields for efficient storage with delta+varint encoding.

* * @author Johannes Lichtenberger */ -public final class ObjectStringNode implements StructNode, ValueNode, ImmutableJsonNode, ReusableNodeProxy { +public final class ObjectStringNode implements StructNode, ValueNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; - + // Mutable structural fields (only parent, no siblings for object values) private long parentKey; - + // Mutable revision tracking private int previousRevision; private int lastModifiedRevision; - + // Mutable hash private long hash; - + // String value (stored as bytes) private byte[] value; - + // Hash function for computing node hashes (mutable for singleton reuse) private LongHashFunction hashFunction; - + // DeweyID support (lazily parsed) private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; @@ -98,44 +98,66 @@ public final class ObjectStringNode implements StructNode, ValueNode, ImmutableJ private boolean isCompressed; /** FSST symbol table for decompression (shared from KeyValueLeafPage) */ private byte[] fsstSymbolTable; - /** Pre-parsed FSST symbol table (avoids re-parsing on every decode) */ - private byte[][] parsedFsstSymbols; /** Decompressed value (lazy allocated on first access if compressed) */ private byte[] decodedValue; // Lazy parsing state (for singleton reuse optimization) // Two-stage lazy parsing: metadata (cheap) vs value (expensive byte[] allocation) - private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) - private long lazyOffset; // Offset where lazy metadata fields start - private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed - private boolean valueParsed; // Whether value byte[] is parsed - private boolean hasHash; // Whether hash is stored (from config) - private long valueOffset; // Offset where value starts (after metadata) - - // Fixed-slot value encoding state (for read path via populateSingletonFromFixedSlot) - private boolean fixedValueEncoding; // Whether value comes from fixed-slot inline payload - private int fixedValueLength; // Length of inline payload bytes - private boolean fixedValueCompressed; // Whether inline payload is FSST compressed - - // Fixed-slot lazy metadata support - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; + private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) + private long lazyOffset; // Offset where lazy metadata fields start + private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed + private boolean valueParsed; // Whether value byte[] is parsed + private long valueOffset; // Offset where value starts (after metadata) + + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.OBJECT_STRING_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ObjectStringNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** - * Primary constructor with all primitive fields. All fields are already parsed - no lazy loading - * needed. + * Primary constructor with all primitive fields. + * All fields are already parsed - no lazy loading needed. */ - public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - byte[] value, LongHashFunction hashFunction, byte[] deweyID) { - this(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, hashFunction, deweyID, false, null); + public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, byte[] value, + LongHashFunction hashFunction, byte[] deweyID) { + this(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, + hashFunction, deweyID, false, null); } /** - * Primary constructor with all primitive fields and compression support. All fields are already - * parsed - no lazy loading needed. + * Primary constructor with all primitive fields and compression support. + * All fields are already parsed - no lazy loading needed. */ - public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - byte[] value, LongHashFunction hashFunction, byte[] deweyID, boolean isCompressed, byte[] fsstSymbolTable) { + public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, byte[] value, + LongHashFunction hashFunction, byte[] deweyID, + boolean isCompressed, byte[] fsstSymbolTable) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -149,23 +171,28 @@ public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** - * Constructor with SirixDeweyID instead of byte array. All fields are already parsed - no lazy - * loading needed. + * Constructor with SirixDeweyID instead of byte array. + * All fields are already parsed - no lazy loading needed. */ - public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID) { - this(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, hashFunction, deweyID, false, null); + public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, byte[] value, + LongHashFunction hashFunction, SirixDeweyID deweyID) { + this(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value, + hashFunction, deweyID, false, null); } /** - * Constructor with SirixDeweyID and compression support. All fields are already parsed - no lazy - * loading needed. + * Constructor with SirixDeweyID and compression support. + * All fields are already parsed - no lazy loading needed. */ - public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long hash, - byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID, boolean isCompressed, byte[] fsstSymbolTable) { + public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long hash, byte[] value, + LongHashFunction hashFunction, SirixDeweyID deweyID, + boolean isCompressed, byte[] fsstSymbolTable) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -179,6 +206,7 @@ public ObjectStringNode(long nodeKey, long parentKey, int previousRevision, int // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } @Override @@ -193,16 +221,34 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.OBJSTRVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJSTRVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJSTRVAL_PARENT_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + return; + } this.parentKey = parentKey; } @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -215,26 +261,75 @@ public void setTypeKey(final int typeKey) {} @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; + this.sirixDeweyID = null; + } + @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJSTRVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJSTRVAL_PREV_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.previousRevision = revision; } @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + return; + } this.lastModifiedRevision = revision; } @Override public long getHash() { - if (!metadataParsed) { - parseMetadataFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -245,9 +340,14 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); - bytes.writeUtf8(getValue()); + final byte[] rawValue = getRawValue(); + if (rawValue != null) { + bytes.write(rawValue); + } return bytes.hashDirect(hashFunction); } @@ -256,67 +356,68 @@ public long computeHash(final BytesOut bytes) { public long getRightSiblingKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setRightSiblingKey(final long rightSibling) {} @Override public long getLeftSiblingKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setLeftSiblingKey(final long leftSibling) {} @Override public long getFirstChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setFirstChildKey(final long firstChild) {} @Override public long getLastChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setLastChildKey(final long lastChild) {} @Override public long getChildCount() { return 0; } - + public void setChildCount(final long childCount) {} @Override public long getDescendantCount() { return 0; } - + public void setDescendantCount(final long descendantCount) {} @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } - // If compressed, decode on first access using pre-parsed symbols when available + // If compressed, decode on first access if (isCompressed && decodedValue == null && value != null) { - final byte[][] parsed = parsedFsstSymbols; - decodedValue = (parsed != null) ? FSSTCompressor.decode(value, parsed) : FSSTCompressor.decode(value, fsstSymbolTable); + decodedValue = FSSTCompressor.decode(value, fsstSymbolTable); } - return isCompressed - ? decodedValue - : value; + return isCompressed ? decodedValue : value; } /** - * Get the raw (possibly compressed) value bytes without FSST decoding. Use this for serialization - * to preserve compression. - * + * Get the raw (possibly compressed) value bytes without FSST decoding. + * Use this for serialization to preserve compression. + * * @return the raw bytes as stored, possibly FSST compressed */ public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } return value; @@ -324,25 +425,49 @@ public byte[] getRawValueWithoutDecompression() { @Override public void setRawValue(final byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.decodedValue = null; + this.valueParsed = true; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.decodedValue = null; - this.fixedValueEncoding = false; this.valueParsed = true; } /** * Set the raw value with compression information. - * + * * @param value the value bytes (possibly compressed) * @param isCompressed true if value is FSST compressed * @param fsstSymbolTable the symbol table for decompression (or null if not compressed) */ public void setRawValue(final byte[] value, boolean isCompressed, byte[] fsstSymbolTable) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.isCompressed = isCompressed; + this.fsstSymbolTable = fsstSymbolTable; + this.decodedValue = null; + this.valueParsed = true; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.isCompressed = isCompressed; this.fsstSymbolTable = fsstSymbolTable; this.decodedValue = null; - this.fixedValueEncoding = false; this.valueParsed = true; } @@ -352,12 +477,15 @@ public void setRawValue(final byte[] value, boolean isCompressed, byte[] fsstSym * @return true if compressed */ public boolean isCompressed() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } return isCompressed; } /** * Set compression state. - * + * * @param isCompressed true if value is compressed */ public void setCompressed(boolean isCompressed) { @@ -375,24 +503,11 @@ public byte[] getFsstSymbolTable() { /** * Set the FSST symbol table. - * + * * @param fsstSymbolTable the symbol table */ public void setFsstSymbolTable(byte[] fsstSymbolTable) { this.fsstSymbolTable = fsstSymbolTable; - this.parsedFsstSymbols = null; - this.decodedValue = null; - } - - /** - * Set the FSST symbol table with pre-parsed symbols to avoid redundant parsing. - * - * @param fsstSymbolTable the raw symbol table bytes - * @param parsedFsstSymbols the pre-parsed symbol arrays - */ - public void setFsstSymbolTable(byte[] fsstSymbolTable, byte[][] parsedFsstSymbols) { - this.fsstSymbolTable = fsstSymbolTable; - this.parsedFsstSymbols = parsedFsstSymbols; this.decodedValue = null; } @@ -437,6 +552,9 @@ public boolean hasRightSibling() { @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJSTRVAL_PREV_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -445,6 +563,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -460,18 +581,15 @@ public void setNodeKey(final long nodeKey) { this.nodeKey = nodeKey; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; - this.sirixDeweyID = null; - } - /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural field (parentKey) immediately. Two-stage lazy parsing: metadata (cheap) vs value - * (expensive byte[] allocation). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural field (parentKey) immediately. + * Two-stage lazy parsing: metadata (cheap) vs value (expensive byte[] allocation). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -479,87 +597,42 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d // STRUCTURAL FIELD - parse immediately (parentKey is the only one for leaf nodes) this.parentKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); - + // Store state for lazy parsing - DON'T parse remaining fields yet this.lazySource = source.getSource(); this.lazyOffset = source.position(); this.metadataParsed = false; this.valueParsed = false; - this.hasHash = config.hashType != HashType.NONE; this.valueOffset = 0; - + // Initialize lazy fields to defaults (will be populated on demand) this.previousRevision = 0; this.lastModifiedRevision = 0; this.hash = 0; this.value = null; } - + /** - * Populate this singleton from fixed-slot inline payload (zero allocation). Sets up lazy value - * parsing from the fixed-slot MemorySegment. CRITICAL: Resets hash to 0 — caller MUST call - * setHash() AFTER this method. - * - * @param source the slot data (MemorySegment) containing inline payload - * @param valueOffset byte offset within source where payload bytes start - * @param valueLength length of payload bytes - * @param compressed whether the payload is FSST compressed + * Parse metadata fields on demand (cheap - just varints and optionally a long). */ - public void setLazyRawValue(final Object source, final long valueOffset, final int valueLength, - final boolean compressed) { - this.lazySource = source; - this.valueOffset = valueOffset; - this.metadataParsed = true; - this.valueParsed = false; - this.fixedValueEncoding = true; - this.fixedValueLength = valueLength; - this.fixedValueCompressed = compressed; - this.value = null; - this.fsstSymbolTable = null; - this.parsedFsstSymbols = null; - this.decodedValue = null; - this.hash = 0L; - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - } - private void parseMetadataFields() { if (metadataParsed) { return; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - + if (lazySource == null) { metadataParsed = true; return; } - + BytesIn bytesIn = createBytesIn(lazyOffset); - + this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.valueOffset = bytesIn.position(); this.metadataParsed = true; } - + /** * Parse value field on demand (expensive - allocates byte[]). */ @@ -567,40 +640,28 @@ private void parseValueField() { if (valueParsed) { return; } - - // Fixed-slot inline payload path (from setLazyRawValue) - if (fixedValueEncoding) { - final BytesIn bytesIn = createBytesIn(valueOffset); - this.isCompressed = fixedValueCompressed; - this.value = new byte[fixedValueLength]; - if (fixedValueLength > 0) { - bytesIn.read(this.value, 0, fixedValueLength); - } - this.valueParsed = true; - return; - } - + if (!metadataParsed) { parseMetadataFields(); } - + if (lazySource == null) { valueParsed = true; return; } - - final BytesIn bytesIn = createBytesIn(valueOffset); - + + BytesIn bytesIn = createBytesIn(valueOffset); + // Read compression flag (1 byte: 0 = none, 1 = FSST) - final byte compressionByte = bytesIn.readByte(); + byte compressionByte = bytesIn.readByte(); this.isCompressed = compressionByte == 1; - - final int length = DeltaVarIntCodec.decodeSigned(bytesIn); + + int length = DeltaVarIntCodec.decodeSigned(bytesIn); this.value = new byte[length]; bytesIn.read(this.value); this.valueParsed = true; } - + private BytesIn createBytesIn(long offset) { if (lazySource instanceof MemorySegment segment) { var bytesIn = new MemorySegmentBytesIn(segment); @@ -615,26 +676,262 @@ private BytesIn createBytesIn(long offset) { } } + // ==================== STATIC WRITE / HEAP OFFSETS / DEWEYID ==================== + + /** + * Encode an ObjectStringNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param rawValue the raw string value bytes (possibly compressed) + * @param isCompressed whether the value is FSST compressed + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final int prevRev, final int lastModRev, + final byte[] rawValue, final boolean isCompressed) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.OBJECT_STRING_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.OBJSTRVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJSTRVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 2: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 3: payload [isCompressed:1][valueLength:varint][value:bytes] + heapOffsets[NodeFieldLayout.OBJSTRVAL_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, isCompressed ? (byte) 1 : (byte) 0); + pos++; + final byte[] valBytes = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, valBytes.length); + if (valBytes.length > 0) { + MemorySegment.copy(valBytes, 0, target, ValueLayout.JAVA_BYTE, pos, valBytes.length); + pos += valBytes.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.metadataParsed = true; + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazySource = null; + } + /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.OBJSTRVAL_PARENT_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.OBJSTRVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION); + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { return page != null; } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 55 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== OWNER PAGE (for resize-in-place) ==================== + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.OBJSTRVAL_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) + this.isCompressed = page.get(ValueLayout.JAVA_BYTE, payloadStart) == 1; + + // Read value length (varint) + final long lenOffset = payloadStart + 1; + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + this.valueParsed = true; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + * + * @param target the target MemorySegment + * @param offset the absolute byte offset to write at + * @return the total number of bytes written + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!metadataParsed) parseMetadataFields(); + if (!valueParsed) parseValueField(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, previousRevision, lastModifiedRevision, value, isCompressed); + } + + /** + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public ObjectStringNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new ObjectStringNode(nodeKey, + readDeltaField(NodeFieldLayout.OBJSTRVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.OBJSTRVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.OBJSTRVAL_LAST_MOD_REVISION), + hash, + value != null ? value.clone() : null, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + isCompressed, fsstSymbolTable != null ? fsstSymbolTable.clone() : null); + } if (!metadataParsed) { parseMetadataFields(); } if (!valueParsed) { parseValueField(); } - return new ObjectStringNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, hash, value != null - ? value.clone() - : null, hashFunction, - deweyIDBytes != null - ? deweyIDBytes.clone() - : null, - isCompressed, fsstSymbolTable != null - ? fsstSymbolTable.clone() - : null); + return new ObjectStringNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + hash, value != null ? value.clone() : null, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + isCompressed, fsstSymbolTable != null ? fsstSymbolTable.clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -679,6 +976,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final ObjectStringNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && Objects.equal(getValue(), other.getValue()); + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && Objects.equal(getValue(), other.getValue()); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/json/StringNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/json/StringNode.java index c5c85ed41..ed5673471 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/json/StringNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/json/StringNode.java @@ -34,6 +34,7 @@ import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.JsonNodeVisitor; import io.sirix.api.visitor.VisitResult; +import io.sirix.node.Bytes; import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; @@ -42,55 +43,54 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.json.ImmutableStringNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableJsonNode; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; +import io.sirix.settings.StringCompressionType; import io.sirix.utils.FSSTCompressor; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * JSON String node. * - *

- * Uses primitive fields for efficient storage with delta+varint encoding. - *

+ *

Uses primitive fields for efficient storage with delta+varint encoding.

* * @author Johannes Lichtenberger */ -public final class StringNode implements StructNode, ValueNode, ImmutableJsonNode, ReusableNodeProxy { +public final class StringNode implements StructNode, ValueNode, ImmutableJsonNode, FlyweightNode { // Node identity (mutable for singleton reuse) private long nodeKey; - + // Mutable structural fields private long parentKey; private long rightSiblingKey; private long leftSiblingKey; - + // Mutable revision tracking private int previousRevision; private int lastModifiedRevision; - + // Mutable hash (computed on demand for value nodes) private long hash; - + // String value (stored as bytes) private byte[] value; - + // Hash function for computing node hashes (mutable for singleton reuse) private LongHashFunction hashFunction; - + // DeweyID support (lazily parsed) private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; @@ -100,46 +100,67 @@ public final class StringNode implements StructNode, ValueNode, ImmutableJsonNod private boolean isCompressed; /** FSST symbol table for decompression (shared from KeyValueLeafPage) */ private byte[] fsstSymbolTable; - /** Pre-parsed FSST symbol table (avoids re-parsing on every decode) */ - private byte[][] parsedFsstSymbols; /** Decompressed value (lazy allocated on first access if compressed) */ private byte[] decodedValue; // Lazy parsing state (for singleton reuse optimization) // Two-stage lazy parsing: metadata (cheap) vs value (expensive byte[] allocation) - private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) - private long lazyOffset; // Offset where lazy metadata fields start - private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed - private boolean valueParsed; // Whether value byte[] is parsed - private boolean hasHash; // Whether hash is stored (from config) - private long valueOffset; // Offset where value starts (after metadata) - - // Fixed-slot value encoding state (for read path via populateSingletonFromFixedSlot) - private boolean fixedValueEncoding; // Whether value comes from fixed-slot inline payload - private int fixedValueLength; // Length of inline payload bytes - private boolean fixedValueCompressed; // Whether inline payload is FSST compressed - - // Fixed-slot lazy metadata support - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; + private Object lazySource; // Source for lazy parsing (MemorySegment or byte[]) + private long lazyOffset; // Offset where lazy metadata fields start + private boolean metadataParsed; // Whether prevRev, lastModRev, hash are parsed + private boolean valueParsed; // Whether value byte[] is parsed + private boolean hasHash; // Whether hash is stored (from config) + private long valueOffset; // Offset where value starts (after metadata) + + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.STRING_VALUE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public StringNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** - * Primary constructor with all primitive fields. All fields are already parsed - no lazy loading - * needed. + * Primary constructor with all primitive fields. + * All fields are already parsed - no lazy loading needed. */ - public StringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, byte[] value, LongHashFunction hashFunction, byte[] deweyID) { - this(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, hash, value, - hashFunction, deweyID, false, null); + public StringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + byte[] value, LongHashFunction hashFunction, byte[] deweyID) { + this(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, + leftSiblingKey, hash, value, hashFunction, deweyID, false, null); } /** - * Primary constructor with all primitive fields and compression support. All fields are already - * parsed - no lazy loading needed. + * Primary constructor with all primitive fields and compression support. + * All fields are already parsed - no lazy loading needed. */ - public StringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, byte[] value, LongHashFunction hashFunction, byte[] deweyID, boolean isCompressed, - byte[] fsstSymbolTable) { + public StringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + byte[] value, LongHashFunction hashFunction, byte[] deweyID, + boolean isCompressed, byte[] fsstSymbolTable) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -155,24 +176,27 @@ public StringNode(long nodeKey, long parentKey, int previousRevision, int lastMo // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** - * Constructor with SirixDeweyID instead of byte array. All fields are already parsed - no lazy - * loading needed. + * Constructor with SirixDeweyID instead of byte array. + * All fields are already parsed - no lazy loading needed. */ - public StringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID) { - this(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, hash, value, - hashFunction, deweyID, false, null); + public StringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID) { + this(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, + leftSiblingKey, hash, value, hashFunction, deweyID, false, null); } /** - * Constructor with SirixDeweyID and compression support. All fields are already parsed - no lazy - * loading needed. + * Constructor with SirixDeweyID and compression support. + * All fields are already parsed - no lazy loading needed. */ - public StringNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long rightSiblingKey, - long leftSiblingKey, long hash, byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID, + public StringNode(long nodeKey, long parentKey, int previousRevision, + int lastModifiedRevision, long rightSiblingKey, long leftSiblingKey, long hash, + byte[] value, LongHashFunction hashFunction, SirixDeweyID deweyID, boolean isCompressed, byte[] fsstSymbolTable) { this.nodeKey = nodeKey; this.parentKey = parentKey; @@ -189,6 +213,211 @@ public StringNode(long nodeKey, long parentKey, int previousRevision, int lastMo // Constructed with all values - mark as fully parsed this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.hash = 0; + this.metadataParsed = true; + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazySource = null; + } + + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.STRVAL_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.STRVAL_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.STRVAL_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.STRVAL_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.STRVAL_LAST_MOD_REVISION); + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.page = null; + this.ownerPage = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + public boolean isBound() { return page != null; } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 55 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) + this.isCompressed = page.get(ValueLayout.JAVA_BYTE, payloadStart) == 1; + + // Read value length (varint) + final long lenOffset = payloadStart + 1; + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + this.valueParsed = true; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a StringNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param rawValue the raw value bytes (possibly compressed) + * @param isCompressed whether the value is FSST compressed + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev, + final byte[] rawValue, final boolean isCompressed) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.STRING_VALUE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.STRVAL_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.STRVAL_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.STRVAL_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.STRVAL_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.STRVAL_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 5: payload [isCompressed:1][valueLength:varint][value:bytes] + heapOffsets[NodeFieldLayout.STRVAL_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, isCompressed ? (byte) 1 : (byte) 0); + pos++; + final byte[] val = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, val.length); + if (val.length > 0) { + MemorySegment.copy(val, 0, target, ValueLayout.JAVA_BYTE, pos, val.length); + pos += val.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!metadataParsed) parseMetadataFields(); + if (!valueParsed) parseValueField(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision, value, isCompressed); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -203,16 +432,38 @@ public long getNodeKey() { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.STRVAL_PARENT_KEY, nodeKey); + } return parentKey; } public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.STRVAL_PARENT_KEY, NodeFieldLayout.STRING_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override @@ -227,26 +478,83 @@ public void setTypeKey(final int typeKey) { @Override public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } + @Override + public void setDeweyIDBytes(final byte[] bytes) { + this.deweyIDBytes = bytes; + this.sirixDeweyID = null; + } + @Override public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.STRVAL_PREV_REVISION, NodeFieldLayout.STRING_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.STRVAL_LAST_MOD_REVISION, NodeFieldLayout.STRING_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!metadataParsed) { - parseMetadataFields(); + if (hash != 0L) { + return hash; } - return hash; + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override @@ -257,38 +565,85 @@ public void setHash(final long hash) { @Override public long computeHash(final BytesOut bytes) { bytes.clear(); - bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); - - bytes.writeLong(getLeftSiblingKey()).writeLong(getRightSiblingKey()); + bytes.writeLong(getNodeKey()) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); - bytes.writeUtf8(getValue()); + final byte[] rawValue = getRawValue(); + if (rawValue != null) { + bytes.write(rawValue); + } return bytes.hashDirect(hashFunction); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.STRVAL_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long rightSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.STRVAL_RIGHT_SIB_KEY, NodeFieldLayout.STRING_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, rightSibling, nodeKey)); + } + @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.STRVAL_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.STRVAL_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long leftSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.STRVAL_LEFT_SIB_KEY, NodeFieldLayout.STRING_VALUE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, leftSibling, nodeKey)); + } + @Override public long getFirstChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setFirstChildKey(final long firstChild) { // Value nodes are leaf nodes - no-op } @@ -297,7 +652,7 @@ public void setFirstChildKey(final long firstChild) { public long getLastChildKey() { return Fixed.NULL_NODE_KEY.getStandardProperty(); } - + public void setLastChildKey(final long lastChild) { // Value nodes are leaf nodes - no-op } @@ -306,7 +661,7 @@ public void setLastChildKey(final long lastChild) { public long getChildCount() { return 0; } - + public void setChildCount(final long childCount) { // Value nodes are leaf nodes - no-op } @@ -315,34 +670,35 @@ public void setChildCount(final long childCount) { public long getDescendantCount() { return 0; } - + public void setDescendantCount(final long descendantCount) { // Value nodes are leaf nodes - no-op } @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } - // If compressed, decode on first access using pre-parsed symbols when available + // If compressed, decode on first access if (isCompressed && decodedValue == null && value != null) { - final byte[][] parsed = parsedFsstSymbols; - decodedValue = (parsed != null) ? FSSTCompressor.decode(value, parsed) : FSSTCompressor.decode(value, fsstSymbolTable); + decodedValue = FSSTCompressor.decode(value, fsstSymbolTable); } - return isCompressed - ? decodedValue - : value; + return isCompressed ? decodedValue : value; } /** - * Get the raw (possibly compressed) value bytes without FSST decoding. Use this for serialization - * to preserve compression. + * Get the raw (possibly compressed) value bytes without FSST decoding. + * Use this for serialization to preserve compression. * * @return the raw bytes as stored, possibly FSST compressed */ public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValueField(); } return value; @@ -350,10 +706,19 @@ public byte[] getRawValueWithoutDecompression() { @Override public void setRawValue(final byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.decodedValue = null; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.decodedValue = null; - this.fixedValueEncoding = false; - this.valueParsed = true; } /** @@ -364,12 +729,23 @@ public void setRawValue(final byte[] value) { * @param fsstSymbolTable the symbol table for decompression (or null if not compressed) */ public void setRawValue(final byte[] value, boolean isCompressed, byte[] fsstSymbolTable) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.isCompressed = isCompressed; + this.fsstSymbolTable = fsstSymbolTable; + this.decodedValue = null; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.isCompressed = isCompressed; this.fsstSymbolTable = fsstSymbolTable; this.decodedValue = null; - this.fixedValueEncoding = false; - this.valueParsed = true; } /** @@ -378,12 +754,15 @@ public void setRawValue(final byte[] value, boolean isCompressed, byte[] fsstSym * @return true if compressed */ public boolean isCompressed() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } return isCompressed; } /** * Set compression state. - * + * * @param isCompressed true if value is compressed */ public void setCompressed(boolean isCompressed) { @@ -401,27 +780,14 @@ public byte[] getFsstSymbolTable() { /** * Set the FSST symbol table. - * + * * @param fsstSymbolTable the symbol table */ public void setFsstSymbolTable(byte[] fsstSymbolTable) { this.fsstSymbolTable = fsstSymbolTable; - this.parsedFsstSymbols = null; this.decodedValue = null; // Reset decoded value when table changes } - /** - * Set the FSST symbol table with pre-parsed symbols to avoid redundant parsing. - * - * @param fsstSymbolTable the raw symbol table bytes - * @param parsedFsstSymbols the pre-parsed symbol arrays - */ - public void setFsstSymbolTable(byte[] fsstSymbolTable, byte[][] parsedFsstSymbols) { - this.fsstSymbolTable = fsstSymbolTable; - this.parsedFsstSymbols = parsedFsstSymbols; - this.decodedValue = null; - } - @Override public String getValue() { // Use getRawValue() which handles FSST decompression @@ -461,16 +827,19 @@ public void decrementDescendantCount() { @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.STRVAL_PREV_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -479,6 +848,9 @@ public int getPreviousRevisionNumber() { @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.STRVAL_LAST_MOD_REVISION); + } if (!metadataParsed) { parseMetadataFields(); } @@ -494,18 +866,15 @@ public void setNodeKey(final long nodeKey) { this.nodeKey = nodeKey; } - public void setDeweyIDBytes(final byte[] deweyIDBytes) { - this.deweyIDBytes = deweyIDBytes; - this.sirixDeweyID = null; - } - /** - * Populate this node from a BytesIn source for singleton reuse. LAZY OPTIMIZATION: Only parses - * structural fields immediately. Two-stage lazy parsing: metadata (cheap) vs value (expensive - * byte[] allocation). + * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately. + * Two-stage lazy parsing: metadata (cheap) vs value (expensive byte[] allocation). */ public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, - final LongHashFunction hashFunction, final ResourceConfiguration config) { + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -523,126 +892,71 @@ public void readFrom(final BytesIn source, final long nodeKey, final byte[] d this.valueParsed = false; this.hasHash = config.hashType != HashType.NONE; this.valueOffset = 0; - + // Initialize lazy fields to defaults (will be populated on demand) this.previousRevision = 0; this.lastModifiedRevision = 0; this.hash = 0; this.value = null; } - - /** - * Populate this singleton from fixed-slot inline payload (zero allocation). Sets up lazy value - * parsing from the fixed-slot MemorySegment. CRITICAL: Resets hash to 0 — caller MUST call - * setHash() AFTER this method. - * - * @param source the slot data (MemorySegment) containing inline payload - * @param valueOffset byte offset within source where payload bytes start - * @param valueLength length of payload bytes - * @param compressed whether the payload is FSST compressed - */ - public void setLazyRawValue(final Object source, final long valueOffset, final int valueLength, - final boolean compressed) { - this.lazySource = source; - this.valueOffset = valueOffset; - this.metadataParsed = true; - this.valueParsed = false; - this.fixedValueEncoding = true; - this.fixedValueLength = valueLength; - this.fixedValueCompressed = compressed; - this.value = null; - this.fsstSymbolTable = null; - this.parsedFsstSymbols = null; - this.decodedValue = null; - this.hash = 0L; - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - } - + /** - * Parse metadata fields on demand (cheap - just varints and optionally a long). Called by getters - * that access prevRev, lastModRev, or hash. + * Parse metadata fields on demand (cheap - just varints and optionally a long). + * Called by getters that access prevRev, lastModRev, or hash. */ private void parseMetadataFields() { if (metadataParsed) { return; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - + if (lazySource == null) { + // Already fully constructed (e.g., from constructor) metadataParsed = true; return; } - + BytesIn bytesIn = createBytesIn(lazyOffset); - + this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); if (hasHash) { - this.hash = bytesIn.readLong(); + bytesIn.readLong(); // skip legacy hash field } // Store position where value starts (for separate value parsing) this.valueOffset = bytesIn.position(); this.metadataParsed = true; } - + /** - * Parse value field on demand (expensive - allocates byte[]). Called by getValue() and - * getRawValue(). + * Parse value field on demand (expensive - allocates byte[]). + * Called by getValue() and getRawValue(). */ private void parseValueField() { if (valueParsed) { return; } - - // Fixed-slot inline payload path (from setLazyRawValue) - if (fixedValueEncoding) { - final BytesIn bytesIn = createBytesIn(valueOffset); - this.isCompressed = fixedValueCompressed; - this.value = new byte[fixedValueLength]; - if (fixedValueLength > 0) { - bytesIn.read(this.value, 0, fixedValueLength); - } - this.valueParsed = true; - return; - } - + // Must parse metadata first to know where value starts if (!metadataParsed) { parseMetadataFields(); } - + if (lazySource == null) { valueParsed = true; return; } - - final BytesIn bytesIn = createBytesIn(valueOffset); - + + BytesIn bytesIn = createBytesIn(valueOffset); + // Read compression flag (1 byte: 0 = none, 1 = FSST) this.isCompressed = bytesIn.readByte() == 1; - - final int length = DeltaVarIntCodec.decodeSigned(bytesIn); + + int length = DeltaVarIntCodec.decodeSigned(bytesIn); this.value = new byte[length]; bytesIn.read(this.value); this.valueParsed = true; } - + /** * Create a BytesIn for reading from the lazy source at the given offset. */ @@ -661,10 +975,27 @@ private BytesIn createBytesIn(long offset) { } /** - * Create a deep copy snapshot of this node. Forces parsing of all lazy fields since snapshot must - * be independent. + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public StringNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new StringNode(nodeKey, + readDeltaField(NodeFieldLayout.STRVAL_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.STRVAL_PREV_REVISION), + readSignedField(NodeFieldLayout.STRVAL_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.STRVAL_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.STRVAL_LEFT_SIB_KEY, nodeKey), + hash, + value != null ? value.clone() : null, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + isCompressed, fsstSymbolTable != null ? fsstSymbolTable.clone() : null); + } // Force parse all lazy fields for snapshot (must be complete and independent) if (!metadataParsed) { parseMetadataFields(); @@ -672,16 +1003,22 @@ public StringNode toSnapshot() { if (!valueParsed) { parseValueField(); } - return new StringNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, value != null - ? value.clone() - : null, - hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null, - isCompressed, fsstSymbolTable != null - ? fsstSymbolTable.clone() - : null); + return new StringNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, + value != null ? value.clone() : null, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + isCompressed, fsstSymbolTable != null ? fsstSymbolTable.clone() : null); + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; } @Override @@ -728,6 +1065,8 @@ public boolean equals(final Object obj) { if (!(obj instanceof final StringNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && Objects.equal(getValue(), other.getValue()); + return nodeKey == other.nodeKey + && parentKey == other.parentKey + && Objects.equal(getValue(), other.getValue()); } } diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactFieldCodec.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactFieldCodec.java deleted file mode 100644 index 454bb31d0..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactFieldCodec.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.node.DeltaVarIntCodec; - -import java.util.Objects; - -/** - * Shared codec primitives used by compact commit encoders. - */ -public final class CompactFieldCodec { - private CompactFieldCodec() {} - - public static void encodeNodeKeyDelta(final BytesOut sink, final long baseNodeKey, final long targetNodeKey) { - DeltaVarIntCodec.encodeDelta(Objects.requireNonNull(sink), targetNodeKey, baseNodeKey); - } - - public static long decodeNodeKeyDelta(final BytesIn source, final long baseNodeKey) { - return DeltaVarIntCodec.decodeDelta(Objects.requireNonNull(source), baseNodeKey); - } - - public static void encodeSignedInt(final BytesOut sink, final int value) { - DeltaVarIntCodec.encodeSigned(Objects.requireNonNull(sink), value); - } - - public static int decodeSignedInt(final BytesIn source) { - return DeltaVarIntCodec.decodeSigned(Objects.requireNonNull(source)); - } - - public static void encodeSignedLong(final BytesOut sink, final long value) { - DeltaVarIntCodec.encodeSignedLong(Objects.requireNonNull(sink), value); - } - - public static long decodeSignedLong(final BytesIn source) { - return DeltaVarIntCodec.decodeSignedLong(Objects.requireNonNull(source)); - } - - public static void encodeNonNegativeInt(final BytesOut sink, final int value) { - if (value < 0) { - throw new IllegalArgumentException("value must be >= 0"); - } - encodeSignedInt(sink, value); - } - - public static int decodeNonNegativeInt(final BytesIn source, final String fieldName) { - final int value = decodeSignedInt(source); - if (value < 0) { - throw new IllegalStateException("Decoded negative value for " + Objects.requireNonNull(fieldName) + ": " + value); - } - return value; - } - - public static int decodeNonNegativeInt(final BytesIn source) { - final int value = decodeSignedInt(source); - if (value < 0) { - throw new IllegalStateException("Decoded negative value: " + value); - } - return value; - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactPageEncoder.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactPageEncoder.java deleted file mode 100644 index 22df5dcc4..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/CompactPageEncoder.java +++ /dev/null @@ -1,214 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.node.NodeKind; - -import java.util.Objects; - -/** - * Lightweight V1 page-level compact encoding helpers. - * - *

- * The methods here are intentionally allocation-minimal and operate directly on primitive vectors. - */ -public final class CompactPageEncoder { - private static final int MAX_RELATIONSHIP_VECTOR_LENGTH = 1 << 20; - - private CompactPageEncoder() {} - - public static void encodeSlotHeader(final BytesOut sink, final SlotHeader slotHeader) { - Objects.requireNonNull(slotHeader, "slotHeader must not be null"); - CompactFieldCodec.encodeSignedLong(sink, slotHeader.nodeKey()); - sink.writeByte(slotHeader.nodeKind().getId()); - CompactFieldCodec.encodeNonNegativeInt(sink, slotHeader.fixedSlotSizeInBytes()); - CompactFieldCodec.encodeNonNegativeInt(sink, slotHeader.payloadSizeInBytes()); - } - - public static SlotHeader decodeSlotHeader(final BytesIn source) { - final MutableSlotHeader header = new MutableSlotHeader(); - decodeSlotHeader(source, header); - return new SlotHeader(header.nodeKey(), header.nodeKind(), header.fixedSlotSizeInBytes(), - header.payloadSizeInBytes()); - } - - /** - * Decode a slot header into a caller-owned reusable object to avoid allocations. - */ - public static void decodeSlotHeader(final BytesIn source, final MutableSlotHeader target) { - Objects.requireNonNull(source, "source must not be null"); - Objects.requireNonNull(target, "target must not be null"); - - final long nodeKey = CompactFieldCodec.decodeSignedLong(source); - - final byte kindId = source.readByte(); - if (kindId < 0) { - throw new IllegalStateException("Invalid negative node kind id: " + kindId); - } - - final NodeKind nodeKind = NodeKind.getKind(kindId); - if (nodeKind == null) { - throw new IllegalStateException("Unknown node kind id: " + kindId); - } - - final int fixedSlotSizeInBytes = CompactFieldCodec.decodeNonNegativeInt(source); - final int payloadSizeInBytes = CompactFieldCodec.decodeNonNegativeInt(source); - target.set(nodeKey, nodeKind, fixedSlotSizeInBytes, payloadSizeInBytes); - } - - public static void encodeRelationshipVector(final BytesOut sink, final long baseNodeKey, - final long[] relationshipNodeKeys) { - Objects.requireNonNull(relationshipNodeKeys, "relationshipNodeKeys must not be null"); - CompactFieldCodec.encodeNonNegativeInt(sink, relationshipNodeKeys.length); - for (final long relationshipNodeKey : relationshipNodeKeys) { - CompactFieldCodec.encodeNodeKeyDelta(sink, baseNodeKey, relationshipNodeKey); - } - } - - public static long[] decodeRelationshipVector(final BytesIn source, final long baseNodeKey) { - final int length = CompactFieldCodec.decodeNonNegativeInt(source); - if (length > MAX_RELATIONSHIP_VECTOR_LENGTH) { - throw new IllegalStateException("Relationship vector length exceeds limit: " + length); - } - - final long[] relationshipNodeKeys = new long[length]; - for (int i = 0; i < length; i++) { - relationshipNodeKeys[i] = CompactFieldCodec.decodeNodeKeyDelta(source, baseNodeKey); - } - return relationshipNodeKeys; - } - - /** - * Decode a relationship vector into a caller-owned reusable buffer. - * - * @return number of decoded entries written into {@code targetBuffer} - */ - public static int decodeRelationshipVector(final BytesIn source, final long baseNodeKey, - final long[] targetBuffer) { - Objects.requireNonNull(targetBuffer, "targetBuffer must not be null"); - final int length = CompactFieldCodec.decodeNonNegativeInt(source); - if (length > MAX_RELATIONSHIP_VECTOR_LENGTH) { - throw new IllegalStateException("Relationship vector length exceeds limit: " + length); - } - if (length > targetBuffer.length) { - throw new IllegalArgumentException( - "Target buffer too small. length=" + length + ", capacity=" + targetBuffer.length); - } - - for (int i = 0; i < length; i++) { - targetBuffer[i] = CompactFieldCodec.decodeNodeKeyDelta(source, baseNodeKey); - } - return length; - } - - public static void encodePayloadBlockHeader(final BytesOut sink, final PayloadBlockHeader payloadBlockHeader) { - Objects.requireNonNull(payloadBlockHeader, "payloadBlockHeader must not be null"); - CompactFieldCodec.encodeSignedLong(sink, payloadBlockHeader.payloadPointer()); - CompactFieldCodec.encodeNonNegativeInt(sink, payloadBlockHeader.payloadLengthInBytes()); - CompactFieldCodec.encodeNonNegativeInt(sink, payloadBlockHeader.payloadFlags()); - } - - public static PayloadBlockHeader decodePayloadBlockHeader(final BytesIn source) { - final MutablePayloadBlockHeader payloadBlockHeader = new MutablePayloadBlockHeader(); - decodePayloadBlockHeader(source, payloadBlockHeader); - return new PayloadBlockHeader(payloadBlockHeader.payloadPointer(), payloadBlockHeader.payloadLengthInBytes(), - payloadBlockHeader.payloadFlags()); - } - - /** - * Decode a payload block header into a caller-owned reusable object to avoid allocations. - */ - public static void decodePayloadBlockHeader(final BytesIn source, final MutablePayloadBlockHeader target) { - Objects.requireNonNull(source, "source must not be null"); - Objects.requireNonNull(target, "target must not be null"); - - final long payloadPointer = CompactFieldCodec.decodeSignedLong(source); - final int payloadLengthInBytes = CompactFieldCodec.decodeNonNegativeInt(source); - final int payloadFlags = CompactFieldCodec.decodeNonNegativeInt(source); - target.set(payloadPointer, payloadLengthInBytes, payloadFlags); - } - - public record SlotHeader(long nodeKey, NodeKind nodeKind, int fixedSlotSizeInBytes, int payloadSizeInBytes) { - public SlotHeader { - Objects.requireNonNull(nodeKind, "nodeKind must not be null"); - if (fixedSlotSizeInBytes < 0) { - throw new IllegalArgumentException("fixedSlotSizeInBytes must be >= 0"); - } - if (payloadSizeInBytes < 0) { - throw new IllegalArgumentException("payloadSizeInBytes must be >= 0"); - } - } - } - - public record PayloadBlockHeader(long payloadPointer, int payloadLengthInBytes, int payloadFlags) { - public PayloadBlockHeader { - if (payloadLengthInBytes < 0) { - throw new IllegalArgumentException("payloadLengthInBytes must be >= 0"); - } - if (payloadFlags < 0) { - throw new IllegalArgumentException("payloadFlags must be >= 0"); - } - } - } - - /** - * Reusable mutable slot header container for allocation-free decode loops. - */ - public static final class MutableSlotHeader { - private long nodeKey; - private NodeKind nodeKind; - private int fixedSlotSizeInBytes; - private int payloadSizeInBytes; - - public long nodeKey() { - return nodeKey; - } - - public NodeKind nodeKind() { - return nodeKind; - } - - public int fixedSlotSizeInBytes() { - return fixedSlotSizeInBytes; - } - - public int payloadSizeInBytes() { - return payloadSizeInBytes; - } - - private void set(final long nodeKey, final NodeKind nodeKind, final int fixedSlotSizeInBytes, - final int payloadSizeInBytes) { - this.nodeKey = nodeKey; - this.nodeKind = nodeKind; - this.fixedSlotSizeInBytes = fixedSlotSizeInBytes; - this.payloadSizeInBytes = payloadSizeInBytes; - } - } - - /** - * Reusable mutable payload block header for allocation-free decode loops. - */ - public static final class MutablePayloadBlockHeader { - private long payloadPointer; - private int payloadLengthInBytes; - private int payloadFlags; - - public long payloadPointer() { - return payloadPointer; - } - - public int payloadLengthInBytes() { - return payloadLengthInBytes; - } - - public int payloadFlags() { - return payloadFlags; - } - - private void set(final long payloadPointer, final int payloadLengthInBytes, final int payloadFlags) { - this.payloadPointer = payloadPointer; - this.payloadLengthInBytes = payloadLengthInBytes; - this.payloadFlags = payloadFlags; - } - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordMaterializer.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordMaterializer.java deleted file mode 100644 index 29ea31891..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordMaterializer.java +++ /dev/null @@ -1,976 +0,0 @@ -package io.sirix.node.layout; - -import io.brackit.query.atomic.QNm; -import io.sirix.access.ResourceConfiguration; -import io.sirix.node.MemorySegmentBytesIn; -import io.sirix.node.NodeKind; -import io.sirix.node.interfaces.DataRecord; -import io.sirix.node.interfaces.StructNode; -import io.sirix.node.json.ArrayNode; -import io.sirix.node.json.BooleanNode; -import io.sirix.node.json.JsonDocumentRootNode; -import io.sirix.node.json.NullNode; -import io.sirix.node.json.NumberNode; -import io.sirix.node.json.ObjectBooleanNode; -import io.sirix.node.json.ObjectKeyNode; -import io.sirix.node.json.ObjectNode; -import io.sirix.node.json.ObjectNullNode; -import io.sirix.node.json.ObjectNumberNode; -import io.sirix.node.json.ObjectStringNode; -import io.sirix.node.json.StringNode; -import io.sirix.node.interfaces.ValueNode; -import io.sirix.node.xml.AttributeNode; -import io.sirix.node.xml.CommentNode; -import io.sirix.node.xml.ElementNode; -import io.sirix.node.xml.NamespaceNode; -import io.sirix.node.xml.PINode; -import io.sirix.node.xml.TextNode; -import io.sirix.node.xml.XmlDocumentRootNode; -import io.sirix.settings.Fixed; -import net.openhft.hashing.LongHashFunction; - -import java.lang.foreign.ValueLayout; - -import java.lang.foreign.MemorySegment; - -/** - * Materializes {@link DataRecord} instances from fixed-slot in-memory layouts. - * - *

- * Supports both payload-free node kinds and inline-payload node kinds (strings, numbers). Each call - * allocates a fresh Java object. This is required because the write path (e.g. - * {@code adaptForInsert}, {@code rollingAdd}) holds live references to multiple records of the same - * kind simultaneously — singletons would cause aliasing corruption. - *

- */ -public final class FixedSlotRecordMaterializer { - private static final QNm EMPTY_QNM = new QNm(""); - - private FixedSlotRecordMaterializer() {} - - /** - * Populate an existing DataRecord singleton from fixed-slot bytes at the given offset within - * {@code data} (zero allocation — avoids {@code asSlice()}). - * - *

- * This method reads structural fields, metadata, and inline payloads from the fixed-slot - * MemorySegment into the existing object's setters. For payload-bearing nodes (strings, numbers), - * uses lazy value loading via {@code setLazyRawValue}/{@code setLazyNumberValue} to defer byte[] - * allocation until the value is actually accessed. - *

- * - *

- * CRITICAL: For payload-bearing nodes, {@code setLazyRawValue}/{@code setLazyNumberValue} - * resets hash to 0. Therefore hash must be set AFTER the lazy value call. - *

- * - * @param existing the singleton to populate (must be the correct type for nodeKind) - * @param nodeKind the node kind - * @param nodeKey the record key - * @param data the backing MemorySegment (may be the full slotMemory) - * @param dataOffset absolute byte offset within {@code data} where this slot's data begins - * @param dataLength length of the slot data in bytes - * @param deweyIdBytes optional DeweyID bytes - * @param resourceConfig resource configuration (unused but kept for API consistency) - * @return true if the singleton was populated, false if the node kind is unsupported - */ - public static boolean populateExisting(final DataRecord existing, final NodeKind nodeKind, final long nodeKey, - final MemorySegment data, final long dataOffset, final int dataLength, final byte[] deweyIdBytes, - final ResourceConfiguration resourceConfig) { - if (existing == null || nodeKind == null || data == null) { - return false; - } - - final NodeKindLayout layout = nodeKind.layoutDescriptor(); - if (!layout.isFixedSlotSupported() || dataLength < layout.fixedSlotSizeInBytes()) { - return false; - } - - if (!layout.hasSupportedPayloads()) { - return false; - } - - return switch (nodeKind) { - case JSON_DOCUMENT -> { - if (!(existing instanceof JsonDocumentRootNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateJsonDocumentFields(node, data, dataOffset, layout); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case XML_DOCUMENT -> { - if (!(existing instanceof XmlDocumentRootNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setFirstChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT -> { - if (!(existing instanceof ObjectNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateObjectFields(node, data, dataOffset, layout); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case ARRAY -> { - if (!(existing instanceof ArrayNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateArrayFields(node, data, dataOffset, layout); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT_KEY -> { - if (!(existing instanceof ObjectKeyNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setNameKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.NAME_KEY)); - node.clearCachedName(); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case BOOLEAN_VALUE -> { - if (!(existing instanceof BooleanNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateBooleanNodeFields(node, data, dataOffset, layout); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case NULL_VALUE -> { - if (!(existing instanceof NullNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateLeafNodeFields(node, data, dataOffset, layout); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT_BOOLEAN_VALUE -> { - if (!(existing instanceof ObjectBooleanNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setValue(SlotLayoutAccessors.readBooleanField(data, dataOffset, layout, StructuralField.BOOLEAN_VALUE)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT_NULL_VALUE -> { - if (!(existing instanceof ObjectNullNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case NAMESPACE -> { - if (!(existing instanceof NamespaceNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.URI_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case ELEMENT -> { - if (!(existing instanceof ElementNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.URI_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - readInlineVectorPayload(node, data, dataOffset, layout, 0, true); - readInlineVectorPayload(node, data, dataOffset, layout, 1, false); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case STRING_VALUE -> { - if (!(existing instanceof StringNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateLeafNodeFields(node, data, dataOffset, layout); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long strPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int strLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - final int strFlags = SlotLayoutAccessors.readPayloadFlags(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + strPointer, strLength, (strFlags & 1) != 0); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case NUMBER_VALUE -> { - if (!(existing instanceof NumberNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateLeafNodeFields(node, data, dataOffset, layout); - // setLazyNumberValue BEFORE setHash — setLazyNumberValue resets hash to 0 - final long numPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int numLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - node.setLazyNumberValue(data, dataOffset + numPointer, numLength); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT_STRING_VALUE -> { - if (!(existing instanceof ObjectStringNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long objStrPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int objStrLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - final int objStrFlags = SlotLayoutAccessors.readPayloadFlags(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + objStrPointer, objStrLength, (objStrFlags & 1) != 0); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case OBJECT_NUMBER_VALUE -> { - if (!(existing instanceof ObjectNumberNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setLazyNumberValue BEFORE setHash — setLazyNumberValue resets hash to 0 - final long objNumPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int objNumLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - node.setLazyNumberValue(data, dataOffset + objNumPointer, objNumLength); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case TEXT -> { - if (!(existing instanceof TextNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateLeafNodeFields(node, data, dataOffset, layout); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long textPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int textLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - final int textFlags = SlotLayoutAccessors.readPayloadFlags(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + textPointer, textLength, (textFlags & 1) != 0); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case COMMENT -> { - if (!(existing instanceof CommentNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - populateLeafNodeFields(node, data, dataOffset, layout); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long commentPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int commentLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - final int commentFlags = SlotLayoutAccessors.readPayloadFlags(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + commentPointer, commentLength, (commentFlags & 1) != 0); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case ATTRIBUTE -> { - if (!(existing instanceof AttributeNode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.URI_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long attrPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int attrLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + attrPointer, attrLength); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - case PROCESSING_INSTRUCTION -> { - if (!(existing instanceof PINode node)) { - yield false; - } - node.setNodeKey(nodeKey); - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.URI_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - // setLazyRawValue BEFORE setHash — setLazyRawValue resets hash to 0 - final long piPointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, 0); - final int piLength = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, 0); - final int piFlags = SlotLayoutAccessors.readPayloadFlags(data, dataOffset, layout, 0); - node.setLazyRawValue(data, dataOffset + piPointer, piLength, (piFlags & 1) != 0); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - node.setDeweyIDBytes(deweyIdBytes); - yield true; - } - default -> false; - }; - } - - /** - * Populate common structural fields for an ObjectNode. - */ - private static void populateObjectFields(final ObjectNode node, final MemorySegment data, final long dataOffset, - final NodeKindLayout layout) { - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - } - - /** - * Populate common structural fields for an ArrayNode. - */ - private static void populateArrayFields(final ArrayNode node, final MemorySegment data, final long dataOffset, - final NodeKindLayout layout) { - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - } - - /** - * Populate fields for a JsonDocumentRootNode. - */ - private static void populateJsonDocumentFields(final JsonDocumentRootNode node, final MemorySegment data, - final long dataOffset, final NodeKindLayout layout) { - node.setParentKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setRightSiblingKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setLeftSiblingKey(Fixed.NULL_NODE_KEY.getStandardProperty()); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setPreviousRevision(0); - node.setLastModifiedRevision(0); - } - - /** - * Populate common fields for leaf nodes with siblings. - */ - private static void populateLeafNodeFields(final StructNode node, final MemorySegment data, final long dataOffset, - final NodeKindLayout layout) { - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - } - - /** - * Populate fields for a BooleanNode (has siblings + boolean value). - */ - private static void populateBooleanNodeFields(final BooleanNode node, final MemorySegment data, final long dataOffset, - final NodeKindLayout layout) { - node.setParentKey(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, dataOffset, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey( - SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, dataOffset, layout, StructuralField.HASH)); - node.setValue(SlotLayoutAccessors.readBooleanField(data, dataOffset, layout, StructuralField.BOOLEAN_VALUE)); - } - - /** - * Materialize a fixed-slot record. - * - * @param nodeKind node kind metadata for this slot - * @param nodeKey record key - * @param data fixed-slot bytes (header + optional inline payload) - * @param deweyIdBytes optional DeweyID bytes - * @param resourceConfig resource configuration - * @return materialized record, or {@code null} if this slot cannot be materialized as fixed format - */ - public static DataRecord materialize(final NodeKind nodeKind, final long nodeKey, final MemorySegment data, - final byte[] deweyIdBytes, final ResourceConfiguration resourceConfig) { - if (nodeKind == null || data == null || resourceConfig == null) { - return null; - } - - final NodeKindLayout layout = nodeKind.layoutDescriptor(); - if (!layout.isFixedSlotSupported() || data.byteSize() < layout.fixedSlotSizeInBytes()) { - return null; - } - - if (!layout.hasSupportedPayloads()) { - return null; - } - - final LongHashFunction hashFunction = resourceConfig.nodeHashFunction; - final long nullNodeKey = Fixed.NULL_NODE_KEY.getStandardProperty(); - - return switch (nodeKind) { - case JSON_DOCUMENT -> { - final JsonDocumentRootNode node = new JsonDocumentRootNode(nodeKey, 0, 0, 0, 0, hashFunction); - node.setParentKey(nullNodeKey); - node.setRightSiblingKey(nullNodeKey); - node.setLeftSiblingKey(nullNodeKey); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setPreviousRevision(0); - node.setLastModifiedRevision(0); - node.setDeweyIDBytes(deweyIdBytes); - yield node; - } - case XML_DOCUMENT -> { - final XmlDocumentRootNode node = new XmlDocumentRootNode(nodeKey, 0, 0, 0, 0, hashFunction); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setDeweyIDBytes(deweyIdBytes); - yield node; - } - case OBJECT -> { - final ObjectNode node = new ObjectNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case ARRAY -> { - final ArrayNode node = new ArrayNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case OBJECT_KEY -> { - final ObjectKeyNode node = new ObjectKeyNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setNameKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.NAME_KEY)); - node.clearCachedName(); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case BOOLEAN_VALUE -> { - final BooleanNode node = new BooleanNode(nodeKey, 0, 0, 0, 0, 0, 0, false, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setValue(SlotLayoutAccessors.readBooleanField(data, layout, StructuralField.BOOLEAN_VALUE)); - yield node; - } - case NULL_VALUE -> { - final NullNode node = new NullNode(nodeKey, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case OBJECT_BOOLEAN_VALUE -> { - final ObjectBooleanNode node = new ObjectBooleanNode(nodeKey, 0, 0, 0, 0, false, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setValue(SlotLayoutAccessors.readBooleanField(data, layout, StructuralField.BOOLEAN_VALUE)); - yield node; - } - case OBJECT_NULL_VALUE -> { - final ObjectNullNode node = new ObjectNullNode(nodeKey, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case NAMESPACE -> { - final NamespaceNode node = - new NamespaceNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes, EMPTY_QNM); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.URI_KEY)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - yield node; - } - case ELEMENT -> { - final ElementNode node = new ElementNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, hashFunction, - deweyIdBytes, null, null, EMPTY_QNM); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.URI_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - readInlineVectorPayload(node, data, layout, 0, true); - readInlineVectorPayload(node, data, layout, 1, false); - node.setName(EMPTY_QNM); - yield node; - } - case STRING_VALUE -> { - final StringNode node = new StringNode(nodeKey, 0, 0, 0, 0, 0, 0, new byte[0], hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - readInlineStringPayload(node, data, layout, 0); - yield node; - } - case NUMBER_VALUE -> { - final NumberNode node = new NumberNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - readInlineNumberPayload(node, data, layout, 0); - yield node; - } - case OBJECT_STRING_VALUE -> { - final ObjectStringNode node = - new ObjectStringNode(nodeKey, 0, 0, 0, 0, new byte[0], hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - readInlineObjectStringPayload(node, data, layout, 0); - yield node; - } - case OBJECT_NUMBER_VALUE -> { - final ObjectNumberNode node = new ObjectNumberNode(nodeKey, 0, 0, 0, 0, 0, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - readInlineNumberPayloadForObject(node, data, layout, 0); - yield node; - } - case TEXT -> { - final TextNode node = new TextNode(nodeKey, 0, 0, 0, 0, 0, 0, new byte[0], false, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setRawValue BEFORE setHash — setRawValue resets hash to 0 - readInlineXmlValuePayload(node, data, layout, 0); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case COMMENT -> { - final CommentNode node = - new CommentNode(nodeKey, 0, 0, 0, 0, 0, 0, new byte[0], false, hashFunction, deweyIdBytes); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setRawValue BEFORE setHash — setRawValue resets hash to 0 - readInlineXmlValuePayload(node, data, layout, 0); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - yield node; - } - case ATTRIBUTE -> { - final AttributeNode node = - new AttributeNode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], hashFunction, deweyIdBytes, EMPTY_QNM); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.URI_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - // setRawValue BEFORE setHash — setRawValue resets hash to 0 - readInlineXmlValuePayload(node, data, layout, 0); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - yield node; - } - case PROCESSING_INSTRUCTION -> { - final PINode node = new PINode(nodeKey, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, new byte[0], false, - hashFunction, deweyIdBytes, EMPTY_QNM); - node.setParentKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PARENT_KEY)); - node.setRightSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.RIGHT_SIBLING_KEY)); - node.setLeftSiblingKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LEFT_SIBLING_KEY)); - node.setFirstChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.FIRST_CHILD_KEY)); - node.setLastChildKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.LAST_CHILD_KEY)); - node.setPathNodeKey(SlotLayoutAccessors.readLongField(data, layout, StructuralField.PATH_NODE_KEY)); - node.setPrefixKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREFIX_KEY)); - node.setLocalNameKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.LOCAL_NAME_KEY)); - node.setURIKey(SlotLayoutAccessors.readIntField(data, layout, StructuralField.URI_KEY)); - node.setPreviousRevision(SlotLayoutAccessors.readIntField(data, layout, StructuralField.PREVIOUS_REVISION)); - node.setLastModifiedRevision( - SlotLayoutAccessors.readIntField(data, layout, StructuralField.LAST_MODIFIED_REVISION)); - node.setChildCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.CHILD_COUNT)); - node.setDescendantCount(SlotLayoutAccessors.readLongField(data, layout, StructuralField.DESCENDANT_COUNT)); - // setRawValue BEFORE setHash — setRawValue resets hash to 0 - readInlineXmlValuePayload(node, data, layout, 0); - node.setHash(SlotLayoutAccessors.readLongField(data, layout, StructuralField.HASH)); - node.setName(EMPTY_QNM); - yield node; - } - default -> null; - }; - } - - /** - * Read inline string payload from the fixed-slot data and set it on the StringNode. - */ - private static void readInlineStringPayload(final StringNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, layout, payloadRefIndex); - final int flags = SlotLayoutAccessors.readPayloadFlags(data, layout, payloadRefIndex); - final boolean isCompressed = (flags & 1) != 0; - - final byte[] payloadBytes; - if (length > 0) { - payloadBytes = new byte[length]; - MemorySegment.copy(data, pointer, MemorySegment.ofArray(payloadBytes), 0, length); - } else { - payloadBytes = new byte[0]; - } - node.setRawValue(payloadBytes, isCompressed, null); - } - - /** - * Read inline string payload from the fixed-slot data and set it on the ObjectStringNode. - */ - private static void readInlineObjectStringPayload(final ObjectStringNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, layout, payloadRefIndex); - final int flags = SlotLayoutAccessors.readPayloadFlags(data, layout, payloadRefIndex); - final boolean isCompressed = (flags & 1) != 0; - - final byte[] payloadBytes; - if (length > 0) { - payloadBytes = new byte[length]; - MemorySegment.copy(data, pointer, MemorySegment.ofArray(payloadBytes), 0, length); - } else { - payloadBytes = new byte[0]; - } - node.setRawValue(payloadBytes, isCompressed, null); - } - - /** - * Read inline number payload from the fixed-slot data and set it on the NumberNode. - */ - private static void readInlineNumberPayload(final NumberNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, layout, payloadRefIndex); - - if (length > 0) { - final MemorySegment payloadSlice = data.asSlice(pointer, length); - final MemorySegmentBytesIn bytesIn = new MemorySegmentBytesIn(payloadSlice); - final Number value = NodeKind.deserializeNumber(bytesIn); - node.setValue(value); - } else { - node.setValue(0); - } - } - - /** - * Read inline number payload from the fixed-slot data and set it on the ObjectNumberNode. - */ - private static void readInlineNumberPayloadForObject(final ObjectNumberNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, layout, payloadRefIndex); - - if (length > 0) { - final MemorySegment payloadSlice = data.asSlice(pointer, length); - final MemorySegmentBytesIn bytesIn = new MemorySegmentBytesIn(payloadSlice); - final Number value = NodeKind.deserializeNumber(bytesIn); - node.setValue(value); - } else { - node.setValue(0); - } - } - - /** - * Read inline vector payload (attribute keys or namespace keys) from fixed-slot data and populate - * the ElementNode's lists. - * - *

- * Each vector entry is stored as an uncompressed 8-byte long in the inline payload area. The count - * is derived from {@code length / Long.BYTES}. - *

- * - *

- * This overload uses zero-based addressing (for slices or materialize cold path). - *

- * - * @param node the ElementNode to populate - * @param data fixed-slot bytes - * @param layout the layout descriptor - * @param payloadRefIndex 0 for attributes, 1 for namespaces - * @param isAttributes true to populate attribute keys, false for namespace keys - */ - public static void readInlineVectorPayload(final ElementNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex, final boolean isAttributes) { - readInlineVectorPayload(node, data, 0L, layout, payloadRefIndex, isAttributes); - } - - /** - * Read inline vector payload with explicit base offset (avoids asSlice allocation). - * - * @param node the ElementNode to populate - * @param data the backing MemorySegment - * @param dataOffset absolute byte offset where slot data begins - * @param layout the layout descriptor - * @param payloadRefIndex 0 for attributes, 1 for namespaces - * @param isAttributes true to populate attribute keys, false for namespace keys - */ - public static void readInlineVectorPayload(final ElementNode node, final MemorySegment data, final long dataOffset, - final NodeKindLayout layout, final int payloadRefIndex, final boolean isAttributes) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, dataOffset, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, dataOffset, layout, payloadRefIndex); - final int count = length / Long.BYTES; - if (isAttributes) { - node.clearAttributeKeys(); - for (int i = 0; i < count; i++) { - node.insertAttribute(data.get(ValueLayout.JAVA_LONG_UNALIGNED, dataOffset + pointer + (long) i * Long.BYTES)); - } - } else { - node.clearNamespaceKeys(); - for (int i = 0; i < count; i++) { - node.insertNamespace(data.get(ValueLayout.JAVA_LONG_UNALIGNED, dataOffset + pointer + (long) i * Long.BYTES)); - } - } - } - - /** - * Read inline value payload from the fixed-slot data and set it on an XML ValueNode. Handles - * TextNode, CommentNode, PINode, and AttributeNode. If the compressed flag is set, the node's - * compressed state is updated accordingly. - */ - private static void readInlineXmlValuePayload(final ValueNode node, final MemorySegment data, - final NodeKindLayout layout, final int payloadRefIndex) { - final long pointer = SlotLayoutAccessors.readPayloadPointer(data, layout, payloadRefIndex); - final int length = SlotLayoutAccessors.readPayloadLength(data, layout, payloadRefIndex); - final int flags = SlotLayoutAccessors.readPayloadFlags(data, layout, payloadRefIndex); - final boolean isCompressed = (flags & 1) != 0; - - final byte[] payloadBytes; - if (length > 0) { - payloadBytes = new byte[length]; - MemorySegment.copy(data, pointer, MemorySegment.ofArray(payloadBytes), 0, length); - } else { - payloadBytes = new byte[0]; - } - node.setRawValue(payloadBytes); - if (isCompressed) { - if (node instanceof TextNode tn) { - tn.setCompressed(true); - } else if (node instanceof CommentNode cn) { - cn.setCompressed(true); - } else if (node instanceof PINode pi) { - pi.setCompressed(true); - } - } - } - -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordProjector.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordProjector.java deleted file mode 100644 index 530c34297..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedSlotRecordProjector.java +++ /dev/null @@ -1,469 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.NodeKind; -import io.sirix.node.MemorySegmentBytesOut; -import io.sirix.node.interfaces.BooleanValueNode; -import io.sirix.node.interfaces.DataRecord; -import io.sirix.node.interfaces.NameNode; -import io.sirix.node.interfaces.NumericValueNode; -import io.sirix.node.interfaces.StructNode; -import io.sirix.node.interfaces.immutable.ImmutableNode; -import io.sirix.node.json.ObjectKeyNode; -import io.sirix.node.json.ObjectStringNode; -import io.sirix.node.json.StringNode; -import io.sirix.node.xml.AttributeNode; -import io.sirix.node.xml.CommentNode; -import io.sirix.node.xml.ElementNode; -import io.sirix.node.xml.PINode; -import io.sirix.node.xml.TextNode; - -import java.lang.foreign.MemorySegment; -import java.lang.foreign.ValueLayout; -import java.util.Objects; - -/** - * Projects record structural metadata into a fixed-slot memory layout. - * - *

- * The projector writes fixed-width structural fields directly, fills payload reference metadata, - * and writes inline payload bytes for VALUE_BLOB payloads (strings, numbers). - */ -public final class FixedSlotRecordProjector { - - /** Unaligned long layout for reading/writing vector entries in inline payload. */ - private static final ValueLayout.OfLong LONG_UNALIGNED = ValueLayout.JAVA_LONG_UNALIGNED; - - /** Pre-zeroed buffer used to clear header bytes without allocating an asSlice(). */ - private static final MemorySegment ZERO_BUFFER = MemorySegment.ofArray(new byte[512]); - - /** Thread-local buffer for serializing number values inline. */ - private static final ThreadLocal NUMBER_BUFFER = - ThreadLocal.withInitial(() -> new MemorySegmentBytesOut(32)); - - private FixedSlotRecordProjector() {} - - /** - * Check whether all payload refs in the layout are supported for inline projection. Supported - * kinds: VALUE_BLOB, ATTRIBUTE_VECTOR, NAMESPACE_VECTOR. - * - * @param layout fixed-slot layout descriptor - * @return {@code true} if all payload refs are supported or there are no payload refs - */ - public static boolean hasSupportedPayloads(final NodeKindLayout layout) { - for (int i = 0, refs = layout.payloadRefCount(); i < refs; i++) { - final PayloadRefKind kind = layout.payloadRef(i).kind(); - if (kind != PayloadRefKind.VALUE_BLOB && kind != PayloadRefKind.ATTRIBUTE_VECTOR - && kind != PayloadRefKind.NAMESPACE_VECTOR) { - return false; - } - } - return true; - } - - /** - * Compute the inline payload byte length for a record projected into the given layout. - * - * @param record the data record - * @param layout the layout descriptor - * @return total inline payload bytes, or {@code -1} if the record cannot be projected - */ - public static int computeInlinePayloadLength(final DataRecord record, final NodeKindLayout layout) { - int total = 0; - for (int i = 0, refs = layout.payloadRefCount(); i < refs; i++) { - final PayloadRef payloadRef = layout.payloadRef(i); - final PayloadRefKind kind = payloadRef.kind(); - if (kind == PayloadRefKind.VALUE_BLOB) { - final int length = getValueBlobLength(record); - if (length < 0) { - return -1; - } - total += length; - } else if (kind == PayloadRefKind.ATTRIBUTE_VECTOR || kind == PayloadRefKind.NAMESPACE_VECTOR) { - final int length = getVectorPayloadLength(record, kind); - if (length < 0) { - return -1; - } - total += length; - } else { - return -1; - } - } - return total; - } - - /** - * Project a record into the given fixed-slot target at the specified base offset. - * - *

- * For payload-bearing nodes (strings, numbers), the target area starting at {@code baseOffset} must - * be sized to include both the fixed-slot header and inline payload bytes. The payload bytes are - * written immediately after the header, and the PayloadRef metadata in the header stores the offset - * (relative to slot data start) and length. - * - * @param record record to project - * @param layout fixed-slot layout descriptor - * @param targetSlot target memory (may be the full slotMemory) - * @param baseOffset absolute byte offset where the slot data begins - * @return {@code true} if projection succeeded, {@code false} if the record cannot be projected - */ - public static boolean project(final DataRecord record, final NodeKindLayout layout, final MemorySegment targetSlot, - final long baseOffset) { - Objects.requireNonNull(record, "record must not be null"); - Objects.requireNonNull(layout, "layout must not be null"); - Objects.requireNonNull(targetSlot, "targetSlot must not be null"); - - if (!layout.isFixedSlotSupported()) { - return false; - } - - // Only supported payload kinds are allowed for inline projection. - if (!hasSupportedPayloads(layout)) { - return false; - } - - final int headerSize = layout.fixedSlotSizeInBytes(); - if (targetSlot.byteSize() - baseOffset < headerSize) { - throw new IllegalArgumentException( - "target slot too small: available=" + (targetSlot.byteSize() - baseOffset) + " < " + headerSize); - } - - // Clear the header portion using bulk copy from pre-zeroed buffer (avoids asSlice allocation). - MemorySegment.copy(ZERO_BUFFER, 0, targetSlot, baseOffset, headerSize); - - final ImmutableNode immutableNode = record instanceof ImmutableNode node - ? node - : null; - final StructNode structNode = record instanceof StructNode node - ? node - : null; - final NameNode nameNode = record instanceof NameNode node - ? node - : null; - final BooleanValueNode booleanValueNode = record instanceof BooleanValueNode node - ? node - : null; - final ObjectKeyNode objectKeyNode = record instanceof ObjectKeyNode node - ? node - : null; - - if (!writeCommonFields(record, immutableNode, targetSlot, baseOffset, layout)) { - return false; - } - if (!writeStructuralFields(structNode, targetSlot, baseOffset, layout)) { - return false; - } - if (!writeNameFields(nameNode, objectKeyNode, targetSlot, baseOffset, layout)) { - return false; - } - if (!writeBooleanField(booleanValueNode, targetSlot, baseOffset, layout)) { - return false; - } - if (!writeInlinePayloadRefs(record, targetSlot, baseOffset, layout)) { - return false; - } - - return true; - } - - private static boolean writeCommonFields(final DataRecord record, final ImmutableNode immutableNode, - final MemorySegment targetSlot, final long baseOffset, final NodeKindLayout layout) { - if (layout.hasField(StructuralField.PREVIOUS_REVISION)) { - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.PREVIOUS_REVISION, - record.getPreviousRevisionNumber()); - } - if (layout.hasField(StructuralField.LAST_MODIFIED_REVISION)) { - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.LAST_MODIFIED_REVISION, - record.getLastModifiedRevisionNumber()); - } - if (layout.hasField(StructuralField.PARENT_KEY)) { - if (immutableNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.PARENT_KEY, - immutableNode.getParentKey()); - } - if (layout.hasField(StructuralField.HASH)) { - if (immutableNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.HASH, immutableNode.getHash()); - } - return true; - } - - private static boolean writeStructuralFields(final StructNode structNode, final MemorySegment targetSlot, - final long baseOffset, final NodeKindLayout layout) { - if (layout.hasField(StructuralField.RIGHT_SIBLING_KEY)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.RIGHT_SIBLING_KEY, - structNode.getRightSiblingKey()); - } - if (layout.hasField(StructuralField.LEFT_SIBLING_KEY)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.LEFT_SIBLING_KEY, - structNode.getLeftSiblingKey()); - } - if (layout.hasField(StructuralField.FIRST_CHILD_KEY)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.FIRST_CHILD_KEY, - structNode.getFirstChildKey()); - } - if (layout.hasField(StructuralField.LAST_CHILD_KEY)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.LAST_CHILD_KEY, - structNode.getLastChildKey()); - } - if (layout.hasField(StructuralField.CHILD_COUNT)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.CHILD_COUNT, - structNode.getChildCount()); - } - if (layout.hasField(StructuralField.DESCENDANT_COUNT)) { - if (structNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.DESCENDANT_COUNT, - structNode.getDescendantCount()); - } - return true; - } - - private static boolean writeNameFields(final NameNode nameNode, final ObjectKeyNode objectKeyNode, - final MemorySegment targetSlot, final long baseOffset, final NodeKindLayout layout) { - if (layout.hasField(StructuralField.PATH_NODE_KEY)) { - if (nameNode == null) { - return false; - } - SlotLayoutAccessors.writeLongField(targetSlot, baseOffset, layout, StructuralField.PATH_NODE_KEY, - nameNode.getPathNodeKey()); - } - if (layout.hasField(StructuralField.PREFIX_KEY)) { - if (nameNode == null) { - return false; - } - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.PREFIX_KEY, - nameNode.getPrefixKey()); - } - if (layout.hasField(StructuralField.LOCAL_NAME_KEY)) { - if (nameNode == null) { - return false; - } - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.LOCAL_NAME_KEY, - nameNode.getLocalNameKey()); - } - if (layout.hasField(StructuralField.URI_KEY)) { - if (nameNode == null) { - return false; - } - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.URI_KEY, nameNode.getURIKey()); - } - if (layout.hasField(StructuralField.NAME_KEY)) { - if (objectKeyNode == null) { - return false; - } - SlotLayoutAccessors.writeIntField(targetSlot, baseOffset, layout, StructuralField.NAME_KEY, - objectKeyNode.getNameKey()); - } - return true; - } - - private static boolean writeBooleanField(final BooleanValueNode booleanValueNode, final MemorySegment targetSlot, - final long baseOffset, final NodeKindLayout layout) { - if (layout.hasField(StructuralField.BOOLEAN_VALUE)) { - if (booleanValueNode == null) { - return false; - } - SlotLayoutAccessors.writeBooleanField(targetSlot, baseOffset, layout, StructuralField.BOOLEAN_VALUE, - booleanValueNode.getValue()); - } - return true; - } - - /** - * Write PayloadRef metadata and inline payload bytes. Supports VALUE_BLOB (strings, numbers), - * ATTRIBUTE_VECTOR, and NAMESPACE_VECTOR. The inline payload is written immediately after the - * fixed-slot header. - */ - private static boolean writeInlinePayloadRefs(final DataRecord record, final MemorySegment targetSlot, - final long baseOffset, final NodeKindLayout layout) { - final int refs = layout.payloadRefCount(); - if (refs == 0) { - return true; - } - - // payloadOffset is relative to slot data start (i.e. starts at headerSize) - int payloadOffset = layout.fixedSlotSizeInBytes(); - - for (int i = 0; i < refs; i++) { - final PayloadRef payloadRef = layout.payloadRef(i); - final PayloadRefKind refKind = payloadRef.kind(); - - if (refKind == PayloadRefKind.ATTRIBUTE_VECTOR) { - if (!(record instanceof ElementNode element)) { - return false; - } - final int count = element.getAttributeCount(); - final int length = count * Long.BYTES; - SlotLayoutAccessors.writePayloadRef(targetSlot, baseOffset, layout, i, payloadOffset, length, 0); - for (int j = 0; j < count; j++) { - targetSlot.set(LONG_UNALIGNED, baseOffset + payloadOffset, element.getAttributeKey(j)); - payloadOffset += Long.BYTES; - } - continue; - } else if (refKind == PayloadRefKind.NAMESPACE_VECTOR) { - if (!(record instanceof ElementNode element)) { - return false; - } - final int count = element.getNamespaceCount(); - final int length = count * Long.BYTES; - SlotLayoutAccessors.writePayloadRef(targetSlot, baseOffset, layout, i, payloadOffset, length, 0); - for (int j = 0; j < count; j++) { - targetSlot.set(LONG_UNALIGNED, baseOffset + payloadOffset, element.getNamespaceKey(j)); - payloadOffset += Long.BYTES; - } - continue; - } - - // VALUE_BLOB handling - if (refKind != PayloadRefKind.VALUE_BLOB) { - return false; - } - - final byte[] payloadBytes; - final int flags; - - if (record instanceof StringNode sn) { - payloadBytes = sn.getRawValueWithoutDecompression(); - flags = sn.isCompressed() - ? 1 - : 0; - } else if (record instanceof ObjectStringNode osn) { - payloadBytes = osn.getRawValueWithoutDecompression(); - flags = osn.isCompressed() - ? 1 - : 0; - } else if (record instanceof TextNode tn) { - payloadBytes = tn.getRawValueWithoutDecompression(); - flags = tn.isCompressed() - ? 1 - : 0; - } else if (record instanceof CommentNode cn) { - payloadBytes = cn.getRawValueWithoutDecompression(); - flags = cn.isCompressed() - ? 1 - : 0; - } else if (record instanceof PINode pi) { - payloadBytes = pi.getRawValueWithoutDecompression(); - flags = pi.isCompressed() - ? 1 - : 0; - } else if (record instanceof AttributeNode an) { - payloadBytes = an.getRawValueWithoutDecompression(); - flags = 0; // attributes never compressed - } else if (record instanceof NumericValueNode nn) { - final MemorySegmentBytesOut buf = NUMBER_BUFFER.get(); - buf.clear(); - NodeKind.serializeNumber(nn.getValue(), buf); - final MemorySegment serialized = buf.getDestination(); - final int numLen = (int) serialized.byteSize(); - SlotLayoutAccessors.writePayloadRef(targetSlot, baseOffset, layout, i, payloadOffset, numLen, 0); - if (numLen > 0) { - MemorySegment.copy(serialized, 0, targetSlot, baseOffset + payloadOffset, numLen); - } - payloadOffset += numLen; - continue; - } else { - return false; - } - - final int length = payloadBytes != null - ? payloadBytes.length - : 0; - SlotLayoutAccessors.writePayloadRef(targetSlot, baseOffset, layout, i, payloadOffset, length, flags); - if (length > 0) { - MemorySegment.copy(MemorySegment.ofArray(payloadBytes), 0, targetSlot, baseOffset + payloadOffset, length); - } - payloadOffset += length; - } - - return true; - } - - /** - * Get the inline payload byte length for a VALUE_BLOB from the given record. - * - * @return byte length, or {@code -1} if the record type is unsupported - */ - private static int getValueBlobLength(final DataRecord record) { - if (record instanceof StringNode sn) { - final byte[] raw = sn.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof ObjectStringNode osn) { - final byte[] raw = osn.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof TextNode tn) { - final byte[] raw = tn.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof CommentNode cn) { - final byte[] raw = cn.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof PINode pi) { - final byte[] raw = pi.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof AttributeNode an) { - final byte[] raw = an.getRawValueWithoutDecompression(); - return raw != null - ? raw.length - : 0; - } - if (record instanceof NumericValueNode nn) { - final MemorySegmentBytesOut buf = NUMBER_BUFFER.get(); - buf.clear(); - NodeKind.serializeNumber(nn.getValue(), buf); - return (int) buf.getDestination().byteSize(); - } - return -1; - } - - /** - * Get the inline payload byte length for an ATTRIBUTE_VECTOR or NAMESPACE_VECTOR. - * - * @return byte length (count * 8), or {@code -1} if the record type is unsupported - */ - private static int getVectorPayloadLength(final DataRecord record, final PayloadRefKind kind) { - if (!(record instanceof ElementNode element)) { - return -1; - } - if (kind == PayloadRefKind.ATTRIBUTE_VECTOR) { - return element.getAttributeCount() * Long.BYTES; - } else if (kind == PayloadRefKind.NAMESPACE_VECTOR) { - return element.getNamespaceCount() * Long.BYTES; - } - return -1; - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedToCompactTransformer.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedToCompactTransformer.java deleted file mode 100644 index f1900147b..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/FixedToCompactTransformer.java +++ /dev/null @@ -1,628 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.access.ResourceConfiguration; -import io.sirix.access.trx.node.HashType; -import io.sirix.node.BytesOut; -import io.sirix.node.DeltaVarIntCodec; -import io.sirix.node.NodeKind; - -import java.lang.foreign.MemorySegment; -import java.lang.foreign.ValueLayout; - -/** - * Zero-allocation byte-level transformer from fixed-slot format to compact (delta-varint) format. - * - *

- * Instead of materializing a DataRecord Java object and re-serializing it, this class reads - * fixed-width fields at known offsets and writes the compact encoding directly. The output is - * byte-identical to - * {@code NodeKind.serialize(sink, FixedSlotRecordMaterializer.materialize(...), config)}. - * - *

- * All field offsets are pre-computed as {@code static final} constants from {@link NodeKindLayouts} - * at class-load time, so the JIT treats them as compile-time constants. - * - *

- * The {@code baseOffset} parameter allows reading from a full page {@link MemorySegment} without - * creating an {@code asSlice()} wrapper, eliminating one heap allocation per slot on the commit path. - */ -public final class FixedToCompactTransformer { - - private static final ValueLayout.OfLong JAVA_LONG_UNALIGNED = ValueLayout.JAVA_LONG.withByteAlignment(1); - private static final ValueLayout.OfInt JAVA_INT_UNALIGNED = ValueLayout.JAVA_INT.withByteAlignment(1); - - // ──────────────────────── JSON OBJECT ──────────────────────── - private static final NodeKindLayout L_OBJ = NodeKindLayouts.layoutFor(NodeKind.OBJECT); - private static final int OBJ_PARENT = L_OBJ.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int OBJ_RSIB = L_OBJ.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int OBJ_LSIB = L_OBJ.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int OBJ_FCHILD = L_OBJ.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int OBJ_LCHILD = L_OBJ.offsetOfOrMinusOne(StructuralField.LAST_CHILD_KEY); - private static final int OBJ_PREV_REV = L_OBJ.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int OBJ_LAST_MOD = L_OBJ.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int OBJ_HASH = L_OBJ.offsetOfOrMinusOne(StructuralField.HASH); - private static final int OBJ_CHILD_CNT = L_OBJ.offsetOfOrMinusOne(StructuralField.CHILD_COUNT); - private static final int OBJ_DESC_CNT = L_OBJ.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - - // ──────────────────────── JSON ARRAY ──────────────────────── - private static final NodeKindLayout L_ARR = NodeKindLayouts.layoutFor(NodeKind.ARRAY); - private static final int ARR_PARENT = L_ARR.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int ARR_RSIB = L_ARR.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int ARR_LSIB = L_ARR.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int ARR_FCHILD = L_ARR.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int ARR_LCHILD = L_ARR.offsetOfOrMinusOne(StructuralField.LAST_CHILD_KEY); - private static final int ARR_PATH = L_ARR.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int ARR_PREV_REV = L_ARR.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int ARR_LAST_MOD = L_ARR.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int ARR_HASH = L_ARR.offsetOfOrMinusOne(StructuralField.HASH); - private static final int ARR_CHILD_CNT = L_ARR.offsetOfOrMinusOne(StructuralField.CHILD_COUNT); - private static final int ARR_DESC_CNT = L_ARR.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - - // ──────────────────────── JSON OBJECT_KEY ──────────────────────── - private static final NodeKindLayout L_OKEY = NodeKindLayouts.layoutFor(NodeKind.OBJECT_KEY); - private static final int OKEY_PARENT = L_OKEY.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int OKEY_RSIB = L_OKEY.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int OKEY_LSIB = L_OKEY.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int OKEY_FCHILD = L_OKEY.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int OKEY_PATH = L_OKEY.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int OKEY_NAME = L_OKEY.offsetOfOrMinusOne(StructuralField.NAME_KEY); - private static final int OKEY_PREV_REV = L_OKEY.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int OKEY_LAST_MOD = L_OKEY.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int OKEY_HASH = L_OKEY.offsetOfOrMinusOne(StructuralField.HASH); - private static final int OKEY_DESC_CNT = L_OKEY.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - - // ──────────────────────── JSON STRING_VALUE ──────────────────────── - private static final NodeKindLayout L_STR = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - private static final int STR_PARENT = L_STR.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int STR_RSIB = L_STR.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int STR_LSIB = L_STR.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int STR_PREV_REV = L_STR.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int STR_LAST_MOD = L_STR.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int STR_HASH = L_STR.offsetOfOrMinusOne(StructuralField.HASH); - private static final int STR_PR0_PTR = L_STR.payloadRef(0).pointerOffset(); - private static final int STR_PR0_LEN = L_STR.payloadRef(0).lengthOffset(); - private static final int STR_PR0_FLAGS = L_STR.payloadRef(0).flagsOffset(); - - // ──────────────────────── JSON NUMBER_VALUE ──────────────────────── - private static final NodeKindLayout L_NUM = NodeKindLayouts.layoutFor(NodeKind.NUMBER_VALUE); - private static final int NUM_PARENT = L_NUM.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int NUM_RSIB = L_NUM.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int NUM_LSIB = L_NUM.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int NUM_PREV_REV = L_NUM.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int NUM_LAST_MOD = L_NUM.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int NUM_HASH = L_NUM.offsetOfOrMinusOne(StructuralField.HASH); - private static final int NUM_PR0_PTR = L_NUM.payloadRef(0).pointerOffset(); - private static final int NUM_PR0_LEN = L_NUM.payloadRef(0).lengthOffset(); - - // ──────────────────────── JSON BOOLEAN_VALUE ──────────────────────── - private static final NodeKindLayout L_BOOL = NodeKindLayouts.layoutFor(NodeKind.BOOLEAN_VALUE); - private static final int BOOL_PARENT = L_BOOL.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int BOOL_RSIB = L_BOOL.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int BOOL_LSIB = L_BOOL.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int BOOL_PREV_REV = L_BOOL.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int BOOL_LAST_MOD = L_BOOL.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int BOOL_HASH = L_BOOL.offsetOfOrMinusOne(StructuralField.HASH); - private static final int BOOL_VAL = L_BOOL.offsetOfOrMinusOne(StructuralField.BOOLEAN_VALUE); - - // ──────────────────────── JSON NULL_VALUE ──────────────────────── - private static final NodeKindLayout L_NULL = NodeKindLayouts.layoutFor(NodeKind.NULL_VALUE); - private static final int NULL_PARENT = L_NULL.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int NULL_RSIB = L_NULL.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int NULL_LSIB = L_NULL.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int NULL_PREV_REV = L_NULL.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int NULL_LAST_MOD = L_NULL.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int NULL_HASH = L_NULL.offsetOfOrMinusOne(StructuralField.HASH); - - // ──────────────────────── JSON OBJECT_STRING_VALUE ──────────────────────── - private static final NodeKindLayout L_OSTR = NodeKindLayouts.layoutFor(NodeKind.OBJECT_STRING_VALUE); - private static final int OSTR_PARENT = L_OSTR.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int OSTR_PREV_REV = L_OSTR.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int OSTR_LAST_MOD = L_OSTR.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int OSTR_HASH = L_OSTR.offsetOfOrMinusOne(StructuralField.HASH); - private static final int OSTR_PR0_PTR = L_OSTR.payloadRef(0).pointerOffset(); - private static final int OSTR_PR0_LEN = L_OSTR.payloadRef(0).lengthOffset(); - private static final int OSTR_PR0_FLAGS = L_OSTR.payloadRef(0).flagsOffset(); - - // ──────────────────────── JSON OBJECT_NUMBER_VALUE ──────────────────────── - private static final NodeKindLayout L_ONUM = NodeKindLayouts.layoutFor(NodeKind.OBJECT_NUMBER_VALUE); - private static final int ONUM_PARENT = L_ONUM.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int ONUM_PREV_REV = L_ONUM.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int ONUM_LAST_MOD = L_ONUM.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int ONUM_HASH = L_ONUM.offsetOfOrMinusOne(StructuralField.HASH); - private static final int ONUM_PR0_PTR = L_ONUM.payloadRef(0).pointerOffset(); - private static final int ONUM_PR0_LEN = L_ONUM.payloadRef(0).lengthOffset(); - - // ──────────────────────── JSON OBJECT_BOOLEAN_VALUE ──────────────────────── - private static final NodeKindLayout L_OBOOL = NodeKindLayouts.layoutFor(NodeKind.OBJECT_BOOLEAN_VALUE); - private static final int OBOOL_PARENT = L_OBOOL.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int OBOOL_PREV_REV = L_OBOOL.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int OBOOL_LAST_MOD = L_OBOOL.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int OBOOL_HASH = L_OBOOL.offsetOfOrMinusOne(StructuralField.HASH); - private static final int OBOOL_VAL = L_OBOOL.offsetOfOrMinusOne(StructuralField.BOOLEAN_VALUE); - - // ──────────────────────── JSON OBJECT_NULL_VALUE ──────────────────────── - private static final NodeKindLayout L_ONULL = NodeKindLayouts.layoutFor(NodeKind.OBJECT_NULL_VALUE); - private static final int ONULL_PARENT = L_ONULL.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int ONULL_PREV_REV = L_ONULL.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int ONULL_LAST_MOD = L_ONULL.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int ONULL_HASH = L_ONULL.offsetOfOrMinusOne(StructuralField.HASH); - - // ──────────────────────── JSON_DOCUMENT ──────────────────────── - private static final NodeKindLayout L_JDOC = NodeKindLayouts.layoutFor(NodeKind.JSON_DOCUMENT); - private static final int JDOC_FCHILD = L_JDOC.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int JDOC_DESC_CNT = L_JDOC.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - - // ──────────────────────── XML_DOCUMENT ──────────────────────── - private static final NodeKindLayout L_XDOC = NodeKindLayouts.layoutFor(NodeKind.XML_DOCUMENT); - private static final int XDOC_FCHILD = L_XDOC.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int XDOC_HASH = L_XDOC.offsetOfOrMinusOne(StructuralField.HASH); - private static final int XDOC_DESC_CNT = L_XDOC.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - - // ──────────────────────── XML ELEMENT ──────────────────────── - private static final NodeKindLayout L_ELEM = NodeKindLayouts.layoutFor(NodeKind.ELEMENT); - private static final int ELEM_PARENT = L_ELEM.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int ELEM_RSIB = L_ELEM.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int ELEM_LSIB = L_ELEM.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int ELEM_FCHILD = L_ELEM.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int ELEM_LCHILD = L_ELEM.offsetOfOrMinusOne(StructuralField.LAST_CHILD_KEY); - private static final int ELEM_PATH = L_ELEM.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int ELEM_PREFIX = L_ELEM.offsetOfOrMinusOne(StructuralField.PREFIX_KEY); - private static final int ELEM_LNAME = L_ELEM.offsetOfOrMinusOne(StructuralField.LOCAL_NAME_KEY); - private static final int ELEM_URI = L_ELEM.offsetOfOrMinusOne(StructuralField.URI_KEY); - private static final int ELEM_PREV_REV = L_ELEM.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int ELEM_LAST_MOD = L_ELEM.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int ELEM_HASH = L_ELEM.offsetOfOrMinusOne(StructuralField.HASH); - private static final int ELEM_CHILD_CNT = L_ELEM.offsetOfOrMinusOne(StructuralField.CHILD_COUNT); - private static final int ELEM_DESC_CNT = L_ELEM.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - private static final int ELEM_ATTR_PR_PTR = L_ELEM.payloadRef(0).pointerOffset(); - private static final int ELEM_ATTR_PR_LEN = L_ELEM.payloadRef(0).lengthOffset(); - private static final int ELEM_NS_PR_PTR = L_ELEM.payloadRef(1).pointerOffset(); - private static final int ELEM_NS_PR_LEN = L_ELEM.payloadRef(1).lengthOffset(); - - // ──────────────────────── XML ATTRIBUTE ──────────────────────── - private static final NodeKindLayout L_ATTR = NodeKindLayouts.layoutFor(NodeKind.ATTRIBUTE); - private static final int ATTR_PARENT = L_ATTR.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int ATTR_PATH = L_ATTR.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int ATTR_PREFIX = L_ATTR.offsetOfOrMinusOne(StructuralField.PREFIX_KEY); - private static final int ATTR_LNAME = L_ATTR.offsetOfOrMinusOne(StructuralField.LOCAL_NAME_KEY); - private static final int ATTR_URI = L_ATTR.offsetOfOrMinusOne(StructuralField.URI_KEY); - private static final int ATTR_PREV_REV = L_ATTR.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int ATTR_LAST_MOD = L_ATTR.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int ATTR_PR0_PTR = L_ATTR.payloadRef(0).pointerOffset(); - private static final int ATTR_PR0_LEN = L_ATTR.payloadRef(0).lengthOffset(); - - // ──────────────────────── XML NAMESPACE ──────────────────────── - private static final NodeKindLayout L_NS = NodeKindLayouts.layoutFor(NodeKind.NAMESPACE); - private static final int NS_PARENT = L_NS.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int NS_PATH = L_NS.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int NS_PREFIX = L_NS.offsetOfOrMinusOne(StructuralField.PREFIX_KEY); - private static final int NS_LNAME = L_NS.offsetOfOrMinusOne(StructuralField.LOCAL_NAME_KEY); - private static final int NS_URI = L_NS.offsetOfOrMinusOne(StructuralField.URI_KEY); - private static final int NS_PREV_REV = L_NS.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int NS_LAST_MOD = L_NS.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - - // ──────────────────────── XML TEXT ──────────────────────── - private static final NodeKindLayout L_TEXT = NodeKindLayouts.layoutFor(NodeKind.TEXT); - private static final int TEXT_PARENT = L_TEXT.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int TEXT_RSIB = L_TEXT.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int TEXT_LSIB = L_TEXT.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int TEXT_PREV_REV = L_TEXT.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int TEXT_LAST_MOD = L_TEXT.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int TEXT_PR0_PTR = L_TEXT.payloadRef(0).pointerOffset(); - private static final int TEXT_PR0_LEN = L_TEXT.payloadRef(0).lengthOffset(); - private static final int TEXT_PR0_FLAGS = L_TEXT.payloadRef(0).flagsOffset(); - - // ──────────────────────── XML COMMENT ──────────────────────── - private static final NodeKindLayout L_CMT = NodeKindLayouts.layoutFor(NodeKind.COMMENT); - private static final int CMT_PARENT = L_CMT.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int CMT_RSIB = L_CMT.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int CMT_LSIB = L_CMT.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int CMT_PREV_REV = L_CMT.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int CMT_LAST_MOD = L_CMT.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int CMT_PR0_PTR = L_CMT.payloadRef(0).pointerOffset(); - private static final int CMT_PR0_LEN = L_CMT.payloadRef(0).lengthOffset(); - private static final int CMT_PR0_FLAGS = L_CMT.payloadRef(0).flagsOffset(); - - // ──────────────────────── XML PROCESSING_INSTRUCTION ──────────────────────── - private static final NodeKindLayout L_PI = NodeKindLayouts.layoutFor(NodeKind.PROCESSING_INSTRUCTION); - private static final int PI_PARENT = L_PI.offsetOfOrMinusOne(StructuralField.PARENT_KEY); - private static final int PI_RSIB = L_PI.offsetOfOrMinusOne(StructuralField.RIGHT_SIBLING_KEY); - private static final int PI_LSIB = L_PI.offsetOfOrMinusOne(StructuralField.LEFT_SIBLING_KEY); - private static final int PI_FCHILD = L_PI.offsetOfOrMinusOne(StructuralField.FIRST_CHILD_KEY); - private static final int PI_LCHILD = L_PI.offsetOfOrMinusOne(StructuralField.LAST_CHILD_KEY); - private static final int PI_PATH = L_PI.offsetOfOrMinusOne(StructuralField.PATH_NODE_KEY); - private static final int PI_PREFIX = L_PI.offsetOfOrMinusOne(StructuralField.PREFIX_KEY); - private static final int PI_LNAME = L_PI.offsetOfOrMinusOne(StructuralField.LOCAL_NAME_KEY); - private static final int PI_URI = L_PI.offsetOfOrMinusOne(StructuralField.URI_KEY); - private static final int PI_PREV_REV = L_PI.offsetOfOrMinusOne(StructuralField.PREVIOUS_REVISION); - private static final int PI_LAST_MOD = L_PI.offsetOfOrMinusOne(StructuralField.LAST_MODIFIED_REVISION); - private static final int PI_HASH = L_PI.offsetOfOrMinusOne(StructuralField.HASH); - private static final int PI_CHILD_CNT = L_PI.offsetOfOrMinusOne(StructuralField.CHILD_COUNT); - private static final int PI_DESC_CNT = L_PI.offsetOfOrMinusOne(StructuralField.DESCENDANT_COUNT); - private static final int PI_PR0_PTR = L_PI.payloadRef(0).pointerOffset(); - private static final int PI_PR0_LEN = L_PI.payloadRef(0).lengthOffset(); - private static final int PI_PR0_FLAGS = L_PI.payloadRef(0).flagsOffset(); - - private FixedToCompactTransformer() {} - - /** - * Transform a fixed-slot record to compact format, writing the result (including the leading - * NodeKind id byte) into {@code sink}. - * - *

- * Reads from {@code memory} at {@code baseOffset + fieldOffset} for each field, eliminating - * the need to create an {@code asSlice()} wrapper per slot. - * - * @param nodeKind the kind of node stored in this slot - * @param nodeKey the absolute node key - * @param memory the page memory segment containing the fixed-slot bytes - * @param baseOffset byte offset within {@code memory} where the slot data starts - * @param config the resource configuration - * @param sink the output sink to write compact bytes into - */ - public static void transform(final NodeKind nodeKind, final long nodeKey, final MemorySegment memory, - final long baseOffset, final ResourceConfiguration config, final BytesOut sink) { - sink.writeByte(nodeKind.getId()); - - switch (nodeKind) { - case OBJECT -> transformObject(nodeKey, memory, baseOffset, config, sink); - case ARRAY -> transformArray(nodeKey, memory, baseOffset, config, sink); - case OBJECT_KEY -> transformObjectKey(nodeKey, memory, baseOffset, config, sink); - case STRING_VALUE -> transformStringValue(nodeKey, memory, baseOffset, config, sink); - case NUMBER_VALUE -> transformNumberValue(nodeKey, memory, baseOffset, config, sink); - case BOOLEAN_VALUE -> transformBooleanValue(nodeKey, memory, baseOffset, config, sink); - case NULL_VALUE -> transformNullValue(nodeKey, memory, baseOffset, config, sink); - case OBJECT_STRING_VALUE -> transformObjectStringValue(nodeKey, memory, baseOffset, config, sink); - case OBJECT_NUMBER_VALUE -> transformObjectNumberValue(nodeKey, memory, baseOffset, config, sink); - case OBJECT_BOOLEAN_VALUE -> transformObjectBooleanValue(nodeKey, memory, baseOffset, config, sink); - case OBJECT_NULL_VALUE -> transformObjectNullValue(nodeKey, memory, baseOffset, config, sink); - case JSON_DOCUMENT -> transformJsonDocument(nodeKey, memory, baseOffset, sink); - case XML_DOCUMENT -> transformXmlDocument(nodeKey, memory, baseOffset, config, sink); - case ELEMENT -> transformElement(nodeKey, memory, baseOffset, config, sink); - case ATTRIBUTE -> transformAttribute(nodeKey, memory, baseOffset, sink); - case NAMESPACE -> transformNamespace(nodeKey, memory, baseOffset, sink); - case TEXT -> transformText(nodeKey, memory, baseOffset, sink); - case COMMENT -> transformComment(nodeKey, memory, baseOffset, sink); - case PROCESSING_INSTRUCTION -> transformProcessingInstruction(nodeKey, memory, baseOffset, config, sink); - default -> - throw new UnsupportedOperationException("FixedToCompactTransformer does not support node kind: " + nodeKind); - } - } - - // ════════════════════════════ JSON STRUCTURAL ════════════════════════════ - - private static void transformObject(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBJ_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBJ_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBJ_LSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBJ_FCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBJ_LCHILD), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OBJ_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OBJ_LAST_MOD)); - if (config.storeChildCount()) { - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + OBJ_CHILD_CNT)); - } - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + OBJ_HASH)); - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + OBJ_DESC_CNT)); - } - } - - private static void transformArray(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_LSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_FCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_LCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ARR_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ARR_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ARR_LAST_MOD)); - if (config.storeChildCount()) { - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + ARR_CHILD_CNT)); - } - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + ARR_HASH)); - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + ARR_DESC_CNT)); - } - } - - private static void transformObjectKey(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OKEY_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OKEY_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OKEY_LSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OKEY_FCHILD), nodeKey); - // LAST_CHILD_KEY is in the fixed layout but NOT written in compact format - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OKEY_NAME)); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OKEY_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OKEY_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OKEY_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + OKEY_HASH)); - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + OKEY_DESC_CNT)); - } - } - - // ════════════════════════════ JSON VALUE (WITH SIBLINGS) ════════════════════════════ - - private static void transformStringValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + STR_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + STR_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + STR_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + STR_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + STR_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + STR_HASH)); - } - final int flags = m.get(JAVA_INT_UNALIGNED, b + STR_PR0_FLAGS); - sink.writeByte((byte) (flags & 1)); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + STR_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + STR_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - private static void transformNumberValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NUM_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NUM_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NUM_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NUM_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NUM_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + NUM_HASH)); - } - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + NUM_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + NUM_PR0_LEN); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - private static void transformBooleanValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + BOOL_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + BOOL_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + BOOL_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + BOOL_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + BOOL_LAST_MOD)); - sink.writeBoolean(m.get(ValueLayout.JAVA_BYTE, b + BOOL_VAL) != 0); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + BOOL_HASH)); - } - } - - private static void transformNullValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NULL_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NULL_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NULL_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NULL_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NULL_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + NULL_HASH)); - } - } - - // ════════════════════════════ JSON VALUE (OBJECT-PROPERTY, NO SIBLINGS) ════════════════════════════ - - private static void transformObjectStringValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OSTR_PARENT), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OSTR_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OSTR_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + OSTR_HASH)); - } - final int flags = m.get(JAVA_INT_UNALIGNED, b + OSTR_PR0_FLAGS); - sink.writeByte((byte) (flags & 1)); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + OSTR_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + OSTR_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - private static void transformObjectNumberValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ONUM_PARENT), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ONUM_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ONUM_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + ONUM_HASH)); - } - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + ONUM_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + ONUM_PR0_LEN); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - private static void transformObjectBooleanValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + OBOOL_PARENT), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OBOOL_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + OBOOL_LAST_MOD)); - sink.writeBoolean(m.get(ValueLayout.JAVA_BYTE, b + OBOOL_VAL) != 0); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + OBOOL_HASH)); - } - } - - private static void transformObjectNullValue(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ONULL_PARENT), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ONULL_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ONULL_LAST_MOD)); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + ONULL_HASH)); - } - } - - // ════════════════════════════ JSON DOCUMENT ════════════════════════════ - - private static void transformJsonDocument(final long nodeKey, final MemorySegment m, final long b, - final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + JDOC_FCHILD), nodeKey); - DeltaVarIntCodec.encodeSignedLong(sink, m.get(JAVA_LONG_UNALIGNED, b + JDOC_DESC_CNT)); - } - - // ════════════════════════════ XML DOCUMENT ════════════════════════════ - - private static void transformXmlDocument(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + XDOC_FCHILD), nodeKey); - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + XDOC_HASH)); - DeltaVarIntCodec.encodeSignedLong(sink, m.get(JAVA_LONG_UNALIGNED, b + XDOC_DESC_CNT)); - } - } - - // ════════════════════════════ XML ELEMENT ════════════════════════════ - - private static void transformElement(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_LSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_FCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_LCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ELEM_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ELEM_PREFIX)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ELEM_LNAME)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ELEM_URI)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ELEM_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ELEM_LAST_MOD)); - if (config.storeChildCount()) { - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + ELEM_CHILD_CNT)); - } - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + ELEM_HASH)); - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + ELEM_DESC_CNT)); - } - // Attribute keys vector - final long attrPointer = m.get(JAVA_LONG_UNALIGNED, b + ELEM_ATTR_PR_PTR); - final int attrLength = m.get(JAVA_INT_UNALIGNED, b + ELEM_ATTR_PR_LEN); - final int attrCount = attrLength / Long.BYTES; - DeltaVarIntCodec.encodeSigned(sink, attrCount); - for (int i = 0; i < attrCount; i++) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + attrPointer + (long) i * Long.BYTES), nodeKey); - } - // Namespace keys vector - final long nsPointer = m.get(JAVA_LONG_UNALIGNED, b + ELEM_NS_PR_PTR); - final int nsLength = m.get(JAVA_INT_UNALIGNED, b + ELEM_NS_PR_LEN); - final int nsCount = nsLength / Long.BYTES; - DeltaVarIntCodec.encodeSigned(sink, nsCount); - for (int i = 0; i < nsCount; i++) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + nsPointer + (long) i * Long.BYTES), nodeKey); - } - } - - // ════════════════════════════ XML ATTRIBUTE ════════════════════════════ - - private static void transformAttribute(final long nodeKey, final MemorySegment m, final long b, - final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ATTR_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + ATTR_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ATTR_PREFIX)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ATTR_LNAME)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ATTR_URI)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ATTR_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + ATTR_LAST_MOD)); - sink.writeByte((byte) 0); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + ATTR_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + ATTR_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - // ════════════════════════════ XML NAMESPACE ════════════════════════════ - - private static void transformNamespace(final long nodeKey, final MemorySegment m, final long b, - final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NS_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + NS_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NS_PREFIX)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NS_LNAME)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NS_URI)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NS_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + NS_LAST_MOD)); - } - - // ════════════════════════════ XML TEXT ════════════════════════════ - - private static void transformText(final long nodeKey, final MemorySegment m, final long b, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + TEXT_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + TEXT_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + TEXT_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + TEXT_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + TEXT_LAST_MOD)); - final int flags = m.get(JAVA_INT_UNALIGNED, b + TEXT_PR0_FLAGS); - sink.writeByte((byte) (flags & 1)); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + TEXT_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + TEXT_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - // ════════════════════════════ XML COMMENT ════════════════════════════ - - private static void transformComment(final long nodeKey, final MemorySegment m, final long b, - final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + CMT_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + CMT_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + CMT_LSIB), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + CMT_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + CMT_LAST_MOD)); - final int flags = m.get(JAVA_INT_UNALIGNED, b + CMT_PR0_FLAGS); - sink.writeByte((byte) (flags & 1)); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + CMT_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + CMT_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } - - // ════════════════════════════ XML PROCESSING_INSTRUCTION ════════════════════════════ - - private static void transformProcessingInstruction(final long nodeKey, final MemorySegment m, final long b, - final ResourceConfiguration config, final BytesOut sink) { - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_PARENT), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_RSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_LSIB), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_FCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_LCHILD), nodeKey); - DeltaVarIntCodec.encodeDelta(sink, m.get(JAVA_LONG_UNALIGNED, b + PI_PATH), nodeKey); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + PI_PREFIX)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + PI_LNAME)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + PI_URI)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + PI_PREV_REV)); - DeltaVarIntCodec.encodeSigned(sink, m.get(JAVA_INT_UNALIGNED, b + PI_LAST_MOD)); - if (config.storeChildCount()) { - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + PI_CHILD_CNT)); - } - if (config.hashType != HashType.NONE) { - sink.writeLong(m.get(JAVA_LONG_UNALIGNED, b + PI_HASH)); - DeltaVarIntCodec.encodeSigned(sink, (int) m.get(JAVA_LONG_UNALIGNED, b + PI_DESC_CNT)); - } - final int flags = m.get(JAVA_INT_UNALIGNED, b + PI_PR0_FLAGS); - sink.writeByte((byte) (flags & 1)); - final long pointer = m.get(JAVA_LONG_UNALIGNED, b + PI_PR0_PTR); - final int length = m.get(JAVA_INT_UNALIGNED, b + PI_PR0_LEN); - DeltaVarIntCodec.encodeSigned(sink, length); - if (length > 0) { - sink.writeSegment(m, b + pointer, length); - } - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayout.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayout.java deleted file mode 100644 index 4bf865624..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayout.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.NodeKind; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -/** - * Fixed-slot layout contract for a {@link NodeKind}. - */ -public final class NodeKindLayout { - private static final int UNSUPPORTED_FIELD_OFFSET = -1; - private static final int FIELD_COUNT = StructuralField.values().length; - - private final NodeKind nodeKind; - private final int fixedSlotSizeInBytes; - private final boolean fixedSlotSupported; - private final int[] offsets; - private final PayloadRef[] payloadRefs; - private final boolean hasSupportedPayloads; - - private NodeKindLayout(final NodeKind nodeKind, final int fixedSlotSizeInBytes, final boolean fixedSlotSupported, - final int[] offsets, final PayloadRef[] payloadRefs) { - this.nodeKind = Objects.requireNonNull(nodeKind); - this.fixedSlotSizeInBytes = fixedSlotSizeInBytes; - this.fixedSlotSupported = fixedSlotSupported; - this.offsets = Objects.requireNonNull(offsets).clone(); - this.payloadRefs = Objects.requireNonNull(payloadRefs).clone(); - this.hasSupportedPayloads = computeHasSupportedPayloads(this.payloadRefs); - } - - private static boolean computeHasSupportedPayloads(final PayloadRef[] refs) { - for (final PayloadRef ref : refs) { - final PayloadRefKind kind = ref.kind(); - if (kind != PayloadRefKind.VALUE_BLOB && kind != PayloadRefKind.ATTRIBUTE_VECTOR - && kind != PayloadRefKind.NAMESPACE_VECTOR) { - return false; - } - } - return true; - } - - public static NodeKindLayout unsupported(final NodeKind nodeKind) { - final int[] unsupportedOffsets = new int[FIELD_COUNT]; - Arrays.fill(unsupportedOffsets, UNSUPPORTED_FIELD_OFFSET); - return new NodeKindLayout(nodeKind, 0, false, unsupportedOffsets, new PayloadRef[0]); - } - - public static Builder builder(final NodeKind nodeKind) { - return new Builder(nodeKind); - } - - public NodeKind nodeKind() { - return nodeKind; - } - - public int fixedSlotSizeInBytes() { - return fixedSlotSizeInBytes; - } - - public boolean isFixedSlotSupported() { - return fixedSlotSupported; - } - - public int offsetOfOrMinusOne(final StructuralField field) { - return offsets[Objects.requireNonNull(field, "field must not be null").ordinal()]; - } - - /** - * Hot-path variant of {@link #offsetOfOrMinusOne} that skips null checks. - * Only use when the field is known to exist in this layout. - */ - public int offsetUnchecked(final StructuralField field) { - return offsets[field.ordinal()]; - } - - public boolean hasField(final StructuralField field) { - return offsetOfOrMinusOne(field) != UNSUPPORTED_FIELD_OFFSET; - } - - public int payloadRefCount() { - return payloadRefs.length; - } - - public PayloadRef payloadRef(final int index) { - return payloadRefs[index]; - } - - /** - * Pre-computed result of whether all payload refs are supported for inline projection. - * Avoids iterating payloadRefs on every moveTo call. - */ - public boolean hasSupportedPayloads() { - return hasSupportedPayloads; - } - - public static final class Builder { - private final NodeKind nodeKind; - private final int[] offsets = new int[FIELD_COUNT]; - private final List payloadRefs = new ArrayList<>(); - private int cursor; - - private Builder(final NodeKind nodeKind) { - this.nodeKind = Objects.requireNonNull(nodeKind); - Arrays.fill(offsets, UNSUPPORTED_FIELD_OFFSET); - } - - public Builder addField(final StructuralField field) { - Objects.requireNonNull(field, "field must not be null"); - if (offsets[field.ordinal()] != UNSUPPORTED_FIELD_OFFSET) { - throw new IllegalArgumentException("Field already registered: " + field); - } - offsets[field.ordinal()] = cursor; - cursor += field.widthInBytes(); - return this; - } - - public Builder addPadding(final int bytes) { - if (bytes < 0) { - throw new IllegalArgumentException("Padding bytes must be >= 0"); - } - cursor += bytes; - return this; - } - - public Builder addPayloadRef(final String name, final PayloadRefKind kind) { - final int pointerOffset = cursor; - final PayloadRef payloadRef = - new PayloadRef(name, kind, pointerOffset, pointerOffset + PayloadRef.POINTER_WIDTH_BYTES, - pointerOffset + PayloadRef.POINTER_WIDTH_BYTES + PayloadRef.LENGTH_WIDTH_BYTES); - payloadRefs.add(payloadRef); - cursor += PayloadRef.TOTAL_WIDTH_BYTES; - return this; - } - - public NodeKindLayout build() { - return new NodeKindLayout(nodeKind, cursor, true, offsets, payloadRefs.toArray(PayloadRef[]::new)); - } - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayouts.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayouts.java deleted file mode 100644 index 2d76a55f1..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/NodeKindLayouts.java +++ /dev/null @@ -1,268 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.NodeKind; - -import java.util.Objects; - -/** - * Registry for fixed-slot layout contracts per {@link NodeKind}. - */ -public final class NodeKindLayouts { - private static final NodeKind[] NODE_KINDS = NodeKind.values(); - private static final NodeKindLayout[] LAYOUTS_BY_ORDINAL = new NodeKindLayout[NODE_KINDS.length]; - - static { - for (final NodeKind kind : NODE_KINDS) { - LAYOUTS_BY_ORDINAL[kind.ordinal()] = NodeKindLayout.unsupported(kind); - } - - registerXmlLayouts(); - registerJsonLayouts(); - } - - private NodeKindLayouts() {} - - public static NodeKindLayout layoutFor(final NodeKind nodeKind) { - return LAYOUTS_BY_ORDINAL[Objects.requireNonNull(nodeKind, "nodeKind must not be null").ordinal()]; - } - - public static boolean hasFixedSlotLayout(final NodeKind nodeKind) { - return layoutFor(nodeKind).isFixedSlotSupported(); - } - - private static void registerXmlLayouts() { - put(NodeKind.XML_DOCUMENT, - NodeKindLayout.builder(NodeKind.XML_DOCUMENT) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .addField(StructuralField.HASH) - .build()); - - put(NodeKind.ELEMENT, - NodeKindLayout.builder(NodeKind.ELEMENT) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.PREFIX_KEY) - .addField(StructuralField.LOCAL_NAME_KEY) - .addField(StructuralField.URI_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .addPayloadRef("attributes", PayloadRefKind.ATTRIBUTE_VECTOR) - .addPayloadRef("namespaces", PayloadRefKind.NAMESPACE_VECTOR) - .build()); - - put(NodeKind.ATTRIBUTE, - NodeKindLayout.builder(NodeKind.ATTRIBUTE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.PREFIX_KEY) - .addField(StructuralField.LOCAL_NAME_KEY) - .addField(StructuralField.URI_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.NAMESPACE, - NodeKindLayout.builder(NodeKind.NAMESPACE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.PREFIX_KEY) - .addField(StructuralField.LOCAL_NAME_KEY) - .addField(StructuralField.URI_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .build()); - - put(NodeKind.TEXT, - NodeKindLayout.builder(NodeKind.TEXT) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.COMMENT, - NodeKindLayout.builder(NodeKind.COMMENT) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.PROCESSING_INSTRUCTION, - NodeKindLayout.builder(NodeKind.PROCESSING_INSTRUCTION) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.PREFIX_KEY) - .addField(StructuralField.LOCAL_NAME_KEY) - .addField(StructuralField.URI_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - } - - private static void registerJsonLayouts() { - put(NodeKind.JSON_DOCUMENT, - NodeKindLayout.builder(NodeKind.JSON_DOCUMENT) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .addField(StructuralField.HASH) - .build()); - - put(NodeKind.OBJECT, - NodeKindLayout.builder(NodeKind.OBJECT) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .build()); - - put(NodeKind.ARRAY, - NodeKindLayout.builder(NodeKind.ARRAY) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.CHILD_COUNT) - .addField(StructuralField.DESCENDANT_COUNT) - .build()); - - put(NodeKind.OBJECT_KEY, - NodeKindLayout.builder(NodeKind.OBJECT_KEY) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.FIRST_CHILD_KEY) - .addField(StructuralField.LAST_CHILD_KEY) - .addField(StructuralField.PATH_NODE_KEY) - .addField(StructuralField.NAME_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.DESCENDANT_COUNT) - .build()); - - put(NodeKind.STRING_VALUE, - NodeKindLayout.builder(NodeKind.STRING_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.NUMBER_VALUE, - NodeKindLayout.builder(NodeKind.NUMBER_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("number", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.BOOLEAN_VALUE, - NodeKindLayout.builder(NodeKind.BOOLEAN_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.BOOLEAN_VALUE) - .addPadding(7) - .build()); - - put(NodeKind.NULL_VALUE, - NodeKindLayout.builder(NodeKind.NULL_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.RIGHT_SIBLING_KEY) - .addField(StructuralField.LEFT_SIBLING_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .build()); - - put(NodeKind.OBJECT_STRING_VALUE, - NodeKindLayout.builder(NodeKind.OBJECT_STRING_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("value", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.OBJECT_NUMBER_VALUE, - NodeKindLayout.builder(NodeKind.OBJECT_NUMBER_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addPayloadRef("number", PayloadRefKind.VALUE_BLOB) - .build()); - - put(NodeKind.OBJECT_BOOLEAN_VALUE, - NodeKindLayout.builder(NodeKind.OBJECT_BOOLEAN_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .addField(StructuralField.BOOLEAN_VALUE) - .addPadding(7) - .build()); - - put(NodeKind.OBJECT_NULL_VALUE, - NodeKindLayout.builder(NodeKind.OBJECT_NULL_VALUE) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.PREVIOUS_REVISION) - .addField(StructuralField.LAST_MODIFIED_REVISION) - .addField(StructuralField.HASH) - .build()); - } - - private static void put(final NodeKind kind, final NodeKindLayout layout) { - LAYOUTS_BY_ORDINAL[kind.ordinal()] = layout; - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRef.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRef.java deleted file mode 100644 index df7bb6a3e..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRef.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.sirix.node.layout; - -import java.util.Objects; - -/** - * Pointer metadata embedded in a fixed slot for out-of-line payload blocks. - */ -public record PayloadRef(String name, PayloadRefKind kind, int pointerOffset, int lengthOffset, int flagsOffset) { - - public static final int POINTER_WIDTH_BYTES = Long.BYTES; - public static final int LENGTH_WIDTH_BYTES = Integer.BYTES; - public static final int FLAGS_WIDTH_BYTES = Integer.BYTES; - public static final int TOTAL_WIDTH_BYTES = POINTER_WIDTH_BYTES + LENGTH_WIDTH_BYTES + FLAGS_WIDTH_BYTES; - - public PayloadRef { - Objects.requireNonNull(name, "name must not be null"); - Objects.requireNonNull(kind, "kind must not be null"); - if (pointerOffset < 0) { - throw new IllegalArgumentException("pointerOffset must be >= 0"); - } - if (lengthOffset != pointerOffset + POINTER_WIDTH_BYTES) { - throw new IllegalArgumentException("lengthOffset must directly follow pointerOffset"); - } - if (flagsOffset != lengthOffset + LENGTH_WIDTH_BYTES) { - throw new IllegalArgumentException("flagsOffset must directly follow lengthOffset"); - } - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRefKind.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRefKind.java deleted file mode 100644 index 497fdb8db..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/PayloadRefKind.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.sirix.node.layout; - -/** - * Classification of out-of-line payload blocks referenced by a fixed slot. - */ -public enum PayloadRefKind { - VALUE_BLOB, ATTRIBUTE_VECTOR, NAMESPACE_VECTOR -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/SlotLayoutAccessors.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/SlotLayoutAccessors.java deleted file mode 100644 index 51ece60a7..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/SlotLayoutAccessors.java +++ /dev/null @@ -1,205 +0,0 @@ -package io.sirix.node.layout; - -import java.lang.foreign.MemorySegment; -import java.lang.foreign.ValueLayout; -import java.util.Objects; - -/** - * Primitive-only accessors for fixed-size slot layouts backed by {@link MemorySegment}. - * - *

- * This utility is designed for hot paths: no boxing, no temporary object allocation. - * - *

- * Each accessor has two overloads: - *

    - *
  • Without {@code baseOffset} — reads/writes relative to the start of the segment (legacy - * slice-based API).
  • - *
  • With {@code long baseOffset} — reads/writes at {@code baseOffset + fieldOffset}, allowing - * callers to pass the full {@code slotMemory} plus an offset instead of allocating an - * {@code asSlice()} per call.
  • - *
- */ -public final class SlotLayoutAccessors { - private static final ValueLayout.OfLong LONG_LAYOUT = ValueLayout.JAVA_LONG_UNALIGNED; - private static final ValueLayout.OfInt INT_LAYOUT = ValueLayout.JAVA_INT_UNALIGNED; - private static final ValueLayout.OfByte BYTE_LAYOUT = ValueLayout.JAVA_BYTE; - - private SlotLayoutAccessors() {} - - // ── readLongField ────────────────────────────────────────────────── - - public static long readLongField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field) { - final int offset = requiredOffset(layout, field); - return slot.get(LONG_LAYOUT, baseOffset + offset); - } - - public static long readLongField(final MemorySegment slot, final NodeKindLayout layout, final StructuralField field) { - return readLongField(slot, 0L, layout, field); - } - - // ── writeLongField ───────────────────────────────────────────────── - - public static void writeLongField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field, final long value) { - final int offset = requiredOffset(layout, field); - slot.set(LONG_LAYOUT, baseOffset + offset, value); - } - - public static void writeLongField(final MemorySegment slot, final NodeKindLayout layout, final StructuralField field, - final long value) { - writeLongField(slot, 0L, layout, field, value); - } - - // ── readIntField ─────────────────────────────────────────────────── - - public static int readIntField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field) { - final int offset = requiredOffset(layout, field); - return slot.get(INT_LAYOUT, baseOffset + offset); - } - - public static int readIntField(final MemorySegment slot, final NodeKindLayout layout, final StructuralField field) { - return readIntField(slot, 0L, layout, field); - } - - // ── writeIntField ────────────────────────────────────────────────── - - public static void writeIntField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field, final int value) { - final int offset = requiredOffset(layout, field); - slot.set(INT_LAYOUT, baseOffset + offset, value); - } - - public static void writeIntField(final MemorySegment slot, final NodeKindLayout layout, final StructuralField field, - final int value) { - writeIntField(slot, 0L, layout, field, value); - } - - // ── readBooleanField ─────────────────────────────────────────────── - - public static boolean readBooleanField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field) { - final int offset = requiredOffset(layout, field); - return slot.get(BYTE_LAYOUT, baseOffset + offset) != 0; - } - - public static boolean readBooleanField(final MemorySegment slot, final NodeKindLayout layout, - final StructuralField field) { - return readBooleanField(slot, 0L, layout, field); - } - - // ── writeBooleanField ────────────────────────────────────────────── - - public static void writeBooleanField(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final StructuralField field, final boolean value) { - final int offset = requiredOffset(layout, field); - slot.set(BYTE_LAYOUT, baseOffset + offset, value - ? (byte) 1 - : (byte) 0); - } - - public static void writeBooleanField(final MemorySegment slot, final NodeKindLayout layout, - final StructuralField field, final boolean value) { - writeBooleanField(slot, 0L, layout, field, value); - } - - // ── readPayloadPointer ───────────────────────────────────────────── - - public static long readPayloadPointer(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final int payloadRefIndex) { - final PayloadRef payloadRef = payloadRef(layout, payloadRefIndex); - return slot.get(LONG_LAYOUT, baseOffset + payloadRef.pointerOffset()); - } - - public static long readPayloadPointer(final MemorySegment slot, final NodeKindLayout layout, - final int payloadRefIndex) { - return readPayloadPointer(slot, 0L, layout, payloadRefIndex); - } - - // ── readPayloadLength ────────────────────────────────────────────── - - public static int readPayloadLength(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final int payloadRefIndex) { - final PayloadRef payloadRef = payloadRef(layout, payloadRefIndex); - return slot.get(INT_LAYOUT, baseOffset + payloadRef.lengthOffset()); - } - - public static int readPayloadLength(final MemorySegment slot, final NodeKindLayout layout, - final int payloadRefIndex) { - return readPayloadLength(slot, 0L, layout, payloadRefIndex); - } - - // ── readPayloadFlags ─────────────────────────────────────────────── - - public static int readPayloadFlags(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final int payloadRefIndex) { - final PayloadRef payloadRef = payloadRef(layout, payloadRefIndex); - return slot.get(INT_LAYOUT, baseOffset + payloadRef.flagsOffset()); - } - - public static int readPayloadFlags(final MemorySegment slot, final NodeKindLayout layout, final int payloadRefIndex) { - return readPayloadFlags(slot, 0L, layout, payloadRefIndex); - } - - // ── writePayloadRef ──────────────────────────────────────────────── - - public static void writePayloadRef(final MemorySegment slot, final long baseOffset, final NodeKindLayout layout, - final int payloadRefIndex, final long pointer, final int length, final int flags) { - if (length < 0) { - throw new IllegalArgumentException("length must be >= 0"); - } - if (flags < 0) { - throw new IllegalArgumentException("flags must be >= 0"); - } - final PayloadRef payloadRef = payloadRef(layout, payloadRefIndex); - slot.set(LONG_LAYOUT, baseOffset + payloadRef.pointerOffset(), pointer); - slot.set(INT_LAYOUT, baseOffset + payloadRef.lengthOffset(), length); - slot.set(INT_LAYOUT, baseOffset + payloadRef.flagsOffset(), flags); - } - - public static void writePayloadRef(final MemorySegment slot, final NodeKindLayout layout, final int payloadRefIndex, - final long pointer, final int length, final int flags) { - writePayloadRef(slot, 0L, layout, payloadRefIndex, pointer, length, flags); - } - - // ── unchecked hot-path variants ───────────────────────────────────── - // These skip null checks and bounds validation for maximum performance. - // Only use when layout and field are known to be valid (e.g., from pre-computed static finals). - - public static long readLongFieldUnchecked(final MemorySegment slot, final long baseOffset, - final NodeKindLayout layout, final StructuralField field) { - return slot.get(LONG_LAYOUT, baseOffset + layout.offsetUnchecked(field)); - } - - public static int readIntFieldUnchecked(final MemorySegment slot, final long baseOffset, - final NodeKindLayout layout, final StructuralField field) { - return slot.get(INT_LAYOUT, baseOffset + layout.offsetUnchecked(field)); - } - - public static boolean readBooleanFieldUnchecked(final MemorySegment slot, final long baseOffset, - final NodeKindLayout layout, final StructuralField field) { - return slot.get(BYTE_LAYOUT, baseOffset + layout.offsetUnchecked(field)) != 0; - } - - // ── helpers ──────────────────────────────────────────────────────── - - private static int requiredOffset(final NodeKindLayout layout, final StructuralField field) { - final int offset = Objects.requireNonNull(layout, "layout must not be null") - .offsetOfOrMinusOne(Objects.requireNonNull(field, "field must not be null")); - if (offset < 0) { - throw new IllegalArgumentException("Field " + field + " is not part of layout for " + layout.nodeKind()); - } - return offset; - } - - private static PayloadRef payloadRef(final NodeKindLayout layout, final int payloadRefIndex) { - Objects.requireNonNull(layout, "layout must not be null"); - if (payloadRefIndex < 0 || payloadRefIndex >= layout.payloadRefCount()) { - throw new IllegalArgumentException("Invalid payloadRefIndex " + payloadRefIndex + " for " + layout.nodeKind() - + ", count=" + layout.payloadRefCount()); - } - return layout.payloadRef(payloadRefIndex); - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/layout/StructuralField.java b/bundles/sirix-core/src/main/java/io/sirix/node/layout/StructuralField.java deleted file mode 100644 index ea5c418ed..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/node/layout/StructuralField.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.sirix.node.layout; - -/** - * Structural fields that are modeled as fixed-width columns in the in-memory slot layout. - */ -public enum StructuralField { - PARENT_KEY(Long.BYTES), RIGHT_SIBLING_KEY(Long.BYTES), LEFT_SIBLING_KEY(Long.BYTES), FIRST_CHILD_KEY( - Long.BYTES), LAST_CHILD_KEY(Long.BYTES), PATH_NODE_KEY(Long.BYTES), PREVIOUS_REVISION( - Integer.BYTES), LAST_MODIFIED_REVISION(Integer.BYTES), PREFIX_KEY(Integer.BYTES), LOCAL_NAME_KEY( - Integer.BYTES), URI_KEY(Integer.BYTES), NAME_KEY(Integer.BYTES), HASH( - Long.BYTES), CHILD_COUNT(Long.BYTES), DESCENDANT_COUNT(Long.BYTES), BOOLEAN_VALUE(Byte.BYTES); - - private final int widthInBytes; - - StructuralField(final int widthInBytes) { - this.widthInBytes = widthInBytes; - } - - public int widthInBytes() { - return widthInBytes; - } -} diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/AttributeNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/AttributeNode.java index 039ec08ac..effecad65 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/AttributeNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/AttributeNode.java @@ -43,30 +43,29 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableAttributeNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import io.sirix.utils.NamePageHash; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.lang.foreign.MemorySegment; - /** * Node representing an attribute, using primitive fields. * * @author Johannes Lichtenberger */ -public final class AttributeNode implements ValueNode, NameNode, ImmutableXmlNode, ReusableNodeProxy { +public final class AttributeNode implements ValueNode, NameNode, ImmutableXmlNode, FlyweightNode { // === PRIMITIVE FIELDS === private long nodeKey; @@ -86,17 +85,35 @@ public final class AttributeNode implements ValueNode, NameNode, ImmutableXmlNod private int lazyValueLength; private boolean valueParsed = true; - // === METADATA LAZY SUPPORT === - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean metadataParsed = true; - // === NON-SERIALIZED FIELDS === private LongHashFunction hashFunction; private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; private QNm qNm; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; + private boolean writeSingleton; + private KeyValueLeafPage ownerPage; + private final int[] heapOffsets; + private static final int FIELD_COUNT = NodeFieldLayout.ATTRIBUTE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public AttributeNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + /** * Primary constructor with all primitive fields. */ @@ -116,6 +133,7 @@ public AttributeNode(long nodeKey, long parentKey, int previousRevision, int las this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -137,6 +155,7 @@ public AttributeNode(long nodeKey, long parentKey, int previousRevision, int las this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; } @Override @@ -156,109 +175,269 @@ public void setNodeKey(long nodeKey) { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ATTR_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_PARENT_KEY, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getPathNodeKey() { - if (!metadataParsed) - parseMetadataFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.ATTR_PATH_NODE_KEY, nodeKey); + } return pathNodeKey; } @Override - public void setPathNodeKey(@NonNegative long pathNodeKey) { + public void setPathNodeKey(@NonNegative final long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return; + } + resizePathNodeKey(pathNodeKey); + return; + } this.pathNodeKey = pathNodeKey; } + private void resizePathNodeKey(final long pathNodeKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_PATH_NODE_KEY, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, pathNodeKey, nodeKey)); + } + @Override public int getPrefixKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.ATTR_PREFIX_KEY); + } return prefixKey; } @Override - public void setPrefixKey(int prefixKey) { + public void setPrefixKey(final int prefixKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_PREFIX_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(prefixKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, prefixKey); + return; + } + resizePrefixKey(prefixKey); + return; + } this.prefixKey = prefixKey; } + private void resizePrefixKey(final int prefixKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_PREFIX_KEY, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, prefixKey)); + } + @Override public int getLocalNameKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.ATTR_LOCAL_NAME_KEY); + } return localNameKey; } @Override - public void setLocalNameKey(int localNameKey) { + public void setLocalNameKey(final int localNameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_LOCAL_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(localNameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, localNameKey); + return; + } + resizeLocalNameKey(localNameKey); + return; + } this.localNameKey = localNameKey; } + private void resizeLocalNameKey(final int localNameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_LOCAL_NAME_KEY, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, localNameKey)); + } + @Override public int getURIKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.ATTR_URI_KEY); + } return uriKey; } @Override - public void setURIKey(int uriKey) { + public void setURIKey(final int uriKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_URI_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(uriKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, uriKey); + return; + } + resizeURIKey(uriKey); + return; + } this.uriKey = uriKey; } + private void resizeURIKey(final int uriKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_URI_KEY, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, uriKey)); + } + @Override public int getPreviousRevisionNumber() { - if (!metadataParsed) - parseMetadataFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.ATTR_PREV_REVISION); + } return previousRevision; } @Override - public void setPreviousRevision(int revision) { + public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_PREV_REVISION, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { - if (!metadataParsed) - parseMetadataFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.ATTR_LAST_MOD_REVISION); + } return lastModifiedRevision; } @Override - public void setLastModifiedRevision(int revision) { + public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ATTR_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ATTR_LAST_MOD_REVISION, NodeFieldLayout.ATTRIBUTE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!metadataParsed) - parseMetadataFields(); - if (hash == 0L && hashFunction != null) { + // Return stored hash if set by rollingAdd/rollingUpdate, else compute on demand + if (hash != 0L) { + return hash; + } + if (hashFunction != null) { hash = computeHash(Bytes.threadLocalHashBuffer()); } return hash; } @Override - public void setHash(long hash) { + public void setHash(final long hash) { this.hash = hash; } @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } return value; } @Override - public void setRawValue(byte[] value) { + public void setRawValue(final byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.valueParsed = true; + this.lazyValueSource = null; + this.lazyValueOffset = 0L; + this.lazyValueLength = 0; + this.hash = 0L; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.valueParsed = true; this.lazyValueSource = null; @@ -278,8 +457,9 @@ public void setLazyRawValue(Object source, long valueOffset, int valueLength) { @Override public String getValue() { - return value != null - ? new String(value, Constants.DEFAULT_ENCODING) + final byte[] rawValue = getRawValue(); + return rawValue != null + ? new String(rawValue, Constants.DEFAULT_ENCODING) : ""; } @@ -298,7 +478,9 @@ public void setName(QNm name) { * interface. */ public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } return value; @@ -319,6 +501,16 @@ public void setTypeKey(int typeKey) {} @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -348,11 +540,272 @@ public LongHashFunction getHashFunction() { return hashFunction; } + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.valueParsed = false; + this.lazyValueSource = null; + this.hash = 0; + } + + @Override + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.ATTR_PARENT_KEY, nk); + this.pathNodeKey = readDeltaField(NodeFieldLayout.ATTR_PATH_NODE_KEY, nk); + this.prefixKey = readSignedField(NodeFieldLayout.ATTR_PREFIX_KEY); + this.localNameKey = readSignedField(NodeFieldLayout.ATTR_LOCAL_NAME_KEY); + this.uriKey = readSignedField(NodeFieldLayout.ATTR_URI_KEY); + this.previousRevision = readSignedField(NodeFieldLayout.ATTR_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.ATTR_LAST_MOD_REVISION); + if (!valueParsed) { + readPayloadFromPage(); + } + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + @Override + public boolean isBound() { return page != null; } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override public boolean isWriteSingleton() { return writeSingleton; } + @Override public void setWriteSingleton(final boolean ws) { this.writeSingleton = ws; } + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 55 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ATTR_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) - always 0 for attributes, but read for format consistency + // skip: payloadStart + 0 + final long lenOffset = payloadStart + 1; + + // Read value length (varint) + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + if (length > 0) { + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + } + this.valueParsed = true; + } + + // ==================== DIRECT WRITE ==================== + + /** + * Encode an AttributeNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param pathNodeKey the path node key + * @param prefixKey the prefix key + * @param localNameKey the local name key + * @param uriKey the URI key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param rawValue the raw value bytes + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long pathNodeKey, + final int prefixKey, final int localNameKey, final int uriKey, + final int prevRev, final int lastModRev, + final byte[] rawValue) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.ATTRIBUTE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.ATTR_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.ATTR_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 2: prefixKey (signed varint) + heapOffsets[NodeFieldLayout.ATTR_PREFIX_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prefixKey); + + // Field 3: localNameKey (signed varint) + heapOffsets[NodeFieldLayout.ATTR_LOCAL_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, localNameKey); + + // Field 4: uriKey (signed varint) + heapOffsets[NodeFieldLayout.ATTR_URI_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, uriKey); + + // Field 5: prevRevision (signed varint) + heapOffsets[NodeFieldLayout.ATTR_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 6: lastModRevision (signed varint) + heapOffsets[NodeFieldLayout.ATTR_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 7: payload [isCompressed=0:1][valueLength:varint][value:bytes] + heapOffsets[NodeFieldLayout.ATTR_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, (byte) 0); // attributes are never compressed + pos++; + final byte[] val = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, val.length); + if (val.length > 0) { + MemorySegment.copy(val, 0, target, ValueLayout.JAVA_BYTE, pos, val.length); + pos += val.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!valueParsed) { + if (page != null) { + readPayloadFromPage(); + } else { + parseLazyValue(); + } + } + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, pathNodeKey, prefixKey, localNameKey, uriKey, + previousRevision, lastModifiedRevision, value); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + // ==================== SNAPSHOT ==================== + + /** + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. + */ + public AttributeNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new AttributeNode(nodeKey, + readDeltaField(NodeFieldLayout.ATTR_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.ATTR_PREV_REVISION), + readSignedField(NodeFieldLayout.ATTR_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.ATTR_PATH_NODE_KEY, nodeKey), + readSignedField(NodeFieldLayout.ATTR_PREFIX_KEY), + readSignedField(NodeFieldLayout.ATTR_LOCAL_NAME_KEY), + readSignedField(NodeFieldLayout.ATTR_URI_KEY), + hash, + value != null ? value.clone() : null, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + qNm); + } + // Unbound mode: force parse all lazy fields for snapshot (must be complete and independent) + if (!valueParsed) { + parseLazyValue(); + } + return new AttributeNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + pathNodeKey, prefixKey, localNameKey, uriKey, hash, + value != null ? value.clone() : null, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + qNm); + } + + // ==================== DESERIALIZATION ==================== + /** * Populate this node from a BytesIn source for singleton reuse. */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight - ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -411,12 +864,12 @@ private static BytesIn createBytesIn(Object source, long offset) { } @Override - public long computeHash(BytesOut bytes) { + public long computeHash(final BytesOut bytes) { if (hashFunction == null) return 0L; bytes.clear(); - bytes.writeLong(nodeKey).writeLong(parentKey).writeByte(getKind().getId()); - bytes.writeInt(prefixKey).writeInt(localNameKey).writeInt(uriKey); + bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeInt(getPrefixKey()).writeInt(getLocalNameKey()).writeInt(getURIKey()); final byte[] rawValue = getRawValue(); if (rawValue != null) { bytes.write(rawValue); @@ -424,48 +877,6 @@ public long computeHash(BytesOut bytes) { return bytes.hashDirect(hashFunction); } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - // lazyValueSource already points to slotData from setLazyRawValue - } - - private void parseMetadataFields() { - if (metadataParsed) { - return; - } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazyValueSource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.pathNodeKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.PATH_NODE_KEY); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - - this.metadataParsed = true; - } - - public AttributeNode toSnapshot() { - if (!metadataParsed) - parseMetadataFields(); - final byte[] rawValue = getRawValue(); - return new AttributeNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, pathNodeKey, prefixKey, - localNameKey, uriKey, hash, rawValue != null - ? rawValue.clone() - : null, - hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null, - qNm); - } - @Override public VisitResult acceptVisitor(XmlNodeVisitor visitor) { return visitor.visit(ImmutableAttributeNode.of(this)); @@ -473,22 +884,23 @@ public VisitResult acceptVisitor(XmlNodeVisitor visitor) { @Override public int hashCode() { - return Objects.hashCode(nodeKey, parentKey, prefixKey, localNameKey, uriKey); + return Objects.hashCode(nodeKey, getParentKey(), getPrefixKey(), getLocalNameKey(), getURIKey()); } @Override public boolean equals(Object obj) { if (!(obj instanceof AttributeNode other)) return false; - return nodeKey == other.nodeKey && parentKey == other.parentKey && prefixKey == other.prefixKey - && localNameKey == other.localNameKey && uriKey == other.uriKey; + return nodeKey == other.nodeKey && getParentKey() == other.getParentKey() + && getPrefixKey() == other.getPrefixKey() + && getLocalNameKey() == other.getLocalNameKey() && getURIKey() == other.getURIKey(); } @Override public @NonNull String toString() { return MoreObjects.toStringHelper(this) .add("nodeKey", nodeKey) - .add("parentKey", parentKey) + .add("parentKey", getParentKey()) .add("qNm", qNm) .add("value", getValue()) .toString(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/CommentNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/CommentNode.java index d0cbcf05a..bd39ec221 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/CommentNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/CommentNode.java @@ -42,14 +42,13 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableComment; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import io.sirix.utils.Compression; @@ -59,17 +58,19 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Comment node implementation using primitive fields. * *

- * Uses primitive fields for efficient storage. + * Uses primitive fields for efficient storage. Supports LeanStore-style flyweight binding + * for zero-copy reads from slotted page MemorySegment. *

* * @author Johannes Lichtenberger */ -public final class CommentNode implements StructNode, ValueNode, ImmutableXmlNode, ReusableNodeProxy { +public final class CommentNode implements StructNode, ValueNode, ImmutableXmlNode, FlyweightNode { // === IMMEDIATE STRUCTURAL FIELDS === private long nodeKey; @@ -91,16 +92,34 @@ public final class CommentNode implements StructNode, ValueNode, ImmutableXmlNod private boolean lazyValueCompressed; private boolean valueParsed = true; - // === METADATA LAZY SUPPORT === - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean metadataParsed = true; - // === NON-SERIALIZED FIELDS === private LongHashFunction hashFunction; private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; + private boolean writeSingleton; + private KeyValueLeafPage ownerPage; + private final int[] heapOffsets; + private static final int FIELD_COUNT = NodeFieldLayout.COMMENT_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public CommentNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + /** * Primary constructor with all primitive fields. */ @@ -118,6 +137,7 @@ public CommentNode(long nodeKey, long parentKey, int previousRevision, int lastM this.isCompressed = isCompressed; this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -137,6 +157,208 @@ public CommentNode(long nodeKey, long parentKey, int previousRevision, int lastM this.isCompressed = isCompressed; this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazyValueSource = null; + this.hash = 0; + } + + @Override + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.COMMENT_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.COMMENT_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.COMMENT_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.COMMENT_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.COMMENT_LAST_MOD_REVISION); + // Hash is not stored on the slotted page; keep current in-memory value + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.ownerPage = null; + this.page = null; + } + + @Override + public boolean isBound() { return page != null; } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override public boolean isWriteSingleton() { return writeSingleton; } + @Override public void setWriteSingleton(final boolean ws) { this.writeSingleton = ws; } + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 55 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.COMMENT_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) + this.isCompressed = page.get(ValueLayout.JAVA_BYTE, payloadStart) == 1; + + // Read value length (varint) + final long lenOffset = payloadStart + 1; + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + this.valueParsed = true; + } + + // ==================== DIRECT WRITE ==================== + + /** + * Encode a CommentNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param rawValue the raw value bytes (possibly compressed) + * @param isCompressed whether the value is compressed + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev, + final byte[] rawValue, final boolean isCompressed) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.COMMENT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.COMMENT_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.COMMENT_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.COMMENT_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: prevRevision (signed varint) + heapOffsets[NodeFieldLayout.COMMENT_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModRevision (signed varint) + heapOffsets[NodeFieldLayout.COMMENT_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 5: payload [isCompressed:1][length:varint][data:bytes] + heapOffsets[NodeFieldLayout.COMMENT_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, isCompressed ? (byte) 1 : (byte) 0); + pos++; + final byte[] val = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, val.length); + if (val.length > 0) { + MemorySegment.copy(val, 0, target, ValueLayout.JAVA_BYTE, pos, val.length); + pos += val.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!valueParsed) parseLazyValue(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision, value, isCompressed); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -156,88 +378,193 @@ public void setNodeKey(long nodeKey) { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.COMMENT_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.COMMENT_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.COMMENT_PARENT_KEY, NodeFieldLayout.COMMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.COMMENT_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } - public void setRightSiblingKey(long key) { + public void setRightSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.COMMENT_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeRightSiblingKey(key); + return; + } this.rightSiblingKey = key; } + private void resizeRightSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.COMMENT_RIGHT_SIB_KEY, NodeFieldLayout.COMMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.COMMENT_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } - public void setLeftSiblingKey(long key) { + public void setLeftSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.COMMENT_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLeftSiblingKey(key); + return; + } this.leftSiblingKey = key; } + private void resizeLeftSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.COMMENT_LEFT_SIB_KEY, NodeFieldLayout.COMMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public int getPreviousRevisionNumber() { - if (!metadataParsed) - parseMetadataFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.COMMENT_PREV_REVISION); + } return previousRevision; } @Override - public void setPreviousRevision(int revision) { + public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.COMMENT_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.COMMENT_PREV_REVISION, NodeFieldLayout.COMMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { - if (!metadataParsed) - parseMetadataFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.COMMENT_LAST_MOD_REVISION); + } return lastModifiedRevision; } @Override - public void setLastModifiedRevision(int revision) { + public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.COMMENT_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.COMMENT_LAST_MOD_REVISION, NodeFieldLayout.COMMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!metadataParsed) - parseMetadataFields(); - if (hash == 0L && hashFunction != null) { - hash = computeHash(Bytes.threadLocalHashBuffer()); + if (hash != 0L) { + return hash; + } + // Hash not stored on page -- compute on demand from node fields + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); } - return hash; + return 0L; } @Override - public void setHash(long hash) { + public void setHash(final long hash) { this.hash = hash; } @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } if (value != null && isCompressed) { @@ -249,6 +576,23 @@ public byte[] getRawValue() { @Override public void setRawValue(byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.valueParsed = true; + this.lazyValueSource = null; + this.lazyValueOffset = 0L; + this.lazyValueLength = 0; + this.lazyValueCompressed = false; + this.isCompressed = false; + this.hash = 0L; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.valueParsed = true; this.lazyValueSource = null; @@ -365,6 +709,16 @@ public void setTypeKey(int typeKey) { @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -399,6 +753,8 @@ public LongHashFunction getHashFunction() { */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -460,16 +816,55 @@ private static BytesIn createBytesIn(Object source, long offset) { * preserve the original compressed bytes. */ public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } return value; } public boolean isCompressed() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } return isCompressed; } + /** + * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. + */ + public CommentNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new CommentNode(nodeKey, + readDeltaField(NodeFieldLayout.COMMENT_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.COMMENT_PREV_REVISION), + readSignedField(NodeFieldLayout.COMMENT_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.COMMENT_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.COMMENT_LEFT_SIB_KEY, nodeKey), + hash, + value != null ? value.clone() : null, + isCompressed, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + // Force parse lazy value for snapshot (must be complete and independent) + if (!valueParsed) { + parseLazyValue(); + } + return new CommentNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, + value != null ? value.clone() : null, + isCompressed, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + @Override public long computeHash(BytesOut bytes) { if (hashFunction == null) { @@ -477,55 +872,11 @@ public long computeHash(BytesOut bytes) { } bytes.clear(); - bytes.writeLong(nodeKey).writeLong(parentKey).writeByte(NodeKind.COMMENT.getId()); + bytes.writeLong(nodeKey).writeLong(getParentKey()).writeByte(NodeKind.COMMENT.getId()); - bytes.writeLong(leftSiblingKey).writeLong(rightSiblingKey); bytes.writeUtf8(new String(getRawValue(), Constants.DEFAULT_ENCODING)); - final var buffer = ((java.nio.ByteBuffer) bytes.underlyingObject()).rewind(); - buffer.limit((int) bytes.readLimit()); - - return hashFunction.hashBytes(buffer); - } - - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - // lazyValueSource already points to slotData from setLazyRawValue - } - - private void parseMetadataFields() { - if (metadataParsed) { - return; - } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazyValueSource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; - return; - } - - this.metadataParsed = true; - } - - public CommentNode toSnapshot() { - if (!metadataParsed) - parseMetadataFields(); - final byte[] rawValue = getRawValue(); - return new CommentNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, rawValue != null - ? rawValue.clone() - : null, - isCompressed, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + return bytes.hashDirect(hashFunction); } @Override @@ -535,13 +886,13 @@ public VisitResult acceptVisitor(XmlNodeVisitor visitor) { @Override public int hashCode() { - return Objects.hashCode(nodeKey, parentKey, getValue()); + return Objects.hashCode(nodeKey, getParentKey(), getValue()); } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof CommentNode other) { - return nodeKey == other.nodeKey && parentKey == other.parentKey + return nodeKey == other.nodeKey && getParentKey() == other.getParentKey() && java.util.Arrays.equals(getRawValue(), other.getRawValue()); } return false; @@ -551,9 +902,9 @@ public boolean equals(@Nullable Object obj) { public @NonNull String toString() { return MoreObjects.toStringHelper(this) .add("nodeKey", nodeKey) - .add("parentKey", parentKey) - .add("rightSiblingKey", rightSiblingKey) - .add("leftSiblingKey", leftSiblingKey) + .add("parentKey", getParentKey()) + .add("rightSiblingKey", getRightSiblingKey()) + .add("leftSiblingKey", getLeftSiblingKey()) .add("value", getValue()) .add("compressed", isCompressed) .toString(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/ElementNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/ElementNode.java index f90521591..49dc2525d 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/ElementNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/ElementNode.java @@ -35,24 +35,23 @@ import io.sirix.access.trx.node.HashType; import io.sirix.api.visitor.VisitResult; import io.sirix.api.visitor.XmlNodeVisitor; +import io.sirix.node.ByteArrayBytesIn; import io.sirix.node.Bytes; import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; import io.sirix.node.DeltaVarIntCodec; +import io.sirix.node.MemorySegmentBytesIn; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableElement; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; - -import java.lang.foreign.MemorySegment; import io.sirix.utils.NamePageHash; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongList; @@ -61,6 +60,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.util.Collections; import java.util.List; @@ -69,11 +70,12 @@ * *

* Uses primitive fields for efficient storage with delta+varint encoding. + * Implements FlyweightNode for LeanStore-style zero-copy page-direct access. *

* * @author Johannes Lichtenberger */ -public final class ElementNode implements StructNode, NameNode, ImmutableXmlNode, ReusableNodeProxy { +public final class ElementNode implements StructNode, NameNode, ImmutableXmlNode, FlyweightNode { // === IMMEDIATE STRUCTURAL FIELDS === private long nodeKey; @@ -104,11 +106,55 @@ public final class ElementNode implements StructNode, NameNode, ImmutableXmlNode private LongList namespaceKeys; private QNm qNm; - // Fixed-slot lazy support + // Lazy parsing state private Object lazySource; - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean lazyFieldsParsed = true; + private long lazyOffset; + private boolean lazyFieldsParsed; + private boolean hasHash; + private boolean storeChildCount; + + // ==================== FLYWEIGHT BINDING ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + /** Whether the payload (attributeKeys, namespaceKeys) has been parsed from page memory. */ + private boolean payloadParsed; + + private static final int FIELD_COUNT = NodeFieldLayout.ELEMENT_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public ElementNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.attributeKeys = new LongArrayList(); + this.namespaceKeys = new LongArrayList(); + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. Used by deserialization @@ -142,6 +188,8 @@ public ElementNode(long nodeKey, long parentKey, int previousRevision, int lastM ? namespaceKeys : new LongArrayList(); this.qNm = qNm; + this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -175,6 +223,421 @@ public ElementNode(long nodeKey, long parentKey, int previousRevision, int lastM ? namespaceKeys : new LongArrayList(); this.qNm = qNm; + this.lazyFieldsParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * Attribute and namespace keys are eagerly read from the payload region because element + * nodes almost always need their attr/ns keys. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.lazyFieldsParsed = true; // No lazy state when bound + this.lazySource = null; + this.payloadParsed = false; + // Eagerly parse payload (attribute/namespace keys) since element nodes always need them + ensurePayloadParsed(); + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + @Override + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.ELEM_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.ELEM_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.ELEM_LEFT_SIB_KEY, nk); + this.firstChildKey = readDeltaField(NodeFieldLayout.ELEM_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.ELEM_LAST_CHILD_KEY, nk); + this.pathNodeKey = readDeltaField(NodeFieldLayout.ELEM_PATH_NODE_KEY, nk); + this.prefixKey = readSignedField(NodeFieldLayout.ELEM_PREFIX_KEY); + this.localNameKey = readSignedField(NodeFieldLayout.ELEM_LOCAL_NAME_KEY); + this.uriKey = readSignedField(NodeFieldLayout.ELEM_URI_KEY); + this.previousRevision = readSignedField(NodeFieldLayout.ELEM_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.ELEM_LAST_MOD_REVISION); + this.hash = readLongField(NodeFieldLayout.ELEM_HASH); + this.childCount = readSignedLongField(NodeFieldLayout.ELEM_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.ELEM_DESCENDANT_COUNT); + // Parse payload (attributeKeys, namespaceKeys) if not already parsed + ensurePayloadParsed(); + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.ownerPage = null; + this.page = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + @Override + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override + public boolean isWriteSingleton() { + return writeSingleton; + } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { + this.writeSingleton = writeSingleton; + } + + @Override + public KeyValueLeafPage getOwnerPage() { + return ownerPage; + } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { + this.ownerPage = ownerPage; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== PAYLOAD PARSING ==================== + + /** + * Parse the attribute and namespace key lists from the page payload region if not yet parsed. + * When bound, reads from page memory; when unbound, does nothing (already materialized). + */ + private void ensurePayloadParsed() { + if (payloadParsed || page == null) { + return; + } + payloadParsed = true; + + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ELEM_PAYLOAD) & 0xFF; + long pos = dataRegionStart + payloadFieldOff; + + // Read attribute count and keys + final int attrCount = DeltaVarIntCodec.decodeSignedFromSegment(page, pos); + pos += DeltaVarIntCodec.readSignedVarintWidth(page, pos); + if (attributeKeys == null) { + attributeKeys = new LongArrayList(attrCount); + } else { + attributeKeys.clear(); + } + for (int i = 0; i < attrCount; i++) { + final long attrKey = DeltaVarIntCodec.decodeDeltaFromSegment(page, pos, nodeKey); + final int width = DeltaVarIntCodec.readDeltaEncodedWidth(page, pos); + pos += width; + attributeKeys.add(attrKey); + } + + // Read namespace count and keys + final int nsCount = DeltaVarIntCodec.decodeSignedFromSegment(page, pos); + pos += DeltaVarIntCodec.readSignedVarintWidth(page, pos); + if (namespaceKeys == null) { + namespaceKeys = new LongArrayList(nsCount); + } else { + namespaceKeys.clear(); + } + for (int i = 0; i < nsCount; i++) { + final long nsKey = DeltaVarIntCodec.decodeDeltaFromSegment(page, pos, nodeKey); + final int width = DeltaVarIntCodec.readDeltaEncodedWidth(page, pos); + pos += width; + namespaceKeys.add(nsKey); + } + } + + // ==================== DIRECT WRITE ==================== + + /** + * Encode an ElementNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * At creation time, attribute and namespace lists are always empty. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param pathNodeKey the path node key + * @param prefixKey the prefix key + * @param localNameKey the local name key + * @param uriKey the URI key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param hash the hash value + * @param childCount the child count + * @param descendantCount the descendant count + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final long firstChildKey, final long lastChildKey, + final long pathNodeKey, final int prefixKey, final int localNameKey, final int uriKey, + final int prevRev, final int lastModRev, final long hash, + final long childCount, final long descendantCount) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.ELEMENT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: lastChildKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 5: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.ELEM_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 6: prefixKey (signed varint) + heapOffsets[NodeFieldLayout.ELEM_PREFIX_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prefixKey); + + // Field 7: localNameKey (signed varint) + heapOffsets[NodeFieldLayout.ELEM_LOCAL_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, localNameKey); + + // Field 8: uriKey (signed varint) + heapOffsets[NodeFieldLayout.ELEM_URI_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, uriKey); + + // Field 9: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.ELEM_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 10: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.ELEM_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 11: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.ELEM_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Field 12: childCount (signed long varint) + heapOffsets[NodeFieldLayout.ELEM_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 13: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.ELEM_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Field 14: PAYLOAD -- at creation, attribute and namespace lists are always empty + heapOffsets[NodeFieldLayout.ELEM_PAYLOAD] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); // attrCount = 0 + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); // nsCount = 0 + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord for structural fields, + * but writes the full payload (attribute/namespace keys) inline since writeNewRecord only writes + * empty lists. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + // Ensure all lazy fields are materialized + if (!lazyFieldsParsed) { + parseLazyFields(); + } + + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.ELEMENT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + final int[] offsets = this.heapOffsets; + + // Field 0: parentKey (delta-varint) + offsets[NodeFieldLayout.ELEM_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + offsets[NodeFieldLayout.ELEM_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSiblingKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + offsets[NodeFieldLayout.ELEM_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSiblingKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + offsets[NodeFieldLayout.ELEM_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: lastChildKey (delta-varint) + offsets[NodeFieldLayout.ELEM_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 5: pathNodeKey (delta-varint) + offsets[NodeFieldLayout.ELEM_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 6: prefixKey (signed varint) + offsets[NodeFieldLayout.ELEM_PREFIX_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prefixKey); + + // Field 7: localNameKey (signed varint) + offsets[NodeFieldLayout.ELEM_LOCAL_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, localNameKey); + + // Field 8: uriKey (signed varint) + offsets[NodeFieldLayout.ELEM_URI_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, uriKey); + + // Field 9: previousRevision (signed varint) + offsets[NodeFieldLayout.ELEM_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, previousRevision); + + // Field 10: lastModifiedRevision (signed varint) + offsets[NodeFieldLayout.ELEM_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModifiedRevision); + + // Field 11: hash (fixed 8 bytes) + offsets[NodeFieldLayout.ELEM_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Field 12: childCount (signed long varint) + offsets[NodeFieldLayout.ELEM_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 13: descendantCount (signed long varint) + offsets[NodeFieldLayout.ELEM_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Field 14: PAYLOAD [attrCount:signed varint][attrKeys:delta...][nsCount:signed varint][nsKeys:delta...] + offsets[NodeFieldLayout.ELEM_PAYLOAD] = (int) (pos - dataStart); + final int attrCount = attributeKeys != null ? attributeKeys.size() : 0; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, attrCount); + for (int i = 0; i < attrCount; i++) { + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, attributeKeys.getLong(i), nodeKey); + } + final int nsCount = namespaceKeys != null ? namespaceKeys.size() : 0; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, nsCount); + for (int i = 0; i < nsCount; i++) { + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, namespaceKeys.getLong(i), nodeKey); + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) offsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + @Override + public int estimateSerializedSize() { + return 128 + (attributeKeys != null ? attributeKeys.size() * 10 : 0) + + (namespaceKeys != null ? namespaceKeys.size() * 10 : 0); } @Override @@ -192,179 +655,459 @@ public void setNodeKey(long nodeKey) { this.nodeKey = nodeKey; } - // === IMMEDIATE STRUCTURAL GETTERS (no lazy parsing) === + // === IMMEDIATE STRUCTURAL GETTERS (dual-mode: page or Java field) === @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_PARENT_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } - public void setRightSiblingKey(long rightSibling) { + public void setRightSiblingKey(final long rightSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(rightSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, rightSibling, nodeKey); + return; + } + resizeRightSiblingKey(rightSibling); + return; + } this.rightSiblingKey = rightSibling; } + private void resizeRightSiblingKey(final long rightSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_RIGHT_SIB_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, rightSibling, nodeKey)); + } + @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } - public void setLeftSiblingKey(long leftSibling) { + public void setLeftSiblingKey(final long leftSibling) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(leftSibling, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, leftSibling, nodeKey); + return; + } + resizeLeftSiblingKey(leftSibling); + return; + } this.leftSiblingKey = leftSibling; } + private void resizeLeftSiblingKey(final long leftSibling) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_LEFT_SIB_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, leftSibling, nodeKey)); + } + @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } - public void setFirstChildKey(long firstChild) { + public void setFirstChildKey(final long firstChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(firstChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, firstChild, nodeKey); + return; + } + resizeFirstChildKey(firstChild); + return; + } this.firstChildKey = firstChild; } + private void resizeFirstChildKey(final long firstChild) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_FIRST_CHILD_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, firstChild, nodeKey)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLastChildKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_LAST_CHILD_KEY, nodeKey); + } return lastChildKey; } - public void setLastChildKey(long lastChild) { + public void setLastChildKey(final long lastChild) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(lastChild, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, lastChild, nodeKey); + return; + } + resizeLastChildKey(lastChild); + return; + } this.lastChildKey = lastChild; } + private void resizeLastChildKey(final long lastChild) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_LAST_CHILD_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, lastChild, nodeKey)); + } + @Override public boolean hasLastChild() { - if (!lazyFieldsParsed) - parseLazyFields(); - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } - // === METADATA/NAME GETTERS === + // === METADATA/NAME GETTERS (dual-mode) === @Override public long getPathNodeKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.ELEM_PATH_NODE_KEY, nodeKey); + } return pathNodeKey; } @Override public void setPathNodeKey(@NonNegative long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return; + } + resizePathNodeKey(pathNodeKey); + return; + } this.pathNodeKey = pathNodeKey; } + private void resizePathNodeKey(final long pathNodeKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_PATH_NODE_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, pathNodeKey, nodeKey)); + } + @Override public int getPrefixKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.ELEM_PREFIX_KEY); + } return prefixKey; } @Override public void setPrefixKey(int prefixKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_PREFIX_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(prefixKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, prefixKey); + return; + } + resizePrefixKey(prefixKey); + return; + } this.prefixKey = prefixKey; } + private void resizePrefixKey(final int prefixKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_PREFIX_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, prefixKey)); + } + @Override public int getLocalNameKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.ELEM_LOCAL_NAME_KEY); + } return localNameKey; } @Override public void setLocalNameKey(int localNameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_LOCAL_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(localNameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, localNameKey); + return; + } + resizeLocalNameKey(localNameKey); + return; + } this.localNameKey = localNameKey; } + private void resizeLocalNameKey(final int localNameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_LOCAL_NAME_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, localNameKey)); + } + @Override public int getURIKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.ELEM_URI_KEY); + } return uriKey; } @Override public void setURIKey(int uriKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_URI_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(uriKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, uriKey); + return; + } + resizeURIKey(uriKey); + return; + } this.uriKey = uriKey; } + private void resizeURIKey(final int uriKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_URI_KEY, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, uriKey)); + } + @Override public int getPreviousRevisionNumber() { - if (!lazyFieldsParsed) + if (page != null) { + return readSignedField(NodeFieldLayout.ELEM_PREV_REVISION); + } + if (!lazyFieldsParsed) { parseLazyFields(); + } return previousRevision; } @Override public void setPreviousRevision(int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_PREV_REVISION, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { - if (!lazyFieldsParsed) + if (page != null) { + return readSignedField(NodeFieldLayout.ELEM_LAST_MOD_REVISION); + } + if (!lazyFieldsParsed) { parseLazyFields(); + } return lastModifiedRevision; } @Override public void setLastModifiedRevision(int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_LAST_MOD_REVISION, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getChildCount() { - if (!lazyFieldsParsed) + if (page != null) { + return readSignedLongField(NodeFieldLayout.ELEM_CHILD_COUNT); + } + if (!lazyFieldsParsed) { parseLazyFields(); + } return childCount; } - public void setChildCount(long childCount) { + public void setChildCount(final long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long childCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_CHILD_COUNT, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, childCount)); + } + @Override public long getDescendantCount() { - if (!lazyFieldsParsed) + if (page != null) { + return readSignedLongField(NodeFieldLayout.ELEM_DESCENDANT_COUNT); + } + if (!lazyFieldsParsed) { parseLazyFields(); + } return descendantCount; } @Override public void setDescendantCount(long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.ELEM_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long descendantCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.ELEM_DESCENDANT_COUNT, NodeFieldLayout.ELEMENT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, descendantCount)); + } + @Override public long getHash() { - if (!lazyFieldsParsed) + if (page != null) { + final long h = readLongField(NodeFieldLayout.ELEM_HASH); + if (h != 0L) return h; + if (hashFunction != null) { + final long computed = computeHash(Bytes.threadLocalHashBuffer()); + setHash(computed); + return computed; + } + return 0L; + } + if (!lazyFieldsParsed) { parseLazyFields(); + } if (hash == 0L && hashFunction != null) { hash = computeHash(Bytes.threadLocalHashBuffer()); } @@ -372,36 +1115,35 @@ public long getHash() { } @Override - public void setHash(long hash) { + public void setHash(final long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.ELEM_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public void incrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount--; + setChildCount(getChildCount() - 1); } @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } // === NON-SERIALIZED FIELD ACCESSORS === @@ -432,6 +1174,16 @@ public void setTypeKey(int typeKey) { @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -461,13 +1213,19 @@ public LongHashFunction getHashFunction() { return hashFunction; } - // === ATTRIBUTE METHODS === + // === ATTRIBUTE METHODS (dual-mode) === public int getAttributeCount() { + if (page != null) { + ensurePayloadParsed(); + } return attributeKeys.size(); } public long getAttributeKey(@NonNegative int index) { + if (page != null) { + ensurePayloadParsed(); + } if (attributeKeys.size() <= index) { return Fixed.NULL_NODE_KEY.getStandardProperty(); } @@ -475,28 +1233,50 @@ public long getAttributeKey(@NonNegative int index) { } public void insertAttribute(@NonNegative long attrKey) { + if (page != null) { + ensurePayloadParsed(); + // Payload changed: must unbind and re-serialize + unbind(); + } attributeKeys.add(attrKey); } public void removeAttribute(@NonNegative long attrNodeKey) { + if (page != null) { + ensurePayloadParsed(); + unbind(); + } attributeKeys.removeIf(key -> key == attrNodeKey); } public void clearAttributeKeys() { + if (page != null) { + ensurePayloadParsed(); + unbind(); + } attributeKeys.clear(); } public List getAttributeKeys() { + if (page != null) { + ensurePayloadParsed(); + } return Collections.unmodifiableList(attributeKeys); } - // === NAMESPACE METHODS === + // === NAMESPACE METHODS (dual-mode) === public int getNamespaceCount() { + if (page != null) { + ensurePayloadParsed(); + } return namespaceKeys.size(); } public long getNamespaceKey(@NonNegative int namespaceKey) { + if (page != null) { + ensurePayloadParsed(); + } if (namespaceKeys.size() <= namespaceKey) { return Fixed.NULL_NODE_KEY.getStandardProperty(); } @@ -504,18 +1284,33 @@ public long getNamespaceKey(@NonNegative int namespaceKey) { } public void insertNamespace(long namespaceKey) { + if (page != null) { + ensurePayloadParsed(); + unbind(); + } namespaceKeys.add(namespaceKey); } public void removeNamespace(long namespaceKey) { + if (page != null) { + ensurePayloadParsed(); + unbind(); + } namespaceKeys.removeIf(key -> key == namespaceKey); } public void clearNamespaceKeys() { + if (page != null) { + ensurePayloadParsed(); + unbind(); + } namespaceKeys.clear(); } public List getNamespaceKeys() { + if (page != null) { + ensurePayloadParsed(); + } return Collections.unmodifiableList(namespaceKeys); } @@ -547,13 +1342,17 @@ public long computeHash(BytesOut bytes) { /** * Populate this node from a BytesIn source for singleton reuse. + * LAZY OPTIMIZATION: Only parses structural fields immediately (NEW ORDER). */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; this.sirixDeweyID = null; + this.payloadParsed = false; if (this.attributeKeys == null) { this.attributeKeys = new LongArrayList(); } @@ -572,35 +1371,19 @@ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFu this.prefixKey = DeltaVarIntCodec.decodeSigned(source); this.localNameKey = DeltaVarIntCodec.decodeSigned(source); this.uriKey = DeltaVarIntCodec.decodeSigned(source); - this.previousRevision = DeltaVarIntCodec.decodeSigned(source); - this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(source); - this.childCount = config.storeChildCount() - ? DeltaVarIntCodec.decodeSigned(source) - : 0L; - if (config.hashType != HashType.NONE) { - this.hash = source.readLong(); - this.descendantCount = DeltaVarIntCodec.decodeSigned(source); - } else { - this.hash = 0L; - this.descendantCount = 0L; - } - - final int attributeCount = DeltaVarIntCodec.decodeSigned(source); - for (int i = 0; i < attributeCount; i++) { - this.attributeKeys.add(DeltaVarIntCodec.decodeDelta(source, nodeKey)); - } - - final int namespaceCount = DeltaVarIntCodec.decodeSigned(source); - for (int i = 0; i < namespaceCount; i++) { - this.namespaceKeys.add(DeltaVarIntCodec.decodeDelta(source, nodeKey)); - } - } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; + // Store for lazy parsing of remaining fields + this.lazySource = source.getSource(); + this.lazyOffset = source.position(); this.lazyFieldsParsed = false; + this.hasHash = config.hashType != HashType.NONE; + this.storeChildCount = config.storeChildCount(); + + this.previousRevision = 0; + this.lastModifiedRevision = 0; + this.childCount = 0; + this.hash = 0; + this.descendantCount = 0; } private void parseLazyFields() { @@ -608,38 +1391,99 @@ private void parseLazyFields() { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.lastChildKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.LAST_CHILD_KEY); - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.pathNodeKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.PATH_NODE_KEY); - this.uriKey = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.URI_KEY); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; + if (lazySource == null) { + lazyFieldsParsed = true; return; } + BytesIn bytesIn; + if (lazySource instanceof MemorySegment segment) { + bytesIn = new MemorySegmentBytesIn(segment); + bytesIn.position(lazyOffset); + } else if (lazySource instanceof byte[] bytes) { + bytesIn = new ByteArrayBytesIn(bytes); + bytesIn.position(lazyOffset); + } else { + throw new IllegalStateException("Unknown lazy source type: " + lazySource.getClass()); + } + + this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); + this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); + this.childCount = storeChildCount + ? DeltaVarIntCodec.decodeSigned(bytesIn) + : 0L; + if (hasHash) { + this.hash = bytesIn.readLong(); + this.descendantCount = DeltaVarIntCodec.decodeSigned(bytesIn); + } else { + this.hash = 0L; + this.descendantCount = 0L; + } + + final int attributeCount = DeltaVarIntCodec.decodeSigned(bytesIn); + for (int i = 0; i < attributeCount; i++) { + this.attributeKeys.add(DeltaVarIntCodec.decodeDelta(bytesIn, nodeKey)); + } + + final int namespaceCount = DeltaVarIntCodec.decodeSigned(bytesIn); + for (int i = 0; i < namespaceCount; i++) { + this.namespaceKeys.add(DeltaVarIntCodec.decodeDelta(bytesIn, nodeKey)); + } + this.lazyFieldsParsed = true; } /** * Create a deep copy snapshot of this node. + * + * @return a new instance with copied values */ public ElementNode toSnapshot() { - if (!lazyFieldsParsed) + if (page != null) { + final long nk = this.nodeKey; + ensurePayloadParsed(); + final ElementNode snapshot = new ElementNode( + nk, + readDeltaField(NodeFieldLayout.ELEM_PARENT_KEY, nk), + readSignedField(NodeFieldLayout.ELEM_PREV_REVISION), + readSignedField(NodeFieldLayout.ELEM_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.ELEM_RIGHT_SIB_KEY, nk), + readDeltaField(NodeFieldLayout.ELEM_LEFT_SIB_KEY, nk), + readDeltaField(NodeFieldLayout.ELEM_FIRST_CHILD_KEY, nk), + readDeltaField(NodeFieldLayout.ELEM_LAST_CHILD_KEY, nk), + readSignedLongField(NodeFieldLayout.ELEM_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.ELEM_DESCENDANT_COUNT), + readLongField(NodeFieldLayout.ELEM_HASH), + readDeltaField(NodeFieldLayout.ELEM_PATH_NODE_KEY, nk), + readSignedField(NodeFieldLayout.ELEM_PREFIX_KEY), + readSignedField(NodeFieldLayout.ELEM_LOCAL_NAME_KEY), + readSignedField(NodeFieldLayout.ELEM_URI_KEY), + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + new LongArrayList(attributeKeys), + new LongArrayList(namespaceKeys), + qNm); + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; + } + if (!lazyFieldsParsed) { parseLazyFields(); - return new ElementNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - firstChildKey, lastChildKey, childCount, descendantCount, hash, pathNodeKey, prefixKey, localNameKey, uriKey, - hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null, - new LongArrayList(attributeKeys), new LongArrayList(namespaceKeys), qNm); + } + final ElementNode snapshot = new ElementNode( + nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, firstChildKey, lastChildKey, + childCount, descendantCount, hash, pathNodeKey, + prefixKey, localNameKey, uriKey, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + new LongArrayList(attributeKeys), + new LongArrayList(namespaceKeys), + qNm); + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/NamespaceNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/NamespaceNode.java index e6ff591e4..efa5e1129 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/NamespaceNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/NamespaceNode.java @@ -34,35 +34,39 @@ import io.sirix.access.ResourceConfiguration; import io.sirix.api.visitor.VisitResult; import io.sirix.api.visitor.XmlNodeVisitor; -import io.sirix.node.BytesIn; import io.sirix.node.Bytes; +import io.sirix.node.BytesIn; import io.sirix.node.BytesOut; import io.sirix.node.DeltaVarIntCodec; import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableNamespace; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; - -import java.lang.foreign.MemorySegment; import io.sirix.utils.NamePageHash; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + /** * Node representing an XML namespace, using primitive fields. * + *

Supports LeanStore-style flyweight binding: when bound to a slotted page MemorySegment, + * getters/setters read/write directly to page memory via the per-record offset table. + * When unbound, they operate on Java primitive fields (normal mode).

+ * * @author Johannes Lichtenberger */ -public final class NamespaceNode implements NameNode, ImmutableXmlNode, Node, ReusableNodeProxy { +public final class NamespaceNode implements NameNode, ImmutableXmlNode, Node, FlyweightNode { // === PRIMITIVE FIELDS === private long nodeKey; @@ -75,23 +79,51 @@ public final class NamespaceNode implements NameNode, ImmutableXmlNode, Node, Re private int lastModifiedRevision; private long hash; - // === FIXED-SLOT LAZY SUPPORT === - private Object lazySource; - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean lazyFieldsParsed = true; - // === NON-SERIALIZED FIELDS === private LongHashFunction hashFunction; private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; private QNm qNm; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + private boolean writeSingleton; + private KeyValueLeafPage ownerPage; + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.NAMESPACE_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public NamespaceNode(final long nodeKey, final LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + /** * Primary constructor with all primitive fields. */ - public NamespaceNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long pathNodeKey, - int prefixKey, int localNameKey, int uriKey, long hash, LongHashFunction hashFunction, byte[] deweyID, QNm qNm) { + public NamespaceNode(final long nodeKey, final long parentKey, final int previousRevision, + final int lastModifiedRevision, final long pathNodeKey, final int prefixKey, + final int localNameKey, final int uriKey, final long hash, + final LongHashFunction hashFunction, final byte[] deweyID, final QNm qNm) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -104,14 +136,16 @@ public NamespaceNode(long nodeKey, long parentKey, int previousRevision, int las this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; } /** * Constructor with SirixDeweyID. */ - public NamespaceNode(long nodeKey, long parentKey, int previousRevision, int lastModifiedRevision, long pathNodeKey, - int prefixKey, int localNameKey, int uriKey, long hash, LongHashFunction hashFunction, SirixDeweyID deweyID, - QNm qNm) { + public NamespaceNode(final long nodeKey, final long parentKey, final int previousRevision, + final int lastModifiedRevision, final long pathNodeKey, final int prefixKey, + final int localNameKey, final int uriKey, final long hash, + final LongHashFunction hashFunction, final SirixDeweyID deweyID, final QNm qNm) { this.nodeKey = nodeKey; this.parentKey = parentKey; this.previousRevision = previousRevision; @@ -124,8 +158,211 @@ public NamespaceNode(long nodeKey, long parentKey, int previousRevision, int las this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + @Override + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.NS_PARENT_KEY, nk); + this.pathNodeKey = readDeltaField(NodeFieldLayout.NS_PATH_NODE_KEY, nk); + this.prefixKey = readSignedField(NodeFieldLayout.NS_PREFIX_KEY); + this.localNameKey = readSignedField(NodeFieldLayout.NS_LOCAL_NAME_KEY); + this.uriKey = readSignedField(NodeFieldLayout.NS_URI_KEY); + this.previousRevision = readSignedField(NodeFieldLayout.NS_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.NS_LAST_MOD_REVISION); + this.hash = readLongField(NodeFieldLayout.NS_HASH); + this.ownerPage = null; + this.page = null; } + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + @Override + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override public boolean isWriteSingleton() { return writeSingleton; } + @Override public void setWriteSingleton(final boolean ws) { this.writeSingleton = ws; } + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== SERIALIZE TO HEAP ==================== + + // ==================== DIRECT WRITE ==================== + + /** + * Encode a NamespaceNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param pathNodeKey the path node key + * @param prefixKey the prefix key + * @param localNameKey the local name key + * @param uriKey the URI key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param hash the hash value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long pathNodeKey, + final int prefixKey, final int localNameKey, final int uriKey, + final int prevRev, final int lastModRev, final long hash) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.NAMESPACE.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.NS_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.NS_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 2: prefixKey (signed varint) + heapOffsets[NodeFieldLayout.NS_PREFIX_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prefixKey); + + // Field 3: localNameKey (signed varint) + heapOffsets[NodeFieldLayout.NS_LOCAL_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, localNameKey); + + // Field 4: uriKey (signed varint) + heapOffsets[NodeFieldLayout.NS_URI_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, uriKey); + + // Field 5: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.NS_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 6: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.NS_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 7: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.NS_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, pathNodeKey, prefixKey, localNameKey, uriKey, + previousRevision, lastModifiedRevision, hash); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; + } + + @Override + public int estimateSerializedSize() { + return 64; + } + + // ==================== NODE IDENTITY ==================== + @Override public NodeKind getKind() { return NodeKind.NAMESPACE; @@ -137,94 +374,248 @@ public long getNodeKey() { } @Override - public void setNodeKey(long nodeKey) { + public void setNodeKey(final long nodeKey) { this.nodeKey = nodeKey; } + // ==================== DUAL-MODE GETTERS/SETTERS ==================== + @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.NS_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_PARENT_KEY, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getPathNodeKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.NS_PATH_NODE_KEY, nodeKey); + } return pathNodeKey; } @Override - public void setPathNodeKey(@NonNegative long pathNodeKey) { + public void setPathNodeKey(@NonNegative final long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return; + } + resizePathNodeKey(pathNodeKey); + return; + } this.pathNodeKey = pathNodeKey; } + private void resizePathNodeKey(final long pathNodeKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_PATH_NODE_KEY, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, pathNodeKey, nodeKey)); + } + @Override public int getPrefixKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.NS_PREFIX_KEY); + } return prefixKey; } @Override - public void setPrefixKey(int prefixKey) { + public void setPrefixKey(final int prefixKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_PREFIX_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(prefixKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, prefixKey); + return; + } + resizePrefixKey(prefixKey); + return; + } this.prefixKey = prefixKey; } + private void resizePrefixKey(final int prefixKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_PREFIX_KEY, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, prefixKey)); + } + @Override public int getLocalNameKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.NS_LOCAL_NAME_KEY); + } return localNameKey; } @Override - public void setLocalNameKey(int localNameKey) { + public void setLocalNameKey(final int localNameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_LOCAL_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(localNameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, localNameKey); + return; + } + resizeLocalNameKey(localNameKey); + return; + } this.localNameKey = localNameKey; } + private void resizeLocalNameKey(final int localNameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_LOCAL_NAME_KEY, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, localNameKey)); + } + @Override public int getURIKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.NS_URI_KEY); + } return uriKey; } @Override - public void setURIKey(int uriKey) { + public void setURIKey(final int uriKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_URI_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(uriKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, uriKey); + return; + } + resizeURIKey(uriKey); + return; + } this.uriKey = uriKey; } + private void resizeURIKey(final int uriKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_URI_KEY, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, uriKey)); + } + @Override public int getPreviousRevisionNumber() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.NS_PREV_REVISION); + } return previousRevision; } @Override - public void setPreviousRevision(int revision) { + public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_PREV_REVISION, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.NS_LAST_MOD_REVISION); + } return lastModifiedRevision; } @Override - public void setLastModifiedRevision(int revision) { + public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.NS_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.NS_LAST_MOD_REVISION, NodeFieldLayout.NAMESPACE_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + // Bound: read hash from MemorySegment. If non-zero (set by rollingAdd/rollingUpdate), + // return it as-is to preserve rolling hash arithmetic. If zero (never set), compute. + final long storedHash = readLongField(NodeFieldLayout.NS_HASH); + if (storedHash != 0L) { + return storedHash; + } + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; + } + // Unbound (in-memory): return stored hash if set by rollingAdd, else compute if (hash == 0L && hashFunction != null) { hash = computeHash(Bytes.threadLocalHashBuffer()); } @@ -232,21 +623,30 @@ public long getHash() { } @Override - public void setHash(long hash) { + public void setHash(final long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.NS_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } + // ==================== OTHER GETTERS/SETTERS ==================== + @Override public QNm getName() { return qNm; } - public void setName(QNm name) { + public void setName(final QNm name) { this.qNm = name; } @Override - public boolean isSameItem(@Nullable Node other) { + public boolean isSameItem(@Nullable final Node other) { return other != null && other.getNodeKey() == nodeKey; } @@ -256,15 +656,25 @@ public int getTypeKey() { } @Override - public void setTypeKey(int typeKey) {} + public void setTypeKey(final int typeKey) {} @Override - public void setDeweyID(SirixDeweyID id) { + public void setDeweyID(final SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } - public void setDeweyIDBytes(byte[] deweyIDBytes) { + public void setDeweyIDBytes(final byte[] deweyIDBytes) { this.deweyIDBytes = deweyIDBytes; this.sirixDeweyID = null; } @@ -289,11 +699,15 @@ public LongHashFunction getHashFunction() { return hashFunction; } + // ==================== DESERIALIZATION ==================== + /** * Populate this node from a BytesIn source for singleton reuse. */ - public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, - ResourceConfiguration config) { + public void readFrom(final BytesIn source, final long nodeKey, final byte[] deweyId, + final LongHashFunction hashFunction, final ResourceConfiguration config) { + // Unbind flyweight — ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -312,55 +726,46 @@ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFu } @Override - public long computeHash(BytesOut bytes) { + public long computeHash(final BytesOut bytes) { if (hashFunction == null) return 0L; bytes.clear(); - bytes.writeLong(nodeKey).writeLong(parentKey).writeByte(getKind().getId()); - bytes.writeInt(prefixKey).writeInt(localNameKey).writeInt(uriKey); + bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeInt(getPrefixKey()).writeInt(getLocalNameKey()).writeInt(getURIKey()); return bytes.hashDirect(hashFunction); } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - - private void parseLazyFields() { - if (lazyFieldsParsed) { - return; - } + // ==================== SNAPSHOT ==================== - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.pathNodeKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.PATH_NODE_KEY); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; + /** + * Create a deep copy snapshot of this node. + * When bound, reads all fields from page memory. When unbound, uses Java fields. + */ + public NamespaceNode toSnapshot() { + if (page != null) { + // Bound mode: read all fields from page + return new NamespaceNode(nodeKey, + readDeltaField(NodeFieldLayout.NS_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.NS_PREV_REVISION), + readSignedField(NodeFieldLayout.NS_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.NS_PATH_NODE_KEY, nodeKey), + readSignedField(NodeFieldLayout.NS_PREFIX_KEY), + readSignedField(NodeFieldLayout.NS_LOCAL_NAME_KEY), + readSignedField(NodeFieldLayout.NS_URI_KEY), + readLongField(NodeFieldLayout.NS_HASH), + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + qNm); } - - this.lazyFieldsParsed = true; + return new NamespaceNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + pathNodeKey, prefixKey, localNameKey, uriKey, hash, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, qNm); } - public NamespaceNode toSnapshot() { - if (!lazyFieldsParsed) - parseLazyFields(); - return new NamespaceNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, pathNodeKey, prefixKey, - localNameKey, uriKey, hash, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null, - qNm); - } + // ==================== VISITOR / OBJECT METHODS ==================== @Override - public VisitResult acceptVisitor(XmlNodeVisitor visitor) { + public VisitResult acceptVisitor(final XmlNodeVisitor visitor) { return visitor.visit(ImmutableNamespace.of(this)); } @@ -370,7 +775,7 @@ public int hashCode() { } @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (!(obj instanceof NamespaceNode other)) return false; return nodeKey == other.nodeKey && parentKey == other.parentKey && prefixKey == other.prefixKey diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/PINode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/PINode.java index 19a6871cd..c4db95167 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/PINode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/PINode.java @@ -44,15 +44,14 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutablePI; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.NameNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import io.sirix.utils.Compression; @@ -63,13 +62,18 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Processing Instruction node using primitive fields. * + *

Supports LeanStore-style flyweight binding for zero-copy reads from a slotted page + * MemorySegment. When bound, all getters/setters operate directly on page memory via + * the per-record offset table. When unbound, they operate on Java primitive fields.

+ * * @author Johannes Lichtenberger */ -public final class PINode implements StructNode, NameNode, ValueNode, ImmutableXmlNode, ReusableNodeProxy { +public final class PINode implements StructNode, NameNode, ValueNode, ImmutableXmlNode, FlyweightNode { // === IMMEDIATE STRUCTURAL FIELDS === private long nodeKey; @@ -101,18 +105,50 @@ public final class PINode implements StructNode, NameNode, ValueNode, ImmutableX private boolean lazyValueCompressed; private boolean valueParsed = true; - // === FIXED-SLOT LAZY SUPPORT === - private Object lazySource; - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean lazyFieldsParsed = true; - // === NON-SERIALIZED FIELDS === private LongHashFunction hashFunction; private SirixDeweyID sirixDeweyID; private byte[] deweyIDBytes; private QNm qNm; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place on varint width changes. */ + private KeyValueLeafPage ownerPage; + + /** Pre-allocated offset array reused across serializations (zero-alloc hot path). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.PI_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public PINode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } + /** * Primary constructor with all primitive fields. */ @@ -140,6 +176,7 @@ public PINode(long nodeKey, long parentKey, int previousRevision, int lastModifi this.hashFunction = hashFunction; this.deweyIDBytes = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -169,6 +206,288 @@ public PINode(long nodeKey, long parentKey, int previousRevision, int lastModifi this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; this.qNm = qNm; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazyValueSource = null; + this.hash = 0; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.PI_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.PI_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.PI_LEFT_SIB_KEY, nk); + this.firstChildKey = readDeltaField(NodeFieldLayout.PI_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.PI_LAST_CHILD_KEY, nk); + this.pathNodeKey = readDeltaField(NodeFieldLayout.PI_PATH_NODE_KEY, nk); + this.prefixKey = readSignedField(NodeFieldLayout.PI_PREFIX_KEY); + this.localNameKey = readSignedField(NodeFieldLayout.PI_LOCAL_NAME_KEY); + this.uriKey = readSignedField(NodeFieldLayout.PI_URI_KEY); + this.previousRevision = readSignedField(NodeFieldLayout.PI_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.PI_LAST_MOD_REVISION); + this.childCount = readSignedLongField(NodeFieldLayout.PI_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.PI_DESCENDANT_COUNT); + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override + public boolean isWriteSingleton() { return writeSingleton; } + + @Override + public void setWriteSingleton(final boolean writeSingleton) { this.writeSingleton = writeSingleton; } + + @Override + public KeyValueLeafPage getOwnerPage() { return ownerPage; } + + @Override + public void setOwnerPage(final KeyValueLeafPage ownerPage) { this.ownerPage = ownerPage; } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 119 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.PI_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) + this.isCompressed = page.get(ValueLayout.JAVA_BYTE, payloadStart) == 1; + + // Read value length (varint) + final long lenOffset = payloadStart + 1; + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + this.valueParsed = true; + } + + // ==================== SERIALIZE TO HEAP ==================== + + /** + * Encode a PINode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param pathNodeKey the path node key + * @param prefixKey the prefix key + * @param localNameKey the local name key + * @param uriKey the URI key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param childCount the child count + * @param descendantCount the descendant count + * @param rawValue the raw value bytes (possibly compressed) + * @param isCompressed whether the value is compressed + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final long firstChildKey, final long lastChildKey, + final long pathNodeKey, final int prefixKey, final int localNameKey, final int uriKey, + final int prevRev, final int lastModRev, + final long childCount, final long descendantCount, + final byte[] rawValue, final boolean isCompressed) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.PROCESSING_INSTRUCTION.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: firstChildKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 4: lastChildKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 5: pathNodeKey (delta-varint) + heapOffsets[NodeFieldLayout.PI_PATH_NODE_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, pathNodeKey, nodeKey); + + // Field 6: prefixKey (signed varint) + heapOffsets[NodeFieldLayout.PI_PREFIX_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prefixKey); + + // Field 7: localNameKey (signed varint) + heapOffsets[NodeFieldLayout.PI_LOCAL_NAME_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, localNameKey); + + // Field 8: uriKey (signed varint) + heapOffsets[NodeFieldLayout.PI_URI_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, uriKey); + + // Field 9: previousRevision (signed varint) + heapOffsets[NodeFieldLayout.PI_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 10: lastModifiedRevision (signed varint) + heapOffsets[NodeFieldLayout.PI_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 11: childCount (signed long varint) + heapOffsets[NodeFieldLayout.PI_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 12: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.PI_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Field 13: payload [isCompressed:1][valueLength:varint][value:bytes] + heapOffsets[NodeFieldLayout.PI_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, isCompressed ? (byte) 1 : (byte) 0); + pos++; + final byte[] val = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, val.length); + if (val.length > 0) { + MemorySegment.copy(val, 0, target, ValueLayout.JAVA_BYTE, pos, val.length); + pos += val.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!valueParsed) parseLazyValue(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + firstChildKey, lastChildKey, pathNodeKey, + prefixKey, localNameKey, uriKey, + previousRevision, lastModifiedRevision, + childCount, descendantCount, value, isCompressed); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -188,202 +507,453 @@ public void setNodeKey(long nodeKey) { @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_PARENT_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } - public void setRightSiblingKey(long key) { + public void setRightSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeRightSiblingKey(key); + return; + } this.rightSiblingKey = key; } + private void resizeRightSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_RIGHT_SIB_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } - public void setLeftSiblingKey(long key) { + public void setLeftSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLeftSiblingKey(key); + return; + } this.leftSiblingKey = key; } + private void resizeLeftSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_LEFT_SIB_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } - public void setFirstChildKey(long key) { + public void setFirstChildKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeFirstChildKey(key); + return; + } this.firstChildKey = key; } + private void resizeFirstChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_FIRST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLastChildKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_LAST_CHILD_KEY, nodeKey); + } return lastChildKey; } - public void setLastChildKey(long key) { + public void setLastChildKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLastChildKey(key); + return; + } this.lastChildKey = key; } + private void resizeLastChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_LAST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLastChild() { - if (!lazyFieldsParsed) - parseLazyFields(); - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.PI_CHILD_COUNT); + } return childCount; } - public void setChildCount(long childCount) { + public void setChildCount(final long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long childCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_CHILD_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, childCount)); + } + @Override public void incrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount--; + setChildCount(getChildCount() - 1); } @Override public long getDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.PI_DESCENDANT_COUNT); + } return descendantCount; } @Override - public void setDescendantCount(long descendantCount) { + public void setDescendantCount(final long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long descendantCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_DESCENDANT_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, descendantCount)); + } + @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override public long getPathNodeKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readDeltaField(NodeFieldLayout.PI_PATH_NODE_KEY, nodeKey); + } return pathNodeKey; } @Override public void setPathNodeKey(@NonNegative long pathNodeKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_PATH_NODE_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(pathNodeKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, pathNodeKey, nodeKey); + return; + } + resizePathNodeKey(pathNodeKey); + return; + } this.pathNodeKey = pathNodeKey; } + private void resizePathNodeKey(final long pathNodeKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_PATH_NODE_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, pathNodeKey, nodeKey)); + } + @Override public int getPrefixKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.PI_PREFIX_KEY); + } return prefixKey; } @Override public void setPrefixKey(int prefixKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_PREFIX_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(prefixKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, prefixKey); + return; + } + resizePrefixKey(prefixKey); + return; + } this.prefixKey = prefixKey; } + private void resizePrefixKey(final int prefixKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_PREFIX_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, prefixKey)); + } + @Override public int getLocalNameKey() { + if (page != null) { + return readSignedField(NodeFieldLayout.PI_LOCAL_NAME_KEY); + } return localNameKey; } @Override public void setLocalNameKey(int localNameKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_LOCAL_NAME_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(localNameKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, localNameKey); + return; + } + resizeLocalNameKey(localNameKey); + return; + } this.localNameKey = localNameKey; } + private void resizeLocalNameKey(final int localNameKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_LOCAL_NAME_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, localNameKey)); + } + @Override public int getURIKey() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.PI_URI_KEY); + } return uriKey; } @Override public void setURIKey(int uriKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_URI_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(uriKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, uriKey); + return; + } + resizeURIKey(uriKey); + return; + } this.uriKey = uriKey; } + private void resizeURIKey(final int uriKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_URI_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, uriKey)); + } + @Override public int getPreviousRevisionNumber() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.PI_PREV_REVISION); + } return previousRevision; } @Override public void setPreviousRevision(int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_PREV_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedField(NodeFieldLayout.PI_LAST_MOD_REVISION); + } return lastModifiedRevision; } @Override public void setLastModifiedRevision(int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.PI_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.PI_LAST_MOD_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!lazyFieldsParsed) - parseLazyFields(); - if (hash == 0L && hashFunction != null) { + // Return stored hash if set by rollingAdd/rollingUpdate, else compute on demand + if (hash != 0L) { + return hash; + } + if (hashFunction != null) { hash = computeHash(Bytes.threadLocalHashBuffer()); } return hash; @@ -396,7 +966,9 @@ public void setHash(long hash) { @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } if (value != null && isCompressed) { @@ -408,6 +980,23 @@ public byte[] getRawValue() { @Override public void setRawValue(byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.valueParsed = true; + this.lazyValueSource = null; + this.lazyValueOffset = 0L; + this.lazyValueLength = 0; + this.lazyValueCompressed = false; + this.isCompressed = false; + this.hash = 0L; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.valueParsed = true; this.lazyValueSource = null; @@ -431,8 +1020,9 @@ public void setLazyRawValue(Object source, long valueOffset, int valueLength, bo @Override public String getValue() { - return value != null - ? new String(value, Constants.DEFAULT_ENCODING) + final byte[] raw = getRawValue(); + return raw != null + ? new String(raw, Constants.DEFAULT_ENCODING) : ""; } @@ -450,13 +1040,18 @@ public void setName(QNm name) { * preserve the original compressed bytes. */ public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseLazyValue(); } return value; } public boolean isCompressed() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } return isCompressed; } @@ -479,6 +1074,16 @@ public void setTypeKey(int typeKey) {} @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -513,6 +1118,8 @@ public LongHashFunction getHashFunction() { */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -588,16 +1195,8 @@ public long computeHash(BytesOut bytes) { if (hashFunction == null) return 0L; bytes.clear(); - bytes.writeLong(nodeKey).writeLong(parentKey).writeByte(getKind().getId()); - bytes.writeLong(childCount) - .writeLong(descendantCount) - .writeLong(leftSiblingKey) - .writeLong(rightSiblingKey) - .writeLong(firstChildKey); - if (lastChildKey != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(lastChildKey); - } - bytes.writeInt(prefixKey).writeInt(localNameKey).writeInt(uriKey); + bytes.writeLong(getNodeKey()).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeInt(getPrefixKey()).writeInt(getLocalNameKey()).writeInt(getURIKey()); final byte[] rawValue = getRawValue(); if (rawValue != null) { bytes.write(rawValue); @@ -605,51 +1204,59 @@ public long computeHash(BytesOut bytes) { return bytes.hashDirect(hashFunction); } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - - private void parseLazyFields() { - if (lazyFieldsParsed) { - return; + /** + * Create a deep copy snapshot of this node. + * + * @return a new instance with copied values + */ + public PINode toSnapshot() { + if (page != null) { + final long nk = this.nodeKey; + if (!valueParsed) { + readPayloadFromPage(); + } + final PINode snapshot = new PINode( + nk, + readDeltaField(NodeFieldLayout.PI_PARENT_KEY, nk), + readSignedField(NodeFieldLayout.PI_PREV_REVISION), + readSignedField(NodeFieldLayout.PI_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.PI_RIGHT_SIB_KEY, nk), + readDeltaField(NodeFieldLayout.PI_LEFT_SIB_KEY, nk), + readDeltaField(NodeFieldLayout.PI_FIRST_CHILD_KEY, nk), + readDeltaField(NodeFieldLayout.PI_LAST_CHILD_KEY, nk), + readSignedLongField(NodeFieldLayout.PI_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.PI_DESCENDANT_COUNT), + hash, + readDeltaField(NodeFieldLayout.PI_PATH_NODE_KEY, nk), + readSignedField(NodeFieldLayout.PI_PREFIX_KEY), + readSignedField(NodeFieldLayout.PI_LOCAL_NAME_KEY), + readSignedField(NodeFieldLayout.PI_URI_KEY), + value != null ? value.clone() : null, + isCompressed, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, + qNm); + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.lastChildKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.LAST_CHILD_KEY); - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.pathNodeKey = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.PATH_NODE_KEY); - this.uriKey = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.URI_KEY); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; + if (!valueParsed) { + parseLazyValue(); } - - this.lazyFieldsParsed = true; - } - - public PINode toSnapshot() { - if (!lazyFieldsParsed) - parseLazyFields(); - final byte[] rawValue = getRawValue(); - return new PINode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - firstChildKey, lastChildKey, childCount, descendantCount, hash, pathNodeKey, prefixKey, localNameKey, uriKey, - rawValue != null - ? rawValue.clone() - : null, - isCompressed, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null, + final PINode snapshot = new PINode( + nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, firstChildKey, lastChildKey, + childCount, descendantCount, hash, pathNodeKey, + prefixKey, localNameKey, uriKey, + value != null ? value.clone() : null, + isCompressed, hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null, qNm); + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; } @Override diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/TextNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/TextNode.java index 4b550b0aa..1d6847193 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/TextNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/TextNode.java @@ -42,14 +42,13 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableText; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; -import io.sirix.node.interfaces.ReusableNodeProxy; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.ValueNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import io.sirix.utils.Compression; @@ -59,6 +58,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; /** * Text node implementation using primitive fields. @@ -70,7 +70,7 @@ * * @author Johannes Lichtenberger */ -public final class TextNode implements StructNode, ValueNode, ImmutableXmlNode, ReusableNodeProxy { +public final class TextNode implements StructNode, ValueNode, ImmutableXmlNode, FlyweightNode { // === IMMEDIATE STRUCTURAL FIELDS === private long nodeKey; @@ -97,15 +97,33 @@ public final class TextNode implements StructNode, ValueNode, ImmutableXmlNode, private long lazyOffset; private boolean metadataParsed; private boolean valueParsed; - private boolean hasHash; private long valueOffset; private boolean fixedValueEncoding; private int fixedValueLength; private boolean fixedValueCompressed; - // Fixed-slot lazy metadata support - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + private MemorySegment page; + private long recordBase; + private long dataRegionStart; + private int slotIndex; + private boolean writeSingleton; + private KeyValueLeafPage ownerPage; + private final int[] heapOffsets; + private static final int FIELD_COUNT = NodeFieldLayout.TEXT_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public TextNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. @@ -126,6 +144,7 @@ public TextNode(long nodeKey, long parentKey, int previousRevision, int lastModi this.deweyIDBytes = deweyID; this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -147,6 +166,212 @@ public TextNode(long nodeKey, long parentKey, int previousRevision, int lastModi this.sirixDeweyID = deweyID; this.metadataParsed = true; this.valueParsed = true; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + this.metadataParsed = true; + this.valueParsed = false; // Payload still needs lazy parsing from page + this.lazySource = null; + this.hash = 0; + } + + @Override + public void unbind() { + if (page == null) return; + final long nk = this.nodeKey; + this.parentKey = readDeltaField(NodeFieldLayout.TEXT_PARENT_KEY, nk); + this.rightSiblingKey = readDeltaField(NodeFieldLayout.TEXT_RIGHT_SIB_KEY, nk); + this.leftSiblingKey = readDeltaField(NodeFieldLayout.TEXT_LEFT_SIB_KEY, nk); + this.previousRevision = readSignedField(NodeFieldLayout.TEXT_PREV_REVISION); + this.lastModifiedRevision = readSignedField(NodeFieldLayout.TEXT_LAST_MOD_REVISION); + // Hash is not stored on the slotted page; keep current in-memory value + // Payload needs to be read from page before unbinding + if (!valueParsed) { + readPayloadFromPage(); + } + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.ownerPage = null; + this.page = null; + } + + @Override + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override public boolean isWriteSingleton() { return writeSingleton; } + @Override public void setWriteSingleton(final boolean ws) { this.writeSingleton = ws; } + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + + @Override + public int estimateSerializedSize() { + final int payloadLen = value != null ? value.length : 0; + return 55 + payloadLen; + } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + /** + * Read the payload (value bytes) directly from page memory when bound. + */ + private void readPayloadFromPage() { + final int payloadFieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.TEXT_PAYLOAD) & 0xFF; + final long payloadStart = dataRegionStart + payloadFieldOff; + + // Read isCompressed flag (1 byte) + this.isCompressed = page.get(ValueLayout.JAVA_BYTE, payloadStart) == 1; + + // Read value length (varint) + final long lenOffset = payloadStart + 1; + final int length = DeltaVarIntCodec.decodeSignedFromSegment(page, lenOffset); + final int lenBytes = DeltaVarIntCodec.readSignedVarintWidth(page, lenOffset); + + // Read value bytes + final long dataOffset = lenOffset + lenBytes; + this.value = new byte[length]; + MemorySegment.copy(page, ValueLayout.JAVA_BYTE, dataOffset, this.value, 0, length); + this.valueParsed = true; + } + + // ==================== DIRECT WRITE ==================== + + /** + * Encode a TextNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param parentKey the parent node key + * @param rightSibKey the right sibling key + * @param leftSibKey the left sibling key + * @param prevRev the previous revision number + * @param lastModRev the last modified revision number + * @param rawValue the raw value bytes (possibly compressed) + * @param isCompressed whether the value is compressed + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long parentKey, final long rightSibKey, final long leftSibKey, + final int prevRev, final int lastModRev, + final byte[] rawValue, final boolean isCompressed) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.TEXT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: parentKey (delta-varint) + heapOffsets[NodeFieldLayout.TEXT_PARENT_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, parentKey, nodeKey); + + // Field 1: rightSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.TEXT_RIGHT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, rightSibKey, nodeKey); + + // Field 2: leftSiblingKey (delta-varint) + heapOffsets[NodeFieldLayout.TEXT_LEFT_SIB_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, leftSibKey, nodeKey); + + // Field 3: prevRevision (signed varint) + heapOffsets[NodeFieldLayout.TEXT_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, prevRev); + + // Field 4: lastModRevision (signed varint) + heapOffsets[NodeFieldLayout.TEXT_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, lastModRev); + + // Field 5: payload [isCompressed:1][length:varint][data:bytes] + heapOffsets[NodeFieldLayout.TEXT_PAYLOAD] = (int) (pos - dataStart); + target.set(ValueLayout.JAVA_BYTE, pos, isCompressed ? (byte) 1 : (byte) 0); + pos++; + final byte[] val = rawValue != null ? rawValue : new byte[0]; + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, val.length); + if (val.length > 0) { + MemorySegment.copy(val, 0, target, ValueLayout.JAVA_BYTE, pos, val.length); + pos += val.length; + } + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + if (!metadataParsed) parseMetadataFields(); + if (!valueParsed) parseValuePayload(); + return writeNewRecord(target, offset, heapOffsets, nodeKey, + parentKey, rightSiblingKey, leftSiblingKey, + previousRevision, lastModifiedRevision, value, isCompressed); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -164,94 +389,204 @@ public void setNodeKey(long nodeKey) { this.nodeKey = nodeKey; } - // === IMMEDIATE STRUCTURAL GETTERS (no lazy parsing) === + // === STRUCTURAL GETTERS (dual-mode: flyweight or primitive) === @Override public long getParentKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.TEXT_PARENT_KEY, nodeKey); + } return parentKey; } - public void setParentKey(long parentKey) { + public void setParentKey(final long parentKey) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.TEXT_PARENT_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(parentKey, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, parentKey, nodeKey); + return; + } + resizeParentKey(parentKey); + return; + } this.parentKey = parentKey; } + private void resizeParentKey(final long parentKey) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.TEXT_PARENT_KEY, NodeFieldLayout.TEXT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, parentKey, nodeKey)); + } + @Override public boolean hasParent() { - return parentKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getParentKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getRightSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.TEXT_RIGHT_SIB_KEY, nodeKey); + } return rightSiblingKey; } - public void setRightSiblingKey(long key) { + public void setRightSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.TEXT_RIGHT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeRightSiblingKey(key); + return; + } this.rightSiblingKey = key; } + private void resizeRightSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.TEXT_RIGHT_SIB_KEY, NodeFieldLayout.TEXT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasRightSibling() { - return rightSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getRightSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLeftSiblingKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.TEXT_LEFT_SIB_KEY, nodeKey); + } return leftSiblingKey; } - public void setLeftSiblingKey(long key) { + public void setLeftSiblingKey(final long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.TEXT_LEFT_SIB_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLeftSiblingKey(key); + return; + } this.leftSiblingKey = key; } + private void resizeLeftSiblingKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.TEXT_LEFT_SIB_KEY, NodeFieldLayout.TEXT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLeftSibling() { - return leftSiblingKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLeftSiblingKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } - // === LAZY GETTERS === + // === LAZY GETTERS (dual-mode) === @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.TEXT_PREV_REVISION); + } if (!metadataParsed) parseMetadataFields(); return previousRevision; } @Override - public void setPreviousRevision(int revision) { + public void setPreviousRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.TEXT_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } this.previousRevision = revision; } + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.TEXT_PREV_REVISION, NodeFieldLayout.TEXT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.TEXT_LAST_MOD_REVISION); + } if (!metadataParsed) parseMetadataFields(); return lastModifiedRevision; } @Override - public void setLastModifiedRevision(int revision) { + public void setLastModifiedRevision(final int revision) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.TEXT_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } this.lastModifiedRevision = revision; } + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.TEXT_LAST_MOD_REVISION, NodeFieldLayout.TEXT_FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); + } + @Override public long getHash() { - if (!metadataParsed) - parseMetadataFields(); - if (hash == 0L && hashFunction != null) { - hash = computeHash(Bytes.threadLocalHashBuffer()); + if (hash != 0L) { + return hash; } - return hash; + // Hash not stored on page -- compute on demand from node fields + if (!metadataParsed) parseMetadataFields(); + if (hashFunction != null) { + return computeHash(Bytes.threadLocalHashBuffer()); + } + return 0L; } @Override - public void setHash(long hash) { + public void setHash(final long hash) { this.hash = hash; } @Override public byte[] getRawValue() { - if (!valueParsed) { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { parseValuePayload(); } if (value != null && isCompressed) { @@ -263,7 +598,22 @@ public byte[] getRawValue() { } @Override - public void setRawValue(byte[] value) { + public void setRawValue(final byte[] value) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.value = value; + this.fixedValueEncoding = false; + this.fixedValueLength = 0; + this.fixedValueCompressed = false; + this.isCompressed = false; + this.hash = 0L; + owner.resizeRecord(this, nk, slot); + return; + } + if (page != null) unbind(); this.value = value; this.fixedValueEncoding = false; this.fixedValueLength = 0; @@ -278,7 +628,6 @@ public void setLazyRawValue(Object source, long valueOffset, int valueLength, bo this.valueOffset = valueOffset; this.metadataParsed = true; this.valueParsed = false; - this.hasHash = false; this.fixedValueEncoding = true; this.fixedValueLength = valueLength; this.fixedValueCompressed = compressed; @@ -296,6 +645,26 @@ public String getValue() { return new String(getRawValue(), Constants.DEFAULT_ENCODING); } + /** + * Returns the raw value bytes without triggering decompression. Used by the fixed-slot projector to + * preserve the original compressed bytes. + */ + public byte[] getRawValueWithoutDecompression() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } else if (!valueParsed) { + parseValuePayload(); + } + return value; + } + + public boolean isCompressed() { + if (page != null && !valueParsed) { + readPayloadFromPage(); + } + return isCompressed; + } + // === LEAF NODE METHODS (no children) === @Override @@ -384,6 +753,16 @@ public void setTypeKey(int typeKey) { @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -413,21 +792,6 @@ public LongHashFunction getHashFunction() { return hashFunction; } - /** - * Returns the raw value bytes without triggering decompression. Used by the fixed-slot projector to - * preserve the original compressed bytes. - */ - public byte[] getRawValueWithoutDecompression() { - if (!valueParsed) { - parseValuePayload(); - } - return value; - } - - public boolean isCompressed() { - return isCompressed; - } - // === HASH COMPUTATION === @Override @@ -437,15 +801,11 @@ public long computeHash(BytesOut bytes) { } bytes.clear(); - bytes.writeLong(nodeKey).writeLong(parentKey).writeByte(NodeKind.TEXT.getId()); + bytes.writeLong(nodeKey).writeLong(getParentKey()).writeByte(NodeKind.TEXT.getId()); - bytes.writeLong(leftSiblingKey).writeLong(rightSiblingKey); bytes.write(getRawValue()); - final var buffer = ((java.nio.ByteBuffer) bytes.underlyingObject()).rewind(); - buffer.limit((int) bytes.readLimit()); - - return hashFunction.hashBytes(buffer); + return bytes.hashDirect(hashFunction); } // === LAZY PARSING === @@ -455,6 +815,8 @@ public long computeHash(BytesOut bytes) { */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; this.nodeKey = nodeKey; this.hashFunction = hashFunction; this.deweyIDBytes = deweyId; @@ -470,8 +832,6 @@ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFu this.lazyOffset = source.position(); this.metadataParsed = false; this.valueParsed = false; - // Text-node hash is not serialized in compact encoding. - this.hasHash = false; this.valueOffset = 0; this.fixedValueEncoding = false; this.fixedValueLength = 0; @@ -485,26 +845,14 @@ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFu this.isCompressed = false; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.fixedSlotLayout = layout; - this.metadataParsed = false; - } - private void parseMetadataFields() { if (metadataParsed) { return; } - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.previousRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.PREVIOUS_REVISION); - this.lastModifiedRevision = SlotLayoutAccessors.readIntField(sd, off, ly, StructuralField.LAST_MODIFIED_REVISION); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.metadataParsed = true; + if (page != null) { + // When bound to a page, metadata is read directly from MemorySegment via getters + metadataParsed = true; return; } @@ -517,9 +865,6 @@ private void parseMetadataFields() { this.previousRevision = DeltaVarIntCodec.decodeSigned(bytesIn); this.lastModifiedRevision = DeltaVarIntCodec.decodeSigned(bytesIn); - if (hasHash) { - this.hash = bytesIn.readLong(); - } this.valueOffset = bytesIn.position(); this.metadataParsed = true; } @@ -529,6 +874,11 @@ private void parseValuePayload() { return; } + if (page != null) { + readPayloadFromPage(); + return; + } + if (!metadataParsed) { parseMetadataFields(); } @@ -574,20 +924,39 @@ private BytesIn createBytesIn(long offset) { /** * Create a deep copy snapshot of this node. + * Forces parsing of all lazy fields since snapshot must be independent. */ public TextNode toSnapshot() { - if (!metadataParsed) + if (page != null) { + // Bound mode: read all fields from page + if (!valueParsed) { + readPayloadFromPage(); + } + return new TextNode(nodeKey, + readDeltaField(NodeFieldLayout.TEXT_PARENT_KEY, nodeKey), + readSignedField(NodeFieldLayout.TEXT_PREV_REVISION), + readSignedField(NodeFieldLayout.TEXT_LAST_MOD_REVISION), + readDeltaField(NodeFieldLayout.TEXT_RIGHT_SIB_KEY, nodeKey), + readDeltaField(NodeFieldLayout.TEXT_LEFT_SIB_KEY, nodeKey), + hash, + value != null ? value.clone() : null, + isCompressed, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); + } + // Force parse all lazy fields for snapshot (must be complete and independent) + if (!metadataParsed) { parseMetadataFields(); - if (!valueParsed) + } + if (!valueParsed) { parseValuePayload(); - - return new TextNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, rightSiblingKey, leftSiblingKey, - hash, value != null - ? value.clone() - : null, - isCompressed, hashFunction, deweyIDBytes != null - ? deweyIDBytes.clone() - : null); + } + return new TextNode(nodeKey, parentKey, previousRevision, lastModifiedRevision, + rightSiblingKey, leftSiblingKey, hash, + value != null ? value.clone() : null, + isCompressed, + hashFunction, + getDeweyIDAsBytes() != null ? getDeweyIDAsBytes().clone() : null); } @Override @@ -597,13 +966,13 @@ public VisitResult acceptVisitor(XmlNodeVisitor visitor) { @Override public int hashCode() { - return Objects.hashCode(nodeKey, parentKey, getValue()); + return Objects.hashCode(nodeKey, getParentKey(), getValue()); } @Override public boolean equals(@Nullable Object obj) { if (obj instanceof TextNode other) { - return nodeKey == other.nodeKey && parentKey == other.parentKey + return nodeKey == other.nodeKey && getParentKey() == other.getParentKey() && java.util.Arrays.equals(getRawValue(), other.getRawValue()); } return false; @@ -613,9 +982,9 @@ public boolean equals(@Nullable Object obj) { public @NonNull String toString() { return MoreObjects.toStringHelper(this) .add("nodeKey", nodeKey) - .add("parentKey", parentKey) - .add("rightSiblingKey", rightSiblingKey) - .add("leftSiblingKey", leftSiblingKey) + .add("parentKey", getParentKey()) + .add("rightSiblingKey", getRightSiblingKey()) + .add("leftSiblingKey", getLeftSiblingKey()) .add("value", getValue()) .add("compressed", isCompressed) .toString(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/node/xml/XmlDocumentRootNode.java b/bundles/sirix-core/src/main/java/io/sirix/node/xml/XmlDocumentRootNode.java index e1be40235..e196cf611 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/node/xml/XmlDocumentRootNode.java +++ b/bundles/sirix-core/src/main/java/io/sirix/node/xml/XmlDocumentRootNode.java @@ -41,15 +41,15 @@ import io.sirix.node.NodeKind; import io.sirix.node.SirixDeweyID; import io.sirix.node.immutable.xml.ImmutableXmlDocumentRootNode; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.Node; import io.sirix.node.interfaces.StructNode; import io.sirix.node.interfaces.immutable.ImmutableXmlNode; -import io.sirix.node.layout.NodeKindLayout; -import io.sirix.node.layout.SlotLayoutAccessors; -import io.sirix.node.layout.StructuralField; +import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.NodeFieldLayout; import io.sirix.settings.Fixed; - import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import net.openhft.hashing.LongHashFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -58,12 +58,12 @@ * Node representing the root of an XML document. This node is guaranteed to exist in revision 0 and * cannot be removed. * - *

- * Uses primitive fields for efficient storage following the ObjectNode pattern. Document root has - * fixed values for nodeKey (0), parentKey (-1), and no siblings. - *

+ *

Uses primitive fields for efficient storage following the ObjectNode pattern. + * Document root has fixed values for nodeKey (0), parentKey (-1), and no siblings.

+ * + *

Supports flyweight binding to a page MemorySegment for zero-copy field access.

*/ -public final class XmlDocumentRootNode implements StructNode, ImmutableXmlNode { +public final class XmlDocumentRootNode implements StructNode, ImmutableXmlNode, FlyweightNode { // === STRUCTURAL FIELDS (immediate) === @@ -98,11 +98,43 @@ public final class XmlDocumentRootNode implements StructNode, ImmutableXmlNode { /** DeweyID as bytes. */ private byte[] deweyIDBytes; - // Fixed-slot lazy support - private Object lazySource; - private long lazyBaseOffset; - private NodeKindLayout fixedSlotLayout; - private boolean lazyFieldsParsed = true; + // ==================== FLYWEIGHT BINDING (LeanStore page-direct access) ==================== + + /** Page MemorySegment when bound (null = primitive mode). */ + private MemorySegment page; + + /** Absolute byte offset of this record in the page (after HEAP_START + heapOffset). */ + private long recordBase; + + /** Absolute byte offset where the data region starts (recordBase + 1 + FIELD_COUNT). */ + private long dataRegionStart; + + /** Slot index in the page directory (for re-serialization). */ + private int slotIndex; + + /** True if this node is a factory-managed write singleton (must not be stored in records[]). */ + private boolean writeSingleton; + + /** Owning page for resize-in-place operations. */ + private KeyValueLeafPage ownerPage; + + /** Reusable offset array for serializeToHeap (avoids allocation). */ + private final int[] heapOffsets; + + private static final int FIELD_COUNT = NodeFieldLayout.XML_DOCUMENT_ROOT_FIELD_COUNT; + + /** + * Constructor for flyweight binding. + * All fields except nodeKey and hashFunction will be read from page memory after bind(). + * + * @param nodeKey the node key + * @param hashFunction the hash function from resource config + */ + public XmlDocumentRootNode(long nodeKey, LongHashFunction hashFunction) { + this.nodeKey = nodeKey; + this.hashFunction = hashFunction; + this.heapOffsets = new int[FIELD_COUNT]; + } /** * Primary constructor with all primitive fields. Used by deserialization @@ -124,6 +156,7 @@ public XmlDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, this.descendantCount = descendantCount; this.hashFunction = hashFunction; this.deweyIDBytes = SirixDeweyID.newRootID().toBytes(); + this.heapOffsets = new int[FIELD_COUNT]; } /** @@ -146,6 +179,193 @@ public XmlDocumentRootNode(long nodeKey, long firstChildKey, long lastChildKey, this.descendantCount = descendantCount; this.hashFunction = hashFunction; this.sirixDeweyID = deweyID; + this.heapOffsets = new int[FIELD_COUNT]; + } + + // ==================== FLYWEIGHT BIND/UNBIND ==================== + + /** + * Bind this node as a flyweight to a page MemorySegment. + * When bound, getters/setters read/write directly to page memory via the offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of this record in the page + * @param nodeKey the node key (for delta decoding) + * @param slotIndex the slot index in the page directory + */ + @Override + public void bind(final MemorySegment page, final long recordBase, final long nodeKey, + final int slotIndex) { + this.page = page; + this.recordBase = recordBase; + this.nodeKey = nodeKey; + this.slotIndex = slotIndex; + this.dataRegionStart = recordBase + 1 + FIELD_COUNT; + } + + /** + * Unbind from page memory and materialize all fields into Java primitives. + * After unbind, the node operates in primitive mode. + */ + @Override + public void unbind() { + if (page == null) { + return; + } + // Materialize all fields from page to Java primitives + final long nk = this.nodeKey; + this.firstChildKey = readDeltaField(NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY, nk); + this.lastChildKey = readDeltaField(NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY, nk); + this.childCount = readSignedLongField(NodeFieldLayout.XDOCROOT_CHILD_COUNT); + this.descendantCount = readSignedLongField(NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT); + this.hash = readLongField(NodeFieldLayout.XDOCROOT_HASH); + this.ownerPage = null; + this.page = null; + } + + @Override + public void clearBinding() { + this.page = null; + this.ownerPage = null; + } + + /** Check if this node is bound to a page MemorySegment. */ + @Override + public boolean isBound() { + return page != null; + } + + @Override + public boolean isBoundTo(final MemorySegment page) { + return this.page == page; + } + + @Override + public int getSlotIndex() { + return slotIndex; + } + + @Override public boolean isWriteSingleton() { return writeSingleton; } + @Override public void setWriteSingleton(final boolean ws) { this.writeSingleton = ws; } + @Override public KeyValueLeafPage getOwnerPage() { return ownerPage; } + @Override public void setOwnerPage(final KeyValueLeafPage p) { this.ownerPage = p; } + + // ==================== FLYWEIGHT FIELD READ HELPERS ==================== + + private long readDeltaField(final int fieldIndex, final long baseKey) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeDeltaFromSegment(page, dataRegionStart + fieldOff, baseKey); + } + + private int readSignedField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedFromSegment(page, dataRegionStart + fieldOff); + } + + private long readSignedLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.decodeSignedLongFromSegment(page, dataRegionStart + fieldOff); + } + + private long readLongField(final int fieldIndex) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + return DeltaVarIntCodec.readLongFromSegment(page, (int) (dataRegionStart + fieldOff)); + } + + // ==================== DIRECT WRITE ==================== + + /** + * Encode an XmlDocumentRootNode record directly to a MemorySegment from parameter values. + * Static -- reads nothing from any instance. Zero field intermediation. + * + * @param target the target MemorySegment (reinterpreted slotted page) + * @param offset absolute byte offset to write at + * @param heapOffsets pre-allocated offset array (reused, FIELD_COUNT elements) + * @param nodeKey the node key (delta base for structural keys) + * @param firstChildKey the first child key + * @param lastChildKey the last child key + * @param childCount the child count + * @param descendantCount the descendant count + * @param hash the hash value + * @return the total number of bytes written + */ + public static int writeNewRecord(final MemorySegment target, final long offset, + final int[] heapOffsets, final long nodeKey, + final long firstChildKey, final long lastChildKey, + final long childCount, final long descendantCount, final long hash) { + long pos = offset; + + // Write nodeKind byte + target.set(ValueLayout.JAVA_BYTE, pos, NodeKind.XML_DOCUMENT.getId()); + pos++; + + // Reserve space for offset table + final long offsetTableStart = pos; + pos += FIELD_COUNT; + + // Data region start + final long dataStart = pos; + + // Field 0: firstChildKey (delta-varint from nodeKey) + heapOffsets[NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, firstChildKey, nodeKey); + + // Field 1: lastChildKey (delta-varint from nodeKey) + heapOffsets[NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeDeltaToSegment(target, pos, lastChildKey, nodeKey); + + // Field 2: childCount (signed long varint) + heapOffsets[NodeFieldLayout.XDOCROOT_CHILD_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, childCount); + + // Field 3: descendantCount (signed long varint) + heapOffsets[NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedLongToSegment(target, pos, descendantCount); + + // Field 4: previousRevision (signed varint) -- always 0 for document root + heapOffsets[NodeFieldLayout.XDOCROOT_PREV_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); + + // Field 5: lastModifiedRevision (signed varint) -- always 0 for document root + heapOffsets[NodeFieldLayout.XDOCROOT_LAST_MOD_REVISION] = (int) (pos - dataStart); + pos += DeltaVarIntCodec.writeSignedToSegment(target, pos, 0); + + // Field 6: hash (fixed 8 bytes) + heapOffsets[NodeFieldLayout.XDOCROOT_HASH] = (int) (pos - dataStart); + DeltaVarIntCodec.writeLongToSegment(target, pos, hash); + pos += Long.BYTES; + + // Write offset table + for (int i = 0; i < FIELD_COUNT; i++) { + target.set(ValueLayout.JAVA_BYTE, offsetTableStart + i, (byte) heapOffsets[i]); + } + + return (int) (pos - offset); + } + + /** + * Serialize this node from Java fields. Delegates to static writeNewRecord. + */ + @Override + public int serializeToHeap(final MemorySegment target, final long offset) { + return writeNewRecord(target, offset, heapOffsets, nodeKey, + firstChildKey, lastChildKey, childCount, descendantCount, hash); + } + + /** + * Get the pre-allocated heap offsets array for use with static writeNewRecord. + */ + public int[] getHeapOffsets() { + return heapOffsets; + } + + /** + * Set DeweyID fields directly after creation, bypassing write-through. + * The DeweyID is already in the page trailer -- this just sets the Java cache fields. + */ + public void setDeweyIDAfterCreation(final SirixDeweyID id, final byte[] bytes) { + this.sirixDeweyID = id; + this.deweyIDBytes = bytes; } @Override @@ -210,83 +430,155 @@ public boolean hasLeftSibling() { @Override public long getFirstChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY, nodeKey); + } return firstChildKey; } @Override public void setFirstChildKey(long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeFirstChildKey(key); + return; + } this.firstChildKey = key; } + private void resizeFirstChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasFirstChild() { - return firstChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getFirstChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getLastChildKey() { + if (page != null) { + return readDeltaField(NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY, nodeKey); + } return lastChildKey; } @Override public void setLastChildKey(long key) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readDeltaEncodedWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeDeltaEncodedWidth(key, nodeKey); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeDeltaToSegment(page, absOff, key, nodeKey); + return; + } + resizeLastChildKey(key); + return; + } this.lastChildKey = key; } + private void resizeLastChildKey(final long key) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeDeltaToSegment(target, off, key, nodeKey)); + } + @Override public boolean hasLastChild() { - return lastChildKey != Fixed.NULL_NODE_KEY.getStandardProperty(); + return getLastChildKey() != Fixed.NULL_NODE_KEY.getStandardProperty(); } @Override public long getChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.XDOCROOT_CHILD_COUNT); + } return childCount; } public void setChildCount(long childCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_CHILD_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(childCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, childCount); + return; + } + resizeChildCount(childCount); + return; + } this.childCount = childCount; } + private void resizeChildCount(final long childCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_CHILD_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, childCount)); + } + @Override public void incrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount++; + setChildCount(getChildCount() + 1); } @Override public void decrementChildCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - childCount--; + setChildCount(getChildCount() - 1); } @Override public long getDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + return readSignedLongField(NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT); + } return descendantCount; } @Override public void setDescendantCount(long descendantCount) { + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedLongEncodedWidth(descendantCount); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedLongToSegment(page, absOff, descendantCount); + return; + } + resizeDescendantCount(descendantCount); + return; + } this.descendantCount = descendantCount; } + private void resizeDescendantCount(final long descendantCount) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedLongToSegment(target, off, descendantCount)); + } + @Override public void incrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount++; + setDescendantCount(getDescendantCount() + 1); } @Override public void decrementDescendantCount() { - if (!lazyFieldsParsed) - parseLazyFields(); - descendantCount--; + setDescendantCount(getDescendantCount() - 1); } @Override @@ -296,33 +588,47 @@ public long computeHash(BytesOut bytes) { } bytes.clear(); - bytes.writeLong(nodeKey).writeLong(getParentKey()).writeByte(getKind().getId()); + bytes.writeLong(nodeKey) + .writeLong(getParentKey()) + .writeByte(getKind().getId()); - bytes.writeLong(childCount) - .writeLong(descendantCount) + bytes.writeLong(getChildCount()) + .writeLong(getDescendantCount()) .writeLong(getLeftSiblingKey()) .writeLong(getRightSiblingKey()) - .writeLong(firstChildKey); + .writeLong(getFirstChildKey()); - if (lastChildKey != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { - bytes.writeLong(lastChildKey); + if (getLastChildKey() != Fixed.INVALID_KEY_FOR_TYPE_CHECK.getStandardProperty()) { + bytes.writeLong(getLastChildKey()); } - final var buffer = ((java.nio.ByteBuffer) bytes.underlyingObject()).rewind(); - buffer.limit((int) bytes.readLimit()); - - return hashFunction.hashBytes(buffer); + return bytes.hashDirect(hashFunction); } @Override public void setHash(long hash) { + if (page != null) { + // Hash is ALWAYS in-place (fixed 8 bytes) + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, + recordBase + 1 + NodeFieldLayout.XDOCROOT_HASH) & 0xFF; + DeltaVarIntCodec.writeLongToSegment(page, dataRegionStart + fieldOff, hash); + return; + } this.hash = hash; } @Override public long getHash() { - if (!lazyFieldsParsed) - parseLazyFields(); + if (page != null) { + final long h = readLongField(NodeFieldLayout.XDOCROOT_HASH); + if (h != 0L) return h; + if (hashFunction != null) { + final long computed = computeHash(Bytes.threadLocalHashBuffer()); + setHash(computed); + return computed; + } + return 0L; + } if (hash == 0L && hashFunction != null) { hash = computeHash(Bytes.threadLocalHashBuffer()); } @@ -331,22 +637,64 @@ public long getHash() { @Override public int getPreviousRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.XDOCROOT_PREV_REVISION); + } return 0; // Document root is always in revision 0 } @Override public void setPreviousRevision(int revision) { - // Document root doesn't track previous revision + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_PREV_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizePreviousRevision(revision); + return; + } + // Document root doesn't track previous revision in primitive mode + } + + private void resizePreviousRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_PREV_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); } @Override public int getLastModifiedRevisionNumber() { + if (page != null) { + return readSignedField(NodeFieldLayout.XDOCROOT_LAST_MOD_REVISION); + } return 0; } @Override public void setLastModifiedRevision(int revision) { - // Document root doesn't track last modified revision + if (page != null) { + final int fieldOff = page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + NodeFieldLayout.XDOCROOT_LAST_MOD_REVISION) & 0xFF; + final long absOff = dataRegionStart + fieldOff; + final int currentWidth = DeltaVarIntCodec.readSignedVarintWidth(page, absOff); + final int newWidth = DeltaVarIntCodec.computeSignedEncodedWidth(revision); + if (newWidth == currentWidth) { + DeltaVarIntCodec.writeSignedToSegment(page, absOff, revision); + return; + } + resizeLastModifiedRevision(revision); + return; + } + // Document root doesn't track last modified revision in primitive mode + } + + private void resizeLastModifiedRevision(final int revision) { + ownerPage.resizeRecordField(this, nodeKey, slotIndex, + NodeFieldLayout.XDOCROOT_LAST_MOD_REVISION, FIELD_COUNT, + (target, off) -> DeltaVarIntCodec.writeSignedToSegment(target, off, revision)); } @Override @@ -388,6 +736,16 @@ public byte[] getDeweyIDAsBytes() { @Override public void setDeweyID(SirixDeweyID id) { + final var owner = this.ownerPage; + if (owner != null) { + final long nk = this.nodeKey; + final int slot = this.slotIndex; + unbind(); + this.sirixDeweyID = id; + this.deweyIDBytes = null; + owner.resizeRecord(this, nk, slot); + return; + } this.sirixDeweyID = id; this.deweyIDBytes = null; } @@ -411,6 +769,9 @@ public LongHashFunction getHashFunction() { */ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFunction hashFunction, ResourceConfiguration config) { + // Unbind flyweight -- ensures getters use Java fields, not stale page reference + this.page = null; + final long firstChildKey = DeltaVarIntCodec.decodeDelta(source, nodeKey); final long childCount = firstChildKey == Fixed.NULL_NODE_KEY.getStandardProperty() ? 0L @@ -437,43 +798,33 @@ public void readFrom(BytesIn source, long nodeKey, byte[] deweyId, LongHashFu this.sirixDeweyID = null; } - public void bindFixedSlotLazy(final MemorySegment slotData, final long baseOffset, final NodeKindLayout layout) { - this.lazyBaseOffset = baseOffset; - this.lazySource = slotData; - this.fixedSlotLayout = layout; - this.lazyFieldsParsed = false; - } - - private void parseLazyFields() { - if (lazyFieldsParsed) { - return; - } - - if (fixedSlotLayout != null) { - final MemorySegment sd = (MemorySegment) lazySource; - final NodeKindLayout ly = fixedSlotLayout; - final long off = this.lazyBaseOffset; - this.childCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.CHILD_COUNT); - this.descendantCount = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.DESCENDANT_COUNT); - this.hash = SlotLayoutAccessors.readLongField(sd, off, ly, StructuralField.HASH); - this.fixedSlotLayout = null; - this.lazyFieldsParsed = true; - return; - } - - this.lazyFieldsParsed = true; - } - /** * Create a deep copy snapshot of this node. * * @return a new instance with copied values */ public XmlDocumentRootNode toSnapshot() { - if (!lazyFieldsParsed) - parseLazyFields(); - final XmlDocumentRootNode snapshot = - new XmlDocumentRootNode(nodeKey, firstChildKey, lastChildKey, childCount, descendantCount, hashFunction); + if (page != null) { + // Bound mode: read all fields from page + final long nk = this.nodeKey; + final XmlDocumentRootNode snapshot = new XmlDocumentRootNode( + nk, + readDeltaField(NodeFieldLayout.XDOCROOT_FIRST_CHILD_KEY, nk), + readDeltaField(NodeFieldLayout.XDOCROOT_LAST_CHILD_KEY, nk), + readSignedLongField(NodeFieldLayout.XDOCROOT_CHILD_COUNT), + readSignedLongField(NodeFieldLayout.XDOCROOT_DESCENDANT_COUNT), + hashFunction); + snapshot.hash = readLongField(NodeFieldLayout.XDOCROOT_HASH); + if (deweyIDBytes != null) { + snapshot.deweyIDBytes = deweyIDBytes.clone(); + } + if (sirixDeweyID != null) { + snapshot.sirixDeweyID = sirixDeweyID; + } + return snapshot; + } + final XmlDocumentRootNode snapshot = new XmlDocumentRootNode( + nodeKey, firstChildKey, lastChildKey, childCount, descendantCount, hashFunction); snapshot.hash = this.hash; if (deweyIDBytes != null) { snapshot.deweyIDBytes = deweyIDBytes.clone(); diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/FlyweightNodeFactory.java b/bundles/sirix-core/src/main/java/io/sirix/page/FlyweightNodeFactory.java new file mode 100644 index 000000000..5560ae37b --- /dev/null +++ b/bundles/sirix-core/src/main/java/io/sirix/page/FlyweightNodeFactory.java @@ -0,0 +1,92 @@ +package io.sirix.page; + +import io.sirix.node.interfaces.FlyweightNode; +import io.sirix.node.json.ArrayNode; +import io.sirix.node.json.BooleanNode; +import io.sirix.node.json.JsonDocumentRootNode; +import io.sirix.node.json.NullNode; +import io.sirix.node.json.NumberNode; +import io.sirix.node.json.ObjectBooleanNode; +import io.sirix.node.json.ObjectKeyNode; +import io.sirix.node.json.ObjectNode; +import io.sirix.node.json.ObjectNullNode; +import io.sirix.node.json.ObjectNumberNode; +import io.sirix.node.json.ObjectStringNode; +import io.sirix.node.json.StringNode; +import io.sirix.node.xml.AttributeNode; +import io.sirix.node.xml.CommentNode; +import io.sirix.node.xml.ElementNode; +import io.sirix.node.xml.NamespaceNode; +import io.sirix.node.xml.PINode; +import io.sirix.node.xml.TextNode; +import io.sirix.node.xml.XmlDocumentRootNode; +import net.openhft.hashing.LongHashFunction; + +import java.lang.foreign.MemorySegment; + +/** + * Factory for creating flyweight node shells and binding them to a slotted page MemorySegment. + * + *

This factory creates minimal node instances (binding shells) that are immediately + * bound to a record in the page heap. After binding, all getters/setters operate + * directly on page memory via the per-record offset table.

+ */ +public final class FlyweightNodeFactory { + + private FlyweightNodeFactory() { + throw new AssertionError("Utility class"); + } + + /** + * Create a flyweight node shell and bind it to a record in the slotted page. + * + * @param page the slotted page MemorySegment + * @param slotIndex the slot index (0 to 1023) + * @param nodeKey the node key for this record + * @param hashFunction the hash function from resource config + * @return the bound flyweight node + * @throws IllegalArgumentException if the nodeKindId is not a known flyweight type + */ + public static FlyweightNode createAndBind(final MemorySegment page, final int slotIndex, + final long nodeKey, final LongHashFunction hashFunction) { + final int heapOffset = PageLayout.getDirHeapOffset(page, slotIndex); + final int nodeKindId = PageLayout.getDirNodeKindId(page, slotIndex); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + + final FlyweightNode node = createShell(nodeKindId, nodeKey, hashFunction); + node.bind(page, recordBase, nodeKey, slotIndex); + return node; + } + + /** + * Create a minimal flyweight node shell for binding. + * The shell has only nodeKey and hashFunction initialized. + * All other fields will be read from page memory after bind(). + */ + private static FlyweightNode createShell(final int nodeKindId, final long nodeKey, + final LongHashFunction hashFunction) { + return switch (nodeKindId) { + case 1 -> new ElementNode(nodeKey, hashFunction); // ELEMENT + case 2 -> new AttributeNode(nodeKey, hashFunction); // ATTRIBUTE + case 3 -> new TextNode(nodeKey, hashFunction); // TEXT + case 7 -> new PINode(nodeKey, hashFunction); // PROCESSING_INSTRUCTION + case 8 -> new CommentNode(nodeKey, hashFunction); // COMMENT + case 9 -> new XmlDocumentRootNode(nodeKey, hashFunction); // XML_DOCUMENT + case 13 -> new NamespaceNode(nodeKey, hashFunction); // NAMESPACE + case 24 -> new ObjectNode(nodeKey, hashFunction); // OBJECT + case 25 -> new ArrayNode(nodeKey, hashFunction); // ARRAY + case 26 -> new ObjectKeyNode(nodeKey, hashFunction); // OBJECT_KEY + case 27 -> new BooleanNode(nodeKey, hashFunction); // BOOLEAN_VALUE + case 28 -> new NumberNode(nodeKey, hashFunction); // NUMBER_VALUE + case 29 -> new NullNode(nodeKey, hashFunction); // NULL_VALUE + case 30 -> new StringNode(nodeKey, hashFunction); // STRING_VALUE + case 31 -> new JsonDocumentRootNode(nodeKey, hashFunction); // JSON_DOCUMENT + case 40 -> new ObjectStringNode(nodeKey, hashFunction); // OBJECT_STRING_VALUE + case 41 -> new ObjectBooleanNode(nodeKey, hashFunction); // OBJECT_BOOLEAN_VALUE + case 42 -> new ObjectNumberNode(nodeKey, hashFunction); // OBJECT_NUMBER_VALUE + case 43 -> new ObjectNullNode(nodeKey, hashFunction); // OBJECT_NULL_VALUE + default -> throw new IllegalArgumentException( + "Unknown flyweight node kind ID: " + nodeKindId); + }; + } +} diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/KeyValueLeafPage.java b/bundles/sirix-core/src/main/java/io/sirix/page/KeyValueLeafPage.java index 0ebe7310d..1cf37cc72 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/page/KeyValueLeafPage.java +++ b/bundles/sirix-core/src/main/java/io/sirix/page/KeyValueLeafPage.java @@ -9,17 +9,14 @@ import io.sirix.cache.MemorySegmentAllocator; import io.sirix.cache.WindowsMemorySegmentAllocator; import io.sirix.index.IndexType; +import io.sirix.node.DeltaVarIntCodec; import io.sirix.node.NodeKind; import io.sirix.node.interfaces.DataRecord; import io.sirix.node.interfaces.DeweyIdSerializer; +import io.sirix.node.interfaces.FlyweightNode; import io.sirix.node.interfaces.RecordSerializer; import io.sirix.node.json.ObjectStringNode; import io.sirix.node.json.StringNode; -import io.sirix.node.layout.FixedToCompactTransformer; -import io.sirix.node.xml.AttributeNode; -import io.sirix.node.xml.CommentNode; -import io.sirix.node.xml.PINode; -import io.sirix.node.xml.TextNode; import io.sirix.page.interfaces.KeyValuePage; import io.sirix.settings.Constants; import io.sirix.settings.DiagnosticSettings; @@ -28,7 +25,6 @@ import io.sirix.utils.ArrayIterator; import io.sirix.utils.OS; import io.sirix.node.BytesOut; -import io.sirix.node.MemorySegmentBytesIn; import io.sirix.node.MemorySegmentBytesOut; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; @@ -45,7 +41,6 @@ import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; -import java.nio.ByteOrder; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -64,66 +59,62 @@ * The page currently is not thread safe (might have to be for concurrent write-transactions)! *

*/ -@SuppressWarnings({"unchecked"}) +@SuppressWarnings({ "unchecked" }) public final class KeyValueLeafPage implements KeyValuePage { private static final Logger LOGGER = LoggerFactory.getLogger(KeyValueLeafPage.class); private static final int INT_SIZE = Integer.BYTES; - + /** - * SIMD vector species for bitmap operations. Uses the preferred species for the current platform - * (256-bit AVX2 or 512-bit AVX-512). + * SIMD vector species for bitmap operations. + * Uses the preferred species for the current platform (256-bit AVX2 or 512-bit AVX-512). */ private static final VectorSpecies LONG_SPECIES = LongVector.SPECIES_PREFERRED; - - /** - * Unaligned int layout for zero-copy deserialization. When slotMemory is a slice of the - * decompression buffer, it may not be 4-byte aligned. - */ - private static final ValueLayout.OfInt JAVA_INT_UNALIGNED = ValueLayout.JAVA_INT.withByteAlignment(1); - + + /** - * Enable detailed memory leak tracking. Accessed via centralized - * {@link DiagnosticSettings#MEMORY_LEAK_TRACKING}. + * Enable detailed memory leak tracking. + * Accessed via centralized {@link DiagnosticSettings#MEMORY_LEAK_TRACKING}. * * @see DiagnosticSettings#isMemoryLeakTrackingEnabled() */ public static final boolean DEBUG_MEMORY_LEAKS = DiagnosticSettings.MEMORY_LEAK_TRACKING; - + // DIAGNOSTIC COUNTERS (enabled via DEBUG_MEMORY_LEAKS) - public static final java.util.concurrent.atomic.AtomicLong PAGES_CREATED = - new java.util.concurrent.atomic.AtomicLong(0); - public static final java.util.concurrent.atomic.AtomicLong PAGES_CLOSED = - new java.util.concurrent.atomic.AtomicLong(0); - public static final java.util.concurrent.ConcurrentHashMap PAGES_BY_TYPE = - new java.util.concurrent.ConcurrentHashMap<>(); - public static final java.util.concurrent.ConcurrentHashMap PAGES_CLOSED_BY_TYPE = - new java.util.concurrent.ConcurrentHashMap<>(); - + public static final java.util.concurrent.atomic.AtomicLong PAGES_CREATED = new java.util.concurrent.atomic.AtomicLong(0); + public static final java.util.concurrent.atomic.AtomicLong PAGES_CLOSED = new java.util.concurrent.atomic.AtomicLong(0); + public static final java.util.concurrent.ConcurrentHashMap PAGES_BY_TYPE = + new java.util.concurrent.ConcurrentHashMap<>(); + public static final java.util.concurrent.ConcurrentHashMap PAGES_CLOSED_BY_TYPE = + new java.util.concurrent.ConcurrentHashMap<>(); + // TRACK ALL LIVE PAGES - for leak detection (use object identity, not recordPageKey) // CRITICAL: Use IdentityHashMap to track by object identity, not equals/hashCode - public static final java.util.Set ALL_LIVE_PAGES = - java.util.Collections.synchronizedSet(java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>())); - + public static final java.util.Set ALL_LIVE_PAGES = + java.util.Collections.synchronizedSet( + java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()) + ); + // LEAK DETECTION: Track finalized pages - public static final java.util.concurrent.atomic.AtomicLong PAGES_FINALIZED_WITHOUT_CLOSE = - new java.util.concurrent.atomic.AtomicLong(0); - + public static final java.util.concurrent.atomic.AtomicLong PAGES_FINALIZED_WITHOUT_CLOSE = new java.util.concurrent.atomic.AtomicLong(0); + // Track finalized pages by type and pageKey for diagnostics - public static final java.util.concurrent.ConcurrentHashMap FINALIZED_BY_TYPE = - new java.util.concurrent.ConcurrentHashMap<>(); - public static final java.util.concurrent.ConcurrentHashMap FINALIZED_BY_PAGE_KEY = - new java.util.concurrent.ConcurrentHashMap<>(); - + public static final java.util.concurrent.ConcurrentHashMap FINALIZED_BY_TYPE = + new java.util.concurrent.ConcurrentHashMap<>(); + public static final java.util.concurrent.ConcurrentHashMap FINALIZED_BY_PAGE_KEY = + new java.util.concurrent.ConcurrentHashMap<>(); + // Track all Page 0 instances for explicit cleanup // CRITICAL: Use synchronized IdentityHashSet to track by object identity, not equals/hashCode // (Multiple Page 0 instances with same recordPageKey/revision would collide in regular Set) - public static final java.util.Set ALL_PAGE_0_INSTANCES = - java.util.Collections.synchronizedSet(java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>())); - + public static final java.util.Set ALL_PAGE_0_INSTANCES = + java.util.Collections.synchronizedSet( + java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()) + ); + /** - * Version counter for detecting page reuse (LeanStore/Umbra approach). Incremented when page is - * evicted and reused for a different logical page. + * Version counter for detecting page reuse (LeanStore/Umbra approach). + * Incremented when page is evicted and reused for a different logical page. */ private final AtomicInteger version = new AtomicInteger(0); @@ -140,8 +131,8 @@ public final class KeyValueLeafPage implements KeyValuePage { private static final int CLOSED_BIT = 4; /** - * Packed state flags: HOT (bit 0), orphaned (bit 1), closed (bit 2). Accessed via VarHandle for - * lock-free CAS operations. + * Packed state flags: HOT (bit 0), orphaned (bit 1), closed (bit 2). + * Accessed via VarHandle for lock-free CAS operations. */ @SuppressWarnings("unused") // Accessed via VarHandle private volatile int stateFlags = 0; @@ -151,28 +142,28 @@ public final class KeyValueLeafPage implements KeyValuePage { static { try { - STATE_FLAGS_HANDLE = MethodHandles.lookup().findVarHandle(KeyValueLeafPage.class, "stateFlags", int.class); + STATE_FLAGS_HANDLE = MethodHandles.lookup() + .findVarHandle(KeyValueLeafPage.class, "stateFlags", int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new ExceptionInInitializerError(e); } } /** - * Guard count for preventing eviction during active use (LeanStore/Umbra pattern). Pages with - * guardCount > 0 cannot be evicted. This is simpler than per-transaction pinning - it's just a - * reference count. + * Guard count for preventing eviction during active use (LeanStore/Umbra pattern). + * Pages with guardCount > 0 cannot be evicted. + * This is simpler than per-transaction pinning - it's just a reference count. */ private final AtomicInteger guardCount = new AtomicInteger(0); /** - * DIAGNOSTIC: Stack trace of where this page was created (only captured when - * DEBUG_MEMORY_LEAKS=true). Used to trace where leaked pages come from. + * DIAGNOSTIC: Stack trace of where this page was created (only captured when DEBUG_MEMORY_LEAKS=true). + * Used to trace where leaked pages come from. */ private final StackTraceElement[] creationStackTrace; /** * Get the creation stack trace for leak diagnostics. - * * @return stack trace from constructor, or null if DEBUG_MEMORY_LEAKS disabled */ public StackTraceElement[] getCreationStackTrace() { @@ -189,17 +180,7 @@ public StackTraceElement[] getCreationStackTrace() { */ private boolean areDeweyIDsStored; - private final boolean doResizeMemorySegmentsIfNeeded; - /** - * Start of free space. - */ - private int slotMemoryFreeSpaceStart; - - /** - * Start of free space. - */ - private int deweyIdMemoryFreeSpaceStart; /** * Start of free space for string values. @@ -229,7 +210,7 @@ public StackTraceElement[] getCreationStackTrace() { /** * References to overflow pages. */ - private volatile Map references; + private final Map references; /** * Key of record page. This is the base key of all contained nodes. @@ -241,54 +222,25 @@ public StackTraceElement[] getCreationStackTrace() { */ private final DataRecord[] records; - /** - * Number of records currently materialized in {@link #records}. - */ - private int inMemoryRecordCount; - - /** - * Adaptive in-memory demotion policy constants. - */ - private static final int MIN_DEMOTION_THRESHOLD = 64; - private static final int MAX_DEMOTION_THRESHOLD = 256; - private static final int DEMOTION_STEP = 32; - - /** - * Current adaptive threshold for demoting materialized records to slot memory. - */ - private int demotionThreshold = MIN_DEMOTION_THRESHOLD; - - /** - * Number of records re-materialized from slot memory since last demotion. - */ - private int rematerializedRecordsSinceLastDemotion; - - /** - * Memory segment for slots and Dewey IDs. - */ - private MemorySegment slotMemory; - private MemorySegment deweyIdMemory; /** - * Memory segment for string values (columnar storage for better compression). Stores all string - * content contiguously, separate from node metadata in slotMemory. + * Memory segment for string values (columnar storage for better compression). + * Stores all string content contiguously, separate from node metadata. */ private MemorySegment stringValueMemory; /** - * FSST symbol table for string compression (shared across all strings in page). Null if FSST - * compression is not used. + * FSST symbol table for string compression (shared across all strings in page). + * Null if FSST compression is not used. */ private byte[] fsstSymbolTable; - /** Pre-parsed FSST symbol table (avoids re-parsing on every encode/decode). */ - private byte[][] parsedFsstSymbols; + /** Reusable flyweight for FSST compression of StringNode slots on slotted page. */ + private final StringNode fsstStringFlyweight = new StringNode(0, null); + + /** Reusable flyweight for FSST compression of ObjectStringNode slots on slotted page. */ + private final ObjectStringNode fsstObjStringFlyweight = new ObjectStringNode(0, null); - /** - * Offset arrays to manage positions within memory segments. - */ - private final int[] slotOffsets; - private final int[] deweyIdOffsets; /** * Offset array for string values (maps slot -> offset in stringValueMemory). @@ -296,45 +248,29 @@ public StackTraceElement[] getCreationStackTrace() { private final int[] stringValueOffsets; /** - * Bitmap tracking which slots are populated (16 longs = 1024 bits). Bit i is set iff slotOffsets[i] - * >= 0. Used for O(k) iteration over populated slots instead of O(1024). + * Bitmap tracking which slots are populated (16 longs = 1024 bits). + * Used for O(k) iteration over populated slots instead of O(1024). + * Materialized from the slotted page bitmap when needed. */ private final long[] slotBitmap; - /** - * Bitmap tracking which populated slots are currently stored in fixed in-memory layout. Bit i set - * => slot i contains fixed-layout bytes (not compact varint/delta bytes). - */ - private final long[] fixedFormatBitmap; - - /** - * Node kind id for fixed-format slots, indexed by slot number. Entries are - * {@link #NO_FIXED_SLOT_KIND} for compact slots or unknown fixed slots. - */ - private final byte[] fixedSlotKinds; - /** * Number of words in the slot bitmap (16 words * 64 bits = 1024 slots). */ private static final int BITMAP_WORDS = 16; /** - * Sentinel for "no fixed slot kind assigned". - */ - private static final byte NO_FIXED_SLOT_KIND = (byte) -1; - - /** - * Bitmap tracking which slots need preservation during lazy copy (16 longs = 1024 bits). Used by - * DIFFERENTIAL, INCREMENTAL (full-dump), and SLIDING_SNAPSHOT versioning. Null means no - * preservation needed (e.g., INCREMENTAL non-full-dump or FULL versioning). At commit time, slots - * marked here but not in records[] are copied from completePageRef. + * Bitmap tracking which slots need preservation during lazy copy (16 longs = 1024 bits). + * Used by DIFFERENTIAL, INCREMENTAL (full-dump), and SLIDING_SNAPSHOT versioning. + * Null means no preservation needed (e.g., INCREMENTAL non-full-dump or FULL versioning). + * At commit time, slots marked here but not in records[] are copied from completePageRef. */ private long[] preservationBitmap; /** - * Reference to the complete page for lazy slot copying at commit time. Set during - * combineRecordPagesForModification, used by addReferences() to copy slots that need preservation - * but weren't modified (records[i] == null). + * Reference to the complete page for lazy slot copying at commit time. + * Set during combineRecordPagesForModification, used by addReferences() to copy + * slots that need preservation but weren't modified (records[i] == null). */ private KeyValueLeafPage completePageRef; @@ -355,6 +291,10 @@ public StackTraceElement[] getCreationStackTrace() { private volatile BytesOut bytes; + /** Compressed page data as MemorySegment (zero-copy path). Arena.ofAuto()-managed. */ + private volatile MemorySegment compressedSegment; + + private volatile byte[] hashCode; private int hash; @@ -362,40 +302,53 @@ public StackTraceElement[] getCreationStackTrace() { // Note: isClosed flag is now packed into stateFlags (bit 2) for lock-free access /** - * Flag indicating whether memory was externally allocated (e.g., by Arena in tests). If true, - * close() should NOT release memory to segmentAllocator since it wasn't allocated by it. + * Flag indicating whether memory was externally allocated (e.g., by Arena in tests). + * If true, close() should NOT release memory to segmentAllocator since it wasn't allocated by it. */ private final boolean externallyAllocatedMemory; + private MemorySegmentAllocator segmentAllocator = + OS.isWindows() ? WindowsMemorySegmentAllocator.getInstance() : LinuxMemorySegmentAllocator.getInstance(); + + /** + * Backing buffer from decompression (for zero-copy deserialization). + * When non-null, this buffer must be released on close(). + */ + private MemorySegment backingBuffer; + /** - * Thread-local serialization buffer for compactFixedSlotsForCommit (avoids per-call allocation). + * Releaser to return backing buffer to allocator. + * Called on close() to return the decompression buffer to the allocator pool. */ - private static final ThreadLocal COMPACT_BUFFER = - ThreadLocal.withInitial(() -> new MemorySegmentBytesOut(256)); + private Runnable backingBufferReleaser; - private MemorySegmentAllocator segmentAllocator = OS.isWindows() - ? WindowsMemorySegmentAllocator.getInstance() - : LinuxMemorySegmentAllocator.getInstance(); + // ==================== UNIFIED PAGE (LeanStore-style) ==================== /** - * Backing buffer from decompression (for zero-copy deserialization). When non-null, this buffer - * contains the slotMemory as a slice and must be released on close(). This enables true zero-copy - * where the decompressed data becomes the page's storage directly. + * Slotted page MemorySegment (PostgreSQL/LeanStore-style: Header + Bitmap + Directory + Heap). + * Stores records in a heap with per-record offset tables, + * enabling O(1) field access via flyweight binding. The page layout is defined by + * {@link PageLayout}: header (32 B) + bitmap (128 B) + directory (8 KB) + heap. + * + *

FlyweightNode records are serialized directly to the heap at createRecord time + * and bound for in-place mutation. Non-FlyweightNode records are serialized to the + * heap at commit time via processEntries. */ - private MemorySegment backingBuffer; + private MemorySegment slottedPage; /** - * Releaser to return backing buffer to allocator. Called on close() to return the decompression - * buffer to the allocator pool. + * Actual capacity in bytes of the slottedPage segment. + * Tracked separately because slottedPage is reinterpreted to Long.MAX_VALUE + * to eliminate JIT bounds checks on MemorySegment get/set operations. */ - private Runnable backingBufferReleaser; + private int slottedPageCapacity; /** - * Constructor which initializes a new {@link KeyValueLeafPage}. Memory is externally provided - * (e.g., by Arena in tests) and will NOT be released by close(). + * Constructor which initializes a new {@link KeyValueLeafPage}. + * Memory is externally provided (e.g., by Arena in tests) and will NOT be released by close(). * - * @param recordPageKey base key assigned to this node page - * @param indexType the index type + * @param recordPageKey base key assigned to this node page + * @param indexType the index type * @param resourceConfig the resource configuration */ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType indexType, @@ -407,11 +360,10 @@ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType i /** * Constructor which initializes a new {@link KeyValueLeafPage}. * - * @param recordPageKey base key assigned to this node page - * @param indexType the index type - * @param resourceConfig the resource configuration - * @param externallyAllocatedMemory if true, memory was allocated externally and won't be released - * by close() + * @param recordPageKey base key assigned to this node page + * @param indexType the index type + * @param resourceConfig the resource configuration + * @param externallyAllocatedMemory if true, memory was allocated externally and won't be released by close() */ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType indexType, final ResourceConfiguration resourceConfig, final int revisionNumber, final MemorySegment slotMemory, @@ -420,6 +372,7 @@ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType i // internal flow. assert resourceConfig != null : "The resource config must not be null!"; + this.references = new ConcurrentHashMap<>(); this.recordPageKey = recordPageKey; this.records = new DataRecord[Constants.NDP_NODE_COUNT]; this.areDeweyIDsStored = resourceConfig.areDeweyIDsStored; @@ -427,27 +380,29 @@ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType i this.resourceConfig = resourceConfig; this.recordPersister = resourceConfig.recordPersister; this.revision = revisionNumber; - this.slotOffsets = new int[Constants.NDP_NODE_COUNT]; - this.deweyIdOffsets = new int[Constants.NDP_NODE_COUNT]; this.stringValueOffsets = new int[Constants.NDP_NODE_COUNT]; - this.slotBitmap = new long[BITMAP_WORDS]; // All bits initially 0 (no slots populated) - this.fixedFormatBitmap = new long[BITMAP_WORDS]; - this.fixedSlotKinds = new byte[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - Arrays.fill(deweyIdOffsets, -1); + this.slotBitmap = new long[BITMAP_WORDS]; // All bits initially 0 (no slots populated) Arrays.fill(stringValueOffsets, -1); - Arrays.fill(fixedSlotKinds, NO_FIXED_SLOT_KIND); - this.doResizeMemorySegmentsIfNeeded = true; - this.slotMemoryFreeSpaceStart = 0; + this.lastSlotIndex = -1; - this.deweyIdMemoryFreeSpaceStart = 0; this.lastDeweyIdIndex = -1; this.stringValueMemoryFreeSpaceStart = 0; this.lastStringValueIndex = -1; - this.slotMemory = slotMemory; - this.deweyIdMemory = deweyIdMemory; this.externallyAllocatedMemory = externallyAllocatedMemory; + // Release passed-in legacy memory if not externally allocated (callers still pass it) + if (!externallyAllocatedMemory) { + if (slotMemory != null && slotMemory.byteSize() > 0) { + segmentAllocator.release(slotMemory); + } + if (deweyIdMemory != null && deweyIdMemory.byteSize() > 0) { + segmentAllocator.release(deweyIdMemory); + } + } + + // Eagerly allocate slotted page — all pages use slotted page format + ensureSlottedPage(); + // Capture creation stack trace for leak tracing (only when diagnostics enabled) if (DEBUG_MEMORY_LEAKS) { this.creationStackTrace = Thread.currentThread().getStackTrace(); @@ -464,15 +419,15 @@ public KeyValueLeafPage(final @NonNegative long recordPageKey, final IndexType i /** * Constructor which reads deserialized data to the {@link KeyValueLeafPage} from the storage. - * Memory is allocated by the global allocator and WILL be released by close(). + * The slotted page will be set by the caller via {@link #setSlottedPage(MemorySegment)}. * - * @param recordPageKey This is the base key of all contained nodes. - * @param revision The current revision. - * @param indexType The index type. - * @param resourceConfig The resource configuration. + * @param recordPageKey This is the base key of all contained nodes. + * @param revision The current revision. + * @param indexType The index type. + * @param resourceConfig The resource configuration. * @param areDeweyIDsStored Determines if DeweyIDs are stored or not. - * @param recordPersister Persistenter. - * @param references References to overflow pages. + * @param recordPersister Persistenter. + * @param references References to overflow pages. */ public KeyValueLeafPage(final long recordPageKey, final int revision, final IndexType indexType, final ResourceConfiguration resourceConfig, final boolean areDeweyIDsStored, @@ -484,137 +439,29 @@ public KeyValueLeafPage(final long recordPageKey, final int revision, final Inde this.resourceConfig = resourceConfig; this.areDeweyIDsStored = areDeweyIDsStored; this.recordPersister = recordPersister; - this.slotMemory = slotMemory; - this.deweyIdMemory = deweyIdMemory; this.references = references; this.records = new DataRecord[Constants.NDP_NODE_COUNT]; - this.slotOffsets = new int[Constants.NDP_NODE_COUNT]; - this.deweyIdOffsets = new int[Constants.NDP_NODE_COUNT]; this.stringValueOffsets = new int[Constants.NDP_NODE_COUNT]; - this.slotBitmap = new long[BITMAP_WORDS]; // Will be populated during deserialization - this.fixedFormatBitmap = new long[BITMAP_WORDS]; - this.fixedSlotKinds = new byte[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - Arrays.fill(deweyIdOffsets, -1); + this.slotBitmap = new long[BITMAP_WORDS]; // Will be populated from slotted page bitmap Arrays.fill(stringValueOffsets, -1); - Arrays.fill(fixedSlotKinds, NO_FIXED_SLOT_KIND); this.stringValueMemoryFreeSpaceStart = 0; this.lastStringValueIndex = -1; - this.doResizeMemorySegmentsIfNeeded = true; - this.slotMemoryFreeSpaceStart = 0; + this.lastSlotIndex = lastSlotIndex; // Memory allocated by global allocator (e.g., during deserialization) - release on close() this.externallyAllocatedMemory = false; - if (areDeweyIDsStored) { - this.deweyIdMemoryFreeSpaceStart = 0; - this.lastDeweyIdIndex = lastDeweyIdIndex; - } else { - this.deweyIdMemoryFreeSpaceStart = 0; - this.lastDeweyIdIndex = -1; - } - - // Capture creation stack trace for leak tracing (only when diagnostics enabled) - if (DEBUG_MEMORY_LEAKS) { - this.creationStackTrace = Thread.currentThread().getStackTrace(); - PAGES_CREATED.incrementAndGet(); - PAGES_BY_TYPE.computeIfAbsent(indexType, _ -> new java.util.concurrent.atomic.AtomicLong(0)).incrementAndGet(); - ALL_LIVE_PAGES.add(this); - if (recordPageKey == 0) { - ALL_PAGE_0_INSTANCES.add(this); - } - } else { - this.creationStackTrace = null; - } - } - - /** - * Zero-copy constructor - slotMemory IS a slice of the decompression buffer. The backing buffer is - * released when this page is closed. - * - *

- * This constructor enables true zero-copy page deserialization where the decompressed data becomes - * the page's storage directly, eliminating the per-slot MemorySegment.copy() calls that were a - * major performance bottleneck. - * - * @param recordPageKey base key assigned to this node page - * @param revision the current revision - * @param indexType the index type - * @param resourceConfig the resource configuration - * @param slotOffsets pre-loaded slot offset array from serialized data - * @param slotMemory slice of decompression buffer (NOT copied) - * @param lastSlotIndex index of the last slot - * @param deweyIdOffsets pre-loaded dewey ID offset array (or null) - * @param deweyIdMemory slice for dewey IDs (or null) - * @param lastDeweyIdIndex index of the last dewey ID slot - * @param references overflow page references - * @param backingBuffer full decompression buffer (for lifecycle management) - * @param backingBufferReleaser returns buffer to allocator on close() - */ - public KeyValueLeafPage(final long recordPageKey, final int revision, final IndexType indexType, - final ResourceConfiguration resourceConfig, final int[] slotOffsets, final MemorySegment slotMemory, - final int lastSlotIndex, final int[] deweyIdOffsets, final MemorySegment deweyIdMemory, - final int lastDeweyIdIndex, final Map references, final MemorySegment backingBuffer, - final Runnable backingBufferReleaser) { - this.recordPageKey = recordPageKey; - this.revision = revision; - this.indexType = indexType; - this.resourceConfig = resourceConfig; - this.recordPersister = resourceConfig.recordPersister; - this.areDeweyIDsStored = resourceConfig.areDeweyIDsStored; - - // Zero-copy: use provided arrays and memory segments directly - this.slotOffsets = slotOffsets; - this.slotMemory = slotMemory; - this.lastSlotIndex = lastSlotIndex; + this.lastDeweyIdIndex = areDeweyIDsStored ? lastDeweyIdIndex : -1; - this.deweyIdOffsets = deweyIdOffsets != null - ? deweyIdOffsets - : new int[Constants.NDP_NODE_COUNT]; - if (deweyIdOffsets == null) { - Arrays.fill(this.deweyIdOffsets, -1); + // Release dummy slotMemory passed by callers (e.g., PageKind allocates a 1-byte dummy) + if (slotMemory != null && slotMemory.byteSize() > 0) { + segmentAllocator.release(slotMemory); } - this.deweyIdMemory = deweyIdMemory; - this.lastDeweyIdIndex = lastDeweyIdIndex; - - // String value memory (columnar storage) - not yet used in legacy constructor - this.stringValueOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(this.stringValueOffsets, -1); - this.stringValueMemory = null; - this.lastStringValueIndex = -1; - this.stringValueMemoryFreeSpaceStart = 0; - this.fsstSymbolTable = null; - - // Build bitmap from provided slotOffsets for O(k) iteration - this.slotBitmap = new long[BITMAP_WORDS]; - this.fixedFormatBitmap = new long[BITMAP_WORDS]; - this.fixedSlotKinds = new byte[Constants.NDP_NODE_COUNT]; - Arrays.fill(this.fixedSlotKinds, NO_FIXED_SLOT_KIND); - for (int i = 0; i < slotOffsets.length; i++) { - if (slotOffsets[i] >= 0) { - slotBitmap[i >>> 6] |= (1L << (i & 63)); - } + if (deweyIdMemory != null && deweyIdMemory.byteSize() > 0) { + segmentAllocator.release(deweyIdMemory); } - // Zero-copy: slotMemory is part of backingBuffer, track for release - this.backingBuffer = backingBuffer; - this.backingBufferReleaser = backingBufferReleaser; - - // If we have a backing buffer, it owns the memory (zero-copy path) - // Otherwise, slotMemory was allocated separately and should be released via allocator - this.externallyAllocatedMemory = (backingBuffer != null); - - this.references = references != null - ? references - : new ConcurrentHashMap<>(); - this.records = new DataRecord[Constants.NDP_NODE_COUNT]; - - // Zero-copy pages are read-only snapshots, don't resize - this.doResizeMemorySegmentsIfNeeded = false; - - // Free space tracking not needed for read-only zero-copy pages - this.slotMemoryFreeSpaceStart = 0; - this.deweyIdMemoryFreeSpaceStart = 0; + // Slotted page is set by caller via setSlottedPage() after construction. // Capture creation stack trace for leak tracing (only when diagnostics enabled) if (DEBUG_MEMORY_LEAKS) { @@ -630,26 +477,7 @@ public KeyValueLeafPage(final long recordPageKey, final int revision, final Inde } } - // Update the last slot index after setting a slot. - private void updateLastSlotIndex(int slotNumber, boolean isSlotMemory) { - if (isSlotMemory) { - if (lastSlotIndex >= 0) { - if (slotOffsets[slotNumber] > slotOffsets[lastSlotIndex]) { - lastSlotIndex = slotNumber; - } - } else { - lastSlotIndex = slotNumber; - } - } else { - if (lastDeweyIdIndex >= 0) { - if (deweyIdOffsets[slotNumber] > deweyIdOffsets[lastDeweyIdIndex]) { - lastDeweyIdIndex = slotNumber; - } - } else { - lastDeweyIdIndex = slotNumber; - } - } - } + @Override public int hashCode() { @@ -673,135 +501,597 @@ public long getPageKey() { } @Override - public DataRecord getRecord(final int offset) { - if (offset < 0 || offset >= Constants.NDP_NODE_COUNT) { - throw new IllegalArgumentException( - "Record offset out of range [0, " + Constants.NDP_NODE_COUNT + "): " + offset); - } + public DataRecord getRecord(int offset) { return records[offset]; } - /** - * Clear the cached record reference at the given offset. - *

- * Used after fixed-slot projection to prevent stale pooled object references from being returned by - * {@link #getRecord(int)}. Fixed-slot bytes are the authoritative source for fixed-format slots, so - * the cached record must be cleared to force re-materialization from those bytes on the next read. - * - * @param offset the slot offset to clear - * @throws IllegalArgumentException if offset is out of range [0, {@link Constants#NDP_NODE_COUNT}) - */ - public void clearRecord(final int offset) { - if (offset < 0 || offset >= Constants.NDP_NODE_COUNT) { - throw new IllegalArgumentException( - "Record offset out of range [0, " + Constants.NDP_NODE_COUNT + "): " + offset); - } - if (records[offset] != null) { - records[offset] = null; - inMemoryRecordCount--; - } - } - @Override public void setRecord(@NonNull final DataRecord record) { addedReferences = false; + // Invalidate stale compressed cache — record mutation means cached bytes are outdated + compressedSegment = null; bytes = null; final var key = record.getNodeKey(); final var offset = (int) (key - ((key >> Constants.NDP_NODE_COUNT_EXPONENT) << Constants.NDP_NODE_COUNT_EXPONENT)); - if (records[offset] == null) { - inMemoryRecordCount++; + + if (record instanceof FlyweightNode fn) { + if (fn.isWriteSingleton()) { + // Write singleton: serialize to heap, never store in records[] (aliasing risk). + if (slottedPage != null && fn.isBoundTo(slottedPage)) { + fn.setOwnerPage(this); + return; + } + ensureSlottedPage(); + if (fn.isBound()) { + fn.unbind(); + } + serializeToHeap(fn, key, offset); + return; + } + // Non-singleton FlyweightNode: unbind if bound, store in records[] for processEntries. + if (fn.isBound()) { + fn.unbind(); + } } + records[offset] = record; } /** - * Get bytes to serialize. + * Store a newly created record, serializing non-FlyweightNode data to the slotted page heap + * immediately. This is called from the createRecord path where node factories may reuse + * singleton objects. By serializing now and nulling records[], we preserve data before the + * singleton is reused for the next node creation. * - * @return bytes + *

For FlyweightNode records, this delegates to {@link #setRecord} which handles heap + * serialization and binding. For non-FlyweightNode on slotted pages, the record is serialized + * to the heap and records[offset] is nulled — prepareRecordForModification will deserialize + * a fresh object from the heap when mutation is needed. + * + * @param record the newly created record */ - public BytesOut getBytes() { - return bytes; + public void setNewRecord(@NonNull final DataRecord record) { + assert !(record instanceof FlyweightNode) + : "FlyweightNode must not go through setNewRecord — use serializeNewRecord"; + addedReferences = false; + compressedSegment = null; + bytes = null; + final var key = record.getNodeKey(); + final var offset = (int) (key - ((key >> Constants.NDP_NODE_COUNT_EXPONENT) << Constants.NDP_NODE_COUNT_EXPONENT)); + records[offset] = record; + } + + public void serializeNewRecord(final FlyweightNode fn, final long nodeKey, final int offset) { + addedReferences = false; + compressedSegment = null; + bytes = null; + serializeToHeap(fn, nodeKey, offset); + // Node stays bound after creation — next factory clearBinding() handles transition } /** - * Set bytes after serialization. + * Serialize a FlyweightNode to the slotted page heap, update directory/bitmap, and bind. * - * @param bytes bytes + *

After this call the node is bound: getters/setters operate on page memory. + * processEntries will skip this record at commit time because {@code fn.isBound()} is true. + * + * @param fn the flyweight node to serialize + * @param nodeKey the node's key + * @param offset the slot index within the page (0-1023) + */ + private void serializeToHeap(final FlyweightNode fn, final long nodeKey, final int offset) { + ensureSlottedPage(); + // Get DeweyID bytes if stored (must capture BEFORE binding overwrites the node state) + final byte[] deweyIdBytes = areDeweyIDsStored ? fn.getDeweyIDAsBytes() : null; + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + + // Ensure heap has enough space for this record (value nodes can be large) + final int heapEnd = PageLayout.getHeapEnd(slottedPage); + final int estimatedSize = fn.estimateSerializedSize() + deweyIdLen + + (areDeweyIDsStored ? PageLayout.DEWEY_ID_TRAILER_SIZE : 0); + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < estimatedSize) { + growSlottedPage(); + } + + // Write directly to heap at current end + final long absOffset = PageLayout.heapAbsoluteOffset(heapEnd); + final int recordBytes = fn.serializeToHeap(slottedPage, absOffset); + + // When DeweyIDs are stored, append DeweyID data + 2-byte trailer + final int totalBytes; + if (areDeweyIDsStored) { + if (deweyIdLen > 0) { + MemorySegment.copy(deweyIdBytes, 0, slottedPage, + java.lang.foreign.ValueLayout.JAVA_BYTE, absOffset + recordBytes, deweyIdLen); + } + totalBytes = recordBytes + deweyIdLen + PageLayout.DEWEY_ID_TRAILER_SIZE; + PageLayout.writeDeweyIdTrailer(slottedPage, absOffset + totalBytes, deweyIdLen); + } else { + totalBytes = recordBytes; + } + + // Update heap end and used counters + PageLayout.setHeapEnd(slottedPage, heapEnd + totalBytes); + PageLayout.setHeapUsed(slottedPage, PageLayout.getHeapUsed(slottedPage) + totalBytes); + + // Update directory entry: [heapOffset][dataLength | nodeKindId] + PageLayout.setDirEntry(slottedPage, offset, heapEnd, totalBytes, + ((NodeKind) fn.getKind()).getId()); + + // Mark slot populated in bitmap and track last slot index (new slots only) + if (!PageLayout.isSlotPopulated(slottedPage, offset)) { + PageLayout.markSlotPopulated(slottedPage, offset); + PageLayout.setPopulatedCount(slottedPage, + PageLayout.getPopulatedCount(slottedPage) + 1); + lastSlotIndex = offset; + } + + // Bind flyweight — all subsequent mutations go directly to page memory + fn.bind(slottedPage, absOffset, nodeKey, offset); + fn.setOwnerPage(this); + } + + // ==================== DIRECT-TO-HEAP CREATION ==================== + + /** + * Prepare the heap for a direct record write. Ensures slotted page exists and has + * enough space. Returns the absolute offset where the caller should write. + * + * @param estimatedRecordSize upper bound on record bytes (from estimateSerializedSize) + * @param deweyIdLen length of DeweyID bytes (0 if none) + * @return absolute byte offset in the slotted page MemorySegment to write at */ - public void setBytes(BytesOut bytes) { - this.bytes = bytes; + public long prepareHeapForDirectWrite(final int estimatedRecordSize, final int deweyIdLen) { + ensureSlottedPage(); + final int deweyOverhead = areDeweyIDsStored + ? deweyIdLen + PageLayout.DEWEY_ID_TRAILER_SIZE : 0; + final int totalEstimated = estimatedRecordSize + deweyOverhead; + int heapEnd = PageLayout.getHeapEnd(slottedPage); + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < totalEstimated) { + growSlottedPage(); + heapEnd = PageLayout.getHeapEnd(slottedPage); + } + return PageLayout.heapAbsoluteOffset(heapEnd); } - @Override - public DataRecord[] records() { - return records; + /** + * Complete a direct record write. Handles DeweyID trailer, directory entry, bitmap, + * heap counters, and flyweight binding. Called after the caller has written the record + * bytes via a static writeNewRecord method. + * + * @param nodeKindId the node kind ID (e.g. NodeKind.OBJECT.getId()) + * @param nodeKey the node key + * @param slotOffset the slot index (0-1023) + * @param recordBytes number of bytes written by writeNewRecord + * @param deweyIdBytes DeweyID bytes (null if not stored) + */ + public void completeDirectWrite(final int nodeKindId, final long nodeKey, + final int slotOffset, final int recordBytes, final byte[] deweyIdBytes) { + addedReferences = false; + compressedSegment = null; + bytes = null; + + final int heapEnd = PageLayout.getHeapEnd(slottedPage); + final long absOffset = PageLayout.heapAbsoluteOffset(heapEnd); + final int deweyIdLen = deweyIdBytes != null ? deweyIdBytes.length : 0; + + // DeweyID trailer + final int totalBytes; + if (areDeweyIDsStored) { + if (deweyIdLen > 0) { + MemorySegment.copy(deweyIdBytes, 0, slottedPage, + java.lang.foreign.ValueLayout.JAVA_BYTE, absOffset + recordBytes, deweyIdLen); + } + totalBytes = recordBytes + deweyIdLen + PageLayout.DEWEY_ID_TRAILER_SIZE; + PageLayout.writeDeweyIdTrailer(slottedPage, absOffset + totalBytes, deweyIdLen); + } else { + totalBytes = recordBytes; + } + + // Update heap counters + PageLayout.setHeapEnd(slottedPage, heapEnd + totalBytes); + PageLayout.setHeapUsed(slottedPage, PageLayout.getHeapUsed(slottedPage) + totalBytes); + + // Directory entry + PageLayout.setDirEntry(slottedPage, slotOffset, heapEnd, totalBytes, nodeKindId); + + // Bitmap + if (!PageLayout.isSlotPopulated(slottedPage, slotOffset)) { + PageLayout.markSlotPopulated(slottedPage, slotOffset); + PageLayout.setPopulatedCount(slottedPage, + PageLayout.getPopulatedCount(slottedPage) + 1); + lastSlotIndex = slotOffset; + } + // NOTE: Caller is responsible for binding the flyweight and setting ownerPage. + // This eliminates interface dispatch (itable stubs) by letting the caller call + // bind()/setOwnerPage() on the concrete type directly. } - public int getInMemoryRecordCount() { - return inMemoryRecordCount; + /** + * Check whether DeweyIDs are stored on this page. + */ + public boolean areDeweyIDsStored() { + return areDeweyIDsStored; } - public int getDemotionThreshold() { - return demotionThreshold; + /** + * Resize a record whose varint width changed. Appends new version at heap end, + * updates directory, re-binds, and sets ownerPage. Old space becomes dead + * (reclaimed on page compaction/rewrite at commit time). + * + * @param fn the flyweight node (unbound, with updated Java fields) + * @param nodeKey the node's key + * @param offset the slot index within the page (0-1023) + */ + public void resizeRecord(final FlyweightNode fn, final long nodeKey, final int offset) { + compressedSegment = null; + bytes = null; + serializeToHeap(fn, nodeKey, offset); } - public boolean shouldDemoteRecords(IndexType currentIndexType) { - return currentIndexType == IndexType.DOCUMENT && inMemoryRecordCount >= demotionThreshold; + /** + * Resize a single field in a bound record by raw-copying unchanged fields and re-encoding + * only the changed field. Avoids the full unbind/re-serialize round-trip of {@link #resizeRecord}. + * + *

Bump-allocates new heap space, calls {@link DeltaVarIntCodec#resizeField} to perform + * three-segment copy (before + changed + after), preserves DeweyID trailer, updates directory, + * and re-binds the flyweight to the new location. + * + *

HFT note: Zero allocations. Uses {@link MemorySegment#copy} (AVX/SSE intrinsics). + * Cold path — only called on varint width change, which is rare (~5% of mutations). + * + * @param fn the bound flyweight node (must be bound to this page's slotted page) + * @param nodeKey the node's key + * @param slotIndex the slot index within the page (0-1023) + * @param fieldIndex the index of the field to resize (0 to fieldCount-1) + * @param fieldCount total number of fields in this record type's offset table + * @param encoder encodes the new field value at the target offset + */ + public void resizeRecordField(final FlyweightNode fn, final long nodeKey, final int slotIndex, + final int fieldIndex, final int fieldCount, final DeltaVarIntCodec.FieldEncoder encoder) { + assert slottedPage != null : "resizeRecordField requires slotted page"; + assert PageLayout.isSlotPopulated(slottedPage, slotIndex) : "slot not populated: " + slotIndex; + + compressedSegment = null; + bytes = null; + + // --- Read old record metadata from directory --- + final int oldHeapOffset = PageLayout.getDirHeapOffset(slottedPage, slotIndex); + final int oldTotalLen = PageLayout.getDirDataLength(slottedPage, slotIndex); + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, slotIndex); + final long oldRecordBase = PageLayout.heapAbsoluteOffset(oldHeapOffset); + + // --- Compute record-only length (excluding DeweyID trailer) --- + final int oldRecordOnlyLen = PageLayout.getRecordOnlyLength(slottedPage, slotIndex); + + // DeweyID portion (between record data and trailer) + final int deweyIdLen; + final int deweyIdTrailerSize; + if (areDeweyIDsStored) { + deweyIdLen = PageLayout.getDeweyIdLength(slottedPage, slotIndex); + deweyIdTrailerSize = PageLayout.DEWEY_ID_TRAILER_SIZE; + } else { + deweyIdLen = 0; + deweyIdTrailerSize = 0; + } + + // --- Estimate new size (old size ± max varint growth of 9 bytes) --- + final int maxNewRecordLen = oldRecordOnlyLen + 9; + final int maxNewTotalLen = maxNewRecordLen + deweyIdLen + deweyIdTrailerSize; + + // --- Ensure heap capacity --- + final int heapEnd = PageLayout.getHeapEnd(slottedPage); + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < maxNewTotalLen) { + growSlottedPage(); + } + + // --- Raw-copy resize: copy unchanged fields, re-encode changed field --- + final long newRecordBase = PageLayout.heapAbsoluteOffset(heapEnd); + final int newRecordLen = DeltaVarIntCodec.resizeField( + slottedPage, oldRecordBase, oldRecordOnlyLen, + fieldCount, fieldIndex, + slottedPage, newRecordBase, + encoder); + + // --- Copy DeweyID data + trailer from old location --- + final int newTotalLen; + if (areDeweyIDsStored) { + // Copy DeweyID bytes (may be 0 length) + if (deweyIdLen > 0) { + final long oldDeweyStart = oldRecordBase + oldRecordOnlyLen; + final long newDeweyStart = newRecordBase + newRecordLen; + MemorySegment.copy(slottedPage, oldDeweyStart, slottedPage, newDeweyStart, deweyIdLen); + } + newTotalLen = newRecordLen + deweyIdLen + deweyIdTrailerSize; + PageLayout.writeDeweyIdTrailer(slottedPage, newRecordBase + newTotalLen, deweyIdLen); + } else { + newTotalLen = newRecordLen; + } + + // --- Update heap counters (old space becomes dead) --- + PageLayout.setHeapEnd(slottedPage, heapEnd + newTotalLen); + // heapUsed: subtract old, add new (net change = newTotalLen - oldTotalLen) + PageLayout.setHeapUsed(slottedPage, + PageLayout.getHeapUsed(slottedPage) + newTotalLen - oldTotalLen); + + // --- Update directory entry --- + PageLayout.setDirEntry(slottedPage, slotIndex, heapEnd, newTotalLen, nodeKindId); + + // --- Re-bind flyweight to new location --- + fn.bind(slottedPage, newRecordBase, nodeKey, slotIndex); + fn.setOwnerPage(this); } - public void onRecordRematerialized() { - rematerializedRecordsSinceLastDemotion++; - if (rematerializedRecordsSinceLastDemotion > demotionThreshold * 2) { - demotionThreshold = Math.min(MAX_DEMOTION_THRESHOLD, demotionThreshold + DEMOTION_STEP); - rematerializedRecordsSinceLastDemotion = 0; + /** + * Zero-copy raw slot bytes from source page to this page's heap. + * Copies the record body + DeweyID trailer verbatim, avoiding deserialize-serialize round-trip. + * + * @param sourcePage the source page to copy from + * @param slotIndex the slot index to copy + */ + public void copySlotFromPage(final KeyValueLeafPage sourcePage, final int slotIndex) { + final MemorySegment srcPage = sourcePage.getSlottedPage(); + if (srcPage == null || !PageLayout.isSlotPopulated(srcPage, slotIndex)) { + return; } + ensureSlottedPage(); + + // Read source slot metadata + final int srcHeapOffset = PageLayout.getDirHeapOffset(srcPage, slotIndex); + final int srcTotalLen = PageLayout.getDirDataLength(srcPage, slotIndex); + final int srcNodeKindId = PageLayout.getDirNodeKindId(srcPage, slotIndex); + + // Ensure destination has enough space + final int heapEnd = PageLayout.getHeapEnd(slottedPage); + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < srcTotalLen) { + growSlottedPage(); + } + + // Copy raw bytes (record body + DeweyID trailer) from source to destination heap + final long srcAbs = PageLayout.heapAbsoluteOffset(srcHeapOffset); + final long dstAbs = PageLayout.heapAbsoluteOffset(heapEnd); + MemorySegment.copy(srcPage, srcAbs, slottedPage, dstAbs, srcTotalLen); + + // Update destination heap end and used counters + PageLayout.setHeapEnd(slottedPage, heapEnd + srcTotalLen); + PageLayout.setHeapUsed(slottedPage, PageLayout.getHeapUsed(slottedPage) + srcTotalLen); + + // Update destination directory entry + PageLayout.setDirEntry(slottedPage, slotIndex, heapEnd, srcTotalLen, srcNodeKindId); + + // Mark slot populated in bitmap + if (!PageLayout.isSlotPopulated(slottedPage, slotIndex)) { + PageLayout.markSlotPopulated(slottedPage, slotIndex); + PageLayout.setPopulatedCount(slottedPage, PageLayout.getPopulatedCount(slottedPage) + 1); + lastSlotIndex = slotIndex; + } + + // Invalidate compressed cache + compressedSegment = null; + bytes = null; + addedReferences = false; } - public int demoteRecordsToSlots(ResourceConfiguration config, MemorySegmentBytesOut reusableOut) { - if (inMemoryRecordCount <= MIN_DEMOTION_THRESHOLD) { - return 0; + /** + * Check if the slotted page has a populated slot for the given record key. + * + * @param recordKey the record key + * @return true if the slot is populated on the slotted page + */ + public boolean hasSlottedPageSlot(final long recordKey) { + if (slottedPage == null) { + return false; } + final int offset = (int) (recordKey - ((recordKey >> Constants.NDP_NODE_COUNT_EXPONENT) + << Constants.NDP_NODE_COUNT_EXPONENT)); + return PageLayout.isSlotPopulated(slottedPage, offset); + } - final RecordSerializer serializer = config.recordPersister; - int demoted = 0; + /** + * Allocate and initialize the slotted page if not yet present. + */ + public void ensureSlottedPage() { + if (slottedPage != null) { + return; + } + final MemorySegment allocated = segmentAllocator.allocate(PageLayout.INITIAL_PAGE_SIZE); + slottedPageCapacity = (int) allocated.byteSize(); + PageLayout.initializePage(allocated, recordPageKey, revision, indexType.getID(), areDeweyIDsStored); + slottedPage = allocated.reinterpret(Long.MAX_VALUE); + } - for (int i = 0; i < records.length && inMemoryRecordCount > MIN_DEMOTION_THRESHOLD; i++) { - final DataRecord record = records[i]; - if (record == null) { - continue; - } + /** + * Grow the slotted page by doubling its size. + * Copies all existing data (header + bitmap + directory + heap) to the new segment. + */ + private void growSlottedPage() { + final int currentSize = slottedPageCapacity; + final int newSize = currentSize * 2; + final MemorySegment grown = segmentAllocator.allocate(newSize); + // Copy all existing data + MemorySegment.copy(slottedPage, 0, grown, 0, currentSize); + // Release old segment (reinterpret back to actual size for allocator) + segmentAllocator.release(slottedPage.reinterpret(currentSize)); + slottedPageCapacity = (int) grown.byteSize(); + slottedPage = grown.reinterpret(Long.MAX_VALUE); + // No rebind needed: the caller (serializeToHeap) will rebind the active flyweight. + } - reusableOut.clear(); - serializer.serialize(reusableOut, record, config); - final MemorySegment segment = reusableOut.getDestination(); + /** + * Write raw slot data to the slotted page heap. + * Used by setSlot() and addReferences() when slottedPage is active. + * Data is stored without a length prefix — the directory entry holds the length. + * + * @param data the raw slot data to store + * @param slotNumber the slot index (0-1023) + * @param nodeKindId the node kind ID (0 for legacy format, >0 for flyweight) + */ + private void setSlotToHeap(final MemorySegment data, final int slotNumber, final int nodeKindId) { + final int recordSize = (int) data.byteSize(); + if (recordSize <= 0) { + return; + } - if (segment.byteSize() > PageConstants.MAX_RECORD_SIZE) { - continue; + // Total allocation includes DeweyID trailer when DeweyIDs are stored + final int totalSize = areDeweyIDsStored + ? recordSize + PageLayout.DEWEY_ID_TRAILER_SIZE + : recordSize; + + // Ensure heap has enough space + int heapEnd = PageLayout.getHeapEnd(slottedPage); + int remaining = slottedPageCapacity - PageLayout.HEAP_START - heapEnd; + if (remaining < totalSize) { + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < totalSize) { + growSlottedPage(); } + heapEnd = PageLayout.getHeapEnd(slottedPage); + } - setSlot(segment, i); - markSlotAsCompactFormat(i); + // Bump-allocate and copy record data to heap + final long absOffset = PageLayout.heapAbsoluteOffset(heapEnd); + MemorySegment.copy(data, 0, slottedPage, absOffset, recordSize); - if (config.areDeweyIDsStored && record.getNodeKey() != 0) { - final byte[] deweyBytes = record.getDeweyIDAsBytes(); - if (deweyBytes != null) { - setDeweyId(deweyBytes, i); - } + // Append DeweyID trailer (initially 0 = no DeweyID yet) + if (areDeweyIDsStored) { + PageLayout.writeDeweyIdTrailer(slottedPage, absOffset + totalSize, 0); + } + + // Update heap end and used counters + PageLayout.setHeapEnd(slottedPage, heapEnd + totalSize); + PageLayout.setHeapUsed(slottedPage, PageLayout.getHeapUsed(slottedPage) + totalSize); + + // Update directory entry with the provided nodeKindId + PageLayout.setDirEntry(slottedPage, slotNumber, heapEnd, totalSize, nodeKindId); + + // Mark slot populated in bitmap and track last slot index (new slots only) + if (!PageLayout.isSlotPopulated(slottedPage, slotNumber)) { + PageLayout.markSlotPopulated(slottedPage, slotNumber); + PageLayout.setPopulatedCount(slottedPage, + PageLayout.getPopulatedCount(slottedPage) + 1); + lastSlotIndex = slotNumber; + } + } + + /** + * Write raw slot data from a source segment at a given offset to the slotted page heap. + * Zero-copy variant for direct page deserialization. + * + * @param source the source MemorySegment containing the data + * @param sourceOffset byte offset within source where data starts + * @param dataSize number of bytes to copy + * @param slotNumber the slot index (0-1023) + * @param nodeKindId the node kind ID (0 for legacy format, 24-43 for flyweight) + */ + void setSlotToHeapDirect(final MemorySegment source, final long sourceOffset, + final int dataSize, final int slotNumber, final int nodeKindId) { + if (dataSize <= 0) { + return; + } + + // Ensure heap has enough space + int heapEnd = PageLayout.getHeapEnd(slottedPage); + final int remaining = slottedPageCapacity - PageLayout.HEAP_START - heapEnd; + if (remaining < dataSize) { + while (slottedPageCapacity - PageLayout.HEAP_START - heapEnd < dataSize) { + growSlottedPage(); } + heapEnd = PageLayout.getHeapEnd(slottedPage); + } - records[i] = null; - inMemoryRecordCount--; - demoted++; + // Bump-allocate and copy data to heap + final long absOffset = PageLayout.heapAbsoluteOffset(heapEnd); + MemorySegment.copy(source, sourceOffset, slottedPage, absOffset, dataSize); + + // Update heap end and used counters + PageLayout.setHeapEnd(slottedPage, heapEnd + dataSize); + PageLayout.setHeapUsed(slottedPage, PageLayout.getHeapUsed(slottedPage) + dataSize); + + // Update directory entry + PageLayout.setDirEntry(slottedPage, slotNumber, heapEnd, dataSize, nodeKindId); + + // Mark slot populated in bitmap + if (!PageLayout.isSlotPopulated(slottedPage, slotNumber)) { + PageLayout.markSlotPopulated(slottedPage, slotNumber); + PageLayout.setPopulatedCount(slottedPage, + PageLayout.getPopulatedCount(slottedPage) + 1); } + } + + /** + * Get bytes to serialize. + * + * @return bytes + */ + public BytesOut getBytes() { + return bytes; + } + + /** + * Set bytes after serialization (legacy byte[] path). + * + * @param bytes bytes + */ + public void setBytes(final BytesOut bytes) { + this.bytes = bytes; + this.compressedSegment = null; + } + + /** + * Get the compressed page data as a MemorySegment (zero-copy path). + * + * @return the compressed segment, or null if not set + */ + public MemorySegment getCompressedSegment() { + return compressedSegment; + } - if (demoted > 0) { - if (rematerializedRecordsSinceLastDemotion < demoted / 4) { - demotionThreshold = Math.max(MIN_DEMOTION_THRESHOLD, demotionThreshold - DEMOTION_STEP); + /** + * Set compressed page data as a MemorySegment (zero-copy path). + * Clears the legacy bytes cache. + * + * @param segment the compressed segment (Arena.ofAuto()-managed) + */ + public void setCompressedSegment(final MemorySegment segment) { + this.compressedSegment = segment; + this.bytes = null; + } + + /** + * Release node object references to allow GC to reclaim them. + *

+ * MUST only be called after {@code addReferences()} has serialized all records into + * {@code slotMemory} and the compressed form is cached via {@code setCompressedSegment()} + * or {@code setBytes()}. After this call, individual records can still be reconstructed + * on demand from {@code slotMemory} via {@code getSlot(offset)} in + * {@link io.sirix.access.trx.page.NodeStorageEngineReader#getValue}. + */ + public void clearRecordsForGC() { + // Unbind flyweight nodes BEFORE clearing — cursors may still hold references. + // Unbinding materializes all fields from page memory (still valid at this point) + // into Java primitives, so reads after page release use correct field values. + if (slottedPage != null) { + for (final DataRecord record : records) { + if (record instanceof FlyweightNode fn && fn.isBound()) { + fn.unbind(); + } } - rematerializedRecordsSinceLastDemotion = 0; } + Arrays.fill(records, null); + } - return demoted; + /** + * Check whether all non-null records have been serialized to slotMemory. + * + * @return {@code true} if {@link #addReferences} has been called and no subsequent + * {@link #setRecord} has invalidated the serialized state + */ + public boolean isAddedReferences() { + return addedReferences; + } + + @Override + public DataRecord[] records() { + return records; } public byte[] getHashCode() { @@ -819,12 +1109,12 @@ public > I values() { } public Map getReferencesMap() { - return references != null ? references : Map.of(); + return references; } /** - * Set reference to the complete page for lazy slot copying at commit time. Used by DIFFERENTIAL, - * INCREMENTAL (full-dump), and SLIDING_SNAPSHOT versioning. + * Set reference to the complete page for lazy slot copying at commit time. + * Used by DIFFERENTIAL, INCREMENTAL (full-dump), and SLIDING_SNAPSHOT versioning. * * @param completePage the complete page to copy slots from */ @@ -842,8 +1132,8 @@ public KeyValueLeafPage getCompletePageRef() { } /** - * Mark a slot for preservation during lazy copy at commit time. At addReferences(), if this slot - * has records[i] == null, it will be copied from completePageRef. + * Mark a slot for preservation during lazy copy at commit time. + * At addReferences(), if this slot has records[i] == null, it will be copied from completePageRef. * * @param slotNumber the slot number to mark for preservation (0 to Constants.NDP_NODE_COUNT-1) */ @@ -861,7 +1151,8 @@ public void markSlotForPreservation(int slotNumber) { * @return true if the slot needs preservation */ public boolean isSlotMarkedForPreservation(int slotNumber) { - return preservationBitmap != null && (preservationBitmap[slotNumber >>> 6] & (1L << (slotNumber & 63))) != 0; + return preservationBitmap != null && + (preservationBitmap[slotNumber >>> 6] & (1L << (slotNumber & 63))) != 0; } /** @@ -882,273 +1173,61 @@ public boolean hasPreservationSlots() { return preservationBitmap != null; } - private static final int ALIGNMENT = 4; // 4-byte alignment for int - - private static int alignOffset(int offset) { - return (offset + ALIGNMENT - 1) & -ALIGNMENT; - } @Override public void setSlot(byte[] recordData, int slotNumber) { - setData(recordData, slotNumber, slotOffsets, slotMemory); - } - - public void setSlotMemory(MemorySegment slotMemory) { - this.slotMemory = slotMemory; - } - - public void setDeweyIdMemory(MemorySegment deweyIdMemory) { - this.deweyIdMemory = deweyIdMemory; + ensureSlottedPage(); + setSlotToHeap(MemorySegment.ofArray(recordData), slotNumber, 0); } @Override public void setSlot(MemorySegment data, int slotNumber) { - setData(data, slotNumber, slotOffsets, slotMemory); + ensureSlottedPage(); + setSlotToHeap(data, slotNumber, 0); } /** - * Set slot data by copying directly from a source MemorySegment. This is the zero-copy path that - * avoids intermediate byte[] allocations during deserialization. + * Set slot data with an explicit nodeKindId. Used during page combining + * to preserve the flyweight format indicator from the source page. * - *

- * Memory layout written to slotMemory: [length (4 bytes)][data (dataSize bytes)] - *

+ * @param data the raw slot data to store + * @param slotNumber the slot index (0-1023) + * @param nodeKindId the node kind ID (0 for legacy, >0 for flyweight) + */ + public void setSlotWithNodeKind(final MemorySegment data, final int slotNumber, final int nodeKindId) { + ensureSlottedPage(); + setSlotToHeap(data, slotNumber, nodeKindId); + } + + /** + * Get the nodeKindId for a slot from the slotted page directory. + * Returns 0 if the slotted page is not initialized or the slot is unpopulated. * - *

- * This method is optimized for the page deserialization hot path where we can copy directly from - * the decompressed page data segment to the target slot memory without creating temporary byte[] - * arrays. - *

+ * @param slotNumber the slot index (0-1023) + * @return the nodeKindId (>0 for flyweight format, 0 for legacy) + */ + public int getSlotNodeKindId(final int slotNumber) { + if (slottedPage == null || !PageLayout.isSlotPopulated(slottedPage, slotNumber)) { + return 0; + } + return PageLayout.getDirNodeKindId(slottedPage, slotNumber); + } + + /** + * Set slot data by copying directly from a source MemorySegment. + * Zero-copy path for page deserialization. * * @param source the source MemorySegment containing the data * @param sourceOffset the byte offset within source where data starts * @param dataSize the number of bytes to copy (must be > 0) * @param slotNumber the slot number (0 to Constants.NDP_NODE_COUNT-1) - * @throws IllegalArgumentException if dataSize <= 0 or slotNumber out of range - * @throws IndexOutOfBoundsException if source bounds would be exceeded */ public void setSlotDirect(MemorySegment source, long sourceOffset, int dataSize, int slotNumber) { - // Validate inputs - if (dataSize <= 0) { - throw new IllegalArgumentException("dataSize must be positive: " + dataSize); - } - if (slotNumber < 0 || slotNumber >= Constants.NDP_NODE_COUNT) { - throw new IllegalArgumentException("slotNumber out of range: " + slotNumber); - } - if (sourceOffset < 0 || sourceOffset + dataSize > source.byteSize()) { - throw new IndexOutOfBoundsException(String.format("Source bounds exceeded: offset=%d, size=%d, segmentSize=%d", - sourceOffset, dataSize, source.byteSize())); - } - - int requiredSize = INT_SIZE + dataSize; // 4 bytes for length prefix + actual data - int currentOffset = slotOffsets[slotNumber]; - int sizeDelta = 0; - - // Check if resizing is needed - if (!hasEnoughSpace(slotOffsets, slotMemory, requiredSize)) { - int newSize = Math.max((int) slotMemory.byteSize() * 2, (int) slotMemory.byteSize() + requiredSize); - slotMemory = resizeMemorySegment(slotMemory, newSize, slotOffsets, true); - } - - if (currentOffset >= 0) { - // Existing slot - check if size changed - int alignedOffset = alignOffset(currentOffset); - int currentSize = INT_SIZE + slotMemory.get(ValueLayout.JAVA_INT, alignedOffset); - - if (currentSize == requiredSize) { - // Same size - overwrite in place (fast path) - slotMemory.set(ValueLayout.JAVA_INT, alignedOffset, dataSize); - MemorySegment.copy(source, sourceOffset, slotMemory, alignedOffset + INT_SIZE, dataSize); - return; - } - sizeDelta = requiredSize - currentSize; - slotOffsets[slotNumber] = alignOffset(currentOffset); - } else { - // New slot - find free space - currentOffset = findFreeSpaceForSlots(requiredSize, true); - slotOffsets[slotNumber] = alignOffset(currentOffset); - updateLastSlotIndex(slotNumber, true); - // Update bitmap for newly populated slot - slotBitmap[slotNumber >>> 6] |= (1L << (slotNumber & 63)); - } - - // Perform shifting if size changed for existing slot - if (sizeDelta != 0) { - shiftSlotMemory(slotNumber, sizeDelta, slotOffsets, slotMemory); - } - - // Write length prefix - int alignedOffset = alignOffset(currentOffset); - slotMemory.set(ValueLayout.JAVA_INT, alignedOffset, dataSize); - - // Verify the write - int verifiedSize = slotMemory.get(ValueLayout.JAVA_INT, alignedOffset); - if (verifiedSize != dataSize) { - throw new IllegalStateException( - String.format("Slot size verification failed: expected=%d, actual=%d (slot: %d, offset: %d)", dataSize, - verifiedSize, slotNumber, alignedOffset)); - } - - // Copy data directly from source segment to slot memory (ZERO-COPY from caller's perspective!) - MemorySegment.copy(source, sourceOffset, slotMemory, alignedOffset + INT_SIZE, dataSize); - - // Update free space tracking - updateFreeSpaceStart(slotOffsets, slotMemory, true); - } - - private MemorySegment setData(Object data, int slotNumber, int[] offsets, MemorySegment memory) { - if (data == null) { - return null; - } - - int dataSize; - - if (data instanceof MemorySegment) { - dataSize = (int) ((MemorySegment) data).byteSize(); - - if (dataSize == 0) { - return null; - } - } else if (data instanceof byte[]) { - dataSize = ((byte[]) data).length; - - if (dataSize == 0) { - return null; - } - } else { - throw new IllegalArgumentException("Data must be either a MemorySegment or a byte array."); - } - - int requiredSize = INT_SIZE + dataSize; - int currentOffset = offsets[slotNumber]; - - int sizeDelta = 0; - - boolean resized = false; - boolean isSlotMemory = memory == slotMemory; - - // Check if resizing is needed. - if (!hasEnoughSpace(offsets, memory, requiredSize + sizeDelta)) { - // Resize the memory segment. - int newSize = Math.max(((int) memory.byteSize()) * 2, ((int) memory.byteSize()) + requiredSize + sizeDelta); - - memory = resizeMemorySegment(memory, newSize, offsets, isSlotMemory); - - resized = true; - } - - if (currentOffset >= 0) { - // Existing slot, check if there's enough space to accommodate the new data. - long alignedOffset = alignOffset(currentOffset); - int currentSize = INT_SIZE + memory.get(ValueLayout.JAVA_INT, alignedOffset); - - if (currentSize == requiredSize) { - // If the size is the same, update it directly. - memory.set(ValueLayout.JAVA_INT, alignedOffset, dataSize); - if (data instanceof MemorySegment) { - MemorySegment.copy((MemorySegment) data, 0, memory, alignedOffset + INT_SIZE, dataSize); - } else { - MemorySegment.copy(data, 0, memory, ValueLayout.JAVA_BYTE, alignedOffset + INT_SIZE, dataSize); - } - - return null; // No resizing needed - } else { - // Calculate sizeDelta based on whether the new data is larger or smaller. - sizeDelta = requiredSize - currentSize; - } - - offsets[slotNumber] = alignOffset(currentOffset); - } else { - // If the slot is empty, determine where to place the new data. - currentOffset = findFreeSpaceForSlots(requiredSize, isSlotMemory); - offsets[slotNumber] = alignOffset(currentOffset); - updateLastSlotIndex(slotNumber, isSlotMemory); - // Update bitmap for newly populated slot (slot memory only) - if (isSlotMemory) { - slotBitmap[slotNumber >>> 6] |= (1L << (slotNumber & 63)); - } - } - - // Perform any necessary shifting. - if (sizeDelta != 0) { - shiftSlotMemory(slotNumber, sizeDelta, offsets, memory); - } - - // Write the new data into the slot. - int alignedOffset = alignOffset(currentOffset); - memory.set(ValueLayout.JAVA_INT, alignedOffset, dataSize); - - // Verify the write - int verifiedSize = memory.get(ValueLayout.JAVA_INT, alignedOffset); - if (verifiedSize <= 0) { - throw new IllegalStateException(String.format("Invalid slot size written: %d (slot: %d, offset: %d)", - verifiedSize, slotNumber, alignedOffset)); - } - - - if (data instanceof MemorySegment) { - MemorySegment.copy((MemorySegment) data, 0, memory, alignedOffset + INT_SIZE, dataSize); - } else { - MemorySegment.copy(data, 0, memory, ValueLayout.JAVA_BYTE, alignedOffset + INT_SIZE, dataSize); - } - - // Update slotMemoryFreeSpaceStart after adding the slot. - updateFreeSpaceStart(offsets, memory, isSlotMemory); - - return resized - ? memory - : null; - } - - private void updateFreeSpaceStart(int[] offsets, MemorySegment memory, boolean isSlotMemory) { - int freeSpaceStart = (int) memory.byteSize() - getAvailableSpace(offsets, memory); - if (isSlotMemory) { - slotMemoryFreeSpaceStart = freeSpaceStart; - } else { - deweyIdMemoryFreeSpaceStart = freeSpaceStart; - } + ensureSlottedPage(); + setSlotToHeapDirect(source, sourceOffset, dataSize, slotNumber, 0); } - boolean hasEnoughSpace(int[] offsets, MemorySegment memory, int requiredDataSize) { - if (!doResizeMemorySegmentsIfNeeded) { - return true; - } - - // Check if the available space can accommodate the new slot. - return getAvailableSpace(offsets, memory) >= requiredDataSize; - } - - int getAvailableSpace(int[] offsets, MemorySegment memory) { - boolean isSlotMemory = memory == slotMemory; - - int lastSlotIndex = getLastIndex(isSlotMemory); - // If no slots are set yet, start from the beginning of the memory. - int lastOffset = (lastSlotIndex >= 0) - ? offsets[lastSlotIndex] - : 0; - - // Align the last offset - int alignedLastOffset = alignOffset(lastOffset); - - // If there is a valid last slot, add its size to the aligned offset. - int lastSlotSize = 0; - if (lastSlotIndex >= 0) { - // The size of the last slot (including the size of the integer that stores the data length) - lastSlotSize = INT_SIZE + memory.get(ValueLayout.JAVA_INT, alignedLastOffset); - } - - // Calculate available space from the end of the last slot to the end of memory. - return (int) memory.byteSize() - alignOffset(alignedLastOffset + lastSlotSize); - } - - int getLastIndex(boolean isSlotMemory) { - if (isSlotMemory) { - return lastSlotIndex; - } else { - return lastDeweyIdIndex; - } - } public int getLastSlotIndex() { return lastSlotIndex; @@ -1158,110 +1237,52 @@ public int getLastDeweyIdIndex() { return lastDeweyIdIndex; } - /** - * Get the slot offsets array for zero-copy serialization. Each element is the byte offset within - * slotMemory where the slot's data begins, or -1 if the slot is empty. - * - * @return the slot offsets array (do not modify) - */ - public int[] getSlotOffsets() { - return slotOffsets; - } /** - * Get the slot bitmap for O(k) iteration over populated slots. Bit i is set (1) iff slot i is - * populated (slotOffsets[i] >= 0). + * Get the slot bitmap for O(k) iteration over populated slots. + * Bit i is set (1) iff slot i is populated (slotOffsets[i] >= 0). * * @return the slot bitmap array (16 longs = 1024 bits, do not modify) */ public long[] getSlotBitmap() { + if (slottedPage != null) { + // Materialize slotted page bitmap into the Java array for VersioningType compatibility + PageLayout.copyBitmapTo(slottedPage, slotBitmap); + } return slotBitmap; } /** - * Returns bitmap of slots stored in fixed in-memory layout. - */ - public long[] getFixedFormatBitmap() { - return fixedFormatBitmap; - } - - /** - * Check if a specific slot is populated using the bitmap. This is O(1) and avoids memory access to - * slotOffsets. + * Check if a specific slot is populated using the bitmap. + * This is O(1) and avoids memory access to slotOffsets. * * @param slotNumber the slot index (0-1023) * @return true if the slot is populated */ public boolean hasSlot(int slotNumber) { - return (slotBitmap[slotNumber >>> 6] & (1L << (slotNumber & 63))) != 0; - } - - /** - * Returns {@code true} if slot data is in fixed in-memory layout. - */ - public boolean isFixedSlotFormat(int slotNumber) { - return (fixedFormatBitmap[slotNumber >>> 6] & (1L << (slotNumber & 63))) != 0; - } - - /** - * Returns node kind for a fixed-format slot, or {@code null} if the slot is compact or fixed - * metadata is unavailable. - */ - public NodeKind getFixedSlotNodeKind(final int slotNumber) { - if (!isFixedSlotFormat(slotNumber)) { - return null; + if (slottedPage != null) { + return PageLayout.isSlotPopulated(slottedPage, slotNumber); } - final byte kindId = fixedSlotKinds[slotNumber]; - if (kindId == NO_FIXED_SLOT_KIND) { - return null; - } - return NodeKind.getKind(kindId); - } - - /** - * Mark slot as fixed-layout in-memory representation. - */ - public void markSlotAsFixedFormat(final int slotNumber) { - fixedFormatBitmap[slotNumber >>> 6] |= (1L << (slotNumber & 63)); - fixedSlotKinds[slotNumber] = NO_FIXED_SLOT_KIND; - } - - /** - * Mark slot as fixed-layout in-memory representation with explicit kind metadata. - */ - public void markSlotAsFixedFormat(final int slotNumber, final NodeKind nodeKind) { - if (nodeKind == null) { - throw new IllegalArgumentException("nodeKind must not be null"); - } - fixedFormatBitmap[slotNumber >>> 6] |= (1L << (slotNumber & 63)); - fixedSlotKinds[slotNumber] = nodeKind.getId(); - } - - /** - * Mark slot as compact serialized representation. - */ - public void markSlotAsCompactFormat(final int slotNumber) { - fixedFormatBitmap[slotNumber >>> 6] &= ~(1L << (slotNumber & 63)); - fixedSlotKinds[slotNumber] = NO_FIXED_SLOT_KIND; + return (slotBitmap[slotNumber >>> 6] & (1L << (slotNumber & 63))) != 0; } /** * Returns a primitive int array of populated slot indices for O(k) iteration. *

- * This enables efficient iteration over only populated slots instead of iterating all 1024 slots - * and checking for null. For sparse pages with k populated slots, this is O(k) instead of O(1024). + * This enables efficient iteration over only populated slots instead of + * iterating all 1024 slots and checking for null. For sparse pages with + * k populated slots, this is O(k) instead of O(1024). *

- * Note: This allocates a new array on each call. For hot paths where the same page is iterated - * multiple times, consider using {@link #forEachPopulatedSlot}. + * Note: This allocates a new array on each call. For hot paths where the + * same page is iterated multiple times, consider using {@link #forEachPopulatedSlot}. *

* Example usage: - * *

{@code
    * int[] slots = page.populatedSlots();
    * for (int i = 0; i < slots.length; i++) {
-   *   int slot = slots[i];
-   *   MemorySegment data = page.getSlot(slot);
-   *   // process data - no null check needed
+   *     int slot = slots[i];
+   *     MemorySegment data = page.getSlot(slot);
+   *     // process data - no null check needed
    * }
    * }
* @@ -1277,17 +1298,20 @@ public int[] populatedSlots() { // Second pass: collect slot indices using Brian Kernighan's algorithm for (int wordIndex = 0; wordIndex < BITMAP_WORDS; wordIndex++) { - long word = slotBitmap[wordIndex]; - int baseSlot = wordIndex << 6; // wordIndex * 64 - while (word != 0) { - int bit = Long.numberOfTrailingZeros(word); + final long word = slottedPage != null + ? PageLayout.getBitmapWord(slottedPage, wordIndex) + : slotBitmap[wordIndex]; + long remaining = word; + final int baseSlot = wordIndex << 6; // wordIndex * 64 + while (remaining != 0) { + final int bit = Long.numberOfTrailingZeros(remaining); result[idx++] = baseSlot + bit; - word &= word - 1; // Clear lowest set bit + remaining &= remaining - 1; // Clear lowest set bit } } return result; } - + /** * Functional interface for slot consumer to enable zero-allocation iteration. */ @@ -1295,26 +1319,24 @@ public int[] populatedSlots() { public interface SlotConsumer { /** * Process a populated slot. - * * @param slotIndex the slot index * @return true to continue iteration, false to stop early */ boolean accept(int slotIndex); } - + /** * Zero-allocation iteration over populated slots. *

- * This method iterates over populated slots without allocating any arrays. The consumer returns - * false to stop iteration early. + * This method iterates over populated slots without allocating any arrays. + * The consumer returns false to stop iteration early. *

* Example usage: - * *

{@code
    * page.forEachPopulatedSlot(slot -> {
-   *   MemorySegment data = page.getSlot(slot);
-   *   // process data
-   *   return true; // continue iteration
+   *     MemorySegment data = page.getSlot(slot);
+   *     // process data
+   *     return true;  // continue iteration
    * });
    * }
* @@ -1324,28 +1346,34 @@ public interface SlotConsumer { public int forEachPopulatedSlot(SlotConsumer consumer) { int processed = 0; for (int wordIndex = 0; wordIndex < BITMAP_WORDS; wordIndex++) { - long word = slotBitmap[wordIndex]; - int baseSlot = wordIndex << 6; // wordIndex * 64 + long word = slottedPage != null + ? PageLayout.getBitmapWord(slottedPage, wordIndex) + : slotBitmap[wordIndex]; + final int baseSlot = wordIndex << 6; // wordIndex * 64 while (word != 0) { - int bit = Long.numberOfTrailingZeros(word); - int slot = baseSlot + bit; + final int bit = Long.numberOfTrailingZeros(word); + final int slot = baseSlot + bit; processed++; if (!consumer.accept(slot)) { return processed; } - word &= word - 1; // Clear lowest set bit + word &= word - 1; // Clear lowest set bit } } return processed; } /** - * Get the count of populated slots using SIMD-accelerated population count. Uses Vector API for - * parallel bitCount across multiple longs. This is O(BITMAP_WORDS / SIMD_WIDTH) instead of O(1024). + * Get the count of populated slots using SIMD-accelerated population count. + * Uses Vector API for parallel bitCount across multiple longs. + * This is O(BITMAP_WORDS / SIMD_WIDTH) instead of O(1024). * * @return number of populated slots */ public int populatedSlotCount() { + if (slottedPage != null) { + return PageLayout.countPopulatedSlots(slottedPage); + } int count = 0; int i = 0; @@ -1367,13 +1395,16 @@ public int populatedSlotCount() { return count; } - + /** * Check if all slots are populated using SIMD-accelerated comparison. * * @return true if all 1024 slots are populated */ public boolean isFullyPopulated() { + if (slottedPage != null) { + return PageLayout.countPopulatedSlots(slottedPage) == PageLayout.SLOT_COUNT; + } // All bits set = 0xFFFFFFFFFFFFFFFF = -1L int i = 0; final int simdWidth = LONG_SPECIES.length(); @@ -1386,7 +1417,7 @@ public boolean isFullyPopulated() { return false; } } - + // Scalar tail for (; i < BITMAP_WORDS; i++) { if (slotBitmap[i] != -1L) { @@ -1395,10 +1426,10 @@ public boolean isFullyPopulated() { } return true; } - + /** - * SIMD-accelerated bitmap OR into destination array. Computes: dest[i] |= src[i] for all bitmap - * words. + * SIMD-accelerated bitmap OR into destination array. + * Computes: dest[i] |= src[i] for all bitmap words. * * @param dest destination bitmap (modified in place) * @param src source bitmap to OR into dest @@ -1407,22 +1438,23 @@ public static void bitmapOr(long[] dest, long[] src) { int i = 0; final int simdWidth = LONG_SPECIES.length(); final int simdBound = BITMAP_WORDS - (BITMAP_WORDS % simdWidth); - + for (; i < simdBound; i += simdWidth) { LongVector destVec = LongVector.fromArray(LONG_SPECIES, dest, i); LongVector srcVec = LongVector.fromArray(LONG_SPECIES, src, i); destVec.or(srcVec).intoArray(dest, i); } - + // Scalar tail for (; i < BITMAP_WORDS; i++) { dest[i] |= src[i]; } } - + /** - * Check if any bits in src are NOT set in dest using SIMD. Returns true if there exist slots in src - * that are not yet in dest. Useful for early termination in page combining. + * Check if any bits in src are NOT set in dest using SIMD. + * Returns true if there exist slots in src that are not yet in dest. + * Useful for early termination in page combining. * * @param dest the "filled" bitmap * @param src the source bitmap to check @@ -1432,7 +1464,7 @@ public static boolean hasNewBits(long[] dest, long[] src) { int i = 0; final int simdWidth = LONG_SPECIES.length(); final int simdBound = BITMAP_WORDS - (BITMAP_WORDS % simdWidth); - + for (; i < simdBound; i += simdWidth) { LongVector destVec = LongVector.fromArray(LONG_SPECIES, dest, i); LongVector srcVec = LongVector.fromArray(LONG_SPECIES, src, i); @@ -1442,7 +1474,7 @@ public static boolean hasNewBits(long[] dest, long[] src) { return true; } } - + // Scalar tail for (; i < BITMAP_WORDS; i++) { if ((src[i] & ~dest[i]) != 0) { @@ -1452,141 +1484,49 @@ public static boolean hasNewBits(long[] dest, long[] src) { return false; } - /** - * Get the slot memory segment for zero-copy serialization. Contains the raw serialized slot data. - * - * @return the slot memory segment - */ - public MemorySegment getSlotMemory() { - return slotMemory; - } /** - * Get the dewey ID offsets array for zero-copy serialization. Each element is the byte offset - * within deweyIdMemory where the dewey ID's data begins, or -1 if empty. - * - * @return the dewey ID offsets array (do not modify) + * Get the slotted page MemorySegment for serialization. + * When non-null, the page uses LeanStore-style heap storage instead of legacy slotMemory. + * + * @return the slotted page segment, or null if not yet initialized */ - public int[] getDeweyIdOffsets() { - return deweyIdOffsets; + public MemorySegment getSlottedPage() { + return slottedPage; } /** - * Get the dewey ID memory segment for zero-copy serialization. Contains the raw serialized dewey ID - * data. - * - * @return the dewey ID memory segment (may be null if not stored) + * Set the slotted page MemorySegment (used during deserialization). + * Releases any previously allocated slotted page. + * + * @param slottedPage the slotted page segment */ - public MemorySegment getDeweyIdMemory() { - return deweyIdMemory; - } - - MemorySegment resizeMemorySegment(MemorySegment oldMemory, int newSize, int[] offsets, boolean isSlotMemory) { - MemorySegment newMemory = segmentAllocator.allocate(newSize); - MemorySegment.copy(oldMemory, 0, newMemory, 0, oldMemory.byteSize()); - segmentAllocator.release(oldMemory); - - if (isSlotMemory) { - slotMemory = newMemory; - } else { - deweyIdMemory = newMemory; - } - - // Update offsets to reference the new memory segment. - for (int i = 0; i < offsets.length; i++) { - if (offsets[i] >= 0) { - offsets[i] = alignOffset(offsets[i]); - updateLastSlotIndex(i, isSlotMemory); - } + public void setSlottedPage(final MemorySegment newSlottedPage) { + // Release old slotted page if different from the new one + if (this.slottedPage != null && this.slottedPage != newSlottedPage) { + segmentAllocator.release(this.slottedPage.reinterpret(slottedPageCapacity)); } + this.slottedPageCapacity = (int) newSlottedPage.byteSize(); + this.slottedPage = newSlottedPage.reinterpret(Long.MAX_VALUE); + } - // Update slotMemoryFreeSpaceStart to reflect the new free space start position. - updateFreeSpaceStart(offsets, newMemory, isSlotMemory); - return newMemory; - } @Override public int getUsedDeweyIdSize() { - return getUsedByteSize(deweyIdOffsets, deweyIdMemory); + // DeweyIDs are inline in the slotted page heap — no separate memory + return 0; } @Override public int getUsedSlotsSize() { - return getUsedByteSize(slotOffsets, slotMemory); + return slottedPage != null ? PageLayout.getHeapUsed(slottedPage) : 0; } public int getSlotMemoryByteSize() { - return (int) slotMemory.byteSize(); - } - - public int getDeweyIdMemoryByteSize() { - return (int) deweyIdMemory.byteSize(); - } - - int getUsedByteSize(int[] offsets, MemorySegment memory) { - if (memory == null) { - return 0; - } - return (int) memory.byteSize() - getAvailableSpace(offsets, memory); + return slottedPage != null ? PageLayout.HEAP_START + PageLayout.getHeapEnd(slottedPage) : 0; } - private void shiftSlotMemory(int slotNumber, int sizeDelta, int[] offsets, MemorySegment memory) { - if (sizeDelta == 0) { - return; // No shift needed if there's no size change. - } - - boolean isSlotMemory = memory == slotMemory; - - // Find the start offset of the slot to be shifted. - int startOffset = offsets[slotNumber]; - int alignedStartOffset = alignOffset(startOffset); - - // Find the smallest offset greater than the current slot's offset. - int shiftStartOffset = Integer.MAX_VALUE; - for (int i = 0; i < offsets.length; i++) { - if (i != slotNumber && offsets[i] >= alignedStartOffset && offsets[i] < shiftStartOffset) { - shiftStartOffset = offsets[i]; - } - } - - if (shiftStartOffset == Integer.MAX_VALUE) { - return; - } - int alignedShiftStartOffset = alignOffset(shiftStartOffset); - - // Calculate the end offset of the memory region to shift. - int lastSlotIndex = getLastIndex(isSlotMemory); - int alignedEndOffset = alignOffset(offsets[lastSlotIndex]); - - // Calculate the size of the last slot, ensuring it is aligned. - int lastSlotSize = INT_SIZE + memory.get(ValueLayout.JAVA_INT, alignedEndOffset); - - // Calculate the end offset of the shift. - int shiftEndOffset = alignedEndOffset + lastSlotSize; - - // Ensure the target slice also stays within bounds. - long targetEndOffset = - alignOffset(alignedShiftStartOffset + sizeDelta) + (shiftEndOffset - alignedShiftStartOffset); - if (targetEndOffset > memory.byteSize()) { - throw new IndexOutOfBoundsException("Calculated targetEndOffset exceeds memory bounds. " + "targetEndOffset: " - + targetEndOffset + ", memory size: " + memory.byteSize() + ", slotNumber: " + (slotNumber - 1)); - } - - // Shift the memory. - // Bulk copy: MemorySegment.copy handles overlapping regions safely (memmove semantics). - final int dstOffset = alignOffset(alignedShiftStartOffset + sizeDelta); - final long copyLength = shiftEndOffset - alignedShiftStartOffset; - MemorySegment.copy(memory, alignedShiftStartOffset, memory, dstOffset, copyLength); - - // Adjust the offsets for all affected slots. - for (int i = 0; i < offsets.length; i++) { - if (i != slotNumber && offsets[i] >= alignedStartOffset) { - offsets[i] = alignOffset(offsets[i] + sizeDelta); - updateLastSlotIndex(i, isSlotMemory); - } - } - } @Override public byte[] getSlotAsByteArray(int slotNumber) { @@ -1602,205 +1542,23 @@ public byte[] getSlotAsByteArray(int slotNumber) { } public boolean isSlotSet(int slotNumber) { - return slotOffsets[slotNumber] != -1; - } - - /** - * Get the absolute data offset for a slot within {@link #getSlotMemory()}, skipping the 4-byte - * length prefix. Use together with {@link #getSlotDataLength(int)} and {@link #getSlotMemory()} to - * avoid allocating a {@code MemorySegment} slice on the hot path. - * - * @param slotNumber the slot index - * @return absolute byte offset into slotMemory where data begins, or {@code -1} if the slot is - * empty - */ - public long getSlotDataOffset(final int slotNumber) { - assert slotNumber >= 0 && slotNumber < slotOffsets.length : "Invalid slot number: " + slotNumber; - final int slotOffset = slotOffsets[slotNumber]; - if (slotOffset < 0) { - return -1; - } - return slotOffset + INT_SIZE; - } - - /** - * Get the data length (in bytes) stored in the given slot, excluding the 4-byte length prefix. Use - * together with {@link #getSlotDataOffset(int)} and {@link #getSlotMemory()} to avoid allocating a - * {@code MemorySegment} slice on the hot path. - * - * @param slotNumber the slot index - * @return data length in bytes, or {@code -1} if the slot is empty - */ - public int getSlotDataLength(final int slotNumber) { - assert slotNumber >= 0 && slotNumber < slotOffsets.length : "Invalid slot number: " + slotNumber; - final int slotOffset = slotOffsets[slotNumber]; - if (slotOffset < 0) { - return -1; - } - return slotMemory.get(JAVA_INT_UNALIGNED, slotOffset); + return slottedPage != null && PageLayout.isSlotPopulated(slottedPage, slotNumber); } @Override public MemorySegment getSlot(int slotNumber) { - // Validate slot memory segment - assert slotMemory != null : "Slot memory segment is null"; - assert slotMemory.byteSize() > 0 : "Slot memory segment has zero length. Page key: " + recordPageKey - + ", revision: " + revision + ", index type: " + indexType; - - // Validate slot number - assert slotNumber >= 0 && slotNumber < slotOffsets.length : "Invalid slot number: " + slotNumber; - - int slotOffset = slotOffsets[slotNumber]; - if (slotOffset < 0) { + if (slottedPage == null || !PageLayout.isSlotPopulated(slottedPage, slotNumber)) { return null; } - - // CRITICAL: Validate memory segment state before reading - if (slotMemory == null) { - throw new IllegalStateException("Slot memory is null for page " + recordPageKey); - } - - // DEFENSIVE: Ensure offset is within segment bounds BEFORE reading - if (slotOffset + INT_SIZE > slotMemory.byteSize()) { - throw new IllegalStateException( - String.format("CORRUPT OFFSET: slot %d has offset %d but would exceed segment (size %d, page %d, rev %d)", - slotNumber, slotOffset, slotMemory.byteSize(), recordPageKey, revision)); - } - - // Read the length from the first 4 bytes at the offset - // Use unaligned access because zero-copy slices may not be 4-byte aligned - int length; - try { - length = slotMemory.get(JAVA_INT_UNALIGNED, slotOffset); - } catch (Exception e) { - throw new IllegalStateException( - String.format("Failed to read length at offset %d (page %d, slot %d, memory size %d)", slotOffset, - recordPageKey, slotNumber, slotMemory.byteSize()), - e); - } - - // DEFENSIVE: Sanity check the length value before using it - if (length < 0 || length > slotMemory.byteSize()) { - throw new IllegalStateException( - String.format("CORRUPT LENGTH at offset %d: %d (segment size: %d, page %d, slot %d, revision: %d)", - slotOffset, length, slotMemory.byteSize(), recordPageKey, slotNumber, revision)); - } - - if (length <= 0) { - // Print memory segment contents around the failing offset - String memoryDump = dumpMemorySegmentAroundOffset(slotOffset, slotNumber); - - String errorMessage = String.format( - "Slot length must be greater than 0, but is %d (slotNumber: %d, offset: %d, revision: %d, page: %d)", length, - slotNumber, slotOffset, revision, recordPageKey); - - // Add comprehensive debugging info - String debugInfo = String.format( - "%s\nMemory segment: size=%d, closed=%s\nSlot offsets around %d: [%d, %d, %d]\nLast slot index: %d, Free space: %d\n%s", - errorMessage, slotMemory.byteSize(), isClosed(), slotNumber, slotNumber > 0 - ? slotOffsets[slotNumber - 1] - : -1, - slotOffsets[slotNumber], slotNumber < slotOffsets.length - 1 - ? slotOffsets[slotNumber + 1] - : -1, - lastSlotIndex, slotMemoryFreeSpaceStart, memoryDump); - - throw new AssertionError(createStackTraceMessage(debugInfo)); - } - - - // Validate that we can read the full data - if (slotOffset + INT_SIZE + length > slotMemory.byteSize()) { - throw new IllegalStateException( - String.format("Slot data extends beyond memory segment: offset=%d, length=%d, total=%d, memory_size=%d", - slotOffset, length, slotOffset + INT_SIZE + length, slotMemory.byteSize())); + final int heapOffset = PageLayout.getDirHeapOffset(slottedPage, slotNumber); + // Use record-only length (excludes inline DeweyID data + 2-byte trailer) + final int recordLength = PageLayout.getRecordOnlyLength(slottedPage, slotNumber); + if (recordLength <= 0) { + return null; } - - // Return the memory segment containing just the data (skip the 4-byte length prefix) - return slotMemory.asSlice(slotOffset + INT_SIZE, length); + return slottedPage.asSlice(PageLayout.HEAP_START + heapOffset, recordLength); } - /** - * Dump the memory segment contents around a specific offset for debugging purposes. - * - * @param offset the offset where the issue occurred - * @param slotNumber the slot number for context - * @return a formatted string showing the memory contents - */ - private String dumpMemorySegmentAroundOffset(int offset, int slotNumber) { - StringBuilder sb = new StringBuilder(); - sb.append("Memory segment dump around failing offset:\n"); - - // Show 64 bytes around the offset (32 before, 32 after) - int startOffset = Math.max(0, offset - 32); - int endOffset = Math.min((int) slotMemory.byteSize(), offset + 64); - - sb.append( - String.format("Dumping bytes %d to %d (offset %d marked with **):\n", startOffset, endOffset - 1, offset)); - - // Hex dump with 16 bytes per line - for (int i = startOffset; i < endOffset; i += 16) { - sb.append(String.format("%04X: ", i)); - - // Hex bytes - for (int j = 0; j < 16 && i + j < endOffset; j++) { - byte b = slotMemory.get(ValueLayout.JAVA_BYTE, i + j); - if (i + j == offset) { - sb.append(String.format("**%02X** ", b & 0xFF)); - } else { - sb.append(String.format("%02X ", b & 0xFF)); - } - } - - // ASCII representation - sb.append(" |"); - for (int j = 0; j < 16 && i + j < endOffset; j++) { - byte b = slotMemory.get(ValueLayout.JAVA_BYTE, i + j); - char c = (b >= 32 && b < 127) - ? (char) b - : '.'; - if (i + j == offset) { - sb.append('*'); - } else { - sb.append(c); - } - } - sb.append("|\n"); - } - - // Also show the specific 4 bytes that should contain the length - sb.append(String.format("\nSpecific 4-byte length value at offset %d:\n", offset)); - if (offset + 4 <= slotMemory.byteSize()) { - for (int i = 0; i < 4; i++) { - byte b = slotMemory.get(ValueLayout.JAVA_BYTE, offset + i); - sb.append(String.format("Byte %d: 0x%02X (%d)\n", i, b & 0xFF, b)); - } - - // Show as little-endian and big-endian integers - try { - int littleEndian = slotMemory.get(ValueLayout.JAVA_INT.withOrder(ByteOrder.LITTLE_ENDIAN), offset); - int bigEndian = slotMemory.get(ValueLayout.JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN), offset); - sb.append(String.format("As little-endian int: %d\n", littleEndian)); - sb.append(String.format("As big-endian int: %d\n", bigEndian)); - } catch (Exception e) { - sb.append("Failed to read as integer: ").append(e.getMessage()).append('\n'); - } - } - - // Show all slot offsets for context - sb.append("\nAll slot offsets:\n"); - for (int i = 0; i < slotOffsets.length; i++) { - if (slotOffsets[i] >= 0) { - sb.append(String.format("Slot %d: offset %d", i, slotOffsets[i])); - if (i == slotNumber) { - sb.append(" <- FAILING SLOT"); - } - sb.append('\n'); - } - } - - return sb.toString(); - } private static String createStackTraceMessage(String message) { @@ -1817,32 +1575,102 @@ private static String createStackTraceMessage(String message) { @Override public void setDeweyId(byte[] deweyId, int offset) { - var memorySegment = setData(MemorySegment.ofArray(deweyId), offset, deweyIdOffsets, deweyIdMemory); - - if (memorySegment != null) { - deweyIdMemory = memorySegment; + if (deweyId == null) { + return; } + ensureSlottedPage(); + setDeweyIdToHeap(MemorySegment.ofArray(deweyId), offset); } @Override public void setDeweyId(MemorySegment deweyId, int offset) { - var memorySegment = setData(deweyId, offset, deweyIdOffsets, deweyIdMemory); + if (deweyId == null) { + return; + } + ensureSlottedPage(); + setDeweyIdToHeap(deweyId, offset); + } + + /** + * Set a DeweyID for a slot by re-allocating the slot's heap region with DeweyID data appended. + * Format: [record data][deweyId data][deweyIdLen:2 bytes (u16)]. + * The old allocation becomes dead heap space. + */ + private void setDeweyIdToHeap(final MemorySegment deweyId, final int slotNumber) { + final int deweyIdLen = (int) deweyId.byteSize(); + if (deweyIdLen == 0) { + return; + } + + final boolean slotExists = PageLayout.isSlotPopulated(slottedPage, slotNumber); + final int oldDataLength; + final int recordLen; + final int nodeKindId; + final long oldAbsStart; + + if (slotExists) { + // Existing slot — read current allocation info + final int oldHeapOffset = PageLayout.getDirHeapOffset(slottedPage, slotNumber); + oldDataLength = PageLayout.getDirDataLength(slottedPage, slotNumber); + nodeKindId = PageLayout.getDirNodeKindId(slottedPage, slotNumber); + recordLen = PageLayout.getRecordOnlyLength(slottedPage, slotNumber); + oldAbsStart = PageLayout.heapAbsoluteOffset(oldHeapOffset); + } else { + // No record yet — DeweyID-only allocation (nodeKindId = 0) + oldDataLength = 0; + recordLen = 0; + nodeKindId = 0; + oldAbsStart = 0; // unused + } + + // New total: record + deweyId + 2-byte trailer + final int newTotalLen = recordLen + deweyIdLen + PageLayout.DEWEY_ID_TRAILER_SIZE; + + // Ensure heap has enough space + int heapEnd = PageLayout.getHeapEnd(slottedPage); + int remaining = slottedPageCapacity - PageLayout.HEAP_START - heapEnd; + while (remaining < newTotalLen) { + growSlottedPage(); + heapEnd = PageLayout.getHeapEnd(slottedPage); + remaining = slottedPageCapacity - PageLayout.HEAP_START - heapEnd; + } + + // Bump-allocate new space + final long newAbsStart = PageLayout.heapAbsoluteOffset(heapEnd); + + // Copy record data from old location (if any) + if (recordLen > 0) { + MemorySegment.copy(slottedPage, oldAbsStart, slottedPage, newAbsStart, recordLen); + } + + // Copy DeweyID data + MemorySegment.copy(deweyId, 0, slottedPage, newAbsStart + recordLen, deweyIdLen); + + // Write DeweyID length trailer (u16 at end) + PageLayout.writeDeweyIdTrailer(slottedPage, newAbsStart + newTotalLen, deweyIdLen); + + // Update heap end (heapUsed: add new, subtract old dead space) + PageLayout.setHeapEnd(slottedPage, heapEnd + newTotalLen); + PageLayout.setHeapUsed(slottedPage, + PageLayout.getHeapUsed(slottedPage) + newTotalLen - oldDataLength); - if (memorySegment != null) { - deweyIdMemory = memorySegment; + // Update directory entry + PageLayout.setDirEntry(slottedPage, slotNumber, heapEnd, newTotalLen, nodeKindId); + + // Mark slot populated if new + if (!slotExists) { + PageLayout.markSlotPopulated(slottedPage, slotNumber); + PageLayout.setPopulatedCount(slottedPage, + PageLayout.getPopulatedCount(slottedPage) + 1); } } @Override public MemorySegment getDeweyId(int offset) { - int deweyIdOffset = deweyIdOffsets[offset]; - if (deweyIdOffset < 0) { + if (slottedPage == null || !PageLayout.isSlotPopulated(slottedPage, offset)) { return null; } - // Use unaligned access because zero-copy slices may not be 4-byte aligned - int deweyIdLength = deweyIdMemory.get(JAVA_INT_UNALIGNED, deweyIdOffset); - deweyIdOffset += INT_SIZE; - return deweyIdMemory.asSlice(deweyIdOffset, deweyIdLength); + return PageLayout.getDeweyId(slottedPage, offset); } @Override @@ -1856,47 +1684,19 @@ public byte[] getDeweyIdAsByteArray(int slotNumber) { return memorySegment.toArray(ValueLayout.JAVA_BYTE); } - public int findFreeSpaceForSlots(int requiredSize, boolean isSlotMemory) { - // Align the start of the free space - int alignedFreeSpaceStart = alignOffset(isSlotMemory - ? slotMemoryFreeSpaceStart - : deweyIdMemoryFreeSpaceStart); - int freeSpaceEnd = isSlotMemory - ? (int) slotMemory.byteSize() - : (int) deweyIdMemory.byteSize(); - - // Check if there's enough space in the current free space range - if (freeSpaceEnd - alignedFreeSpaceStart >= requiredSize) { - return alignedFreeSpaceStart; - } - - int freeMemoryStart = isSlotMemory - ? slotMemoryFreeSpaceStart - : deweyIdMemoryFreeSpaceStart; - int freeMemoryEnd = isSlotMemory - ? (int) slotMemory.byteSize() - : (int) deweyIdMemory.byteSize(); - throw new IllegalStateException("Not enough space in memory segment to store the data (freeSpaceStart " - + freeMemoryStart + " requiredSize: " + requiredSize + ", maxLength: " + freeMemoryEnd + ")"); - } @Override public > C newInstance(@NonNegative long recordPageKey, @NonNull IndexType indexType, @NonNull StorageEngineReader storageEngineReader) { - // Direct allocation (no pool) - ResourceConfiguration config = storageEngineReader.getResourceSession().getResourceConfig(); - MemorySegmentAllocator allocator = OS.isWindows() - ? WindowsMemorySegmentAllocator.getInstance() - : LinuxMemorySegmentAllocator.getInstance(); - - MemorySegment slotMemory = allocator.allocate(SIXTYFOUR_KB); - MemorySegment deweyIdMemory = config.areDeweyIDsStored - ? allocator.allocate(SIXTYFOUR_KB) - : null; - - // Memory allocated from global allocator - should be released on close() - return (C) new KeyValueLeafPage(recordPageKey, indexType, config, storageEngineReader.getRevisionNumber(), slotMemory, - deweyIdMemory, false // NOT externally allocated - release memory on close() + final ResourceConfiguration config = storageEngineReader.getResourceSession().getResourceConfig(); + return (C) new KeyValueLeafPage( + recordPageKey, + indexType, + config, + storageEngineReader.getRevisionNumber(), + null, + null, + false ); } @@ -1913,7 +1713,7 @@ public String toString() { @Override public int size() { - return getNumberOfNonNullEntries(records) + (references != null ? references.size() : 0); + return getNumberOfNonNullEntries(records) + references.size(); } private int getNumberOfNonNullEntries(final DataRecord[] entries) { @@ -1935,12 +1735,12 @@ public boolean isClosed() { /** * Finalizer for detecting page leaks during development. *

- * This method logs a warning if a page is garbage collected without being properly closed, - * indicating a potential memory leak. The warning is only generated when diagnostic settings are - * enabled. + * This method logs a warning if a page is garbage collected without being + * properly closed, indicating a potential memory leak. The warning is only + * generated when diagnostic settings are enabled. *

- * Note: Finalizers are deprecated in modern Java. This is retained solely for leak detection - * during development and testing. + * Note: Finalizers are deprecated in modern Java. This is retained + * solely for leak detection during development and testing. * * @deprecated Finalizers are discouraged. This exists only for leak detection. */ @@ -1949,26 +1749,25 @@ public boolean isClosed() { protected void finalize() { if (!isClosed() && DEBUG_MEMORY_LEAKS) { PAGES_FINALIZED_WITHOUT_CLOSE.incrementAndGet(); - + // Track by type and pageKey for detailed leak analysis if (indexType != null) { - FINALIZED_BY_TYPE.computeIfAbsent(indexType, _ -> new java.util.concurrent.atomic.AtomicLong(0)) - .incrementAndGet(); + FINALIZED_BY_TYPE.computeIfAbsent(indexType, _ -> new java.util.concurrent.atomic.AtomicLong(0)).incrementAndGet(); } - FINALIZED_BY_PAGE_KEY.computeIfAbsent(recordPageKey, _ -> new java.util.concurrent.atomic.AtomicLong(0)) - .incrementAndGet(); - + FINALIZED_BY_PAGE_KEY.computeIfAbsent(recordPageKey, _ -> new java.util.concurrent.atomic.AtomicLong(0)).incrementAndGet(); + // Log leak information (only when diagnostics enabled) if (LOGGER.isWarnEnabled()) { StringBuilder leakMsg = new StringBuilder(); leakMsg.append(String.format("Page leak detected: pageKey=%d, type=%s, revision=%d - not closed explicitly", recordPageKey, indexType, revision)); - + if (creationStackTrace != null && LOGGER.isDebugEnabled()) { leakMsg.append("\n Creation stack trace:"); for (int i = 2; i < Math.min(creationStackTrace.length, 8); i++) { StackTraceElement frame = creationStackTrace[i]; - leakMsg.append(String.format("\n at %s.%s(%s:%d)", frame.getClassName(), frame.getMethodName(), + leakMsg.append(String.format("\n at %s.%s(%s:%d)", + frame.getClassName(), frame.getMethodName(), frame.getFileName(), frame.getLineNumber())); } } @@ -1980,14 +1779,15 @@ protected void finalize() { /** * Closes this page and releases associated memory resources. *

- * This method is thread-safe and idempotent. If the page has active guards (indicating it's in use - * by a transaction), the close operation is skipped to prevent data corruption. + * This method is thread-safe and idempotent. If the page has active guards + * (indicating it's in use by a transaction), the close operation is skipped + * to prevent data corruption. *

- * Memory segments allocated by the global allocator are returned to the pool. Externally allocated - * memory (e.g., test arenas) is not released. + * Memory segments allocated by the global allocator are returned to the pool. + * Externally allocated memory (e.g., test arenas) is not released. *

- * For zero-copy pages, the backing buffer (from decompression) is released via the - * backingBufferReleaser callback. + * For zero-copy pages, the backing buffer (from decompression) is released + * via the backingBufferReleaser callback. */ @Override public synchronized void close() { @@ -2001,8 +1801,8 @@ public synchronized void close() { int currentGuardCount = guardCount.get(); if (currentGuardCount > 0) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Close skipped for guarded page: pageKey={}, type={}, guardCount={}", recordPageKey, indexType, - currentGuardCount); + LOGGER.debug("Close skipped for guarded page: pageKey={}, type={}, guardCount={}", + recordPageKey, indexType, currentGuardCount); } return; } @@ -2017,8 +1817,7 @@ public synchronized void close() { // Update diagnostic counters if tracking is enabled if (DEBUG_MEMORY_LEAKS) { PAGES_CLOSED.incrementAndGet(); - PAGES_CLOSED_BY_TYPE.computeIfAbsent(indexType, _ -> new java.util.concurrent.atomic.AtomicLong(0)) - .incrementAndGet(); + PAGES_CLOSED_BY_TYPE.computeIfAbsent(indexType, _ -> new java.util.concurrent.atomic.AtomicLong(0)).incrementAndGet(); ALL_LIVE_PAGES.remove(this); if (recordPageKey == 0) { ALL_PAGE_0_INSTANCES.remove(this); @@ -2034,63 +1833,61 @@ public synchronized void close() { } backingBufferReleaser = null; backingBuffer = null; - // For zero-copy pages, all memory segments are slices of backingBuffer, don't release separately - slotMemory = null; - deweyIdMemory = null; - stringValueMemory = null; // CRITICAL: Must be nulled for columnar string storage + stringValueMemory = null; // CRITICAL: Must be nulled for columnar string storage } else if (!externallyAllocatedMemory) { - // Release memory segments to the allocator pool (non-zero-copy path) + // Release memory segments to the allocator pool try { - if (slotMemory != null && slotMemory.byteSize() > 0) { - segmentAllocator.release(slotMemory); - } - if (deweyIdMemory != null && deweyIdMemory.byteSize() > 0) { - segmentAllocator.release(deweyIdMemory); - } if (stringValueMemory != null && stringValueMemory.byteSize() > 0) { segmentAllocator.release(stringValueMemory); } } catch (Throwable e) { LOGGER.debug("Failed to release memory segments for page {}: {}", recordPageKey, e.getMessage()); } - slotMemory = null; - deweyIdMemory = null; stringValueMemory = null; } + + // Unbind all flyweight nodes BEFORE releasing memory — they may still be + // referenced by cursors/transactions and must fall back to Java field values. + if (slottedPage != null) { + for (final DataRecord record : records) { + if (record instanceof FlyweightNode fn && fn.isBound()) { + fn.unbind(); + } + } + try { + segmentAllocator.release(slottedPage.reinterpret(slottedPageCapacity)); + } catch (Throwable e) { + LOGGER.debug("Failed to release slotted page for page {}: {}", recordPageKey, e.getMessage()); + } + slottedPage = null; + slottedPageCapacity = 0; + } // Clear FSST symbol table fsstSymbolTable = null; - parsedFsstSymbols = null; // Clear references to aid garbage collection Arrays.fill(records, null); - inMemoryRecordCount = 0; - demotionThreshold = MIN_DEMOTION_THRESHOLD; - rematerializedRecordsSinceLastDemotion = 0; - if (references != null) { - references.clear(); - } + references.clear(); bytes = null; + compressedSegment = null; hashCode = null; } /** - * Get the actual memory size used by this page's memory segments. Used for accurate Caffeine cache - * weighing. + * Get the actual memory size used by this page's memory segments. + * Used for accurate Caffeine cache weighing. * * @return Total size in bytes of all memory segments used by this page */ public long getActualMemorySize() { long total = 0; - if (slotMemory != null) { - total += slotMemory.byteSize(); - } - if (deweyIdMemory != null) { - total += deweyIdMemory.byteSize(); - } if (stringValueMemory != null) { total += stringValueMemory.byteSize(); } + if (slottedPage != null) { + total += slottedPageCapacity; + } return total; } @@ -2104,24 +1901,12 @@ public byte[] getFsstSymbolTable() { } /** - * Set the FSST symbol table for string compression. Pre-parses the symbol table to avoid - * redundant parsing on every encode/decode call. - * + * Set the FSST symbol table for string compression. + * * @param symbolTable the symbol table bytes */ public void setFsstSymbolTable(byte[] symbolTable) { this.fsstSymbolTable = symbolTable; - this.parsedFsstSymbols = - (symbolTable != null && symbolTable.length > 0) ? FSSTCompressor.parseSymbolTable(symbolTable) : null; - } - - /** - * Get the pre-parsed FSST symbol table. - * - * @return the parsed symbol arrays, or null if FSST is not used - */ - public byte[][] getParsedFsstSymbols() { - return parsedFsstSymbols; } /** @@ -2134,12 +1919,12 @@ public MemorySegment getStringValueSegment(int slotNumber) { if (stringValueMemory == null || slotNumber < 0 || slotNumber >= stringValueOffsets.length) { return null; } - + int offset = stringValueOffsets[slotNumber]; if (offset < 0) { return null; } - + // Calculate length: either to next offset or to end of used space int length; if (slotNumber == lastStringValueIndex) { @@ -2159,11 +1944,11 @@ public MemorySegment getStringValueSegment(int slotNumber) { length = stringValueMemoryFreeSpaceStart - offset; } } - + if (length <= 0) { return null; } - + return stringValueMemory.asSlice(offset, length); } @@ -2175,11 +1960,15 @@ public MemorySegment getStringValueSegment(int slotNumber) { * @param lastStringValueIndex the index of the last string value slot * @param stringValueMemoryFreeSpaceStart the end of used space in stringValueMemory */ - public void setStringValueData(MemorySegment stringValueMemory, int[] stringValueOffsets, int lastStringValueIndex, - int stringValueMemoryFreeSpaceStart) { + public void setStringValueData( + MemorySegment stringValueMemory, + int[] stringValueOffsets, + int lastStringValueIndex, + int stringValueMemoryFreeSpaceStart + ) { this.stringValueMemory = stringValueMemory; if (stringValueOffsets != null) { - System.arraycopy(stringValueOffsets, 0, this.stringValueOffsets, 0, + System.arraycopy(stringValueOffsets, 0, this.stringValueOffsets, 0, Math.min(stringValueOffsets.length, this.stringValueOffsets.length)); } this.lastStringValueIndex = lastStringValueIndex; @@ -2233,23 +2022,16 @@ public int getStringValueMemoryFreeSpaceStart() { @Override public List getReferences() { - if (references == null || references.isEmpty()) { - return List.of(); - } return List.of(references.values().toArray(new PageReference[0])); } @Override - public void commit(final @NonNull StorageEngineWriter storageEngineWriter) { - addReferences(storageEngineWriter.getResourceSession().getResourceConfig()); - final var refs = references; - if (refs == null) { - return; - } - for (final PageReference reference : refs.values()) { + public void commit(final @NonNull StorageEngineWriter pageWriteTrx) { + addReferences(pageWriteTrx.getResourceSession().getResourceConfig()); + for (final PageReference reference : references.values()) { if (!(reference.getPage() == null && reference.getKey() == Constants.NULL_ID_LONG && reference.getLogKey() == Constants.NULL_ID_LONG)) { - storageEngineWriter.commit(reference); + pageWriteTrx.commit(reference); } } } @@ -2266,36 +2048,28 @@ public boolean setOrCreateReference(int offset, PageReference pageReference) { @Override public void setPageReference(final long key, @NonNull final PageReference reference) { - referencesOrInit().put(key, reference); + references.put(key, reference); } @Override public Set> referenceEntrySet() { - return references != null ? references.entrySet() : Set.of(); + return references.entrySet(); } @Override public PageReference getPageReference(final long key) { - return references != null ? references.get(key) : null; - } - - private Map referencesOrInit() { - var refs = references; - if (refs == null) { - refs = new ConcurrentHashMap<>(); - references = refs; - } - return refs; + return references.get(key); } @Override public MemorySegment slots() { - return slotMemory; + return slottedPage; } @Override public MemorySegment deweyIds() { - return deweyIdMemory; + // DeweyIDs are inline in the slotted page heap — no separate memory + return null; } @Override @@ -2309,8 +2083,8 @@ public int getRevision() { } /** - * Get the current version of this page frame. Used for detecting page reuse via version counter - * check. + * Get the current version of this page frame. + * Used for detecting page reuse via version counter check. * * @return current version number */ @@ -2319,22 +2093,25 @@ public int getVersion() { } /** - * Increment the version counter. Called when the page frame is reused for a different logical page. + * Increment the version counter. + * Called when the page frame is reused for a different logical page. */ public void incrementVersion() { version.incrementAndGet(); } /** - * Acquire a guard on this page (increment guard count). Pages with active guards cannot be evicted. + * Acquire a guard on this page (increment guard count). + * Pages with active guards cannot be evicted. */ public void acquireGuard() { guardCount.incrementAndGet(); } /** - * Try to acquire a guard on this page. Returns false if the page is orphaned or closed (cannot be - * used). This is the synchronized version that prevents race conditions with close(). + * Try to acquire a guard on this page. + * Returns false if the page is orphaned or closed (cannot be used). + * This is the synchronized version that prevents race conditions with close(). * * @return true if guard was acquired, false if page is orphaned/closed */ @@ -2348,9 +2125,9 @@ public synchronized boolean tryAcquireGuard() { } /** - * Release a guard on this page (decrement guard count). If the page is orphaned and this was the - * last guard, the page is closed. This ensures deterministic cleanup without relying on - * GC/finalizers. + * Release a guard on this page (decrement guard count). + * If the page is orphaned and this was the last guard, the page is closed. + * This ensures deterministic cleanup without relying on GC/finalizers. */ public synchronized void releaseGuard() { guardCount.decrementAndGet(); @@ -2362,8 +2139,9 @@ public synchronized void releaseGuard() { } /** - * Mark this page as orphaned using lock-free CAS. Called when the page is removed from cache but - * still has active guards. The page will be closed when the last guard is released. + * Mark this page as orphaned using lock-free CAS. + * Called when the page is removed from cache but still has active guards. + * The page will be closed when the last guard is released. */ public void markOrphaned() { int current; @@ -2385,7 +2163,8 @@ public boolean isOrphaned() { } /** - * Get the current guard count. Used by ClockSweeper to check if page can be evicted. + * Get the current guard count. + * Used by ClockSweeper to check if page can be evicted. * * @return current guard count */ @@ -2394,11 +2173,12 @@ public int getGuardCount() { } /** - * Mark this page as recently accessed (set HOT bit). Called on every page access for clock eviction - * algorithm. + * Mark this page as recently accessed (set HOT bit). + * Called on every page access for clock eviction algorithm. *

- * Uses opaque memory access (no memory barriers) for maximum performance. The HOT bit is advisory - - * stale reads are acceptable and will at worst give a page an extra second chance during eviction. + * Uses opaque memory access (no memory barriers) for maximum performance. + * The HOT bit is advisory - stale reads are acceptable and will at worst + * give a page an extra second chance during eviction. *

*/ public void markAccessed() { @@ -2443,56 +2223,48 @@ public void clearHot() { } /** - * Reset page data structures for reuse. Clears records and internal state but keeps MemorySegments - * allocated. Used when evicting a page to prepare frame for reuse. + * Reset page data structures for reuse. + * Clears records and internal state but keeps MemorySegments allocated. + * Used when evicting a page to prepare frame for reuse. */ public void reset() { // Clear record arrays Arrays.fill(records, null); - inMemoryRecordCount = 0; - demotionThreshold = MIN_DEMOTION_THRESHOLD; - rematerializedRecordsSinceLastDemotion = 0; - - // Clear offsets - Arrays.fill(slotOffsets, -1); - Arrays.fill(deweyIdOffsets, -1); // Clear slot bitmap (all slots now empty) Arrays.fill(slotBitmap, 0L); - Arrays.fill(fixedFormatBitmap, 0L); - Arrays.fill(fixedSlotKinds, NO_FIXED_SLOT_KIND); - // Reset free space pointers - slotMemoryFreeSpaceStart = 0; - deweyIdMemoryFreeSpaceStart = 0; - lastSlotIndex = -1; - lastDeweyIdIndex = areDeweyIDsStored - ? -1 - : -1; + // Reset slotted page state (bitmap and heap pointers) + if (slottedPage != null) { + PageLayout.initializePage(slottedPage, recordPageKey, revision, + indexType.getID(), areDeweyIDsStored); + } + // Reset index trackers + lastSlotIndex = -1; + lastDeweyIdIndex = -1; + // Clear references - if (references != null) { - references.clear(); - } + references.clear(); addedReferences = false; - + // Clear cached data bytes = null; hashCode = null; - + // CRITICAL: Guard count MUST be 0 before reset int currentGuardCount = guardCount.get(); if (currentGuardCount != 0) { - throw new IllegalStateException(String.format( - "CRITICAL BUG: reset() called on page with active guards! " - + "Page %d (%s) rev=%d guardCount=%d - this will cause guard count corruption!", - recordPageKey, indexType, revision, currentGuardCount)); + throw new IllegalStateException( + String.format("CRITICAL BUG: reset() called on page with active guards! " + + "Page %d (%s) rev=%d guardCount=%d - this will cause guard count corruption!", + recordPageKey, indexType, revision, currentGuardCount)); } hash = 0; - + // Clear HOT bit using lock-free operation clearHot(); - + // NOTE: We do NOT release MemorySegments here - they stay allocated // The allocator's release() method is called separately if needed } @@ -2504,36 +2276,22 @@ public void addReferences(final ResourceConfiguration resourceConfiguration) { // This is the deferred work from combineRecordPagesForModification for DIFFERENTIAL, // INCREMENTAL (full-dump), and SLIDING_SNAPSHOT versioning types. if (preservationBitmap != null && completePageRef != null) { - for (int wordIndex = 0; wordIndex < BITMAP_WORDS; wordIndex++) { - long word = preservationBitmap[wordIndex]; - int baseSlot = wordIndex << 6; - while (word != 0) { - int bit = Long.numberOfTrailingZeros(word); - int slotIndex = baseSlot + bit; - - // Preserve only when the slot is still absent from the modified page. - // This keeps write-intent data authoritative and avoids rematerialization churn. - if (records[slotIndex] == null && !hasSlot(slotIndex)) { - MemorySegment slotData = completePageRef.getSlot(slotIndex); - if (slotData != null) { - setSlot(slotData, slotIndex); - final NodeKind fixedNodeKind = completePageRef.getFixedSlotNodeKind(slotIndex); - if (fixedNodeKind != null) { - markSlotAsFixedFormat(slotIndex, fixedNodeKind); - } else { - markSlotAsCompactFormat(slotIndex); - } - } - - if (areDeweyIDsStored) { - MemorySegment deweyId = completePageRef.getDeweyId(slotIndex); - if (deweyId != null) { - setDeweyId(deweyId, slotIndex); - } + for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { + // Check if slot needs preservation AND wasn't modified (neither in records[] nor in slot data) + boolean needsPreservation = (preservationBitmap[i >>> 6] & (1L << (i & 63))) != 0; + if (needsPreservation && records[i] == null && getSlot(i) == null) { + // Copy slot from completePage, preserving nodeKindId + MemorySegment slotData = completePageRef.getSlot(i); + if (slotData != null) { + setSlotWithNodeKind(slotData, i, completePageRef.getSlotNodeKindId(i)); + } + // Copy deweyId too if stored + if (areDeweyIDsStored) { + MemorySegment deweyId = completePageRef.getDeweyId(i); + if (deweyId != null) { + setDeweyId(deweyId, i); } } - - word &= word - 1; } } } @@ -2542,11 +2300,8 @@ public void addReferences(final ResourceConfiguration resourceConfiguration) { processEntries(resourceConfiguration, records); for (int i = 0; i < records.length; i++) { final DataRecord record = records[i]; - if (record != null && record.getNodeKey() != 0) { - final byte[] deweyBytes = record.getDeweyIDAsBytes(); - if (deweyBytes != null) { - setDeweyId(deweyBytes, i); - } + if (record != null && record.getDeweyID() != null && record.getNodeKey() != 0) { + setDeweyId(record.getDeweyID().toBytes(), i); } } } else { @@ -2567,10 +2322,37 @@ private void processEntries(final ResourceConfiguration resourceConfiguration, f // This eliminates ~N allocations where N = number of non-null records. var reusableOut = new MemorySegmentBytesOut(tempArena, 256); - for (final DataRecord record : records) { + for (int i = 0; i < records.length; i++) { + final DataRecord record = records[i]; if (record == null) { + // Write singletons (FlyweightNode.isWriteSingleton()) are never stored in records[] — + // their data is already serialized to the slotted page heap via serializeToHeap() in setRecord(). continue; } + if (record instanceof FlyweightNode fn) { + if (fn.isBound()) { + // Record data is already in the heap via serializeToHeap() — skip serialization. + // However, DeweyIDs are stored separately and may have been updated after binding + // (e.g., by computeNewDeweyIDs during moveSubtreeToFirstChild). Since records[i] + // is about to be nulled, persist the DeweyID to the heap now. + if (areDeweyIDsStored && fn.getDeweyID() != null && fn.getNodeKey() != 0) { + final byte[] deweyIdBytes = fn.getDeweyIDAsBytes(); + if (deweyIdBytes != null && deweyIdBytes.length > 0) { + setDeweyIdToHeap(MemorySegment.ofArray(deweyIdBytes), i); + } + } + records[i] = null; + continue; + } + // Unbound flyweight (e.g., value mutation caused unbind): re-serialize to slotted page heap. + if (slottedPage != null) { + final long nodeKey = record.getNodeKey(); + final int offset = StorageEngineReader.recordPageOffset(nodeKey); + serializeToHeap(fn, nodeKey, offset); + records[i] = null; + continue; + } + } final var recordID = record.getNodeKey(); final var offset = StorageEngineReader.recordPageOffset(recordID); @@ -2588,78 +2370,21 @@ private void processEntries(final ResourceConfiguration resourceConfiguration, f final var reference = new PageReference(); reference.setPage(new OverflowPage(persistentBuffer)); - referencesOrInit().put(recordID, reference); + references.put(recordID, reference); } else { - // Normal record: setSlot copies data to slotMemory, so temp buffer is fine + // Normal record: setSlot copies data to slotted page heap (slotted page heap) setSlot(buffer, offset); - markSlotAsCompactFormat(offset); } + // Clear record reference after serialization — snapshot isolation. + // Data is now in slotMemory/slottedPage; prevents cross-transaction aliasing. + records[i] = null; } } // Confined arena automatically closes here, freeing all temporary buffers } - void compactFixedSlotsForCommit(final ResourceConfiguration resourceConfiguration) { - // Quick exit: no fixed-format slots → nothing to compact. - boolean hasFixedSlots = false; - for (int w = 0; w < BITMAP_WORDS; w++) { - if (fixedFormatBitmap[w] != 0) { - hasFixedSlots = true; - break; - } - } - if (!hasFixedSlots) { - return; - } - - // In-place compaction: compact format is always <= fixed format in size, - // so we overwrite each fixed slot at its current offset. No buffer allocation needed. - // Any gaps left by shrunk slots are eliminated by downstream compactLengthPrefixedRegion(). - final MemorySegmentBytesOut compactBuffer = COMPACT_BUFFER.get(); - - for (int wordIndex = 0; wordIndex < BITMAP_WORDS; wordIndex++) { - long word = fixedFormatBitmap[wordIndex]; - final int baseSlot = wordIndex << 6; - while (word != 0) { - final int bit = Long.numberOfTrailingZeros(word); - final int slotIndex = baseSlot + bit; - final long nodeKey = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + slotIndex; - - final NodeKind nodeKind = getFixedSlotNodeKind(slotIndex); - final int slotOffset = slotOffsets[slotIndex]; - final long slotDataOffset = slotOffset + INT_SIZE; - - if (nodeKind == null) { - throw new IllegalStateException("Missing fixed-slot metadata for node key " + nodeKey); - } - - // Direct byte-level transformation: fixed → compact without materializing a DataRecord. - // Uses base-offset into slotMemory to avoid asSlice() allocation per slot. - compactBuffer.clear(); - FixedToCompactTransformer.transform(nodeKind, nodeKey, slotMemory, slotDataOffset, resourceConfiguration, - compactBuffer); - final MemorySegment compactBytes = compactBuffer.getDestination(); - final int compactSize = (int) compactBytes.byteSize(); - if (compactSize > PageConstants.MAX_RECORD_SIZE) { - throw new IllegalStateException("Compacted record exceeds max size for node key " + nodeKey); - } - - // Overwrite in-place: compact bytes fit within the fixed slot's footprint. - // Offset unchanged — only the length prefix and data bytes are rewritten. - slotMemory.set(JAVA_INT_UNALIGNED, slotOffset, compactSize); - MemorySegment.copy(compactBytes, 0, slotMemory, slotOffset + INT_SIZE, compactSize); - - word &= word - 1; - } - } - - // Clear fixed-format tracking — all slots are now compact format. - Arrays.fill(fixedFormatBitmap, 0L); - Arrays.fill(fixedSlotKinds, NO_FIXED_SLOT_KIND); - } - /** - * Build FSST symbol table from all string values in this page. This should be called before - * serialization to enable page-level compression. + * Build FSST symbol table from all string values in this page. + * This should be called before serialization to enable page-level compression. * * @param resourceConfig the resource configuration * @return true if FSST compression is enabled and symbol table was built @@ -2669,57 +2394,58 @@ public boolean buildFsstSymbolTable(ResourceConfiguration resourceConfig) { return false; } + final int stringValueId = NodeKind.STRING_VALUE.getId(); + final int objectStringValueId = NodeKind.OBJECT_STRING_VALUE.getId(); + // Collect all string values from StringNode and ObjectStringNode java.util.ArrayList stringSamples = new java.util.ArrayList<>(); + // Scan records[] for non-FlyweightNode string records (legacy path) for (final DataRecord record : records) { if (record == null) { continue; } if (record instanceof StringNode stringNode) { - addStringSample(stringSamples, stringNode.getRawValueWithoutDecompression()); + byte[] value = stringNode.getRawValueWithoutDecompression(); + if (value != null && value.length > 0) { + stringSamples.add(value); + } } else if (record instanceof ObjectStringNode objectStringNode) { - addStringSample(stringSamples, objectStringNode.getRawValueWithoutDecompression()); - } else if (record instanceof TextNode textNode) { - addStringSample(stringSamples, textNode.getRawValueWithoutDecompression()); - } else if (record instanceof CommentNode commentNode) { - addStringSample(stringSamples, commentNode.getRawValueWithoutDecompression()); - } else if (record instanceof PINode piNode) { - addStringSample(stringSamples, piNode.getRawValueWithoutDecompression()); - } else if (record instanceof AttributeNode attrNode) { - addStringSample(stringSamples, attrNode.getRawValueWithoutDecompression()); + byte[] value = objectStringNode.getRawValueWithoutDecompression(); + if (value != null && value.length > 0) { + stringSamples.add(value); + } } } - // Include string values only present in slot memory (demoted records). - // This is commit-path work and preserves FSST sample completeness. - // Fixed-format slots are already compacted before this method runs. - for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { - if (records[i] != null) { - continue; - } - - final MemorySegment slot = getSlot(i); - if (slot == null) { - continue; - } - - final long nodeKey = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + i; - final DataRecord record = resourceConfig.recordPersister.deserialize(new MemorySegmentBytesIn(slot), nodeKey, - getDeweyIdAsByteArray(i), resourceConfig); - - if (record instanceof StringNode stringNode) { - addStringSample(stringSamples, stringNode.getRawValueWithoutDecompression()); - } else if (record instanceof ObjectStringNode objectStringNode) { - addStringSample(stringSamples, objectStringNode.getRawValueWithoutDecompression()); - } else if (record instanceof TextNode textNode) { - addStringSample(stringSamples, textNode.getRawValueWithoutDecompression()); - } else if (record instanceof CommentNode commentNode) { - addStringSample(stringSamples, commentNode.getRawValueWithoutDecompression()); - } else if (record instanceof PINode piNode) { - addStringSample(stringSamples, piNode.getRawValueWithoutDecompression()); - } else if (record instanceof AttributeNode attrNode) { - addStringSample(stringSamples, attrNode.getRawValueWithoutDecompression()); + // Scan slotted page for FlyweightNode strings (zero records[] path) + if (slottedPage != null) { + for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { + if (records[i] != null) continue; // Already scanned above + if (!PageLayout.isSlotPopulated(slottedPage, i)) continue; + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, i); + if (nodeKindId == stringValueId || nodeKindId == objectStringValueId) { + final int heapOff = PageLayout.getDirHeapOffset(slottedPage, i); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOff); + final long nodeKey = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + i; + if (nodeKindId == stringValueId) { + fsstStringFlyweight.bind(slottedPage, recordBase, nodeKey, i); + try { + byte[] value = fsstStringFlyweight.getRawValueWithoutDecompression(); + if (value != null && value.length > 0) stringSamples.add(value); + } finally { + fsstStringFlyweight.clearBinding(); + } + } else { + fsstObjStringFlyweight.bind(slottedPage, recordBase, nodeKey, i); + try { + byte[] value = fsstObjStringFlyweight.getRawValueWithoutDecompression(); + if (value != null && value.length > 0) stringSamples.add(value); + } finally { + fsstObjStringFlyweight.clearBinding(); + } + } + } } } @@ -2732,7 +2458,6 @@ public boolean buildFsstSymbolTable(ResourceConfiguration resourceConfig) { if (candidateTable != null && candidateTable.length > 0 && FSSTCompressor.isCompressionBeneficial(stringSamples, candidateTable)) { this.fsstSymbolTable = candidateTable; - this.parsedFsstSymbols = FSSTCompressor.parseSymbolTable(candidateTable); return true; } } @@ -2740,37 +2465,29 @@ public boolean buildFsstSymbolTable(ResourceConfiguration resourceConfig) { return false; } - private static void addStringSample(java.util.ArrayList samples, byte[] value) { - if (value != null && value.length > 0) { - samples.add(value); - } - } - /** - * Compress all string values in the page using the pre-built FSST symbol table. This modifies the - * string nodes in place to use compressed values. Must be called after buildFsstSymbolTable(). + * Compress all string values in the page using the pre-built FSST symbol table. + * This modifies the string nodes in place to use compressed values. + * Must be called after buildFsstSymbolTable(). */ public void compressStringValues() { if (fsstSymbolTable == null || fsstSymbolTable.length == 0) { return; } - // Use pre-parsed symbols to avoid re-parsing for every string node - final byte[][] symbols = parsedFsstSymbols; - if (symbols == null || symbols.length == 0) { - return; - } + final int stringValueId = NodeKind.STRING_VALUE.getId(); + final int objectStringValueId = NodeKind.OBJECT_STRING_VALUE.getId(); + // Compress records[] strings (legacy path) for (final DataRecord record : records) { if (record == null) { continue; } if (record instanceof StringNode stringNode) { if (!stringNode.isCompressed()) { - final byte[] originalValue = stringNode.getRawValueWithoutDecompression(); + byte[] originalValue = stringNode.getRawValueWithoutDecompression(); if (originalValue != null && originalValue.length > 0) { - final byte[] compressedValue = FSSTCompressor.encode(originalValue, symbols); - // Only use compressed value if it's actually smaller + byte[] compressedValue = FSSTCompressor.encode(originalValue, fsstSymbolTable); if (compressedValue.length < originalValue.length) { stringNode.setRawValue(compressedValue, true, fsstSymbolTable); } @@ -2778,10 +2495,9 @@ public void compressStringValues() { } } else if (record instanceof ObjectStringNode objectStringNode) { if (!objectStringNode.isCompressed()) { - final byte[] originalValue = objectStringNode.getRawValueWithoutDecompression(); + byte[] originalValue = objectStringNode.getRawValueWithoutDecompression(); if (originalValue != null && originalValue.length > 0) { - final byte[] compressedValue = FSSTCompressor.encode(originalValue, symbols); - // Only use compressed value if it's actually smaller + byte[] compressedValue = FSSTCompressor.encode(originalValue, fsstSymbolTable); if (compressedValue.length < originalValue.length) { objectStringNode.setRawValue(compressedValue, true, fsstSymbolTable); } @@ -2789,27 +2505,71 @@ public void compressStringValues() { } } } + + // Compress slotted page strings (zero records[] path) + if (slottedPage != null) { + for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { + if (records[i] != null) continue; // Already handled above + if (!PageLayout.isSlotPopulated(slottedPage, i)) continue; + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, i); + if (nodeKindId != stringValueId && nodeKindId != objectStringValueId) continue; + + final int heapOff = PageLayout.getDirHeapOffset(slottedPage, i); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOff); + final long nodeKey = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + i; + + if (nodeKindId == stringValueId) { + fsstStringFlyweight.bind(slottedPage, recordBase, nodeKey, i); + fsstStringFlyweight.setOwnerPage(this); // Enable write-through + try { + byte[] originalValue = fsstStringFlyweight.getRawValueWithoutDecompression(); + if (originalValue != null && originalValue.length > 0 && !fsstStringFlyweight.isCompressed()) { + byte[] compressed = FSSTCompressor.encode(originalValue, fsstSymbolTable); + if (compressed.length < originalValue.length) { + fsstStringFlyweight.setRawValue(compressed, true, fsstSymbolTable); + } + } + } finally { + fsstStringFlyweight.setOwnerPage(null); + fsstStringFlyweight.clearBinding(); + } + } else { + fsstObjStringFlyweight.bind(slottedPage, recordBase, nodeKey, i); + fsstObjStringFlyweight.setOwnerPage(this); // Enable write-through + try { + byte[] originalValue = fsstObjStringFlyweight.getRawValueWithoutDecompression(); + if (originalValue != null && originalValue.length > 0 && !fsstObjStringFlyweight.isCompressed()) { + byte[] compressed = FSSTCompressor.encode(originalValue, fsstSymbolTable); + if (compressed.length < originalValue.length) { + fsstObjStringFlyweight.setRawValue(compressed, true, fsstSymbolTable); + } + } + } finally { + fsstObjStringFlyweight.setOwnerPage(null); + fsstObjStringFlyweight.clearBinding(); + } + } + } + } } /** - * Set the FSST symbol table on all string nodes after deserialization. This allows nodes to use - * lazy decompression. + * Set the FSST symbol table on all string nodes after deserialization. + * This allows nodes to use lazy decompression. */ public void propagateFsstSymbolTableToNodes() { if (fsstSymbolTable == null || fsstSymbolTable.length == 0) { return; } - final byte[][] parsed = parsedFsstSymbols; - for (final DataRecord record : records) { if (record == null) { continue; } if (record instanceof StringNode stringNode) { - stringNode.setFsstSymbolTable(fsstSymbolTable, parsed); + stringNode.setFsstSymbolTable(fsstSymbolTable); } else if (record instanceof ObjectStringNode objectStringNode) { - objectStringNode.setFsstSymbolTable(fsstSymbolTable, parsed); + objectStringNode.setFsstSymbolTable(fsstSymbolTable); } } } @@ -2820,51 +2580,48 @@ public void propagateFsstSymbolTableToNodes() { * @param slotNumber the slot number in the page (0 to NDP_NODE_COUNT-1) * @param value the raw string value bytes */ - private record StringValueEntry(int slotNumber, byte[] value) { - } + private record StringValueEntry(int slotNumber, byte[] value) {} /** - * Collect string values into columnar storage for better compression. Groups all string data - * contiguously in stringValueMemory, which enables better FSST compression patterns and more - * efficient storage. + * Collect string values into columnar storage for better compression. + * Groups all string data contiguously in stringValueMemory, which enables + * better FSST compression patterns and more efficient storage. * - *

- * This should be called before serialization when columnar storage is desired. The columnar layout - * stores: [length1:4][data1:N][length2:4][data2:M]... with stringValueOffsets pointing to each - * entry's start. + *

This should be called before serialization when columnar storage is desired. + * The columnar layout stores: [length1:4][data1:N][length2:4][data2:M]... + * with stringValueOffsets pointing to each entry's start. * - *

- * Invariants maintained: + *

Invariants maintained: *

    - *
  • P4: All offsets are valid: 0 ≤ offset < stringValueMemory.byteSize()
  • - *
  • No overlapping entries
  • - *
  • Sequential layout with no gaps
  • + *
  • P4: All offsets are valid: 0 ≤ offset < stringValueMemory.byteSize()
  • + *
  • No overlapping entries
  • + *
  • Sequential layout with no gaps
  • *
*/ public void collectStringsForColumnarStorage() { java.util.List entries = new java.util.ArrayList<>(); int totalSize = 0; - + for (int i = 0; i < records.length; i++) { DataRecord record = records[i]; byte[] value = null; - + if (record instanceof StringNode sn) { value = sn.getRawValueWithoutDecompression(); } else if (record instanceof ObjectStringNode osn) { value = osn.getRawValueWithoutDecompression(); } - + if (value != null && value.length > 0) { entries.add(new StringValueEntry(i, value)); totalSize += INT_SIZE + value.length; // 4 bytes length prefix + data } } - + if (entries.isEmpty()) { return; } - + // Allocate columnar segment if needed if (stringValueMemory == null || stringValueMemory.byteSize() < totalSize) { if (stringValueMemory != null && !externallyAllocatedMemory) { @@ -2872,30 +2629,30 @@ public void collectStringsForColumnarStorage() { } stringValueMemory = segmentAllocator.allocate(totalSize); } - + // Store all string values contiguously int offset = 0; for (StringValueEntry entry : entries) { // Validate offset bounds (P4 invariant) if (offset + INT_SIZE + entry.value.length > stringValueMemory.byteSize()) { - throw new IllegalStateException( - String.format("Columnar storage overflow: offset=%d, entrySize=%d, memorySize=%d", offset, - INT_SIZE + entry.value.length, stringValueMemory.byteSize())); + throw new IllegalStateException(String.format( + "Columnar storage overflow: offset=%d, entrySize=%d, memorySize=%d", + offset, INT_SIZE + entry.value.length, stringValueMemory.byteSize())); } - + stringValueOffsets[entry.slotNumber] = offset; - + // Write length prefix stringValueMemory.set(java.lang.foreign.ValueLayout.JAVA_INT, offset, entry.value.length); - + // Write data using bulk copy - MemorySegment.copy(entry.value, 0, stringValueMemory, java.lang.foreign.ValueLayout.JAVA_BYTE, offset + INT_SIZE, - entry.value.length); - + MemorySegment.copy(entry.value, 0, stringValueMemory, + java.lang.foreign.ValueLayout.JAVA_BYTE, offset + INT_SIZE, entry.value.length); + offset += INT_SIZE + entry.value.length; lastStringValueIndex = Math.max(lastStringValueIndex, entry.slotNumber); } - + stringValueMemoryFreeSpaceStart = offset; } diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/NodeFieldLayout.java b/bundles/sirix-core/src/main/java/io/sirix/page/NodeFieldLayout.java new file mode 100644 index 000000000..872887bd0 --- /dev/null +++ b/bundles/sirix-core/src/main/java/io/sirix/page/NodeFieldLayout.java @@ -0,0 +1,337 @@ +package io.sirix.page; + +import io.sirix.node.NodeKind; + +/** + * Defines the per-record offset table layout for each {@link NodeKind}. + * + *

Each record in the heap has an offset table with one byte per field, + * enabling O(1) access to any field without parsing preceding varints. + * This class defines the field count and field index constants for each NodeKind. + * + *

Offset Table Format

+ *
+ * [nodeKind: 1 byte]
+ * [fieldOffsetTable: fieldCount × 1 byte]
+ * [data region: varint fields + hash + optional payload]
+ * 
+ * + *

ALL fields are always present in the slotted page format, including hash (0 when unused) + * and childCount/descendantCount (0 when unused). This eliminates conditional field logic + * and makes the offset table size fixed per NodeKind. + */ +public final class NodeFieldLayout { + + private NodeFieldLayout() { + throw new AssertionError("Utility class"); + } + + /** Fixed width of hash field in bytes (always 8). */ + public static final int HASH_WIDTH = Long.BYTES; + + // ==================== OBJECT NODE (10 fields) ==================== + + /** Total field count for OBJECT nodes. */ + public static final int OBJECT_FIELD_COUNT = 10; + + public static final int OBJECT_PARENT_KEY = 0; + public static final int OBJECT_RIGHT_SIB_KEY = 1; + public static final int OBJECT_LEFT_SIB_KEY = 2; + public static final int OBJECT_FIRST_CHILD_KEY = 3; + public static final int OBJECT_LAST_CHILD_KEY = 4; + public static final int OBJECT_PREV_REVISION = 5; + public static final int OBJECT_LAST_MOD_REVISION = 6; + public static final int OBJECT_HASH = 7; + public static final int OBJECT_CHILD_COUNT = 8; + public static final int OBJECT_DESCENDANT_COUNT = 9; + + // ==================== ARRAY NODE (11 fields) ==================== + + /** Total field count for ARRAY nodes. */ + public static final int ARRAY_FIELD_COUNT = 11; + + public static final int ARRAY_PARENT_KEY = 0; + public static final int ARRAY_RIGHT_SIB_KEY = 1; + public static final int ARRAY_LEFT_SIB_KEY = 2; + public static final int ARRAY_FIRST_CHILD_KEY = 3; + public static final int ARRAY_LAST_CHILD_KEY = 4; + public static final int ARRAY_PATH_NODE_KEY = 5; + public static final int ARRAY_PREV_REVISION = 6; + public static final int ARRAY_LAST_MOD_REVISION = 7; + public static final int ARRAY_HASH = 8; + public static final int ARRAY_CHILD_COUNT = 9; + public static final int ARRAY_DESCENDANT_COUNT = 10; + + // ==================== OBJECT_KEY NODE (10 fields) ==================== + + /** Total field count for OBJECT_KEY nodes. */ + public static final int OBJECT_KEY_FIELD_COUNT = 10; + + public static final int OBJKEY_PARENT_KEY = 0; + public static final int OBJKEY_RIGHT_SIB_KEY = 1; + public static final int OBJKEY_LEFT_SIB_KEY = 2; + public static final int OBJKEY_FIRST_CHILD_KEY = 3; + public static final int OBJKEY_NAME_KEY = 4; + public static final int OBJKEY_PATH_NODE_KEY = 5; + public static final int OBJKEY_PREV_REVISION = 6; + public static final int OBJKEY_LAST_MOD_REVISION = 7; + public static final int OBJKEY_HASH = 8; + public static final int OBJKEY_DESCENDANT_COUNT = 9; + + // ==================== STRING_VALUE NODE (7 fields + payload) ==================== + + /** Total field count for STRING_VALUE nodes (excluding payload). */ + public static final int STRING_VALUE_FIELD_COUNT = 6; + + public static final int STRVAL_PARENT_KEY = 0; + public static final int STRVAL_RIGHT_SIB_KEY = 1; + public static final int STRVAL_LEFT_SIB_KEY = 2; + public static final int STRVAL_PREV_REVISION = 3; + public static final int STRVAL_LAST_MOD_REVISION = 4; + /** Points to the start of [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int STRVAL_PAYLOAD = 5; + + // ==================== NUMBER_VALUE NODE (7 fields + payload) ==================== + + /** Total field count for NUMBER_VALUE nodes (excluding payload). */ + public static final int NUMBER_VALUE_FIELD_COUNT = 6; + + public static final int NUMVAL_PARENT_KEY = 0; + public static final int NUMVAL_RIGHT_SIB_KEY = 1; + public static final int NUMVAL_LEFT_SIB_KEY = 2; + public static final int NUMVAL_PREV_REVISION = 3; + public static final int NUMVAL_LAST_MOD_REVISION = 4; + /** Points to the start of [numberType:1][numberData:variable]. */ + public static final int NUMVAL_PAYLOAD = 5; + + // ==================== BOOLEAN_VALUE NODE (7 fields) ==================== + + /** Total field count for BOOLEAN_VALUE nodes. */ + public static final int BOOLEAN_VALUE_FIELD_COUNT = 6; + + public static final int BOOLVAL_PARENT_KEY = 0; + public static final int BOOLVAL_RIGHT_SIB_KEY = 1; + public static final int BOOLVAL_LEFT_SIB_KEY = 2; + public static final int BOOLVAL_PREV_REVISION = 3; + public static final int BOOLVAL_LAST_MOD_REVISION = 4; + public static final int BOOLVAL_VALUE = 5; + + // ==================== NULL_VALUE NODE (6 fields) ==================== + + /** Total field count for NULL_VALUE nodes. */ + public static final int NULL_VALUE_FIELD_COUNT = 5; + + public static final int NULLVAL_PARENT_KEY = 0; + public static final int NULLVAL_RIGHT_SIB_KEY = 1; + public static final int NULLVAL_LEFT_SIB_KEY = 2; + public static final int NULLVAL_PREV_REVISION = 3; + public static final int NULLVAL_LAST_MOD_REVISION = 4; + + // ==================== OBJECT_STRING_VALUE NODE (5 fields + payload) ==================== + + /** Total field count for OBJECT_STRING_VALUE nodes (excluding payload). */ + public static final int OBJECT_STRING_VALUE_FIELD_COUNT = 4; + + public static final int OBJSTRVAL_PARENT_KEY = 0; + public static final int OBJSTRVAL_PREV_REVISION = 1; + public static final int OBJSTRVAL_LAST_MOD_REVISION = 2; + /** Points to the start of [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int OBJSTRVAL_PAYLOAD = 3; + + // ==================== OBJECT_NUMBER_VALUE NODE (5 fields + payload) ==================== + + /** Total field count for OBJECT_NUMBER_VALUE nodes (excluding payload). */ + public static final int OBJECT_NUMBER_VALUE_FIELD_COUNT = 4; + + public static final int OBJNUMVAL_PARENT_KEY = 0; + public static final int OBJNUMVAL_PREV_REVISION = 1; + public static final int OBJNUMVAL_LAST_MOD_REVISION = 2; + /** Points to the start of [numberType:1][numberData:variable]. */ + public static final int OBJNUMVAL_PAYLOAD = 3; + + // ==================== OBJECT_BOOLEAN_VALUE NODE (5 fields) ==================== + + /** Total field count for OBJECT_BOOLEAN_VALUE nodes. */ + public static final int OBJECT_BOOLEAN_VALUE_FIELD_COUNT = 4; + + public static final int OBJBOOLVAL_PARENT_KEY = 0; + public static final int OBJBOOLVAL_PREV_REVISION = 1; + public static final int OBJBOOLVAL_LAST_MOD_REVISION = 2; + public static final int OBJBOOLVAL_VALUE = 3; + + // ==================== OBJECT_NULL_VALUE NODE (4 fields) ==================== + + /** Total field count for OBJECT_NULL_VALUE nodes. */ + public static final int OBJECT_NULL_VALUE_FIELD_COUNT = 3; + + public static final int OBJNULLVAL_PARENT_KEY = 0; + public static final int OBJNULLVAL_PREV_REVISION = 1; + public static final int OBJNULLVAL_LAST_MOD_REVISION = 2; + + // ==================== JSON_DOCUMENT_ROOT (7 fields) ==================== + + /** Total field count for JSON_DOCUMENT_ROOT nodes. */ + public static final int JSON_DOCUMENT_ROOT_FIELD_COUNT = 7; + + public static final int JDOCROOT_FIRST_CHILD_KEY = 0; + public static final int JDOCROOT_LAST_CHILD_KEY = 1; + public static final int JDOCROOT_CHILD_COUNT = 2; + public static final int JDOCROOT_DESCENDANT_COUNT = 3; + public static final int JDOCROOT_PREV_REVISION = 4; + public static final int JDOCROOT_LAST_MOD_REVISION = 5; + public static final int JDOCROOT_HASH = 6; + + // ==================== XML ELEMENT NODE (15 fields + payload) ==================== + + /** Total field count for ELEMENT nodes (excluding attr/ns payload). */ + public static final int ELEMENT_FIELD_COUNT = 15; + + public static final int ELEM_PARENT_KEY = 0; + public static final int ELEM_RIGHT_SIB_KEY = 1; + public static final int ELEM_LEFT_SIB_KEY = 2; + public static final int ELEM_FIRST_CHILD_KEY = 3; + public static final int ELEM_LAST_CHILD_KEY = 4; + public static final int ELEM_PATH_NODE_KEY = 5; + public static final int ELEM_PREFIX_KEY = 6; + public static final int ELEM_LOCAL_NAME_KEY = 7; + public static final int ELEM_URI_KEY = 8; + public static final int ELEM_PREV_REVISION = 9; + public static final int ELEM_LAST_MOD_REVISION = 10; + public static final int ELEM_HASH = 11; + public static final int ELEM_CHILD_COUNT = 12; + public static final int ELEM_DESCENDANT_COUNT = 13; + /** Points to [attrCount:varint][attrKeys:delta...][nsCount:varint][nsKeys:delta...]. */ + public static final int ELEM_PAYLOAD = 14; + + // ==================== XML ATTRIBUTE NODE (9 fields + payload) ==================== + + /** Total field count for ATTRIBUTE nodes (excluding value payload). */ + public static final int ATTRIBUTE_FIELD_COUNT = 8; + + public static final int ATTR_PARENT_KEY = 0; + public static final int ATTR_PATH_NODE_KEY = 1; + public static final int ATTR_PREFIX_KEY = 2; + public static final int ATTR_LOCAL_NAME_KEY = 3; + public static final int ATTR_URI_KEY = 4; + public static final int ATTR_PREV_REVISION = 5; + public static final int ATTR_LAST_MOD_REVISION = 6; + /** Points to [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int ATTR_PAYLOAD = 7; + + // ==================== XML TEXT NODE (7 fields + payload) ==================== + + /** Total field count for TEXT nodes (excluding value payload). */ + public static final int TEXT_FIELD_COUNT = 6; + + public static final int TEXT_PARENT_KEY = 0; + public static final int TEXT_RIGHT_SIB_KEY = 1; + public static final int TEXT_LEFT_SIB_KEY = 2; + public static final int TEXT_PREV_REVISION = 3; + public static final int TEXT_LAST_MOD_REVISION = 4; + /** Points to [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int TEXT_PAYLOAD = 5; + + // ==================== XML COMMENT NODE (7 fields + payload) ==================== + + /** Total field count for COMMENT nodes (excluding value payload). */ + public static final int COMMENT_FIELD_COUNT = 6; + + public static final int COMMENT_PARENT_KEY = 0; + public static final int COMMENT_RIGHT_SIB_KEY = 1; + public static final int COMMENT_LEFT_SIB_KEY = 2; + public static final int COMMENT_PREV_REVISION = 3; + public static final int COMMENT_LAST_MOD_REVISION = 4; + /** Points to [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int COMMENT_PAYLOAD = 5; + + // ==================== XML PI NODE (15 fields + payload) ==================== + + /** Total field count for PROCESSING_INSTRUCTION nodes (excluding value payload). */ + public static final int PI_FIELD_COUNT = 14; + + public static final int PI_PARENT_KEY = 0; + public static final int PI_RIGHT_SIB_KEY = 1; + public static final int PI_LEFT_SIB_KEY = 2; + public static final int PI_FIRST_CHILD_KEY = 3; + public static final int PI_LAST_CHILD_KEY = 4; + public static final int PI_PATH_NODE_KEY = 5; + public static final int PI_PREFIX_KEY = 6; + public static final int PI_LOCAL_NAME_KEY = 7; + public static final int PI_URI_KEY = 8; + public static final int PI_PREV_REVISION = 9; + public static final int PI_LAST_MOD_REVISION = 10; + public static final int PI_CHILD_COUNT = 11; + public static final int PI_DESCENDANT_COUNT = 12; + /** Points to [isCompressed:1][valueLength:varint][value:bytes]. */ + public static final int PI_PAYLOAD = 13; + + // ==================== XML NAMESPACE NODE (8 fields) ==================== + + /** Total field count for NAMESPACE nodes. */ + public static final int NAMESPACE_FIELD_COUNT = 8; + + public static final int NS_PARENT_KEY = 0; + public static final int NS_PATH_NODE_KEY = 1; + public static final int NS_PREFIX_KEY = 2; + public static final int NS_LOCAL_NAME_KEY = 3; + public static final int NS_URI_KEY = 4; + public static final int NS_PREV_REVISION = 5; + public static final int NS_LAST_MOD_REVISION = 6; + public static final int NS_HASH = 7; + + // ==================== XML DOCUMENT ROOT NODE (7 fields) ==================== + + /** Total field count for XML_DOCUMENT_ROOT nodes. */ + public static final int XML_DOCUMENT_ROOT_FIELD_COUNT = 7; + + public static final int XDOCROOT_FIRST_CHILD_KEY = 0; + public static final int XDOCROOT_LAST_CHILD_KEY = 1; + public static final int XDOCROOT_CHILD_COUNT = 2; + public static final int XDOCROOT_DESCENDANT_COUNT = 3; + public static final int XDOCROOT_PREV_REVISION = 4; + public static final int XDOCROOT_LAST_MOD_REVISION = 5; + public static final int XDOCROOT_HASH = 6; + + // ==================== FIELD COUNT LOOKUP ==================== + + /** + * Get the field count for a given NodeKind. + * + * @param kindId the NodeKind byte ID + * @return the field count, or -1 if unknown/unsupported + */ + public static int fieldCountForKind(final int kindId) { + return switch (kindId) { + case 1 -> ELEMENT_FIELD_COUNT; // ELEMENT + case 2 -> ATTRIBUTE_FIELD_COUNT; // ATTRIBUTE + case 3 -> TEXT_FIELD_COUNT; // TEXT + case 7 -> PI_FIELD_COUNT; // PROCESSING_INSTRUCTION + case 8 -> COMMENT_FIELD_COUNT; // COMMENT + case 9 -> XML_DOCUMENT_ROOT_FIELD_COUNT; // XML_DOCUMENT + case 13 -> NAMESPACE_FIELD_COUNT; // NAMESPACE + case 24 -> OBJECT_FIELD_COUNT; // OBJECT + case 25 -> ARRAY_FIELD_COUNT; // ARRAY + case 26 -> OBJECT_KEY_FIELD_COUNT; // OBJECT_KEY + case 27 -> BOOLEAN_VALUE_FIELD_COUNT; // BOOLEAN_VALUE + case 28 -> NUMBER_VALUE_FIELD_COUNT; // NUMBER_VALUE + case 29 -> NULL_VALUE_FIELD_COUNT; // NULL_VALUE + case 30 -> STRING_VALUE_FIELD_COUNT; // STRING_VALUE + case 31 -> JSON_DOCUMENT_ROOT_FIELD_COUNT; // JSON_DOCUMENT_ROOT + case 40 -> OBJECT_STRING_VALUE_FIELD_COUNT; // OBJECT_STRING_VALUE + case 41 -> OBJECT_BOOLEAN_VALUE_FIELD_COUNT; // OBJECT_BOOLEAN_VALUE + case 42 -> OBJECT_NUMBER_VALUE_FIELD_COUNT; // OBJECT_NUMBER_VALUE + case 43 -> OBJECT_NULL_VALUE_FIELD_COUNT; // OBJECT_NULL_VALUE + default -> -1; + }; + } + + /** + * Get the field count for a given NodeKind enum. + * + * @param kind the NodeKind + * @return the field count, or -1 if unsupported + */ + public static int fieldCountForKind(final NodeKind kind) { + return fieldCountForKind(kind.getId()); + } +} diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/PageKind.java b/bundles/sirix-core/src/main/java/io/sirix/page/PageKind.java index cf839f007..26e0ca04c 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/page/PageKind.java +++ b/bundles/sirix-core/src/main/java/io/sirix/page/PageKind.java @@ -43,7 +43,6 @@ import java.lang.foreign.ValueLayout; import io.sirix.node.Utils; import io.sirix.node.Bytes; -import io.sirix.node.interfaces.DeweyIdSerializer; import io.sirix.node.interfaces.RecordSerializer; import io.sirix.page.delegates.BitmapReferencesPage; import io.sirix.page.delegates.FullReferencesPage; @@ -84,343 +83,253 @@ public Page deserializePage(final ResourceConfiguration resourceConfig, final By switch (binaryVersion) { case V0 -> { - final long recordPageKey = Utils.getVarLong(source); - final int revision = source.readInt(); - final IndexType indexType = IndexType.getType(source.readByte()); - final int lastSlotIndex = source.readInt(); - - // Read compressed slot offsets (delta + bit-packed) - final int[] slotOffsets = SlotOffsetCodec.decode(source); - - // Read slotMemory size - final int slotMemorySize = source.readInt(); - - // ZERO-COPY: Slice decompression buffer directly as slotMemory - // This eliminates per-slot MemorySegment.copy() calls (major performance win) - final boolean canZeroCopy = decompressionResult != null && source instanceof MemorySegmentBytesIn; - final MemorySegment slotMemory; - final MemorySegment backingBuffer; - final Runnable backingBufferReleaser; - - MemorySegmentAllocator memorySegmentAllocator = OS.isWindows() - ? WindowsMemorySegmentAllocator.getInstance() - : LinuxMemorySegmentAllocator.getInstance(); - if (canZeroCopy) { - // Zero-copy path: slice decompression buffer directly - final MemorySegment sourceSegment = ((MemorySegmentBytesIn) source).getSource(); - slotMemory = sourceSegment.asSlice(source.position(), slotMemorySize); - source.skip(slotMemorySize); - - // Transfer buffer ownership to page - backingBufferReleaser = decompressionResult.transferOwnership(); - backingBuffer = decompressionResult.backingBuffer(); - } else { - // Fallback: allocate and copy (for non-MemorySegment sources or no decompressionResult) - MemorySegmentAllocator allocator = memorySegmentAllocator; - slotMemory = allocator.allocate(slotMemorySize); - - // Copy slot data - if (source instanceof MemorySegmentBytesIn msSource) { - MemorySegment.copy(msSource.getSource(), source.position(), slotMemory, 0, slotMemorySize); - source.skip(slotMemorySize); - } else { - byte[] slotData = new byte[slotMemorySize]; - source.read(slotData); - MemorySegment.copy(slotData, 0, slotMemory, java.lang.foreign.ValueLayout.JAVA_BYTE, 0, slotMemorySize); - } - backingBuffer = null; - backingBufferReleaser = null; - } + return deserializeSlottedPage(resourceConfig, source); + } + default -> throw new IllegalStateException("Unknown binary encoding version: " + binaryVersion); + } + } - // Read dewey ID data if stored - final boolean areDeweyIDsStored = resourceConfig.areDeweyIDsStored; - final RecordSerializer recordPersister = resourceConfig.recordPersister; - final int[] deweyIdOffsets; - final MemorySegment deweyIdMemory; - final int lastDeweyIdIndex; - - if (areDeweyIDsStored && recordPersister instanceof DeweyIdSerializer) { - lastDeweyIdIndex = source.readInt(); - - // Read compressed dewey ID offsets (delta + bit-packed) - deweyIdOffsets = SlotOffsetCodec.decode(source); - - // Read deweyIdMemory size and data - final int deweyIdMemorySize = source.readInt(); - - if (canZeroCopy && deweyIdMemorySize > 1) { - // Zero-copy for dewey IDs too (part of same backing buffer) - final MemorySegment sourceSegment = ((MemorySegmentBytesIn) source).getSource(); - deweyIdMemory = sourceSegment.asSlice(source.position(), deweyIdMemorySize); - source.skip(deweyIdMemorySize); - } else if (deweyIdMemorySize > 1) { - // Allocate and copy - MemorySegmentAllocator allocator = memorySegmentAllocator; - deweyIdMemory = allocator.allocate(deweyIdMemorySize); - - if (source instanceof MemorySegmentBytesIn msSource) { - MemorySegment.copy(msSource.getSource(), source.position(), deweyIdMemory, 0, deweyIdMemorySize); - source.skip(deweyIdMemorySize); - } else { - byte[] deweyData = new byte[deweyIdMemorySize]; - source.read(deweyData); - MemorySegment.copy(deweyData, 0, deweyIdMemory, java.lang.foreign.ValueLayout.JAVA_BYTE, 0, - deweyIdMemorySize); - } - } else { - deweyIdMemory = null; - source.skip(1); // Skip placeholder byte - } - } else { - deweyIdOffsets = null; - deweyIdMemory = null; - lastDeweyIdIndex = -1; - } + private Page deserializeSlottedPage(final ResourceConfiguration resourceConfig, final BytesIn source) { + final long recordPageKey = Utils.getVarLong(source); + final int revision = source.readInt(); + final IndexType indexType = IndexType.getType(source.readByte()); - // Read overlong entries bitmap - final var overlongEntriesBitmap = SerializationType.deserializeBitSet(source); - - // Read overlong entries - final int overlongEntrySize = source.readInt(); - final Map references = new LinkedHashMap<>(overlongEntrySize); - var setBit = -1; - - for (int index = 0; index < overlongEntrySize; index++) { - setBit = overlongEntriesBitmap.nextSetBit(setBit + 1); - assert setBit >= 0; - final long key = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + setBit; - final PageReference reference = new PageReference(); - reference.setKey(source.readLong()); - references.put(key, reference); - } + final MemorySegmentAllocator memorySegmentAllocator = + OS.isWindows() ? WindowsMemorySegmentAllocator.getInstance() : LinuxMemorySegmentAllocator.getInstance(); - // Read FSST symbol table for string compression - byte[] fsstSymbolTable = null; - final int fsstSymbolTableLength = source.readInt(); - if (fsstSymbolTableLength > 0) { - fsstSymbolTable = new byte[fsstSymbolTableLength]; - source.read(fsstSymbolTable); - } + // 1. Read header (32B) + bitmap (128B) — 160 bytes + final byte[] headerBitmapBytes = new byte[PageLayout.DIR_OFF]; + source.read(headerBitmapBytes); + final MemorySegment headerBitmapSeg = MemorySegment.ofArray(headerBitmapBytes); + final int populatedCount = PageLayout.getPopulatedCount(headerBitmapSeg); - // Read columnar string storage if present - // Format: [hasColumnar:1][size:4][offsets:bit-packed][data:N] - MemorySegment stringValueMemory = null; - int[] stringValueOffsets = null; - int lastStringValueIndex = -1; - int stringValueMemorySize = 0; - - byte hasColumnar = source.readByte(); - if (hasColumnar == 1) { - stringValueMemorySize = source.readInt(); - stringValueOffsets = SlotOffsetCodec.decode(source); - - // Find last string value index from offsets - for (int i = stringValueOffsets.length - 1; i >= 0; i--) { - if (stringValueOffsets[i] >= 0) { - lastStringValueIndex = i; - break; - } - } - - // Read columnar data - zero-copy if possible - if (canZeroCopy && stringValueMemorySize > 0) { - final MemorySegment sourceSegment = ((MemorySegmentBytesIn) source).getSource(); - stringValueMemory = sourceSegment.asSlice(source.position(), stringValueMemorySize); - source.skip(stringValueMemorySize); - } else if (stringValueMemorySize > 0) { - stringValueMemory = memorySegmentAllocator.allocate(stringValueMemorySize); - if (source instanceof MemorySegmentBytesIn msSource) { - MemorySegment.copy(msSource.getSource(), source.position(), stringValueMemory, 0, - stringValueMemorySize); - source.skip(stringValueMemorySize); - } else { - byte[] stringData = new byte[stringValueMemorySize]; - source.read(stringData); - MemorySegment.copy(stringData, 0, stringValueMemory, java.lang.foreign.ValueLayout.JAVA_BYTE, 0, - stringValueMemorySize); - } - } - } + // 2. Read compact dir entries: populatedCount × 4 bytes + final int[] compactDir = new int[populatedCount]; + for (int i = 0; i < populatedCount; i++) { + compactDir[i] = source.readInt(); + } - // Create page - use the zero-copy constructor for both paths - // (it properly handles slotOffsets; backingBuffer/releaser can be null for non-zero-copy) - KeyValueLeafPage page = new KeyValueLeafPage(recordPageKey, revision, indexType, resourceConfig, slotOffsets, - slotMemory, lastSlotIndex, deweyIdOffsets, deweyIdMemory, lastDeweyIdIndex, references, backingBuffer, - backingBufferReleaser); - - // Set FSST symbol table if present and propagate to any deserialized nodes - if (fsstSymbolTable != null) { - page.setFsstSymbolTable(fsstSymbolTable); - // Propagate symbol table to nodes that are already in the records array - // Lazily-deserialized nodes will get the table when accessed via getRecord() - page.propagateFsstSymbolTableToNodes(); - } + // 3. Read heap size + final int heapSize = source.readInt(); - // Set columnar string storage if present - if (stringValueMemory != null && stringValueOffsets != null) { - page.setStringValueData(stringValueMemory, stringValueOffsets, lastStringValueIndex, stringValueMemorySize); - } + // 4. Allocate slotted page MemorySegment + final int allocSize = Math.max(PageLayout.HEAP_START + heapSize, PageLayout.INITIAL_PAGE_SIZE); + final MemorySegment slottedPage = memorySegmentAllocator.allocate(allocSize); + + // 5. Copy header + bitmap into page + MemorySegment.copy(headerBitmapSeg, 0, slottedPage, 0, PageLayout.DIR_OFF); - return page; + // 6. Zero-fill directory region (will be rebuilt from compact dir) + slottedPage.asSlice(PageLayout.DIR_OFF, PageLayout.DIR_SIZE).fill((byte) 0); + + // 7. Read heap data into page at HEAP_START + if (source instanceof MemorySegmentBytesIn msSource) { + MemorySegment.copy(msSource.getSource(), source.position(), slottedPage, PageLayout.HEAP_START, heapSize); + source.skip(heapSize); + } else { + final byte[] heapData = new byte[heapSize]; + source.read(heapData); + MemorySegment.copy(heapData, 0, slottedPage, ValueLayout.JAVA_BYTE, PageLayout.HEAP_START, heapData.length); + } + + // 8. Zero-fill remainder of allocated page + final long usedEnd = PageLayout.HEAP_START + heapSize; + if (allocSize > usedEnd) { + slottedPage.asSlice(usedEnd, allocSize - usedEnd).fill((byte) 0); + } + + // 9. Rebuild full directory via prefix sums from compact dir entries + int entryIdx = 0; + int heapOffset = 0; + for (int w = 0; w < PageLayout.BITMAP_WORDS; w++) { + long word = PageLayout.getBitmapWord(slottedPage, w); + while (word != 0) { + final int bit = Long.numberOfTrailingZeros(word); + final int slot = (w << 6) | bit; + final int packed = compactDir[entryIdx++]; + final int dataLength = packed >>> 8; + final int nodeKindId = packed & 0xFF; + PageLayout.setDirEntry(slottedPage, slot, heapOffset, dataLength, nodeKindId); + heapOffset += dataLength; + word &= word - 1; // clear lowest set bit } - default -> throw new IllegalStateException(); } + + // 10. Set heapEnd and heapUsed (both = heapSize since deserialized heap is contiguous/defragmented) + PageLayout.setHeapEnd(slottedPage, heapSize); + PageLayout.setHeapUsed(slottedPage, heapSize); + + final boolean areDeweyIDsStored = resourceConfig.areDeweyIDsStored; + final RecordSerializer recordPersister = resourceConfig.recordPersister; + + // Read overlong entries + final var overlongEntriesBitmap = SerializationType.deserializeBitSet(source); + final int overlongEntrySize = source.readInt(); + final Map references = new LinkedHashMap<>(overlongEntrySize); + var setBit = -1; + for (int index = 0; index < overlongEntrySize; index++) { + setBit = overlongEntriesBitmap.nextSetBit(setBit + 1); + assert setBit >= 0; + final long key = (recordPageKey << Constants.NDP_NODE_COUNT_EXPONENT) + setBit; + final PageReference reference = new PageReference(); + reference.setKey(source.readLong()); + references.put(key, reference); + } + + // Read FSST symbol table + byte[] fsstSymbolTable = null; + final int fsstSymbolTableLength = source.readInt(); + if (fsstSymbolTableLength > 0) { + fsstSymbolTable = new byte[fsstSymbolTableLength]; + source.read(fsstSymbolTable); + } + + // Create page with dummy slotMemory; slotted page overrides all slot operations + final MemorySegment dummySlotMemory = memorySegmentAllocator.allocate(1); + final KeyValueLeafPage page = new KeyValueLeafPage( + recordPageKey, revision, indexType, resourceConfig, + areDeweyIDsStored, recordPersister, references, + dummySlotMemory, null, -1, -1); + + page.setSlottedPage(slottedPage); + + if (fsstSymbolTable != null) { + page.setFsstSymbolTable(fsstSymbolTable); + } + + return page; } @Override public void serializePage(final ResourceConfiguration resourceConfig, final BytesOut sink, final Page page, final SerializationType type) { - KeyValueLeafPage keyValueLeafPage = (KeyValueLeafPage) page; + final KeyValueLeafPage keyValueLeafPage = (KeyValueLeafPage) page; - final var bytes = keyValueLeafPage.getBytes(); + // Check for zero-copy compressed segment first + final MemorySegment cachedSegment = keyValueLeafPage.getCompressedSegment(); + if (cachedSegment != null) { + sink.writeSegment(cachedSegment, 0, cachedSegment.byteSize()); + return; + } + // Legacy byte[] cache fallback + final var bytes = keyValueLeafPage.getBytes(); if (bytes != null) { sink.write(bytes.toByteArray()); return; } + // Ensure slotted page exists — ALL pages use slotted page format V0 + keyValueLeafPage.ensureSlottedPage(); + sink.writeByte(KEYVALUELEAFPAGE.id); sink.writeByte(resourceConfig.getBinaryEncodingVersion().byteVersion()); - // Variables from keyValueLeafPage - final long recordPageKey = keyValueLeafPage.getPageKey(); - final IndexType indexType = keyValueLeafPage.getIndexType(); - final RecordSerializer recordPersister = resourceConfig.recordPersister; final Map references = keyValueLeafPage.getReferencesMap(); - // Compact fixed-format slots to delta+varint encoding first. - // This must happen before FSST so the symbol table builder sees compact bytes. - keyValueLeafPage.compactFixedSlotsForCommit(resourceConfig); - // Build FSST symbol table and compress strings BEFORE addReferences() serializes them keyValueLeafPage.buildFsstSymbolTable(resourceConfig); keyValueLeafPage.compressStringValues(); - // Add references to overflow pages if necessary. + // addReferences: serializes records to slotted page heap via processEntries, + // copies preserved slots from completePageRef for DIFFERENTIAL/INCREMENTAL versioning keyValueLeafPage.addReferences(resourceConfig); - // Write page key. - Utils.putVarLong(sink, recordPageKey); - // Write revision number. + // Write metadata + Utils.putVarLong(sink, keyValueLeafPage.getPageKey()); sink.writeInt(keyValueLeafPage.getRevision()); - // Write index type. - sink.writeByte(indexType.getID()); - // Write last slot index. - sink.writeInt(keyValueLeafPage.getLastSlotIndex()); - - // Write compressed slot offsets (delta + bit-packed) - ~75% smaller than raw int[1024] - final int[] slotOffsets = keyValueLeafPage.getSlotOffsets(); - final MemorySegment slotMemory = keyValueLeafPage.getSlotMemory(); - final CompactedRegion compactedSlots = compactLengthPrefixedRegion(slotOffsets, slotMemory); - SlotOffsetCodec.encode(sink, compactedSlots.offsets(), keyValueLeafPage.getLastSlotIndex()); - - // Write compacted slotMemory region. - int slotMemoryUsedSize = compactedSlots.usedSize(); - if (slotMemoryUsedSize == 0) { - slotMemoryUsedSize = 1; - } - sink.writeInt(slotMemoryUsedSize); - if (compactedSlots.usedSize() > 0) { - writeCompactedLengthPrefixedRegion(sink, slotMemory, slotOffsets, compactedSlots.offsets(), - compactedSlots.usedSize()); - } else { - sink.writeByte((byte) 0); + sink.writeByte(keyValueLeafPage.getIndexType().getID()); + + // Write compact on-disk format: header+bitmap, compact dir, heap (no 8KB slot directory) + final MemorySegment slottedPage = keyValueLeafPage.getSlottedPage(); + + // 1. Write header (32B) + bitmap (128B) — 160 bytes + sink.writeSegment(slottedPage, 0, PageLayout.DIR_OFF); + + // 2. Single-pass bitmap scan: write compact dir entries, collect heap copy info + final int populatedCount = PageLayout.getPopulatedCount(slottedPage); + final int[] slotHeapOffsets = new int[populatedCount]; + final int[] slotDataLengths = new int[populatedCount]; + int entryIdx = 0; + int totalHeapSize = 0; + + for (int w = 0; w < PageLayout.BITMAP_WORDS; w++) { + long word = PageLayout.getBitmapWord(slottedPage, w); + while (word != 0) { + final int bit = Long.numberOfTrailingZeros(word); + final int slot = (w << 6) | bit; + final int dataLength = PageLayout.getDirDataLength(slottedPage, slot); + final int nodeKindId = PageLayout.getDirNodeKindId(slottedPage, slot); + sink.writeInt(PageLayout.packCompactDirEntry(dataLength, nodeKindId)); + slotHeapOffsets[entryIdx] = PageLayout.getDirHeapOffset(slottedPage, slot); + slotDataLengths[entryIdx] = dataLength; + totalHeapSize += dataLength; + entryIdx++; + word &= word - 1; // clear lowest set bit + } } - // Write dewey ID data if stored - if (resourceConfig.areDeweyIDsStored && recordPersister instanceof DeweyIdSerializer) { - // Write last dewey ID index - sink.writeInt(keyValueLeafPage.getLastDeweyIdIndex()); - - // Write compressed dewey ID offsets (delta + bit-packed) - final int[] deweyIdOffsets = keyValueLeafPage.getDeweyIdOffsets(); - final MemorySegment deweyIdMemory = keyValueLeafPage.getDeweyIdMemory(); - final CompactedRegion compactedDeweyIds = compactLengthPrefixedRegion(deweyIdOffsets, deweyIdMemory); - SlotOffsetCodec.encode(sink, compactedDeweyIds.offsets(), keyValueLeafPage.getLastDeweyIdIndex()); - - // Write compacted deweyIdMemory region. - int deweyIdMemoryUsedSize = compactedDeweyIds.usedSize(); - if (deweyIdMemoryUsedSize == 0) { - deweyIdMemoryUsedSize = 1; - } - sink.writeInt(deweyIdMemoryUsedSize); - if (compactedDeweyIds.usedSize() > 0) { - writeCompactedLengthPrefixedRegion(sink, deweyIdMemory, deweyIdOffsets, compactedDeweyIds.offsets(), - compactedDeweyIds.usedSize()); - } else { - // Write a single byte placeholder if no dewey ID memory - sink.writeByte((byte) 0); - } + // 3. Write heap size + sink.writeInt(totalHeapSize); + + // 4. Write heap data: slot by slot in bitmap order (contiguous on disk) + for (int i = 0; i < populatedCount; i++) { + sink.writeSegment(slottedPage, PageLayout.HEAP_START + slotHeapOffsets[i], slotDataLengths[i]); } - // Write overlong entries bitmap (entries bitmap is not needed - slot presence determined by - // slotOffsets) + // Write overlong entries + writeOverlongEntries(sink, references); + + // Write FSST symbol table + writeFsstSymbolTable(sink, keyValueLeafPage); + + // Compress the serialized data + compressAndCache(resourceConfig, sink, keyValueLeafPage); + + // Release node object references — all data is now in the slotted page + compressed cache + keyValueLeafPage.clearRecordsForGC(); + } + + private static void writeOverlongEntries(final BytesOut sink, final Map references) { var overlongEntriesBitmap = new BitSet(Constants.NDP_NODE_COUNT); - final var overlongEntriesSortedByKey = references.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList(); + final var overlongEntriesSortedByKey = references.entrySet().stream() + .sorted(Map.Entry.comparingByKey()).toList(); for (final Map.Entry entry : overlongEntriesSortedByKey) { final var pageOffset = StorageEngineReader.recordPageOffset(entry.getKey()); overlongEntriesBitmap.set(pageOffset); } SerializationType.serializeBitSet(sink, overlongEntriesBitmap); - - // Write overlong entries. sink.writeInt(overlongEntriesSortedByKey.size()); for (final var entry : overlongEntriesSortedByKey) { - // Write key in persistent storage. sink.writeLong(entry.getValue().getKey()); } + } - // Write FSST symbol table for string compression - final byte[] fsstSymbolTable = keyValueLeafPage.getFsstSymbolTable(); + private static void writeFsstSymbolTable(final BytesOut sink, final KeyValueLeafPage page) { + final byte[] fsstSymbolTable = page.getFsstSymbolTable(); if (fsstSymbolTable != null && fsstSymbolTable.length > 0) { sink.writeInt(fsstSymbolTable.length); sink.write(fsstSymbolTable); } else { - sink.writeInt(0); // No symbol table - } - - // Write columnar string storage if present - // Format: [hasColumnar:1][size:4][offsets:bit-packed][data:N] - if (keyValueLeafPage.hasColumnarStringStorage()) { - sink.writeByte((byte) 1); // Has columnar data - final int[] stringValueOffsets = keyValueLeafPage.getStringValueOffsets(); - final MemorySegment stringValueMemory = keyValueLeafPage.getStringValueMemory(); - final CompactedRegion compactedStrings = compactLengthPrefixedRegion(stringValueOffsets, stringValueMemory); - final int columnarSize = compactedStrings.usedSize(); - sink.writeInt(columnarSize); - - // Write bit-packed string value offsets - SlotOffsetCodec.encode(sink, compactedStrings.offsets(), keyValueLeafPage.getLastStringValueIndex()); - - // Write compacted columnar string data. - if (columnarSize > 0) { - writeCompactedLengthPrefixedRegion(sink, stringValueMemory, stringValueOffsets, compactedStrings.offsets(), - columnarSize); - } - } else { - sink.writeByte((byte) 0); // No columnar data + sink.writeInt(0); } + } + private static void compressAndCache(final ResourceConfiguration resourceConfig, final BytesOut sink, + final KeyValueLeafPage keyValueLeafPage) { final BytesIn uncompressedBytes = sink.bytesForRead(); - final long uncompressedLength = sink.writePosition(); final ByteHandlerPipeline pipeline = resourceConfig.byteHandlePipeline; - final byte[] compressedPage; if (pipeline.supportsMemorySegments() && uncompressedBytes instanceof MemorySegmentBytesIn segmentIn) { - // MemorySegment path — compress directly without materializing uncompressed byte[] - final MemorySegment uncompressedSegment = segmentIn.getSource().asSlice(0, uncompressedLength); - final MemorySegment compressedSegment = pipeline.compress(uncompressedSegment); - compressedPage = segmentToByteArray(compressedSegment); + final MemorySegment uncompressed = segmentIn.getSource().asSlice(0, sink.writePosition()); + final MemorySegment compressed = pipeline.compress(uncompressed); + keyValueLeafPage.setCompressedSegment(compressed); } else { - // Fallback: materialize to byte[] for stream-based compression final byte[] uncompressedArray = uncompressedBytes.toByteArray(); - compressedPage = compressViaStream(pipeline, uncompressedArray); + final byte[] compressedPage = compressViaStream(pipeline, uncompressedArray); + keyValueLeafPage.setBytes(Bytes.wrapForWrite(compressedPage)); } - - // Cache compressed form for writers, but leave the sink unmodified (uncompressed) - // so in-memory round-trips that bypass the ByteHandler still work. - keyValueLeafPage.setBytes(Bytes.wrapForWrite(compressedPage)); } }, @@ -572,14 +481,21 @@ public Page deserializePage(final ResourceConfiguration resourceConfiguration, f final int currentMaxLevelOfRecordToRevisionsIndirectPages = source.readByte() & 0xFF; if (source.readBoolean()) { - // noinspection DataFlowIssue + //noinspection DataFlowIssue user = new User(source.readUtf8(), UUID.fromString(source.readUtf8())); } - return new RevisionRootPage(delegate, revision, maxNodeKeyInDocumentIndex, maxNodeKeyInChangedNodesIndex, - maxNodeKeyInRecordToRevisionsIndex, revisionTimestamp, commitMessage, - currentMaxLevelOfDocumentIndexIndirectPages, currentMaxLevelOfChangedNodesIndirectPages, - currentMaxLevelOfRecordToRevisionsIndirectPages, user); + return new RevisionRootPage(delegate, + revision, + maxNodeKeyInDocumentIndex, + maxNodeKeyInChangedNodesIndex, + maxNodeKeyInRecordToRevisionsIndex, + revisionTimestamp, + commitMessage, + currentMaxLevelOfDocumentIndexIndirectPages, + currentMaxLevelOfChangedNodesIndirectPages, + currentMaxLevelOfRecordToRevisionsIndirectPages, + user); } default -> throw new IllegalStateException(); } @@ -595,7 +511,7 @@ public void serializePage(final ResourceConfiguration resourceConfig, final Byte Page delegate = revisionRootPage.delegate(); PageKind.serializeDelegate(sink, delegate, type); - // initial variables from RevisionRootPage, to serialize + //initial variables from RevisionRootPage, to serialize final Instant commitTimestamp = revisionRootPage.getCommitTimestamp(); final int revision = revisionRootPage.getRevision(); final long maxNodeKeyInDocumentIndex = revisionRootPage.getMaxNodeKeyInDocumentIndex(); @@ -608,9 +524,8 @@ public void serializePage(final ResourceConfiguration resourceConfig, final Byte revisionRootPage.getCurrentMaxLevelOfChangedNodesIndexIndirectPages(); final int currentMaxLevelOfRecordToRevisionsIndirectPages = revisionRootPage.getCurrentMaxLevelOfRecordToRevisionsIndexIndirectPages(); - final long revisionTimestamp = commitTimestamp == null - ? Instant.now().toEpochMilli() - : commitTimestamp.toEpochMilli(); + final long revisionTimestamp = + commitTimestamp == null ? Instant.now().toEpochMilli() : commitTimestamp.toEpochMilli(); revisionRootPage.setRevisionTimestamp(revisionTimestamp); sink.writeInt(revision); @@ -775,7 +690,7 @@ public void serializePage(final ResourceConfiguration resourceConfig, final Byte OverflowPage overflowPage = (OverflowPage) page; sink.writeByte(OVERFLOWPAGE.id); sink.writeByte(resourceConfig.getBinaryEncodingVersion().byteVersion()); - + // Write byte array directly byte[] data = overflowPage.getDataBytes(); sink.writeInt(data.length); @@ -873,28 +788,28 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, BytesOu public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration, BytesIn source, @NonNull SerializationType type, final ByteHandler.DecompressionResult decompressionResult) { final BinaryEncodingVersion binaryVersion = BinaryEncodingVersion.fromByte(source.readByte()); - + // Read header final long recordPageKey = Utils.getVarLong(source); final int revision = source.readInt(); final IndexType indexType = IndexType.getType(source.readByte()); final int entryCount = source.readInt(); final int usedSlotMemorySize = source.readInt(); - + // Read slot offsets (allocate MAX_ENTRIES to allow insertions after deserialization) final int[] slotOffsets = new int[HOTLeafPage.MAX_ENTRIES]; for (int i = 0; i < entryCount; i++) { slotOffsets[i] = source.readInt(); } - + // Read slot memory (zero-copy when possible) - MemorySegmentAllocator allocator = OS.isWindows() - ? WindowsMemorySegmentAllocator.getInstance() + MemorySegmentAllocator allocator = OS.isWindows() + ? WindowsMemorySegmentAllocator.getInstance() : LinuxMemorySegmentAllocator.getInstance(); - + final MemorySegment slotMemory; final Runnable releaser; - + // Note: For zero-copy we use just the needed size, but for regular allocation we use DEFAULT_SIZE // to allow insertions after deserialization. final boolean canZeroCopy = decompressionResult != null && source instanceof MemorySegmentBytesIn; @@ -918,9 +833,9 @@ public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration final MemorySegment segmentToRelease = slotMemory; releaser = () -> allocator.release(segmentToRelease); } - - return new HOTLeafPage(recordPageKey, revision, indexType, slotMemory, releaser, slotOffsets, entryCount, - usedSlotMemorySize); + + return new HOTLeafPage(recordPageKey, revision, indexType, slotMemory, releaser, + slotOffsets, entryCount, usedSlotMemorySize); } @Override @@ -929,14 +844,14 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul HOTLeafPage hotLeaf = (HOTLeafPage) page; sink.writeByte(HOT_LEAF_PAGE.id); sink.writeByte(resourceConfig.getBinaryEncodingVersion().byteVersion()); - + // Write header Utils.putVarLong(sink, hotLeaf.getPageKey()); sink.writeInt(hotLeaf.getRevision()); sink.writeByte(hotLeaf.getIndexType().getID()); sink.writeInt(hotLeaf.getEntryCount()); sink.writeInt(hotLeaf.getUsedSlotsSize()); - + // Write slot offsets int entryCount = hotLeaf.getEntryCount(); for (int i = 0; i < entryCount; i++) { @@ -948,7 +863,7 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul sink.writeInt(0); } } - + // Write slot memory (bulk copy) MemorySegment slots = hotLeaf.slots(); int usedSize = hotLeaf.getUsedSlotsSize(); @@ -966,7 +881,7 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration, BytesIn source, @NonNull SerializationType type, final ByteHandler.DecompressionResult decompressionResult) { final BinaryEncodingVersion binaryVersion = BinaryEncodingVersion.fromByte(source.readByte()); - + // Read header final long pageKey = Utils.getVarLong(source); final int revision = source.readInt(); @@ -974,17 +889,17 @@ public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration final byte nodeTypeId = source.readByte(); final byte layoutTypeId = source.readByte(); final int numChildren = source.readInt(); - + final HOTIndirectPage.NodeType nodeType = HOTIndirectPage.NodeType.values()[nodeTypeId]; - + // Read discriminative bits based on layout type final byte initialBytePos = source.readByte(); final long bitMask = source.readLong(); - + // Read partial keys final byte[] partialKeys = new byte[numChildren]; source.read(partialKeys); - + // Read child references (simple key-only format) final PageReference[] children = new PageReference[numChildren]; for (int i = 0; i < numChildren; i++) { @@ -993,7 +908,7 @@ public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration ref.setKey(childKey); children[i] = ref; } - + // Create appropriate node type return switch (nodeType) { case BI_NODE -> { @@ -1007,8 +922,8 @@ public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration int discriminativeBitPos = (initialBytePos & 0xFF) * 8 + bitWithinByte; yield HOTIndirectPage.createBiNode(pageKey, revision, discriminativeBitPos, children[0], children[1]); } - case SPAN_NODE -> - HOTIndirectPage.createSpanNode(pageKey, revision, initialBytePos, bitMask, partialKeys, children); + case SPAN_NODE -> HOTIndirectPage.createSpanNode(pageKey, revision, + initialBytePos, bitMask, partialKeys, children); case MULTI_NODE -> { byte[] childIndexArray = new byte[256]; source.read(childIndexArray); @@ -1023,7 +938,7 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul HOTIndirectPage hotIndirect = (HOTIndirectPage) page; sink.writeByte(HOT_INDIRECT_PAGE.id); sink.writeByte(resourceConfig.getBinaryEncodingVersion().byteVersion()); - + // Write header Utils.putVarLong(sink, hotIndirect.getPageKey()); sink.writeInt(hotIndirect.getRevision()); @@ -1031,24 +946,22 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul sink.writeByte((byte) hotIndirect.getNodeType().ordinal()); sink.writeByte((byte) hotIndirect.getLayoutType().ordinal()); sink.writeInt(hotIndirect.getNumChildren()); - + // Write discriminative bits properly based on layout type sink.writeByte((byte) hotIndirect.getInitialBytePos()); sink.writeLong(hotIndirect.getBitMask()); - + // Write partial keys byte[] partialKeysData = hotIndirect.getPartialKeys(); sink.write(partialKeysData); - + // Write child references for (int i = 0; i < hotIndirect.getNumChildren(); i++) { PageReference ref = hotIndirect.getChildReference(i); - long key = ref != null - ? ref.getKey() - : Constants.NULL_ID_LONG; + long key = ref != null ? ref.getKey() : Constants.NULL_ID_LONG; sink.writeLong(key); } - + // For MultiNode, write the 256-byte child index array if (hotIndirect.getNodeType() == HOTIndirectPage.NodeType.MULTI_NODE) { byte[] childIdx = hotIndirect.getChildIndex(); @@ -1071,15 +984,16 @@ public Page deserializePage(@NonNull ResourceConfiguration resourceConfiguration @NonNull SerializationType type, final ByteHandler.DecompressionResult decompressionResult) { // Skip binary version byte for now source.readByte(); - + // Read page key (stored before calling deserialize) final long pageKey = Utils.getVarLong(source); - + try { // Create a DataInputStream wrapper for BitmapChunkPage.deserialize byte[] remaining = source.toByteArray(); - java.io.DataInputStream dis = new java.io.DataInputStream(new java.io.ByteArrayInputStream(remaining, - (int) source.position(), remaining.length - (int) source.position())); + java.io.DataInputStream dis = new java.io.DataInputStream( + new java.io.ByteArrayInputStream(remaining, (int) source.position(), + remaining.length - (int) source.position())); return BitmapChunkPage.deserialize(dis, pageKey); } catch (java.io.IOException e) { throw new UncheckedIOException("Failed to deserialize BitmapChunkPage", e); @@ -1092,10 +1006,10 @@ public void serializePage(@NonNull ResourceConfiguration resourceConfig, @NonNul BitmapChunkPage chunkPage = (BitmapChunkPage) page; sink.writeByte(BITMAP_CHUNK_PAGE.id); sink.writeByte(resourceConfig.getBinaryEncodingVersion().byteVersion()); - + // Write page key Utils.putVarLong(sink, chunkPage.getPageKey()); - + try { // Serialize to byte array first, then write ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -1121,9 +1035,9 @@ private static void serializeDelegate(BytesOut sink, Page delegate, Serializa switch (delegate) { case ReferencesPage4 page -> type.serializeReferencesPage4(sink, page.getReferences(), page.getOffsets()); case BitmapReferencesPage page -> - type.serializeBitmapReferencesPage(sink, page.getReferences(), page.getBitmap()); + type.serializeBitmapReferencesPage(sink, page.getReferences(), page.getBitmap()); case FullReferencesPage ignored -> - type.serializeFullReferencesPage(sink, ((FullReferencesPage) delegate).getReferencesArray()); + type.serializeFullReferencesPage(sink, ((FullReferencesPage) delegate).getReferencesArray()); default -> throw new IllegalStateException("Unexpected value: " + delegate); } } @@ -1179,7 +1093,7 @@ private static Int2IntMap deserializeCurrentMaxLevelsOfIndirectPages(final Bytes /** * Constructor. * - * @param id unique identifier + * @param id unique identifier * @param clazz class */ PageKind(final byte id, final Class clazz) { @@ -1196,9 +1110,30 @@ public byte getID() { return id; } + /** + * Compress the serialized page using the configured {@link ByteHandlerPipeline} and write the + * compressed bytes back to the provided sink. Uses the MemorySegment path when available to + * avoid intermediate byte[] allocations. + */ + private static byte[] compress(ResourceConfiguration resourceConfig, + BytesIn uncompressedBytes, + byte[] uncompressedArray, + long uncompressedLength) { + final ByteHandlerPipeline pipeline = resourceConfig.byteHandlePipeline; + + if (pipeline.supportsMemorySegments() && uncompressedBytes instanceof MemorySegmentBytesIn segmentIn) { + MemorySegment uncompressedSegment = segmentIn.getSource().asSlice(0, uncompressedLength); + MemorySegment compressedSegment = pipeline.compress(uncompressedSegment); + return segmentToByteArray(compressedSegment); + } + + final byte[] compressedBytes = compressViaStream(pipeline, uncompressedArray); + return compressedBytes; + } + private static byte[] compressViaStream(ByteHandlerPipeline pipeline, byte[] uncompressedArray) { try (final ByteArrayOutputStream output = new ByteArrayOutputStream(uncompressedArray.length); - final DataOutputStream dataOutput = new DataOutputStream(pipeline.serialize(output))) { + final DataOutputStream dataOutput = new DataOutputStream(pipeline.serialize(output))) { dataOutput.write(uncompressedArray); dataOutput.flush(); return output.toByteArray(); @@ -1208,94 +1143,15 @@ private static byte[] compressViaStream(ByteHandlerPipeline pipeline, byte[] unc } private static byte[] segmentToByteArray(MemorySegment segment) { - return segment.toArray(java.lang.foreign.ValueLayout.JAVA_BYTE); - } - - private static final ValueLayout.OfInt JAVA_INT_UNALIGNED = ValueLayout.JAVA_INT.withByteAlignment(1); - private static final int LENGTH_PREFIX_BYTES = Integer.BYTES; - private static final int LENGTH_PREFIX_ALIGNMENT = Integer.BYTES; - - private static CompactedRegion compactLengthPrefixedRegion(final int[] sourceOffsets, - final MemorySegment sourceMemory) { - final int[] compactedOffsets = new int[sourceOffsets.length]; - Arrays.fill(compactedOffsets, -1); - - if (sourceMemory == null) { - return new CompactedRegion(compactedOffsets, 0); - } - - int writeOffset = 0; - for (int slot = 0; slot < sourceOffsets.length; slot++) { - final int sourceOffset = sourceOffsets[slot]; - if (sourceOffset < 0) { - continue; - } - - final int alignedWriteOffset = alignOffset(writeOffset); - compactedOffsets[slot] = alignedWriteOffset; - - final int entryLength = readLengthPrefix(sourceMemory, sourceOffset); - writeOffset = alignedWriteOffset + LENGTH_PREFIX_BYTES + entryLength; - } - - return new CompactedRegion(compactedOffsets, writeOffset); - } - - private static void writeCompactedLengthPrefixedRegion(final BytesOut sink, final MemorySegment sourceMemory, - final int[] sourceOffsets, final int[] compactedOffsets, final int usedSize) { - int writeOffset = 0; - for (int slot = 0; slot < sourceOffsets.length; slot++) { - final int sourceOffset = sourceOffsets[slot]; - if (sourceOffset < 0) { - continue; - } - - final int targetOffset = compactedOffsets[slot]; - while (writeOffset < targetOffset) { - sink.writeByte((byte) 0); - writeOffset++; - } - - final int entryLength = readLengthPrefix(sourceMemory, sourceOffset); - final int totalEntrySize = LENGTH_PREFIX_BYTES + entryLength; - sink.writeSegment(sourceMemory, sourceOffset, totalEntrySize); - writeOffset += totalEntrySize; - } - - if (writeOffset != usedSize) { - throw new IllegalStateException( - "Compacted region size mismatch. expected=" + usedSize + ", actual=" + writeOffset); - } - } - - private static int alignOffset(final int offset) { - return (offset + LENGTH_PREFIX_ALIGNMENT - 1) & -LENGTH_PREFIX_ALIGNMENT; - } - - private static int readLengthPrefix(final MemorySegment sourceMemory, final int sourceOffset) { - if (sourceOffset < 0 || sourceOffset + LENGTH_PREFIX_BYTES > sourceMemory.byteSize()) { - throw new IllegalStateException("Invalid source offset for compact serialization: " + sourceOffset - + ", memorySize=" + sourceMemory.byteSize()); - } - - final int length = sourceMemory.get(JAVA_INT_UNALIGNED, sourceOffset); - if (length <= 0 || sourceOffset + LENGTH_PREFIX_BYTES + (long) length > sourceMemory.byteSize()) { - throw new IllegalStateException("Invalid length prefix during compact serialization. offset=" + sourceOffset - + ", length=" + length + ", memorySize=" + sourceMemory.byteSize()); - } - - return length; - } - - private record CompactedRegion(int[] offsets, int usedSize) { + return segment.toArray(ValueLayout.JAVA_BYTE); } /** * Serialize page. * * @param ResourceConfiguration the read only page transaction - * @param sink {@link BytesOut} instance - * @param page {@link Page} implementation + * @param sink {@link BytesOut} instance + * @param page {@link Page} implementation */ public abstract void serializePage(final ResourceConfiguration ResourceConfiguration, final BytesOut sink, final Page page, final SerializationType type); @@ -1304,7 +1160,7 @@ public abstract void serializePage(final ResourceConfiguration ResourceConfigura * Deserialize page. * * @param resourceConfiguration the resource configuration - * @param source {@link BytesIn} instance + * @param source {@link BytesIn} instance * @return page instance implementing the {@link Page} interface */ public Page deserializePage(final ResourceConfiguration resourceConfiguration, final BytesIn source, @@ -1315,14 +1171,13 @@ public Page deserializePage(final ResourceConfiguration resourceConfiguration, f /** * Deserialize page with optional DecompressionResult for zero-copy support. * - *

- * When decompressionResult is provided, KeyValueLeafPages can take ownership of the decompression - * buffer and use it directly as slotMemory. + *

When decompressionResult is provided, KeyValueLeafPages can take ownership + * of the decompression buffer and use it directly as slotMemory. * * @param resourceConfiguration the resource configuration - * @param source {@link BytesIn} instance - * @param type serialization type - * @param decompressionResult optional decompression result for zero-copy (may be null) + * @param source {@link BytesIn} instance + * @param type serialization type + * @param decompressionResult optional decompression result for zero-copy (may be null) * @return page instance implementing the {@link Page} interface */ public abstract Page deserializePage(final ResourceConfiguration resourceConfiguration, final BytesIn source, @@ -1337,7 +1192,7 @@ public abstract Page deserializePage(final ResourceConfiguration resourceConfigu public static PageKind getKind(final byte id) { final PageKind page = INSTANCEFORID.get(id); if (page == null) { - throw new IllegalStateException(); + throw new IllegalStateException("Unknown PageKind id: " + id + " (0x" + Integer.toHexString(id & 0xFF) + ")"); } return page; } diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/PageLayout.java b/bundles/sirix-core/src/main/java/io/sirix/page/PageLayout.java new file mode 100644 index 000000000..3d9e79eb2 --- /dev/null +++ b/bundles/sirix-core/src/main/java/io/sirix/page/PageLayout.java @@ -0,0 +1,655 @@ +package io.sirix.page; + +import io.sirix.settings.Constants; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +/** + * Slotted page layout (PostgreSQL/LeanStore-style: Header + Bitmap + Directory + Heap) for {@link KeyValueLeafPage}. + * + *

All page metadata, slot directory, and record data live in a single contiguous + * {@link MemorySegment}. In-memory format = on-disk format. ZERO conversion at commit time. + * + *

Page Layout

+ *
+ * Offset   Size      Field
+ * ──────── ───────── ──────────────────────
+ * 0        8         recordPageKey (long)
+ * 8        4         revision (int)
+ * 12       2         populatedCount (u16)
+ * 14       4         heapEnd (int, relative to HEAP_START)
+ * 18       4         heapUsed (int, bytes of live data)
+ * 22       1         indexType (byte)
+ * 23       1         flags (bit 0: areDeweyIDsStored, bit 1: hasFsstTable)
+ * 24       8         reserved
+ * ──────── ───────── ──────────────────────
+ * 32       128       slotBitmap (16 longs = 1024 bits)
+ * ──────── ───────── ──────────────────────
+ * 160      8192      slotDirectory (1024 × 8 bytes)
+ *                      [heapOffset:4][dataLength:3 + nodeKindId:1]
+ * ──────── ───────── ──────────────────────
+ * 8352     ...       heap (bump-allocated forward, compact varint records)
+ * 
+ * + *

Slot Directory Entry (8 bytes each)

+ *
+ * Bytes 0-3: heapOffset (int) — offset into heap region (relative to HEAP_START)
+ * Bytes 4-6: dataLength (3 bytes, unsigned) — total byte length of the record in the heap
+ * Byte 7:   nodeKindId (1 byte) — NodeKind ordinal for fast dispatch
+ * 
+ * + *

Heap Record Format

+ *
+ * [nodeKind: 1 byte]
+ * [fieldOffsetTable: fieldCount × 1 byte] — O(1) access to any field
+ * [data region: varint fields + hash + optional payload]
+ * 
+ */ +public final class PageLayout { + + private PageLayout() { + throw new AssertionError("Utility class"); + } + + // ==================== PAGE SIZING ==================== + + /** Initial page allocation size (64 KB). */ + public static final int INITIAL_PAGE_SIZE = 64 * 1024; + + /** Maximum slots per page (must match Constants.NDP_NODE_COUNT). */ + public static final int SLOT_COUNT = Constants.NDP_NODE_COUNT; // 1024 + + /** Slot count exponent: 2^10 = 1024 */ + public static final int SLOT_COUNT_EXPONENT = Constants.NDP_NODE_COUNT_EXPONENT; // 10 + + // ==================== HEADER LAYOUT (32 bytes) ==================== + + /** Total header size in bytes. */ + public static final int HEADER_SIZE = 32; + + /** Offset of recordPageKey (long, 8 bytes). */ + public static final int OFF_RECORD_PAGE_KEY = 0; + + /** Offset of revision (int, 4 bytes). */ + public static final int OFF_REVISION = 8; + + /** Offset of populatedCount (unsigned short, 2 bytes). */ + public static final int OFF_POPULATED_COUNT = 12; + + /** Offset of heapEnd (int, 4 bytes) — relative to HEAP_START. */ + public static final int OFF_HEAP_END = 14; + + /** Offset of heapUsed (int, 4 bytes) — bytes of live (non-abandoned) data. */ + public static final int OFF_HEAP_USED = 18; + + /** Offset of indexType (byte, 1 byte). */ + public static final int OFF_INDEX_TYPE = 22; + + /** Offset of flags (byte, 1 byte). */ + public static final int OFF_FLAGS = 23; + + /** Offset of reserved region (8 bytes). */ + public static final int OFF_RESERVED = 24; + + // ==================== FLAGS ==================== + + /** Flag bit 0: DeweyIDs are stored inline in records. */ + public static final int FLAG_DEWEY_IDS_STORED = 1; + + /** Flag bit 1: FSST symbol table is present. */ + public static final int FLAG_HAS_FSST_TABLE = 2; + + // ==================== BITMAP LAYOUT (128 bytes) ==================== + + /** Offset where the slot bitmap starts. */ + public static final int BITMAP_OFF = HEADER_SIZE; // 32 + + /** Number of 64-bit words in the bitmap. */ + public static final int BITMAP_WORDS = 16; + + /** Total size of the bitmap region in bytes. */ + public static final int BITMAP_SIZE = BITMAP_WORDS * Long.BYTES; // 128 + + // ==================== SLOT DIRECTORY LAYOUT (8192 bytes) ==================== + + /** Offset where the slot directory starts. */ + public static final int DIR_OFF = BITMAP_OFF + BITMAP_SIZE; // 160 + + /** Size of each directory entry in bytes. */ + public static final int DIR_ENTRY_SIZE = 8; + + /** Total size of the slot directory region in bytes. */ + public static final int DIR_SIZE = SLOT_COUNT * DIR_ENTRY_SIZE; // 8192 + + // ==================== COMPACT DIRECTORY (on-disk only) ==================== + + /** Size of each compact directory entry in bytes (dataLength:3 + nodeKindId:1). */ + public static final int COMPACT_DIR_ENTRY_SIZE = 4; + + /** + * Pack a compact directory entry: top 3 bytes = dataLength, bottom byte = nodeKindId. + * Same bit layout as existing directory entry bytes 4-7. + */ + public static int packCompactDirEntry(final int dataLength, final int nodeKindId) { + return (dataLength << 8) | (nodeKindId & 0xFF); + } + + /** Unpack data length from a compact directory entry. */ + public static int unpackDataLength(final int packed) { + return packed >>> 8; + } + + /** Unpack node kind ID from a compact directory entry. */ + public static int unpackNodeKindId(final int packed) { + return packed & 0xFF; + } + + // ==================== HEAP LAYOUT ==================== + + /** Offset where the heap region starts (bump-allocated forward). */ + public static final int HEAP_START = DIR_OFF + DIR_SIZE; // 8352 + + // ==================== DIRECTORY ENTRY FIELD OFFSETS ==================== + + /** Offset within a directory entry for the heap offset (int, 4 bytes). */ + private static final int DIRENTRY_OFF_HEAP_OFFSET = 0; + + /** Offset within a directory entry for dataLength (3 bytes) + nodeKindId (1 byte). */ + private static final int DIRENTRY_OFF_LENGTH_AND_KIND = 4; + + /** Sentinel value indicating an empty directory entry. */ + public static final int DIR_ENTRY_EMPTY = -1; + + // ==================== VALUE LAYOUTS ==================== + + private static final ValueLayout.OfLong JAVA_LONG_UNALIGNED = + ValueLayout.JAVA_LONG_UNALIGNED; + + private static final ValueLayout.OfInt JAVA_INT_UNALIGNED = + ValueLayout.JAVA_INT.withByteAlignment(1); + + private static final ValueLayout.OfShort JAVA_SHORT_UNALIGNED = + ValueLayout.JAVA_SHORT.withByteAlignment(1); + + // ==================== HEADER ACCESSORS ==================== + + /** Read the recordPageKey from the page header. */ + public static long getRecordPageKey(final MemorySegment page) { + return page.get(JAVA_LONG_UNALIGNED, OFF_RECORD_PAGE_KEY); + } + + /** Write the recordPageKey into the page header. */ + public static void setRecordPageKey(final MemorySegment page, final long key) { + page.set(JAVA_LONG_UNALIGNED, OFF_RECORD_PAGE_KEY, key); + } + + /** Read the revision from the page header. */ + public static int getRevision(final MemorySegment page) { + return page.get(JAVA_INT_UNALIGNED, OFF_REVISION); + } + + /** Write the revision into the page header. */ + public static void setRevision(final MemorySegment page, final int revision) { + page.set(JAVA_INT_UNALIGNED, OFF_REVISION, revision); + } + + /** Read the populated slot count from the page header. */ + public static int getPopulatedCount(final MemorySegment page) { + return Short.toUnsignedInt(page.get(JAVA_SHORT_UNALIGNED, OFF_POPULATED_COUNT)); + } + + /** Write the populated slot count into the page header. */ + public static void setPopulatedCount(final MemorySegment page, final int count) { + page.set(JAVA_SHORT_UNALIGNED, OFF_POPULATED_COUNT, (short) count); + } + + /** Read the heap end position (relative to HEAP_START) from the page header. */ + public static int getHeapEnd(final MemorySegment page) { + return page.get(JAVA_INT_UNALIGNED, OFF_HEAP_END); + } + + /** Write the heap end position (relative to HEAP_START) into the page header. */ + public static void setHeapEnd(final MemorySegment page, final int heapEnd) { + page.set(JAVA_INT_UNALIGNED, OFF_HEAP_END, heapEnd); + } + + /** Read the heap used bytes from the page header. */ + public static int getHeapUsed(final MemorySegment page) { + return page.get(JAVA_INT_UNALIGNED, OFF_HEAP_USED); + } + + /** Write the heap used bytes into the page header. */ + public static void setHeapUsed(final MemorySegment page, final int heapUsed) { + page.set(JAVA_INT_UNALIGNED, OFF_HEAP_USED, heapUsed); + } + + /** Read the index type byte from the page header. */ + public static byte getIndexType(final MemorySegment page) { + return page.get(ValueLayout.JAVA_BYTE, OFF_INDEX_TYPE); + } + + /** Write the index type byte into the page header. */ + public static void setIndexType(final MemorySegment page, final byte indexType) { + page.set(ValueLayout.JAVA_BYTE, OFF_INDEX_TYPE, indexType); + } + + /** Read the flags byte from the page header. */ + public static byte getFlags(final MemorySegment page) { + return page.get(ValueLayout.JAVA_BYTE, OFF_FLAGS); + } + + /** Write the flags byte into the page header. */ + public static void setFlags(final MemorySegment page, final byte flags) { + page.set(ValueLayout.JAVA_BYTE, OFF_FLAGS, flags); + } + + /** Check if DeweyIDs are stored (flag bit 0). */ + public static boolean areDeweyIDsStored(final MemorySegment page) { + return (getFlags(page) & FLAG_DEWEY_IDS_STORED) != 0; + } + + /** Check if FSST symbol table is present (flag bit 1). */ + public static boolean hasFsstTable(final MemorySegment page) { + return (getFlags(page) & FLAG_HAS_FSST_TABLE) != 0; + } + + // ==================== BITMAP ACCESSORS ==================== + + /** Read a bitmap word (one of 16 longs). */ + public static long getBitmapWord(final MemorySegment page, final int wordIndex) { + return page.get(JAVA_LONG_UNALIGNED, BITMAP_OFF + ((long) wordIndex << 3)); + } + + /** Write a bitmap word (one of 16 longs). */ + public static void setBitmapWord(final MemorySegment page, final int wordIndex, final long word) { + page.set(JAVA_LONG_UNALIGNED, BITMAP_OFF + ((long) wordIndex << 3), word); + } + + /** Check if a slot is populated in the bitmap. */ + public static boolean isSlotPopulated(final MemorySegment page, final int slotIndex) { + final long word = getBitmapWord(page, slotIndex >>> 6); + return (word & (1L << (slotIndex & 63))) != 0; + } + + /** Set a slot's bit in the bitmap. */ + public static void markSlotPopulated(final MemorySegment page, final int slotIndex) { + final int wordIndex = slotIndex >>> 6; + final long word = getBitmapWord(page, wordIndex); + setBitmapWord(page, wordIndex, word | (1L << (slotIndex & 63))); + } + + /** Clear a slot's bit in the bitmap. */ + public static void clearSlotPopulated(final MemorySegment page, final int slotIndex) { + final int wordIndex = slotIndex >>> 6; + final long word = getBitmapWord(page, wordIndex); + setBitmapWord(page, wordIndex, word & ~(1L << (slotIndex & 63))); + } + + /** + * Count populated slots using Long.bitCount across all bitmap words. + * + * @return number of populated slots (0 to 1024) + */ + public static int countPopulatedSlots(final MemorySegment page) { + int count = 0; + for (int i = 0; i < BITMAP_WORDS; i++) { + count += Long.bitCount(getBitmapWord(page, i)); + } + return count; + } + + /** + * Copy the entire bitmap from the page into a Java long[] array. + * Useful for snapshot/iteration without repeated segment access. + */ + public static void copyBitmapTo(final MemorySegment page, final long[] dest) { + for (int i = 0; i < BITMAP_WORDS; i++) { + dest[i] = getBitmapWord(page, i); + } + } + + /** + * Write the entire bitmap from a Java long[] array into the page. + */ + public static void copyBitmapFrom(final MemorySegment page, final long[] src) { + for (int i = 0; i < BITMAP_WORDS; i++) { + setBitmapWord(page, i, src[i]); + } + } + + // ==================== SLOT DIRECTORY ACCESSORS ==================== + + /** + * Compute the absolute byte offset of a slot directory entry. + */ + public static long dirEntryOffset(final int slotIndex) { + return DIR_OFF + (long) slotIndex * DIR_ENTRY_SIZE; + } + + /** + * Read the heap offset from a slot directory entry. + * Returns {@link #DIR_ENTRY_EMPTY} if the slot is not populated + * (caller must check bitmap first for correctness). + */ + public static int getDirHeapOffset(final MemorySegment page, final int slotIndex) { + return page.get(JAVA_INT_UNALIGNED, dirEntryOffset(slotIndex) + DIRENTRY_OFF_HEAP_OFFSET); + } + + /** + * Read the data length from a slot directory entry. + * The length is stored in 3 bytes (unsigned, big-endian packing within the 4-byte field). + * Byte layout: [dataLength_b2][dataLength_b1][dataLength_b0][nodeKindId] + * + * @return data length in bytes (0 to 16,777,215) + */ + public static int getDirDataLength(final MemorySegment page, final int slotIndex) { + final long base = dirEntryOffset(slotIndex) + DIRENTRY_OFF_LENGTH_AND_KIND; + // Read 4 bytes as int, mask off the lowest byte (nodeKindId) + final int packed = page.get(JAVA_INT_UNALIGNED, base); + return packed >>> 8; // top 3 bytes = data length + } + + /** + * Read the node kind ID from a slot directory entry (lowest byte of the 4-byte field). + * + * @return node kind ID (0-255) + */ + public static int getDirNodeKindId(final MemorySegment page, final int slotIndex) { + final long base = dirEntryOffset(slotIndex) + DIRENTRY_OFF_LENGTH_AND_KIND; + final int packed = page.get(JAVA_INT_UNALIGNED, base); + return packed & 0xFF; + } + + /** + * Write a complete slot directory entry. + * + * @param page the page MemorySegment + * @param slotIndex the slot index (0 to SLOT_COUNT-1) + * @param heapOffset offset into the heap region (relative to HEAP_START) + * @param dataLength total byte length of the record in the heap (0 to 16,777,215) + * @param nodeKindId NodeKind ordinal (0 to 255) + */ + public static void setDirEntry(final MemorySegment page, final int slotIndex, + final int heapOffset, final int dataLength, final int nodeKindId) { + final long base = dirEntryOffset(slotIndex); + page.set(JAVA_INT_UNALIGNED, base + DIRENTRY_OFF_HEAP_OFFSET, heapOffset); + // Pack: top 3 bytes = dataLength, bottom byte = nodeKindId + final int packed = (dataLength << 8) | (nodeKindId & 0xFF); + page.set(JAVA_INT_UNALIGNED, base + DIRENTRY_OFF_LENGTH_AND_KIND, packed); + } + + /** + * Clear a slot directory entry (set to all zeros). + */ + public static void clearDirEntry(final MemorySegment page, final int slotIndex) { + final long base = dirEntryOffset(slotIndex); + page.set(JAVA_LONG_UNALIGNED, base, 0L); + } + + // ==================== HEAP MANAGEMENT ==================== + + /** + * Compute the absolute byte offset for a heap-relative offset. + * + * @param heapRelativeOffset offset relative to HEAP_START + * @return absolute byte offset in the page + */ + public static long heapAbsoluteOffset(final int heapRelativeOffset) { + return HEAP_START + (long) heapRelativeOffset; + } + + /** + * Allocate space in the heap using bump allocation. + * Updates the heapEnd in the page header. + * + * @param page the page MemorySegment + * @param size number of bytes to allocate + * @return heap-relative offset of the allocated region + * @throws IllegalStateException if the page doesn't have enough space + */ + public static int allocateHeap(final MemorySegment page, final int size) { + final int heapEnd = getHeapEnd(page); + final int newHeapEnd = heapEnd + size; + + // Check if we've exceeded the page capacity + if (HEAP_START + newHeapEnd > page.byteSize()) { + throw new IllegalStateException( + "Heap overflow: need " + (HEAP_START + newHeapEnd) + + " bytes but page is " + page.byteSize() + " bytes"); + } + + setHeapEnd(page, newHeapEnd); + // Update heapUsed (for live data tracking — initially same as heapEnd) + setHeapUsed(page, getHeapUsed(page) + size); + return heapEnd; + } + + /** + * Get the remaining heap capacity in bytes. + * + * @param page the page MemorySegment + * @return remaining bytes available for heap allocation + */ + public static int heapCapacityRemaining(final MemorySegment page) { + return (int) page.byteSize() - HEAP_START - getHeapEnd(page); + } + + /** + * Compute the heap fragmentation ratio. + * + * @return ratio of dead space to total allocated (0.0 = no fragmentation, 1.0 = all dead) + */ + public static double heapFragmentation(final MemorySegment page) { + final int heapEnd = getHeapEnd(page); + if (heapEnd == 0) { + return 0.0; + } + final int heapUsed = getHeapUsed(page); + return 1.0 - ((double) heapUsed / heapEnd); + } + + // ==================== PAGE INITIALIZATION ==================== + + /** + * Initialize a fresh page MemorySegment with default header values. + * Zeroes the bitmap and directory regions. + * + * @param page the page MemorySegment (must be at least INITIAL_PAGE_SIZE) + * @param recordPageKey the page key + * @param revision the revision number + * @param indexType the index type byte + * @param areDeweyIDs whether DeweyIDs will be stored + */ + public static void initializePage(final MemorySegment page, final long recordPageKey, + final int revision, final byte indexType, final boolean areDeweyIDs) { + // Clear header + bitmap + directory (all zeros) + page.asSlice(0, HEAP_START).fill((byte) 0); + + // Write header fields + setRecordPageKey(page, recordPageKey); + setRevision(page, revision); + setPopulatedCount(page, 0); + setHeapEnd(page, 0); + setHeapUsed(page, 0); + setIndexType(page, indexType); + byte flags = 0; + if (areDeweyIDs) { + flags |= (byte) FLAG_DEWEY_IDS_STORED; + } + setFlags(page, flags); + } + + // ==================== PAGE RESIZING ==================== + + /** + * Compute the new page size for growth (doubling strategy). + * + * @param currentSize the current page size + * @param needed the minimum total size needed + * @return the new page size (at least needed, typically 2x current) + */ + public static int computeGrowthSize(final int currentSize, final int needed) { + int newSize = currentSize; + while (newSize < needed) { + newSize <<= 1; // double + } + return newSize; + } + + // ==================== RECORD OFFSET TABLE ==================== + + /** + * Read a field offset from a record's offset table. + * The offset table starts at recordBase + 1 (after the nodeKind byte). + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of the record start + * @param fieldIndex the field index (0 to fieldCount-1) + * @return the field offset (0-255) relative to the data region start + */ + public static int readFieldOffset(final MemorySegment page, final long recordBase, + final int fieldIndex) { + return page.get(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex) & 0xFF; + } + + /** + * Write a field offset into a record's offset table. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of the record start + * @param fieldIndex the field index (0 to fieldCount-1) + * @param offset the field offset (0-255) relative to the data region start + */ + public static void writeFieldOffset(final MemorySegment page, final long recordBase, + final int fieldIndex, final int offset) { + page.set(ValueLayout.JAVA_BYTE, recordBase + 1 + fieldIndex, (byte) offset); + } + + /** + * Compute the absolute offset of the data region for a record. + * Data region starts after: [nodeKind: 1 byte] + [offset table: fieldCount bytes]. + * + * @param recordBase absolute byte offset of the record start + * @param fieldCount number of fields in the offset table + * @return absolute byte offset where the data region begins + */ + public static long dataRegionStart(final long recordBase, final int fieldCount) { + return recordBase + 1 + fieldCount; + } + + /** + * Read the nodeKind byte from a record in the heap. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of the record start + * @return the nodeKind byte + */ + public static byte readRecordKind(final MemorySegment page, final long recordBase) { + return page.get(ValueLayout.JAVA_BYTE, recordBase); + } + + /** + * Write the nodeKind byte for a record in the heap. + * + * @param page the page MemorySegment + * @param recordBase absolute byte offset of the record start + * @param kindId the nodeKind byte + */ + public static void writeRecordKind(final MemorySegment page, final long recordBase, + final byte kindId) { + page.set(ValueLayout.JAVA_BYTE, recordBase, kindId); + } + + // ==================== DEWEY ID INLINE SUPPORT ==================== + + /** + * Size of the DeweyID length trailer appended to each heap allocation + * when FLAG_DEWEY_IDS_STORED is set. Unsigned 16-bit (0-65535). + */ + public static final int DEWEY_ID_TRAILER_SIZE = 2; + + /** + * Read the DeweyID length from the trailer at the end of a slot's heap allocation. + * Returns 0 if DeweyIDs are not stored or the slot has no DeweyID. + * + * @param page the page MemorySegment + * @param slotIndex the slot index + * @return DeweyID data length in bytes (0 if none) + */ + public static int getDeweyIdLength(final MemorySegment page, final int slotIndex) { + if (!areDeweyIDsStored(page)) { + return 0; + } + final int dataLength = getDirDataLength(page, slotIndex); + if (dataLength < DEWEY_ID_TRAILER_SIZE) { + return 0; + } + final int heapOffset = getDirHeapOffset(page, slotIndex); + final long trailerPos = heapAbsoluteOffset(heapOffset) + dataLength - DEWEY_ID_TRAILER_SIZE; + return Short.toUnsignedInt(page.get(JAVA_SHORT_UNALIGNED, trailerPos)); + } + + /** + * Get the record-only data length (excluding DeweyID data and trailer). + * When DeweyIDs are not stored, this equals the full dataLength. + * + * @param page the page MemorySegment + * @param slotIndex the slot index + * @return record-only byte length + */ + public static int getRecordOnlyLength(final MemorySegment page, final int slotIndex) { + final int dataLength = getDirDataLength(page, slotIndex); + if (!areDeweyIDsStored(page)) { + return dataLength; + } + if (dataLength < DEWEY_ID_TRAILER_SIZE) { + return dataLength; + } + final int deweyIdLen = getDeweyIdLength(page, slotIndex); + return dataLength - deweyIdLen - DEWEY_ID_TRAILER_SIZE; + } + + /** + * Get the DeweyID data from a slot's heap allocation as a MemorySegment slice. + * Returns null if the slot has no DeweyID. + * + * @param page the page MemorySegment + * @param slotIndex the slot index + * @return MemorySegment slice containing DeweyID bytes, or null + */ + public static MemorySegment getDeweyId(final MemorySegment page, final int slotIndex) { + final int deweyIdLen = getDeweyIdLength(page, slotIndex); + if (deweyIdLen == 0) { + return null; + } + final int dataLength = getDirDataLength(page, slotIndex); + final int heapOffset = getDirHeapOffset(page, slotIndex); + // DeweyID data is between record data and the 2-byte trailer + final long deweyIdStart = heapAbsoluteOffset(heapOffset) + dataLength + - DEWEY_ID_TRAILER_SIZE - deweyIdLen; + return page.asSlice(deweyIdStart, deweyIdLen); + } + + /** + * Write a 2-byte DeweyID length trailer (u16) at the end of a heap allocation. + * + * @param page the page MemorySegment + * @param absEnd absolute byte position of the allocation end (where trailer goes) + * @param deweyIdLen length of the DeweyID data (0 if none) + */ + public static void writeDeweyIdTrailer(final MemorySegment page, final long absEnd, + final int deweyIdLen) { + page.set(JAVA_SHORT_UNALIGNED, absEnd - DEWEY_ID_TRAILER_SIZE, (short) deweyIdLen); + } + + // ==================== COMPACTION ==================== + + /** + * Check whether the heap needs compaction. + * Returns true when dead space exceeds the compaction threshold (25%). + */ + public static boolean needsCompaction(final MemorySegment page) { + return heapFragmentation(page) > 0.25; + } +} diff --git a/bundles/sirix-core/src/main/java/io/sirix/page/SlotOffsetCodec.java b/bundles/sirix-core/src/main/java/io/sirix/page/SlotOffsetCodec.java deleted file mode 100644 index 009977289..000000000 --- a/bundles/sirix-core/src/main/java/io/sirix/page/SlotOffsetCodec.java +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (c) 2023, Sirix Contributors - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of the nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.sirix.page; - -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.settings.Constants; - -import java.util.Arrays; -import java.util.BitSet; - -// ThreadLocal buffers for HFT-level performance (avoid allocation on hot path) - -/** - * Codec for compressing slot offset arrays using bit-packing. - * - *

- * This compression technique is inspired by best practices from DuckDB, RocksDB, and Apache - * Parquet. It achieves significant space savings compared to raw int[1024] arrays: - *

- *
    - *
  • Sparse pages (10% full): ~95% savings
  • - *
  • Realistic pages (80% full): ~50% savings
  • - *
  • Full pages: ~50% savings
  • - *
- * - *

Format

- * - *
- * ┌─────────────────────────────────────────────────────────────────┐
- * │                    Compressed Slot Offsets                       │
- * ├─────────────────────────────────────────────────────────────────┤
- * │ Presence Bitmap   │ Bit Width │ Bit-Packed Offsets              │
- * │ (BitSet ~128B)    │ (1 byte)  │ (N×bitWidth bits)               │
- * └─────────────────────────────────────────────────────────────────┘
- * 
- * - *

Algorithm

- *
    - *
  1. Build a presence bitmap indicating which slots are populated (offset >= 0)
  2. - *
  3. Extract populated offsets in slot order
  4. - *
  5. Determine minimum bit width needed to represent max offset
  6. - *
  7. Bit-pack all offsets using the computed bit width
  8. - *
- * - *

- * Note: Delta encoding is not used because slot offsets are not guaranteed to be monotonically - * increasing when iterated in slot order. - *

- * - * @author Johannes Lichtenberger - */ -public final class SlotOffsetCodec { - - /** - * ThreadLocal buffer for reading packed bytes during decode. Sized to handle worst case: 1024 slots - * × 32 bits = 4096 bytes. Avoids allocation on every page read (critical read path). - */ - private static final ThreadLocal DECODE_BUFFER = - ThreadLocal.withInitial(() -> new byte[Constants.NDP_NODE_COUNT * 4]); // 4KB max - - /** - * ThreadLocal buffer for writing packed bytes during encode. Avoids allocation on every page write. - */ - private static final ThreadLocal ENCODE_BUFFER = - ThreadLocal.withInitial(() -> new byte[Constants.NDP_NODE_COUNT * 4]); // 4KB max - - /** - * ThreadLocal BitSet for encode operations. Reused across calls to avoid allocation. - */ - private static final ThreadLocal ENCODE_BITSET = - ThreadLocal.withInitial(() -> new BitSet(Constants.NDP_NODE_COUNT)); - - /** - * ThreadLocal array for populated offsets during encode. - */ - private static final ThreadLocal ENCODE_OFFSETS = - ThreadLocal.withInitial(() -> new int[Constants.NDP_NODE_COUNT]); - - private SlotOffsetCodec() { - throw new AssertionError("May not be instantiated!"); - } - - /** - * Encode slot offsets using bit-packing. - * - *

- * Format: [presence bitmap][bitWidth (1B)][bit-packed offsets] - *

- * - * @param sink the output to write compressed data to - * @param slotOffsets the slot offset array (length must be Constants.NDP_NODE_COUNT) - * @param lastSlotIndex unused, kept for API compatibility - * @throws NullPointerException if sink or slotOffsets is null - * @throws IllegalArgumentException if slotOffsets.length != Constants.NDP_NODE_COUNT - */ - public static void encode(final BytesOut sink, final int[] slotOffsets, final int lastSlotIndex) { - if (sink == null) { - throw new NullPointerException("sink must not be null"); - } - if (slotOffsets == null) { - throw new NullPointerException("slotOffsets must not be null"); - } - if (slotOffsets.length != Constants.NDP_NODE_COUNT) { - throw new IllegalArgumentException( - "slotOffsets.length must be " + Constants.NDP_NODE_COUNT + " but was " + slotOffsets.length); - } - - // 1. Build presence bitmap by scanning all slots (reuse ThreadLocal BitSet) - // Note: lastSlotIndex tracks the slot with largest OFFSET, not highest slot NUMBER - // So we must scan all slots to find all populated ones - final BitSet presence = ENCODE_BITSET.get(); - presence.clear(); // Reset for reuse - int populatedCount = 0; - - for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { - if (slotOffsets[i] >= 0) { - presence.set(i); - populatedCount++; - } - } - - SerializationType.serializeBitSet(sink, presence); - - if (populatedCount == 0) { - return; // Empty page - just bitmap is sufficient - } - - // 2. Extract populated offsets in slot order and find max (reuse ThreadLocal array) - final int[] populatedOffsets = ENCODE_OFFSETS.get(); - int idx = 0; - int maxOffset = 0; - for (int i = presence.nextSetBit(0); i >= 0; i = presence.nextSetBit(i + 1)) { - final int offset = slotOffsets[i]; - populatedOffsets[idx++] = offset; - maxOffset = Math.max(maxOffset, offset); - } - - // 3. Determine bit width from max offset (not deltas - offsets aren't monotonic) - int bitWidth = 32 - Integer.numberOfLeadingZeros(maxOffset); - if (bitWidth == 0) { - bitWidth = 1; // Minimum 1 bit - } - - sink.writeByte((byte) bitWidth); - - // 4. Bit-pack all offsets directly (pass count since array may be larger) - writeBitPacked(sink, populatedOffsets, populatedCount, bitWidth); - } - - /** - * Decode compressed slot offsets back to int[1024] array. - * - *

- * Empty slots are marked with -1. - *

- *

- * Optimized to avoid intermediate array allocation by reading directly into sparse result. - *

- * - * @param source the input to read compressed data from - * @return the decoded slot offset array of length Constants.NDP_NODE_COUNT - * @throws NullPointerException if source is null - */ - public static int[] decode(final BytesIn source) { - if (source == null) { - throw new NullPointerException("source must not be null"); - } - - final int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - // 1. Read presence bitmap - final BitSet presence = SerializationType.deserializeBitSet(source); - final int populatedCount = presence.cardinality(); - - if (populatedCount == 0) { - return slotOffsets; // Empty page - } - - // 2. Read bit width and unpack directly into sparse result array - final int bitWidth = source.readByte() & 0xFF; - readBitPackedDirect(source, presence, slotOffsets, populatedCount, bitWidth); - - return slotOffsets; - } - - /** - * Pack integers using exactly bitWidth bits each. - * - *

- * Values are packed in little-endian bit order within each byte. - *

- * - * @param sink the output to write packed data to - * @param values the integer values to pack (all values used) - * @param bitWidth the number of bits to use per value (1-32) - */ - static void writeBitPacked(final BytesOut sink, final int[] values, final int bitWidth) { - writeBitPacked(sink, values, values.length, bitWidth); - } - - /** - * Pack integers using exactly bitWidth bits each. - * - *

- * Values are packed in little-endian bit order within each byte. - *

- *

- * Uses ThreadLocal buffer to avoid allocation on hot path. - *

- * - * @param sink the output to write packed data to - * @param values the integer values to pack - * @param count the number of values to pack from the array - * @param bitWidth the number of bits to use per value (1-32) - */ - static void writeBitPacked(final BytesOut sink, final int[] values, final int count, final int bitWidth) { - if (count == 0) { - return; - } - - // Calculate total bytes needed - final int totalBits = count * bitWidth; - final int totalBytes = (totalBits + 7) / 8; - - // Reuse ThreadLocal buffer to avoid allocation - final byte[] packed = ENCODE_BUFFER.get(); - Arrays.fill(packed, 0, totalBytes, (byte) 0); // Clear only needed portion - int bitPos = 0; - - for (int i = 0; i < count; i++) { - final int value = values[i]; - // Write 'bitWidth' bits of value starting at bitPos - final int bytePos = bitPos / 8; - final int bitOffset = bitPos % 8; - - // Handle value spanning multiple bytes (up to 5 bytes for 32-bit values) - final long shifted = ((long) value) << bitOffset; - final int bytesNeeded = (bitOffset + bitWidth + 7) / 8; - - for (int b = 0; b < bytesNeeded && bytePos + b < totalBytes; b++) { - packed[bytePos + b] |= (byte) (shifted >>> (b * 8)); - } - bitPos += bitWidth; - } - - sink.write(packed, 0, totalBytes); - } - - /** - * Unpack integers from bitWidth-bit packed format directly into sparse result array. - *

- * Optimized to avoid intermediate array allocation by writing directly to the positions indicated - * by the presence BitSet. - *

- * - * @param source the input to read packed data from - * @param presence BitSet indicating which slots are populated - * @param result the sparse result array to write values into - * @param count the number of integers to unpack - * @param bitWidth the number of bits per value (1-32) - */ - private static void readBitPackedDirect(final BytesIn source, final BitSet presence, final int[] result, - final int count, final int bitWidth) { - if (count == 0) { - return; - } - - final int totalBits = count * bitWidth; - final int totalBytes = (totalBits + 7) / 8; - - // Reuse ThreadLocal buffer to avoid allocation on hot path - final byte[] packed = DECODE_BUFFER.get(); - source.read(packed, 0, totalBytes); - - int bitPos = 0; - final int mask = (bitWidth == 32) - ? -1 - : (1 << bitWidth) - 1; - - // Iterate through presence bits and write directly to result positions - for (int slotIndex = presence.nextSetBit(0); slotIndex >= 0; slotIndex = presence.nextSetBit(slotIndex + 1)) { - final int bytePos = bitPos / 8; - final int bitOffset = bitPos % 8; - - // Read up to 5 bytes to handle 32-bit values spanning byte boundaries - long accumulated = 0; - for (int b = 0; b < 5 && bytePos + b < totalBytes; b++) { - accumulated |= ((long) (packed[bytePos + b] & 0xFF)) << (b * 8); - } - - result[slotIndex] = (int) ((accumulated >>> bitOffset) & mask); - bitPos += bitWidth; - } - } - - /** - * Unpack integers from bitWidth-bit packed format. - *

- * Uses ThreadLocal buffer to avoid allocation on hot path. - *

- * - * @param source the input to read packed data from - * @param count the number of integers to unpack - * @param bitWidth the number of bits per value (1-32) - * @return the unpacked integer values - */ - static int[] readBitPacked(final BytesIn source, final int count, final int bitWidth) { - if (count == 0) { - return new int[0]; - } - - final int totalBits = count * bitWidth; - final int totalBytes = (totalBits + 7) / 8; - - // Reuse ThreadLocal buffer to avoid allocation - final byte[] packed = DECODE_BUFFER.get(); - source.read(packed, 0, totalBytes); - - final int[] values = new int[count]; - int bitPos = 0; - final int mask = (bitWidth == 32) - ? -1 - : (1 << bitWidth) - 1; - - for (int i = 0; i < count; i++) { - final int bytePos = bitPos / 8; - final int bitOffset = bitPos % 8; - - // Read up to 5 bytes to handle 32-bit values spanning byte boundaries - long accumulated = 0; - for (int b = 0; b < 5 && bytePos + b < totalBytes; b++) { - accumulated |= ((long) (packed[bytePos + b] & 0xFF)) << (b * 8); - } - - values[i] = (int) ((accumulated >>> bitOffset) & mask); - bitPos += bitWidth; - } - - return values; - } -} - - diff --git a/bundles/sirix-core/src/main/java/io/sirix/settings/VersioningType.java b/bundles/sirix-core/src/main/java/io/sirix/settings/VersioningType.java index 9b725b048..03cf621a0 100644 --- a/bundles/sirix-core/src/main/java/io/sirix/settings/VersioningType.java +++ b/bundles/sirix-core/src/main/java/io/sirix/settings/VersioningType.java @@ -29,6 +29,7 @@ import io.sirix.page.BitmapChunkPage; import io.sirix.page.HOTLeafPage; import io.sirix.page.KeyValueLeafPage; +import io.sirix.page.PageLayout; import io.sirix.page.PageFragmentKeyImpl; import io.sirix.page.PageReference; import io.sirix.page.interfaces.KeyValuePage; @@ -37,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.foreign.MemorySegment; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -61,18 +61,23 @@ public > T combineRecordPages(fi final @NonNegative int revToRestore, final StorageEngineReader storageEngineReader) { assert pages.size() == 1 : "Only one version of the page!"; var firstPage = pages.getFirst(); - T completePage = firstPage.newInstance(firstPage.getPageKey(), firstPage.getIndexType(), storageEngineReader); + T completePage = firstPage.newInstance(firstPage.getPageKey(), firstPage.getIndexType(), storageEngineReader); - if (firstPage instanceof KeyValueLeafPage firstKvp) { - copyAllSlotsFromBitmap(firstPage, completePage, firstKvp.getSlotBitmap(), null); - } else { - for (int i = 0; i < firstPage.size(); i++) { - var slot = firstPage.getSlot(i); - if (slot == null) { - continue; - } - copySlotAndDeweyId(firstPage, completePage, i, slot); + final KeyValueLeafPage srcKvl = (KeyValueLeafPage) firstPage; + final KeyValueLeafPage dstKvl = (KeyValueLeafPage) completePage; + dstKvl.ensureSlottedPage(); + + // Use populatedSlots() for O(k) bitmap-driven iteration + final int[] populated = srcKvl.populatedSlots(); + for (final int i : populated) { + var slot = firstPage.getSlot(i); + + if (slot == null) { + continue; } + + dstKvl.setSlotWithNodeKind(slot, i, srcKvl.getSlotNodeKindId(i)); + completePage.setDeweyId(firstPage.getDeweyId(i), i); } // Propagate FSST symbol table for string compression @@ -93,17 +98,19 @@ public > PageContainer combineRe // FULL versioning stores complete pages, so both complete and modified can be the same final T modifiedPage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); - // Copy data once (not twice) - if (firstPage instanceof KeyValueLeafPage firstKvp) { - copyAllSlotsFromBitmap(firstPage, modifiedPage, firstKvp.getSlotBitmap(), null); - } else { - for (int i = 0; i < firstPage.size(); i++) { - var slot = firstPage.getSlot(i); - if (slot == null) { - continue; - } - copySlotAndDeweyId(firstPage, modifiedPage, i, slot); + final KeyValueLeafPage srcKvl = (KeyValueLeafPage) firstPage; + final KeyValueLeafPage dstKvl = (KeyValueLeafPage) modifiedPage; + dstKvl.ensureSlottedPage(); + + // Copy data once (not twice) - use populatedSlots() for O(k) iteration + final int[] populated = srcKvl.populatedSlots(); + for (final int i : populated) { + var slot = firstPage.getSlot(i); + if (slot == null) { + continue; } + dstKvl.setSlotWithNodeKind(slot, i, srcKvl.getSlotNodeKindId(i)); + modifiedPage.setDeweyId(firstPage.getDeweyId(i), i); } // Propagate FSST symbol table from the original page @@ -119,7 +126,7 @@ public > PageContainer combineRe @Override public int[] getRevisionRoots(@NonNegative int previousRevision, @NonNegative int revsToRestore) { - return new int[] {previousRevision}; + return new int[] { previousRevision }; } }, @@ -137,9 +144,7 @@ public > T combineRecordPages(fi final T pageToReturn = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); final T latest = pages.get(0); - final T fullDump = pages.size() == 1 - ? pages.get(0) - : pages.get(1); + final T fullDump = pages.size() == 1 ? pages.get(0) : pages.get(1); assert latest.getPageKey() == recordPageKey; assert fullDump.getPageKey() == recordPageKey; @@ -147,9 +152,18 @@ public > T combineRecordPages(fi // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage latestKvp = (KeyValueLeafPage) latest; final KeyValueLeafPage returnKvp = (KeyValueLeafPage) pageToReturn; + returnKvp.ensureSlottedPage(); // Copy all populated slots from latest page - copyAllSlotsFromBitmap(firstPage, pageToReturn, latestKvp.getSlotBitmap(), null); + final int[] latestSlots = latestKvp.populatedSlots(); + for (int i = 0; i < latestSlots.length; i++) { + final int offset = latestSlots[i]; + returnKvp.setSlotWithNodeKind(firstPage.getSlot(offset), offset, latestKvp.getSlotNodeKindId(offset)); + var deweyId = firstPage.getDeweyId(offset); + if (deweyId != null) { + pageToReturn.setDeweyId(deweyId, offset); + } + } // Copy references from latest for (final Map.Entry entry : latest.referenceEntrySet()) { @@ -162,7 +176,22 @@ public > T combineRecordPages(fi final long[] filledBitmap = returnKvp.getSlotBitmap(); // Use bitmap iteration for O(k) on fullDump - copyMissingSlotsFromBitmap(fullDump, pageToReturn, fullDumpKvp.getSlotBitmap(), filledBitmap, null); + final int[] fullDumpSlots = fullDumpKvp.populatedSlots(); + for (int i = 0; i < fullDumpSlots.length; i++) { + final int offset = fullDumpSlots[i]; + // Check if slot already filled using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from latest + } + + var recordData = fullDump.getSlot(offset); + returnKvp.setSlotWithNodeKind(recordData, offset, fullDumpKvp.getSlotNodeKindId(offset)); + + var deweyId = fullDump.getDeweyId(offset); + if (deweyId != null) { + pageToReturn.setDeweyId(deweyId, offset); + } + } // Fill reference gaps for (final Entry entry : fullDump.referenceEntrySet()) { @@ -191,33 +220,44 @@ public > PageContainer combineRe final int revision = storageEngineReader.getUberPage().getRevisionNumber(); // Update pageFragments on original reference - final List pageFragmentKeys = List.of(new PageFragmentKeyImpl(firstPage.getRevision(), - reference.getKey(), (int) storageEngineReader.getDatabaseId(), (int) storageEngineReader.getResourceId())); + final List pageFragmentKeys = List.of(new PageFragmentKeyImpl( + firstPage.getRevision(), + reference.getKey(), + (int) storageEngineReader.getDatabaseId(), + (int) storageEngineReader.getResourceId())); reference.setPageFragments(pageFragmentKeys); final T completePage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); final T modifiedPage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); - // DIAGNOSTIC - if (KeyValueLeafPage.DEBUG_MEMORY_LEAKS && recordPageKey == 0) { - LOGGER.debug("DIFFERENTIAL combineForMod created: complete=" + System.identityHashCode(completePage) - + ", modified=" + System.identityHashCode(modifiedPage)); - } - final T latest = firstPage; - final T fullDump = pages.size() == 1 - ? firstPage - : pages.get(1); + final T fullDump = pages.size() == 1 ? firstPage : pages.get(1); final boolean isFullDumpRevision = revision % revToRestore == 0; // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage latestKvp = (KeyValueLeafPage) latest; final KeyValueLeafPage completeKvp = (KeyValueLeafPage) completePage; final KeyValueLeafPage modifiedKvp = (KeyValueLeafPage) modifiedPage; + completeKvp.ensureSlottedPage(); + modifiedKvp.ensureSlottedPage(); // Copy all populated slots from latest to completePage using bitmap iteration // For modifiedPage: use lazy copy - mark for preservation, actual copy deferred to commit time - copyAllSlotsFromBitmap(firstPage, completePage, latestKvp.getSlotBitmap(), modifiedKvp); + final int[] latestSlots = latestKvp.populatedSlots(); + for (int i = 0; i < latestSlots.length; i++) { + final int offset = latestSlots[i]; + var recordData = firstPage.getSlot(offset); + var deweyId = firstPage.getDeweyId(offset); + + completeKvp.setSlotWithNodeKind(recordData, offset, latestKvp.getSlotNodeKindId(offset)); + if (deweyId != null) { + completePage.setDeweyId(deweyId, offset); + } + + // LAZY COPY: Mark slot for preservation instead of copying + // Actual copy from completePage happens in addReferences() if records[offset] == null + modifiedKvp.markSlotForPreservation(offset); + } // Copy references from latest for (final Map.Entry entry : latest.referenceEntrySet()) { @@ -229,12 +269,30 @@ public > PageContainer combineRe if (completeKvp.populatedSlotCount() < Constants.NDP_NODE_COUNT && pages.size() == 2) { final KeyValueLeafPage fullDumpKvp = (KeyValueLeafPage) fullDump; final long[] filledBitmap = completeKvp.getSlotBitmap(); - + // Use bitmap iteration on fullDump - copyMissingSlotsFromBitmap(fullDump, completePage, fullDumpKvp.getSlotBitmap(), filledBitmap, isFullDumpRevision - ? modifiedKvp - : null); + final int[] fullDumpSlots = fullDumpKvp.populatedSlots(); + for (int j = 0; j < fullDumpSlots.length; j++) { + final int offset = fullDumpSlots[j]; + // Check if slot already filled using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from latest + } + + var recordData = fullDump.getSlot(offset); + var deweyId = fullDump.getDeweyId(offset); + + completeKvp.setSlotWithNodeKind(recordData, offset, fullDumpKvp.getSlotNodeKindId(offset)); + if (deweyId != null) { + completePage.setDeweyId(deweyId, offset); + } + if (isFullDumpRevision) { + // LAZY COPY: Mark slot for preservation instead of copying + modifiedKvp.markSlotForPreservation(offset); + } + } + // Fill reference gaps from fullDump for (final Map.Entry entry : fullDump.referenceEntrySet()) { if (completePage.getPageReference(entry.getKey()) == null) { @@ -255,7 +313,7 @@ public > PageContainer combineRe modifiedKvp.setCompletePageRef(completeKvp); final var pageContainer = PageContainer.getInstance(completePage, modifiedPage); - log.put(reference, pageContainer); // TIL will remove from caches before mutating + log.put(reference, pageContainer); // TIL will remove from caches before mutating return pageContainer; } @@ -264,9 +322,9 @@ public int[] getRevisionRoots(@NonNegative int previousRevision, @NonNegative in final int revisionsToRestore = previousRevision % revsToRestore; final int lastFullDump = previousRevision - revisionsToRestore; if (lastFullDump == previousRevision) { - return new int[] {lastFullDump}; + return new int[] { lastFullDump }; } else { - return new int[] {previousRevision, lastFullDump}; + return new int[] { previousRevision, lastFullDump }; } } }, @@ -287,11 +345,12 @@ public > T combineRecordPages(fi // Track which slots are already filled using bitmap from pageToReturn // This enables O(k) iteration instead of O(1024) final KeyValueLeafPage returnPage = (KeyValueLeafPage) pageToReturn; + returnPage.ensureSlottedPage(); final long[] filledBitmap = returnPage.getSlotBitmap(); - + // Track slot count incrementally - CRITICAL: don't call populatedSlotCount() in loop int filledSlotCount = 0; - + for (final T page : pages) { assert page.getPageKey() == recordPageKey; if (filledSlotCount == Constants.NDP_NODE_COUNT) { @@ -300,8 +359,28 @@ public > T combineRecordPages(fi // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage kvPage = (KeyValueLeafPage) page; - filledSlotCount = copyMissingSlotsFromBitmapUntilFull(page, pageToReturn, kvPage.getSlotBitmap(), filledBitmap, - filledSlotCount, null); + final int[] populatedSlots = kvPage.populatedSlots(); + + for (final int offset : populatedSlots) { + // Check if slot already filled using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from newer fragment + } + + final var recordData = page.getSlot(offset); + returnPage.setSlotWithNodeKind(recordData, offset, kvPage.getSlotNodeKindId(offset)); + filledBitmap[offset >>> 6] |= (1L << (offset & 63)); + filledSlotCount++; + + final var deweyId = page.getDeweyId(offset); + if (deweyId != null) { + pageToReturn.setDeweyId(deweyId, offset); + } + + if (filledSlotCount == Constants.NDP_NODE_COUNT) { + break; + } + } if (filledSlotCount < Constants.NDP_NODE_COUNT) { for (final Entry entry : page.referenceEntrySet()) { @@ -329,10 +408,13 @@ public > PageContainer combineRe final T firstPage = pages.getFirst(); final long recordPageKey = firstPage.getPageKey(); final var previousPageFragmentKeys = new ArrayList(reference.getPageFragments().size() + 1); - previousPageFragmentKeys.add(new PageFragmentKeyImpl(firstPage.getRevision(), reference.getKey(), - (int) storageEngineReader.getDatabaseId(), (int) storageEngineReader.getResourceId())); - for (int i = 0, previousRefKeysSize = reference.getPageFragments().size(); i < previousRefKeysSize - && previousPageFragmentKeys.size() < revToRestore - 1; i++) { + previousPageFragmentKeys.add(new PageFragmentKeyImpl( + firstPage.getRevision(), + reference.getKey(), + (int) storageEngineReader.getDatabaseId(), + (int) storageEngineReader.getResourceId())); + for (int i = 0, previousRefKeysSize = reference.getPageFragments().size(); + i < previousRefKeysSize && previousPageFragmentKeys.size() < revToRestore - 1; i++) { previousPageFragmentKeys.add(reference.getPageFragments().get(i)); } @@ -343,15 +425,12 @@ public > PageContainer combineRe final T modifiedPage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); final boolean isFullDump = pages.size() == revToRestore; - // DIAGNOSTIC - if (KeyValueLeafPage.DEBUG_MEMORY_LEAKS && recordPageKey == 0) { - LOGGER.debug("INCREMENTAL combineForMod created: complete=" + System.identityHashCode(completePage) - + ", modified=" + System.identityHashCode(modifiedPage)); - } + final long[] inWindowBitmap = new long[PageLayout.BITMAP_WORDS]; - // Use bitmap for O(k) iteration instead of O(1024) final KeyValueLeafPage completeKvp = (KeyValueLeafPage) completePage; final KeyValueLeafPage modifiedKvp = (KeyValueLeafPage) modifiedPage; + completeKvp.ensureSlottedPage(); + modifiedKvp.ensureSlottedPage(); final long[] filledBitmap = completeKvp.getSlotBitmap(); // Track slot count incrementally - CRITICAL: don't call populatedSlotCount() in loop @@ -365,10 +444,35 @@ public > PageContainer combineRe // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage kvPage = (KeyValueLeafPage) page; - filledSlotCount = copyMissingSlotsFromBitmapUntilFull(page, completePage, kvPage.getSlotBitmap(), filledBitmap, - filledSlotCount, isFullDump - ? modifiedKvp - : null); + final int[] populatedSlots = kvPage.populatedSlots(); + + for (final int offset : populatedSlots) { + // Check if slot already filled using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from newer fragment + } + + final var recordData = page.getSlot(offset); + completeKvp.setSlotWithNodeKind(recordData, offset, kvPage.getSlotNodeKindId(offset)); + filledBitmap[offset >>> 6] |= (1L << (offset & 63)); + filledSlotCount++; + + if (isFullDump) { + // LAZY COPY: Mark slot for preservation instead of copying + // Actual copy from completePage happens in addReferences() if records[offset] == null + modifiedKvp.markSlotForPreservation(offset); + } + + final var deweyId = page.getDeweyId(offset); + if (deweyId != null) { + completePage.setDeweyId(deweyId, offset); + // DeweyId will be lazily copied along with slot in addReferences() + } + + if (filledSlotCount == Constants.NDP_NODE_COUNT) { + break; + } + } if (filledSlotCount < Constants.NDP_NODE_COUNT) { for (final Entry entry : page.referenceEntrySet()) { @@ -395,7 +499,7 @@ public > PageContainer combineRe } final var pageContainer = PageContainer.getInstance(completePage, modifiedPage); - log.put(reference, pageContainer); // TIL will remove from caches before mutating + log.put(reference, pageContainer); // TIL will remove from caches before mutating return pageContainer; } @@ -435,11 +539,12 @@ public > T combineRecordPages(fi // Track which slots are already filled using bitmap from returnVal // This enables O(k) iteration instead of O(1024) final KeyValueLeafPage returnKvp = (KeyValueLeafPage) returnVal; + returnKvp.ensureSlottedPage(); final long[] filledBitmap = returnKvp.getSlotBitmap(); - + // Track slot count incrementally - CRITICAL: don't call populatedSlotCount() in loop int filledSlotCount = 0; - + for (final T page : pages) { assert page.getPageKey() == recordPageKey; if (filledSlotCount == Constants.NDP_NODE_COUNT) { @@ -448,8 +553,30 @@ public > T combineRecordPages(fi // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage kvPage = (KeyValueLeafPage) page; - filledSlotCount = copyMissingSlotsFromBitmapUntilFull(page, returnVal, kvPage.getSlotBitmap(), filledBitmap, - filledSlotCount, null); + final int[] populatedSlots = kvPage.populatedSlots(); + + for (final int offset : populatedSlots) { + // Check if slot already filled using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from newer fragment + } + + final var recordData = page.getSlot(offset); + returnKvp.setSlotWithNodeKind(recordData, offset, kvPage.getSlotNodeKindId(offset)); + // Keep local bitmap in sync with slotted page bitmap so that older fragments + // don't overwrite newer data (setSlotToHeap only updates the slotted page bitmap) + filledBitmap[offset >>> 6] |= (1L << (offset & 63)); + filledSlotCount++; + + final var deweyId = page.getDeweyId(offset); + if (deweyId != null) { + returnVal.setDeweyId(deweyId, offset); + } + + if (filledSlotCount == Constants.NDP_NODE_COUNT) { + break; + } + } if (filledSlotCount < Constants.NDP_NODE_COUNT) { for (final Entry entry : page.referenceEntrySet()) { @@ -472,15 +599,18 @@ public > T combineRecordPages(fi @Override public > PageContainer combineRecordPagesForModification( - final List pages, final int revToRestore, final StorageEngineReader storageEngineReader, - final PageReference reference, final TransactionIntentLog log) { + final List pages, final int revToRestore, final StorageEngineReader storageEngineReader, final PageReference reference, + final TransactionIntentLog log) { final T firstPage = pages.getFirst(); final long recordPageKey = firstPage.getPageKey(); final var previousPageFragmentKeys = new ArrayList(reference.getPageFragments().size() + 1); - previousPageFragmentKeys.add(new PageFragmentKeyImpl(firstPage.getRevision(), reference.getKey(), - (int) storageEngineReader.getDatabaseId(), (int) storageEngineReader.getResourceId())); - for (int i = 0, previousRefKeysSize = reference.getPageFragments().size(); i < previousRefKeysSize - && previousPageFragmentKeys.size() < revToRestore - 1; i++) { + previousPageFragmentKeys.add(new PageFragmentKeyImpl( + firstPage.getRevision(), + reference.getKey(), + (int) storageEngineReader.getDatabaseId(), + (int) storageEngineReader.getResourceId())); + for (int i = 0, previousRefKeysSize = reference.getPageFragments().size(); + i < previousRefKeysSize && previousPageFragmentKeys.size() < revToRestore - 1; i++) { previousPageFragmentKeys.add(reference.getPageFragments().get(i)); } @@ -491,29 +621,23 @@ public > PageContainer combineRe // This saves 64KB allocation per combine operation final T completePage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); final T modifyingPage = firstPage.newInstance(recordPageKey, firstPage.getIndexType(), storageEngineReader); - + // OPTIMIZATION: Use bitmap (128 bytes) instead of temp page (64KB) // inWindowBitmap tracks which slots exist in the sliding window - final long[] inWindowBitmap = new long[SLOT_BITMAP_WORD_COUNT]; - - // DIAGNOSTIC - if (KeyValueLeafPage.DEBUG_MEMORY_LEAKS && recordPageKey == 0) { - LOGGER.debug("SLIDING_SNAPSHOT combineForMod created 2 pages + bitmap: complete=" - + System.identityHashCode(completePage) + ", modifying=" + System.identityHashCode(modifyingPage)); - } - + final long[] inWindowBitmap = new long[16]; // 16 * 64 = 1024 bits + final KeyValueLeafPage completeKvp = (KeyValueLeafPage) completePage; final KeyValueLeafPage modifyingKvp = (KeyValueLeafPage) modifyingPage; + completeKvp.ensureSlottedPage(); + modifyingKvp.ensureSlottedPage(); final long[] filledBitmap = completeKvp.getSlotBitmap(); - + final boolean hasOutOfWindowPage = (pages.size() == revToRestore); - final int lastInWindowIndex = hasOutOfWindowPage - ? pages.size() - 2 - : pages.size() - 1; - + final int lastInWindowIndex = hasOutOfWindowPage ? pages.size() - 2 : pages.size() - 1; + // Track slot count incrementally - CRITICAL: don't call populatedSlotCount() in loop int filledSlotCount = 0; - + // Phase 1: Process in-window fragments, track populated slots in bitmap for (int i = 0; i <= lastInWindowIndex; i++) { final T page = pages.get(i); @@ -521,8 +645,31 @@ public > PageContainer combineRe // Use bitmap iteration for O(k) instead of O(1024) final KeyValueLeafPage kvPage = (KeyValueLeafPage) page; - filledSlotCount = copyMissingSlotsFromBitmapUntilFullTrackingWindow(page, completePage, kvPage.getSlotBitmap(), - filledBitmap, inWindowBitmap, filledSlotCount); + final int[] populatedSlots = kvPage.populatedSlots(); + + for (final int offset : populatedSlots) { + // Mark slot as in-window (for Phase 2 check) + inWindowBitmap[offset >>> 6] |= (1L << (offset & 63)); + + // Check if slot already filled in completePage using bitmap (O(1)) + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) != 0) { + continue; // Already filled from newer fragment + } + + final var recordData = page.getSlot(offset); + completeKvp.setSlotWithNodeKind(recordData, offset, kvPage.getSlotNodeKindId(offset)); + filledBitmap[offset >>> 6] |= (1L << (offset & 63)); + filledSlotCount++; + + final var deweyId = page.getDeweyId(offset); + if (deweyId != null) { + completePage.setDeweyId(deweyId, offset); + } + + if (filledSlotCount == Constants.NDP_NODE_COUNT) { + break; // Page is full + } + } // Handle references for (final Entry entry : page.referenceEntrySet()) { @@ -533,7 +680,7 @@ public > PageContainer combineRe } if (filledSlotCount == Constants.NDP_NODE_COUNT) { - break; // Page is full + break; // Page is full } } @@ -545,9 +692,29 @@ public > PageContainer combineRe assert outOfWindowPage.getPageKey() == recordPageKey; final KeyValueLeafPage outOfWindowKvp = (KeyValueLeafPage) outOfWindowPage; - copyOutOfWindowSlotsAndMarkPreservation(outOfWindowPage, completePage, outOfWindowKvp.getSlotBitmap(), - filledBitmap, inWindowBitmap, modifyingKvp); - + final int[] populatedSlots = outOfWindowKvp.populatedSlots(); + + for (final int offset : populatedSlots) { + final var recordData = outOfWindowPage.getSlot(offset); + final var deweyId = outOfWindowPage.getDeweyId(offset); + + // Add to completePage if not already filled + if ((filledBitmap[offset >>> 6] & (1L << (offset & 63))) == 0) { + completeKvp.setSlotWithNodeKind(recordData, offset, outOfWindowKvp.getSlotNodeKindId(offset)); + filledBitmap[offset >>> 6] |= (1L << (offset & 63)); + if (deweyId != null) { + completePage.setDeweyId(deweyId, offset); + } + } + + // If slot is NOT in the sliding window, mark for preservation in modifyingPage + // (these are records falling out of the window that need to be written) + if ((inWindowBitmap[offset >>> 6] & (1L << (offset & 63))) == 0) { + // LAZY COPY: Mark slot for preservation instead of copying + modifyingKvp.markSlotForPreservation(offset); + } + } + // Handle references from out-of-window page for (final Entry entry : outOfWindowPage.referenceEntrySet()) { final Long key = entry.getKey(); @@ -560,7 +727,7 @@ public > PageContainer combineRe modifyingPage.setPageReference(key, entry.getValue()); } } - + // Set completePage reference for lazy copying at commit time modifyingKvp.setCompletePageRef(completeKvp); } @@ -570,7 +737,7 @@ public > PageContainer combineRe propagateFsstSymbolTable(firstPage, modifyingPage); final var pageContainer = PageContainer.getInstance(completePage, modifyingPage); - log.put(reference, pageContainer); // TIL will remove from caches before mutating + log.put(reference, pageContainer); // TIL will remove from caches before mutating return pageContainer; } @@ -596,7 +763,6 @@ private int[] convertIntegers(final List integers) { }; private static final Logger LOGGER = LoggerFactory.getLogger(VersioningType.class); - private static final int SLOT_BITMAP_WORD_COUNT = (Constants.NDP_NODE_COUNT + Long.SIZE - 1) >>> 6; public static VersioningType fromString(String versioningType) { for (final var type : values()) { @@ -608,10 +774,10 @@ public static VersioningType fromString(String versioningType) { } /** - * Method to reconstruct a complete {@link KeyValuePage} with the help of partly filled pages plus a - * revision-delta which determines the necessary steps back. + * Method to reconstruct a complete {@link KeyValuePage} with the help of partly filled pages plus + * a revision-delta which determines the necessary steps back. * - * @param pages the base of the complete {@link KeyValuePage} + * @param pages the base of the complete {@link KeyValuePage} * @param revsToRestore the number of revisions needed to build the complete record page * @return the complete {@link KeyValuePage} */ @@ -622,10 +788,10 @@ public abstract > T combineRecor * Method to reconstruct a complete {@link KeyValuePage} for reading as well as a * {@link KeyValuePage} for serializing with the nodes to write. * - * @param pages the base of the complete {@link KeyValuePage} + * @param pages the base of the complete {@link KeyValuePage} * @param revsToRestore the revisions needed to build the complete record page * @return a {@link PageContainer} holding a complete {@link KeyValuePage} for reading and one for - * writing + * writing */ public abstract > PageContainer combineRecordPagesForModification( final List pages, final @NonNegative int revsToRestore, final StorageEngineReader storageEngineReader, @@ -635,192 +801,70 @@ public abstract > PageContainer * Get all revision root page numbers which are needed to restore a {@link KeyValuePage}. * * @param previousRevision the previous revision - * @param revsToRestore number of revisions to restore + * @param revsToRestore number of revisions to restore * @return revision root page numbers needed to restore a {@link KeyValuePage} */ public abstract int[] getRevisionRoots(final @NonNegative int previousRevision, final @NonNegative int revsToRestore); /** - * Propagate FSST symbol table from source page to target page. This is needed when combining page - * fragments to ensure the combined page can decompress string values. + * Propagate FSST symbol table from source page to target page. + * This is needed when combining page fragments to ensure the combined page + * can decompress string values. * * @param sourcePage the source page with the FSST symbol table * @param targetPage the target page to set the symbol table on * @param the data record type * @param the key-value page type */ - protected static > void propagateFsstSymbolTable(final T sourcePage, - final T targetPage) { - if (sourcePage instanceof KeyValueLeafPage sourceKvp && targetPage instanceof KeyValueLeafPage targetKvp) { - final byte[] fsstSymbolTable = sourceKvp.getFsstSymbolTable(); + protected static > void propagateFsstSymbolTable( + final T sourcePage, final T targetPage) { + if (sourcePage instanceof KeyValueLeafPage sourceKvp + && targetPage instanceof KeyValueLeafPage targetKvp) { + byte[] fsstSymbolTable = sourceKvp.getFsstSymbolTable(); if (fsstSymbolTable != null && fsstSymbolTable.length > 0) { targetKvp.setFsstSymbolTable(fsstSymbolTable); } } } - private static > void copySlotAndDeweyId(final T sourcePage, - final T targetPage, final int offset, final MemorySegment slot) { - if (slot == null) { - return; - } - targetPage.setSlot(slot, offset); - final var deweyId = sourcePage.getDeweyId(offset); - if (deweyId != null) { - targetPage.setDeweyId(deweyId, offset); - } - } - - private static > void copyAllSlotsFromBitmap(final T sourcePage, - final T targetPage, final long[] sourceBitmap, final KeyValueLeafPage preserveTargetPage) { - for (int wordIndex = 0; wordIndex < sourceBitmap.length; wordIndex++) { - long word = sourceBitmap[wordIndex]; - final int baseOffset = wordIndex << 6; - while (word != 0) { - final int offset = baseOffset + Long.numberOfTrailingZeros(word); - copySlotAndDeweyId(sourcePage, targetPage, offset, sourcePage.getSlot(offset)); - if (preserveTargetPage != null) { - preserveTargetPage.markSlotForPreservation(offset); - } - word &= word - 1; - } - } - } - - private static > void copyMissingSlotsFromBitmap(final T sourcePage, - final T targetPage, final long[] sourceBitmap, final long[] targetBitmap, - final KeyValueLeafPage preserveTargetPage) { - for (int wordIndex = 0; wordIndex < sourceBitmap.length; wordIndex++) { - long word = sourceBitmap[wordIndex]; - final int baseOffset = wordIndex << 6; - while (word != 0) { - final int offset = baseOffset + Long.numberOfTrailingZeros(word); - if (!isBitSet(targetBitmap, offset)) { - copySlotAndDeweyId(sourcePage, targetPage, offset, sourcePage.getSlot(offset)); - if (preserveTargetPage != null) { - preserveTargetPage.markSlotForPreservation(offset); - } - } - word &= word - 1; - } - } - } - - private static > int copyMissingSlotsFromBitmapUntilFull( - final T sourcePage, final T targetPage, final long[] sourceBitmap, final long[] targetBitmap, int filledSlotCount, - final KeyValueLeafPage preserveTargetPage) { - for (int wordIndex = 0; wordIndex < sourceBitmap.length; wordIndex++) { - long word = sourceBitmap[wordIndex]; - final int baseOffset = wordIndex << 6; - while (word != 0) { - final int offset = baseOffset + Long.numberOfTrailingZeros(word); - if (!isBitSet(targetBitmap, offset)) { - copySlotAndDeweyId(sourcePage, targetPage, offset, sourcePage.getSlot(offset)); - if (preserveTargetPage != null) { - preserveTargetPage.markSlotForPreservation(offset); - } - filledSlotCount++; - if (filledSlotCount == Constants.NDP_NODE_COUNT) { - return filledSlotCount; - } - } - word &= word - 1; - } - } - return filledSlotCount; - } - - private static > int copyMissingSlotsFromBitmapUntilFullTrackingWindow( - final T sourcePage, final T targetPage, final long[] sourceBitmap, final long[] targetBitmap, - final long[] inWindowBitmap, int filledSlotCount) { - for (int wordIndex = 0; wordIndex < sourceBitmap.length; wordIndex++) { - long word = sourceBitmap[wordIndex]; - final int baseOffset = wordIndex << 6; - while (word != 0) { - final int offset = baseOffset + Long.numberOfTrailingZeros(word); - setBit(inWindowBitmap, offset); - if (!isBitSet(targetBitmap, offset)) { - copySlotAndDeweyId(sourcePage, targetPage, offset, sourcePage.getSlot(offset)); - filledSlotCount++; - if (filledSlotCount == Constants.NDP_NODE_COUNT) { - return filledSlotCount; - } - } - word &= word - 1; - } - } - return filledSlotCount; - } - - private static > void copyOutOfWindowSlotsAndMarkPreservation( - final T outOfWindowPage, final T completePage, final long[] outOfWindowBitmap, final long[] completePageBitmap, - final long[] inWindowBitmap, final KeyValueLeafPage modifyingPage) { - for (int wordIndex = 0; wordIndex < outOfWindowBitmap.length; wordIndex++) { - long word = outOfWindowBitmap[wordIndex]; - final int baseOffset = wordIndex << 6; - while (word != 0) { - final int offset = baseOffset + Long.numberOfTrailingZeros(word); - - // Add to completePage if not already filled. - if (!isBitSet(completePageBitmap, offset)) { - copySlotAndDeweyId(outOfWindowPage, completePage, offset, outOfWindowPage.getSlot(offset)); - } - - // If slot is NOT in the sliding window, mark for preservation in modifyingPage. - if (!isBitSet(inWindowBitmap, offset)) { - modifyingPage.markSlotForPreservation(offset); - } - word &= word - 1; - } - } - } - - private static void setBit(final long[] bitmap, final int offset) { - bitmap[offset >>> 6] |= (1L << (offset & 63)); - } - - private static boolean isBitSet(final long[] bitmap, final int offset) { - return (bitmap[offset >>> 6] & (1L << (offset & 63))) != 0; - } - // ===== HOT Leaf Page Combining Methods ===== /** * Combine multiple HOT leaf page fragments into a single complete page. * - *

- * Unlike slot-based combining for KeyValueLeafPage, HOT pages use key-based merging with - * NodeReferences OR semantics. Newer fragments take precedence, and tombstones (empty - * NodeReferences) indicate deletions. - *

+ *

Unlike slot-based combining for KeyValueLeafPage, HOT pages use key-based + * merging with NodeReferences OR semantics. Newer fragments take precedence, + * and tombstones (empty NodeReferences) indicate deletions.

* * @param pages the list of HOT leaf page fragments (newest first) * @param revToRestore the revision to restore * @param storageEngineReader the storage engine reader * @return the combined HOT leaf page */ - public HOTLeafPage combineHOTLeafPages(final List pages, final @NonNegative int revToRestore, + public HOTLeafPage combineHOTLeafPages( + final List pages, + final @NonNegative int revToRestore, final StorageEngineReader storageEngineReader) { - + if (pages.isEmpty()) { throw new IllegalArgumentException("No pages to combine"); } - + if (pages.size() == 1) { return pages.getFirst(); } - + // Start with a copy of the newest page HOTLeafPage result = pages.getFirst().copy(); - + // Merge older fragments (skip first as it's already the base) for (int i = 1; i < pages.size(); i++) { HOTLeafPage olderPage = pages.get(i); - + // Merge each entry from older page for (int j = 0; j < olderPage.getEntryCount(); j++) { byte[] key = olderPage.getKey(j); - + // Check if key exists in result int existingIdx = result.findEntry(key); if (existingIdx < 0) { @@ -835,17 +879,15 @@ public HOTLeafPage combineHOTLeafPages(final List pages, final @Non // If key exists in newer fragment, it takes precedence (already in result) } } - + return result; } /** * Combine HOT leaf page fragments for modification (COW). * - *

- * Creates a copy of the combined page for modification while preserving the original for readers - * (Copy-on-Write isolation). - *

+ *

Creates a copy of the combined page for modification while preserving + * the original for readers (Copy-on-Write isolation).

* * @param pages the list of HOT leaf page fragments (newest first) * @param revToRestore the revision to restore @@ -854,20 +896,23 @@ public HOTLeafPage combineHOTLeafPages(final List pages, final @Non * @param log the transaction intent log * @return the page container with complete and modified pages */ - public PageContainer combineHOTLeafPagesForModification(final List pages, - final @NonNegative int revToRestore, final StorageEngineReader storageEngineReader, final PageReference reference, + public PageContainer combineHOTLeafPagesForModification( + final List pages, + final @NonNegative int revToRestore, + final StorageEngineReader storageEngineReader, + final PageReference reference, final TransactionIntentLog log) { - + // Combine fragments HOTLeafPage completePage = combineHOTLeafPages(pages, revToRestore, storageEngineReader); - + // Create COW copy for modification HOTLeafPage modifiedPage = completePage.copy(); - + // Create container with both pages final var pageContainer = PageContainer.getInstance(completePage, modifiedPage); log.put(reference, pageContainer); - + return pageContainer; } @@ -876,23 +921,23 @@ public PageContainer combineHOTLeafPagesForModification(final List /** * Combine BitmapChunkPage fragments according to versioning strategy. * - *

- * Takes a list of bitmap chunk page fragments (newest first) and combines them into a complete - * bitmap representing the current state. - *

+ *

Takes a list of bitmap chunk page fragments (newest first) and combines them + * into a complete bitmap representing the current state.

* * @param fragments the list of bitmap chunk page fragments (newest first) * @param revToRestore the revision to restore * @param storageEngineReader the storage engine reader * @return the combined bitmap chunk page with complete data */ - public BitmapChunkPage combineBitmapChunks(final List fragments, final @NonNegative int revToRestore, + public BitmapChunkPage combineBitmapChunks( + final List fragments, + final @NonNegative int revToRestore, final StorageEngineReader storageEngineReader) { - + if (fragments.isEmpty()) { throw new IllegalArgumentException("No fragments to combine"); } - + if (fragments.size() == 1) { BitmapChunkPage singlePage = fragments.getFirst(); if (singlePage.isDeleted()) { @@ -902,41 +947,38 @@ public BitmapChunkPage combineBitmapChunks(final List fragments return singlePage; // Full snapshot - already complete } // Delta page without base - shouldn't happen in valid state - LOGGER.warn("Single delta page without base for chunk range [{}, {})", singlePage.getRangeStart(), - singlePage.getRangeEnd()); + LOGGER.warn("Single delta page without base for chunk range [{}, {})", + singlePage.getRangeStart(), singlePage.getRangeEnd()); return singlePage.copyAsFull(singlePage.getRevision()); } - + // Find the base snapshot (should be the last/oldest page) BitmapChunkPage basePage = fragments.getLast(); if (!basePage.isFullSnapshot() && !basePage.isDeleted()) { - LOGGER.warn("Base page is not a full snapshot for chunk range [{}, {})", basePage.getRangeStart(), - basePage.getRangeEnd()); + LOGGER.warn("Base page is not a full snapshot for chunk range [{}, {})", + basePage.getRangeStart(), basePage.getRangeEnd()); } - + // Start with base bitmap - org.roaringbitmap.longlong.Roaring64Bitmap combined = basePage.getBitmap() != null - ? basePage.getBitmap().clone() - : new org.roaringbitmap.longlong.Roaring64Bitmap(); - + org.roaringbitmap.longlong.Roaring64Bitmap combined = + basePage.getBitmap() != null ? basePage.getBitmap().clone() : new org.roaringbitmap.longlong.Roaring64Bitmap(); + // Apply deltas from oldest to newest (skip base) for (int i = fragments.size() - 2; i >= 0; i--) { BitmapChunkPage deltaPage = fragments.get(i); - + if (deltaPage.isDeleted()) { // Tombstone - clear everything combined = new org.roaringbitmap.longlong.Roaring64Bitmap(); continue; } - + if (deltaPage.isFullSnapshot()) { // Full snapshot replaces everything - combined = deltaPage.getBitmap() != null - ? deltaPage.getBitmap().clone() - : new org.roaringbitmap.longlong.Roaring64Bitmap(); + combined = deltaPage.getBitmap() != null ? deltaPage.getBitmap().clone() : new org.roaringbitmap.longlong.Roaring64Bitmap(); continue; } - + if (deltaPage.isDelta()) { // Apply additions if (deltaPage.getAdditions() != null && !deltaPage.getAdditions().isEmpty()) { @@ -948,20 +990,25 @@ public BitmapChunkPage combineBitmapChunks(final List fragments } } } - + // Create result as full snapshot BitmapChunkPage newestPage = fragments.getFirst(); - return BitmapChunkPage.createFull(newestPage.getPageKey(), newestPage.getRevision(), newestPage.getIndexType(), - newestPage.getRangeStart(), newestPage.getRangeEnd(), combined); + return BitmapChunkPage.createFull( + newestPage.getPageKey(), + newestPage.getRevision(), + newestPage.getIndexType(), + newestPage.getRangeStart(), + newestPage.getRangeEnd(), + combined + ); } /** * Prepare a BitmapChunkPage for modification. * - *

- * Loads existing fragments, combines them, and creates a new page for the current transaction. The - * versioning strategy determines whether to create a full snapshot or a delta page. - *

+ *

Loads existing fragments, combines them, and creates a new page for + * the current transaction. The versioning strategy determines whether + * to create a full snapshot or a delta page.

* * @param fragments existing page fragments (newest first), may be empty for new chunks * @param currentRevision the current transaction revision @@ -974,30 +1021,34 @@ public BitmapChunkPage combineBitmapChunks(final List fragments * @param storageEngineReader the storage engine reader * @return the page container with complete and modified pages */ - public PageContainer prepareBitmapChunkForModification(final List fragments, - final int currentRevision, final @NonNegative int revsToRestore, final long rangeStart, final long rangeEnd, - final io.sirix.index.IndexType indexType, final PageReference reference, final TransactionIntentLog log, + public PageContainer prepareBitmapChunkForModification( + final List fragments, + final int currentRevision, + final @NonNegative int revsToRestore, + final long rangeStart, + final long rangeEnd, + final io.sirix.index.IndexType indexType, + final PageReference reference, + final TransactionIntentLog log, final StorageEngineReader storageEngineReader) { - + // Determine if we should create a full snapshot final boolean isFullDump = shouldStoreBitmapFullSnapshot(fragments, currentRevision, revsToRestore); - - final long pageKey = reference.getKey() >= 0 - ? reference.getKey() - : allocateNewPageKey(storageEngineReader); - + + final long pageKey = reference.getKey() >= 0 ? reference.getKey() : allocateNewPageKey(storageEngineReader); + if (fragments.isEmpty()) { // New chunk - create empty full snapshot - BitmapChunkPage newPage = - BitmapChunkPage.createEmptyFull(pageKey, currentRevision, indexType, rangeStart, rangeEnd); + BitmapChunkPage newPage = BitmapChunkPage.createEmptyFull( + pageKey, currentRevision, indexType, rangeStart, rangeEnd); PageContainer container = PageContainer.getInstance(newPage, newPage); log.put(reference, container); return container; } - + // Combine existing fragments BitmapChunkPage completePage = combineBitmapChunks(fragments, revsToRestore, storageEngineReader); - + // Create modified page based on versioning strategy BitmapChunkPage modifiedPage; if (isFullDump) { @@ -1005,9 +1056,10 @@ public PageContainer prepareBitmapChunkForModification(final List - * Strategy-specific logic: - *

+ *

Strategy-specific logic:

*
    - *
  • FULL: Always returns true
  • - *
  • INCREMENTAL: Returns true when chain length >= revsToRestore - 1
  • - *
  • DIFFERENTIAL: Returns true when currentRevision % revsToRestore == 0
  • - *
  • SLIDING_SNAPSHOT: Same as INCREMENTAL with window-based GC
  • + *
  • FULL: Always returns true
  • + *
  • INCREMENTAL: Returns true when chain length >= revsToRestore - 1
  • + *
  • DIFFERENTIAL: Returns true when currentRevision % revsToRestore == 0
  • + *
  • SLIDING_SNAPSHOT: Same as INCREMENTAL with window-based GC
  • *
* * @param fragments existing fragments (for chain length check) @@ -1031,14 +1081,16 @@ public PageContainer prepareBitmapChunkForModification(final List fragments, final int currentRevision, + public boolean shouldStoreBitmapFullSnapshot( + final List fragments, + final int currentRevision, final @NonNegative int revsToRestore) { - + // First revision is always full if (currentRevision == 1 || fragments.isEmpty()) { return true; } - + return switch (this) { case FULL -> true; case DIFFERENTIAL -> currentRevision % revsToRestore == 0; @@ -1047,16 +1099,12 @@ public boolean shouldStoreBitmapFullSnapshot(final List fragmen } /** - * Allocate a fallback page key for bitmap chunk pages when no reference key exists yet. - * - *

- * Using a monotonic nanoTime source preserves lock-free behavior on this path and keeps allocations - * at zero. This method remains a placeholder until bitmap chunks are wired into the regular - * revision-root page-key allocator. - *

+ * Allocate a new page key. + * This is a placeholder - actual implementation uses RevisionRootPage. */ - private long allocateNewPageKey(final StorageEngineReader storageEngineReader) { - return System.nanoTime(); + private long allocateNewPageKey(StorageEngineReader storageEngineReader) { + // TODO: Integrate with RevisionRootPage page key allocation + return System.nanoTime(); // Temporary unique key } } diff --git a/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlNodeReadOnlyTrxImplTest.java b/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlNodeReadOnlyTrxImplTest.java index eac140ac5..eaa8561bc 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlNodeReadOnlyTrxImplTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlNodeReadOnlyTrxImplTest.java @@ -120,7 +120,7 @@ public void testConventions() { } @Test - public void testMoveToReusesXmlElementSingleton() { + public void testMoveToReadsCorrectXmlElements() { final InternalXmlNodeReadOnlyTrx rtx = (InternalXmlNodeReadOnlyTrx) holder.getXmlNodeReadTrx(); assertTrue(rtx.moveToDocumentRoot()); @@ -143,13 +143,16 @@ public void testMoveToReusesXmlElementSingleton() { assertTrue(secondElementKey != Fixed.NULL_NODE_KEY.getStandardProperty()); assertTrue(rtx.moveTo(firstElementKey)); - final StructNode firstSingleton = rtx.getStructuralNode(); - assertEquals(firstElementKey, firstSingleton.getNodeKey()); + final StructNode firstNode = rtx.getStructuralNode(); + assertEquals(firstElementKey, firstNode.getNodeKey()); assertTrue(rtx.moveTo(secondElementKey)); - final StructNode secondSingleton = rtx.getStructuralNode(); - assertSame(firstSingleton, secondSingleton); - assertEquals(secondElementKey, secondSingleton.getNodeKey()); + final StructNode secondNode = rtx.getStructuralNode(); + assertEquals(secondElementKey, secondNode.getNodeKey()); + + // Verify moving back reads correct data + assertTrue(rtx.moveTo(firstElementKey)); + assertEquals(firstElementKey, rtx.getStructuralNode().getNodeKey()); } } diff --git a/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlResourceSessionTest.java b/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlResourceSessionTest.java index 64a21642c..d2537fe98 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlResourceSessionTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/access/node/xml/XmlResourceSessionTest.java @@ -81,8 +81,8 @@ public void testClosed() { try { rtx.getAttributeCount(); fail(); - } catch (final AssertionError e) { - // assertNotClosed() uses a Java assert — throws AssertionError with -ea. + } catch (final IllegalStateException e) { + // assertNotClosed() throws IllegalStateException when transaction is closed. } finally { holder.getResourceSession().close(); } diff --git a/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/json/JsonNodeFactoryImplTest.java b/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/json/JsonNodeFactoryImplTest.java index 68c91af2a..718967650 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/json/JsonNodeFactoryImplTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/json/JsonNodeFactoryImplTest.java @@ -47,7 +47,9 @@ import io.sirix.node.json.ObjectNumberNode; import io.sirix.node.json.ObjectStringNode; import io.sirix.node.json.StringNode; +import io.sirix.page.KeyValueLeafPage; import io.sirix.page.RevisionRootPage; +import io.sirix.settings.Constants; import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; import org.junit.After; @@ -95,13 +97,25 @@ public void setUp() { doReturn(resourceSession).when(storageEngineWriter).getResourceSession(); when(storageEngineWriter.getRevisionNumber()).thenReturn(0); - // Mock createRecord to return the node passed to it + // Mock createRecord to return the node passed to it (used by PathNode, DeweyIDNode) when(storageEngineWriter.createRecord(any(DataRecord.class), any(IndexType.class), anyInt())).thenAnswer( invocation -> invocation.getArgument(0)); // Mock createNameKey for ObjectKeyNode when(storageEngineWriter.createNameKey(anyString(), any(NodeKind.class))).thenReturn(5); // Return a dummy name key + // Mock allocation methods for direct-to-heap creation + final KeyValueLeafPage testKvl = new KeyValueLeafPage(0, IndexType.DOCUMENT, resourceConfig, 0, null, null); + final long[] allocatedKey = {0}; + doAnswer(inv -> { + allocatedKey[0] = nodeCounter++; + return null; + }).when(storageEngineWriter).allocateForDocumentCreation(); + when(storageEngineWriter.getAllocKvl()).thenReturn(testKvl); + when(storageEngineWriter.getAllocNodeKey()).thenAnswer(inv -> allocatedKey[0]); + when(storageEngineWriter.getAllocSlotOffset()).thenAnswer( + inv -> (int) (allocatedKey[0] % Constants.NDP_NODE_COUNT)); + factory = new JsonNodeFactoryImpl(LongHashFunction.xx3(), storageEngineWriter); } @@ -133,8 +147,8 @@ public void testCreateJsonObjectNullNode_NoStructuralFields() { // CRITICAL: Object* value nodes should have NO children assertEquals(Fixed.NULL_NODE_KEY.getStandardProperty(), node.getFirstChildKey()); - // Verify storageEngineWriter.createRecord was called - verify(storageEngineWriter).createRecord(eq(node), eq(IndexType.DOCUMENT), eq(-1)); + // Verify allocateForDocumentCreation was called (direct-to-heap path) + verify(storageEngineWriter).allocateForDocumentCreation(); } @Test diff --git a/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImplTest.java b/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImplTest.java index 132fe82f8..d7ed9b19d 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImplTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/access/trx/node/xml/XmlNodeFactoryImplTest.java @@ -1,7 +1,10 @@ package io.sirix.access.trx.node.xml; import io.brackit.query.atomic.QNm; +import io.sirix.XmlTestHelper; +import io.sirix.access.ResourceConfiguration; import io.sirix.api.StorageEngineWriter; +import io.sirix.api.xml.XmlResourceSession; import io.sirix.index.IndexType; import io.sirix.node.NodeKind; import io.sirix.node.interfaces.DataRecord; @@ -11,9 +14,12 @@ import io.sirix.node.xml.NamespaceNode; import io.sirix.node.xml.PINode; import io.sirix.node.xml.TextNode; -import io.sirix.settings.Fixed; +import io.sirix.page.KeyValueLeafPage; import io.sirix.page.RevisionRootPage; +import io.sirix.settings.Constants; +import io.sirix.settings.Fixed; import net.openhft.hashing.LongHashFunction; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -28,6 +34,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -35,10 +42,16 @@ public final class XmlNodeFactoryImplTest { private StorageEngineWriter storageEngineWriter; private XmlNodeFactoryImpl factory; + private XmlResourceSession resourceSession; private long nodeCounter; @Before public void setUp() { + XmlTestHelper.deleteEverything(); + final var database = XmlTestHelper.getDatabase(XmlTestHelper.PATHS.PATH1.getFile()); + resourceSession = database.beginResourceSession(XmlTestHelper.RESOURCE); + final ResourceConfiguration resourceConfig = resourceSession.getResourceConfig(); + storageEngineWriter = mock(StorageEngineWriter.class); final RevisionRootPage revisionRootPage = mock(RevisionRootPage.class); when(storageEngineWriter.getActualRevisionRootPage()).thenReturn(revisionRootPage); @@ -48,9 +61,30 @@ public void setUp() { invocation -> invocation.getArgument(0)); when(storageEngineWriter.createNameKey(anyString(), any(NodeKind.class))).thenAnswer( invocation -> Math.abs(invocation.getArgument(0, String.class).hashCode())); + + // Mock allocation methods for direct-to-heap creation + final KeyValueLeafPage testKvl = new KeyValueLeafPage(0, IndexType.DOCUMENT, resourceConfig, 0, null, null); + final long[] allocatedKey = {0}; + doAnswer(inv -> { + allocatedKey[0] = nodeCounter++; + return null; + }).when(storageEngineWriter).allocateForDocumentCreation(); + when(storageEngineWriter.getAllocKvl()).thenReturn(testKvl); + when(storageEngineWriter.getAllocNodeKey()).thenAnswer(inv -> allocatedKey[0]); + when(storageEngineWriter.getAllocSlotOffset()).thenAnswer( + inv -> (int) (allocatedKey[0] % Constants.NDP_NODE_COUNT)); + factory = new XmlNodeFactoryImpl(LongHashFunction.xx3(), storageEngineWriter); } + @After + public void tearDown() { + if (resourceSession != null) { + resourceSession.close(); + } + XmlTestHelper.closeEverything(); + } + @Test public void testFactoryReusesAttributeNodeProxy() { final AttributeNode first = factory.createAttributeNode(5L, new QNm("urn:first", "p1", "att1"), diff --git a/bundles/sirix-core/src/test/java/io/sirix/access/trx/page/NodeStorageEngineReaderTest.java b/bundles/sirix-core/src/test/java/io/sirix/access/trx/page/NodeStorageEngineReaderTest.java index 3498c3029..1d752d85b 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/access/trx/page/NodeStorageEngineReaderTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/access/trx/page/NodeStorageEngineReaderTest.java @@ -6,8 +6,6 @@ import io.sirix.cache.BufferManager; import io.sirix.cache.TransactionIntentLog; import io.sirix.index.IndexType; -import io.sirix.node.NodeKind; -import io.sirix.page.KeyValueLeafPage; import io.sirix.page.UberPage; import org.checkerframework.checker.nullness.qual.NonNull; @@ -16,9 +14,6 @@ import io.sirix.io.Reader; import io.sirix.settings.Constants; -import java.lang.foreign.Arena; - -import static io.sirix.cache.LinuxMemorySegmentAllocator.SIXTYFOUR_KB; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -43,31 +38,6 @@ public void testRecordPageOffset() { assertEquals(Constants.NDP_NODE_COUNT - 1, StorageEngineReader.recordPageOffset(1023)); } - @Test(expected = IllegalStateException.class) - public void testGetValueFailsFastForUnsupportedFixedSlotMaterialization() { - final InternalResourceSession resourceManagerMock = createResourceManagerMock(); - final ResourceConfiguration config = new ResourceConfiguration.Builder("foobar").build(); - - try (final var trx = new NodeStorageEngineReader(1, resourceManagerMock, new UberPage(), 0, mock(Reader.class), - mock(BufferManager.class), mock(RevisionRootPageReader.class), mock(TransactionIntentLog.class))) { - try (Arena arena = Arena.ofConfined()) { - final KeyValueLeafPage page = - new KeyValueLeafPage(0L, IndexType.DOCUMENT, config, 1, arena.allocate(SIXTYFOUR_KB), null); - try { - final long nodeKey = 7L; - final int slot = StorageEngineReader.recordPageOffset(nodeKey); - final int fixedSize = NodeKind.STRING_VALUE.layoutDescriptor().fixedSlotSizeInBytes(); - page.setSlot(new byte[fixedSize], slot); - page.markSlotAsFixedFormat(slot, NodeKind.STRING_VALUE); - - trx.getValue(page, nodeKey); - } finally { - page.close(); - } - } - } - } - @NonNull private InternalResourceSession createResourceManagerMock() { final var resourceManagerMock = mock(InternalResourceSession.class); diff --git a/bundles/sirix-core/src/test/java/io/sirix/diff/DiffTestHelper.java b/bundles/sirix-core/src/test/java/io/sirix/diff/DiffTestHelper.java index 7bb0ae884..18d814417 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/diff/DiffTestHelper.java +++ b/bundles/sirix-core/src/test/java/io/sirix/diff/DiffTestHelper.java @@ -163,9 +163,7 @@ static void verifyOptimizedFullDiffFirst(final DiffObserver listener) { .diffListener(eq(DiffType.SAME), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); inOrder.verify(listener, times(2)) .diffListener(eq(DiffType.INSERTED), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); - inOrder.verify(listener, times(1)) - .diffListener(eq(DiffType.SAME), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); - inOrder.verify(listener, times(4)) + inOrder.verify(listener, times(5)) .diffListener(eq(DiffType.SAMEHASH), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); inOrder.verify(listener, times(1)).diffDone(); } @@ -187,9 +185,7 @@ static void verifyOptimizedStructuralDiffFirst(final DiffObserver listener) { .diffListener(eq(DiffType.SAME), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); inOrder.verify(listener, times(2)) .diffListener(eq(DiffType.INSERTED), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); - inOrder.verify(listener, times(1)) - .diffListener(eq(DiffType.SAME), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); - inOrder.verify(listener, times(4)) + inOrder.verify(listener, times(5)) .diffListener(eq(DiffType.SAMEHASH), isA(Long.class), isA(Long.class), isA(DiffDepth.class)); inOrder.verify(listener, times(1)).diffDone(); } diff --git a/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactFieldCodecTest.java b/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactFieldCodecTest.java deleted file mode 100644 index 6cd863b63..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactFieldCodecTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.Bytes; -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.node.DeltaVarIntCodec; -import io.sirix.settings.Fixed; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -final class CompactFieldCodecTest { - - @Test - void signedIntegerRoundTrip() { - final int[] values = {Integer.MIN_VALUE, -1000, -1, 0, 1, 1000, Integer.MAX_VALUE}; - for (final int value : values) { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeSignedInt(sink, value); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertEquals(value, CompactFieldCodec.decodeSignedInt(source), "Roundtrip mismatch for " + value); - } - } - - @Test - void signedLongRoundTrip() { - final long[] values = {Long.MIN_VALUE, -1000L, -1L, 0L, 1L, 1000L, Long.MAX_VALUE}; - for (final long value : values) { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeSignedLong(sink, value); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertEquals(value, CompactFieldCodec.decodeSignedLong(source), "Roundtrip mismatch for " + value); - } - } - - @Test - void deltaNodeKeyRoundTripWithCornerCases() { - final long nullNodeKey = Fixed.NULL_NODE_KEY.getStandardProperty(); - final long[][] cases = {{100L, 101L}, {100L, 99L}, {100L, 100L}, {100L, nullNodeKey}, - {Long.MAX_VALUE - 2, Long.MAX_VALUE - 1}, {Long.MIN_VALUE + 2, Long.MIN_VALUE + 1}}; - - for (final long[] sample : cases) { - final long baseNodeKey = sample[0]; - final long targetNodeKey = sample[1]; - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeNodeKeyDelta(sink, baseNodeKey, targetNodeKey); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertEquals(targetNodeKey, CompactFieldCodec.decodeNodeKeyDelta(source, baseNodeKey), - "Delta roundtrip mismatch for base=" + baseNodeKey + ", target=" + targetNodeKey); - } - } - - @Test - void decodeNonNegativeRejectsNegativeNumbers() { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - DeltaVarIntCodec.encodeSigned(sink, -1); - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertThrows(IllegalStateException.class, () -> CompactFieldCodec.decodeNonNegativeInt(source)); - } -} diff --git a/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactPageEncoderTest.java b/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactPageEncoderTest.java deleted file mode 100644 index 3955b2007..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/node/layout/CompactPageEncoderTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.Bytes; -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.node.NodeKind; -import io.sirix.settings.Fixed; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -final class CompactPageEncoderTest { - private static final int LENGTH_LIMIT_PLUS_ONE = (1 << 20) + 1; - - @Test - void slotHeaderRoundTrip() { - final CompactPageEncoder.SlotHeader header = new CompactPageEncoder.SlotHeader(42L, NodeKind.OBJECT, 96, 128); - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodeSlotHeader(sink, header); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - final CompactPageEncoder.SlotHeader decoded = CompactPageEncoder.decodeSlotHeader(source); - - assertEquals(header.nodeKey(), decoded.nodeKey()); - assertEquals(header.nodeKind(), decoded.nodeKind()); - assertEquals(header.fixedSlotSizeInBytes(), decoded.fixedSlotSizeInBytes()); - assertEquals(header.payloadSizeInBytes(), decoded.payloadSizeInBytes()); - } - - @Test - void slotHeaderRoundTripWithReusableHeader() { - final CompactPageEncoder.SlotHeader header = new CompactPageEncoder.SlotHeader(73L, NodeKind.ELEMENT, 160, 256); - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodeSlotHeader(sink, header); - - final CompactPageEncoder.MutableSlotHeader mutableHeader = new CompactPageEncoder.MutableSlotHeader(); - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - CompactPageEncoder.decodeSlotHeader(source, mutableHeader); - - assertEquals(header.nodeKey(), mutableHeader.nodeKey()); - assertEquals(header.nodeKind(), mutableHeader.nodeKind()); - assertEquals(header.fixedSlotSizeInBytes(), mutableHeader.fixedSlotSizeInBytes()); - assertEquals(header.payloadSizeInBytes(), mutableHeader.payloadSizeInBytes()); - } - - @Test - void relationshipVectorRoundTripWithNullSentinel() { - final long baseNodeKey = 512L; - final long[] relationships = {513L, 511L, 512L, Fixed.NULL_NODE_KEY.getStandardProperty(), 600L}; - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodeRelationshipVector(sink, baseNodeKey, relationships); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - final long[] decoded = CompactPageEncoder.decodeRelationshipVector(source, baseNodeKey); - assertArrayEquals(relationships, decoded); - } - - @Test - void relationshipVectorDecodesIntoReusableBuffer() { - final long baseNodeKey = 2048L; - final long[] relationships = {2049L, 2050L, 2047L}; - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodeRelationshipVector(sink, baseNodeKey, relationships); - - final long[] reusableBuffer = new long[8]; - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - final int decodedLength = CompactPageEncoder.decodeRelationshipVector(source, baseNodeKey, reusableBuffer); - - assertEquals(relationships.length, decodedLength); - for (int i = 0; i < decodedLength; i++) { - assertEquals(relationships[i], reusableBuffer[i]); - } - } - - @Test - void decodeSlotHeaderRejectsUnknownNodeKind() { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeSignedLong(sink, 99L); - sink.writeByte((byte) 120); - CompactFieldCodec.encodeNonNegativeInt(sink, 16); - CompactFieldCodec.encodeNonNegativeInt(sink, 0); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertThrows(IllegalStateException.class, () -> CompactPageEncoder.decodeSlotHeader(source)); - } - - @Test - void payloadBlockHeaderRoundTrip() { - final CompactPageEncoder.PayloadBlockHeader header = new CompactPageEncoder.PayloadBlockHeader(8192L, 1024, 5); - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodePayloadBlockHeader(sink, header); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - final CompactPageEncoder.PayloadBlockHeader decoded = CompactPageEncoder.decodePayloadBlockHeader(source); - - assertEquals(header.payloadPointer(), decoded.payloadPointer()); - assertEquals(header.payloadLengthInBytes(), decoded.payloadLengthInBytes()); - assertEquals(header.payloadFlags(), decoded.payloadFlags()); - } - - @Test - void payloadBlockHeaderRoundTripWithReusableHeader() { - final CompactPageEncoder.PayloadBlockHeader header = new CompactPageEncoder.PayloadBlockHeader(16384L, 2048, 9); - - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodePayloadBlockHeader(sink, header); - - final CompactPageEncoder.MutablePayloadBlockHeader mutableHeader = - new CompactPageEncoder.MutablePayloadBlockHeader(); - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - CompactPageEncoder.decodePayloadBlockHeader(source, mutableHeader); - - assertEquals(header.payloadPointer(), mutableHeader.payloadPointer()); - assertEquals(header.payloadLengthInBytes(), mutableHeader.payloadLengthInBytes()); - assertEquals(header.payloadFlags(), mutableHeader.payloadFlags()); - } - - @Test - void decodeRelationshipVectorRejectsInvalidLength() { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeSignedInt(sink, -1); - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertThrows(IllegalStateException.class, () -> CompactPageEncoder.decodeRelationshipVector(source, 10L)); - } - - @Test - void decodePayloadBlockHeaderRejectsNegativeLength() { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeSignedLong(sink, 123L); - CompactFieldCodec.encodeSignedInt(sink, -1); - CompactFieldCodec.encodeSignedInt(sink, 0); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertThrows(IllegalStateException.class, () -> CompactPageEncoder.decodePayloadBlockHeader(source)); - } - - @Test - void decodeRelationshipVectorRejectsLengthOverLimit() { - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactFieldCodec.encodeNonNegativeInt(sink, LENGTH_LIMIT_PLUS_ONE); - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - assertThrows(IllegalStateException.class, () -> CompactPageEncoder.decodeRelationshipVector(source, 10L)); - } - - @Test - void decodeRelationshipVectorIntoBufferRejectsInsufficientCapacity() { - final long baseNodeKey = 7L; - final long[] relationships = {8L, 9L, 10L}; - final BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - CompactPageEncoder.encodeRelationshipVector(sink, baseNodeKey, relationships); - - final BytesIn source = Bytes.wrapForRead(sink.toByteArray()); - final long[] tooSmallBuffer = new long[2]; - assertThrows(IllegalArgumentException.class, - () -> CompactPageEncoder.decodeRelationshipVector(source, baseNodeKey, tooSmallBuffer)); - } -} diff --git a/bundles/sirix-core/src/test/java/io/sirix/node/layout/FixedSlotRecordProjectorTest.java b/bundles/sirix-core/src/test/java/io/sirix/node/layout/FixedSlotRecordProjectorTest.java deleted file mode 100644 index d3666d361..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/node/layout/FixedSlotRecordProjectorTest.java +++ /dev/null @@ -1,595 +0,0 @@ -package io.sirix.node.layout; - -import io.brackit.query.atomic.QNm; -import io.sirix.node.NodeKind; -import io.sirix.node.MemorySegmentBytesIn; -import io.sirix.node.json.BooleanNode; -import io.sirix.node.json.NumberNode; -import io.sirix.node.json.ObjectKeyNode; -import io.sirix.node.json.ObjectNumberNode; -import io.sirix.node.json.ObjectStringNode; -import io.sirix.node.json.StringNode; -import io.sirix.node.xml.AttributeNode; -import io.sirix.node.xml.CommentNode; -import io.sirix.node.xml.ElementNode; -import io.sirix.node.xml.PINode; -import io.sirix.node.xml.TextNode; -import it.unimi.dsi.fastutil.longs.LongArrayList; -import net.openhft.hashing.LongHashFunction; -import org.junit.jupiter.api.Test; - -import java.lang.foreign.Arena; -import java.lang.foreign.MemorySegment; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class FixedSlotRecordProjectorTest { - - @Test - void projectBooleanNodeIntoFixedSlot() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.BOOLEAN_VALUE); - final BooleanNode node = - new BooleanNode(42L, 7L, 11, 13, 17L, 19L, 23L, true, LongHashFunction.xx3(), (byte[]) null); - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - assertEquals(23L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - assertTrue(SlotLayoutAccessors.readBooleanField(slot, layout, StructuralField.BOOLEAN_VALUE)); - } - } - - @Test - void projectObjectKeyNodeIntoFixedSlot() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.OBJECT_KEY); - final ObjectKeyNode node = - new ObjectKeyNode(128L, 3L, 5L, 8, 9, 11L, 13L, 17L, 19, 23L, 29L, LongHashFunction.xx3(), (byte[]) null); - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - assertEquals(3L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(11L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(13L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.FIRST_CHILD_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LAST_CHILD_KEY)); - assertEquals(5L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PATH_NODE_KEY)); - assertEquals(19, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.NAME_KEY)); - assertEquals(8, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(9, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - assertEquals(29L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - assertEquals(23L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.DESCENDANT_COUNT)); - } - } - - @Test - void projectStringNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - final byte[] value = "hello world".getBytes(StandardCharsets.UTF_8); - final StringNode node = - new StringNode(42L, 7L, 11, 13, 17L, 19L, 23L, value, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - - // Verify structural fields - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - assertEquals(23L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - - // Verify PayloadRef metadata - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final int flags = SlotLayoutAccessors.readPayloadFlags(slot, layout, 0); - - assertEquals(layout.fixedSlotSizeInBytes(), (int) pointer); - assertEquals(value.length, length); - assertEquals(0, flags); // not compressed - - // Verify inline payload bytes - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void projectObjectStringNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.OBJECT_STRING_VALUE); - final byte[] value = "test value".getBytes(StandardCharsets.UTF_8); - final ObjectStringNode node = - new ObjectStringNode(42L, 7L, 11, 13, 23L, value, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - - // Verify structural fields - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - assertEquals(23L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - - // Verify inline payload bytes - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void projectNumberNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.NUMBER_VALUE); - final NumberNode node = new NumberNode(42L, 7L, 11, 13, 17L, 19L, 23L, 42.5, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertTrue(inlinePayload > 0); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - - // Verify structural fields - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(23L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - - // Verify PayloadRef metadata - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - assertEquals(layout.fixedSlotSizeInBytes(), (int) pointer); - assertTrue(length > 0); - - // Verify inline payload can be deserialized back to number - final MemorySegment payloadSlice = slot.asSlice(pointer, length); - final Number deserialized = NodeKind.deserializeNumber(new MemorySegmentBytesIn(payloadSlice)); - assertEquals(42.5, deserialized.doubleValue(), 0.0001); - } - } - - @Test - void projectObjectNumberNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.OBJECT_NUMBER_VALUE); - final ObjectNumberNode node = - new ObjectNumberNode(42L, 7L, 11, 13, 23L, 123, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertTrue(inlinePayload > 0); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - - // Verify inline payload can be deserialized back to number - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final MemorySegment payloadSlice = slot.asSlice(pointer, length); - final Number deserialized = NodeKind.deserializeNumber(new MemorySegmentBytesIn(payloadSlice)); - assertEquals(123, deserialized.intValue()); - } - } - - @Test - void projectNumberNodeBigDecimalRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.NUMBER_VALUE); - final BigDecimal bigDecimalValue = new BigDecimal("12345678901234567890.12345"); - final NumberNode node = - new NumberNode(42L, 7L, 11, 13, 17L, 19L, 23L, bigDecimalValue, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertTrue(inlinePayload > 0); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final MemorySegment payloadSlice = slot.asSlice(pointer, length); - final Number deserialized = NodeKind.deserializeNumber(new MemorySegmentBytesIn(payloadSlice)); - assertEquals(bigDecimalValue, deserialized); - } - } - - @Test - void projectCompressedStringPreservesFlags() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - final byte[] rawCompressed = {0x01, 0x02, 0x03, 0x04}; - final StringNode node = new StringNode(42L, 7L, 11, 13, 17L, 19L, 23L, rawCompressed, LongHashFunction.xx3(), - (byte[]) null, true, null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - final int flags = SlotLayoutAccessors.readPayloadFlags(slot, layout, 0); - assertEquals(1, flags); // compressed flag set - } - } - - @Test - void hasSupportedPayloadsReturnsTrueForValueBlob() { - final NodeKindLayout stringLayout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - assertTrue(FixedSlotRecordProjector.hasSupportedPayloads(stringLayout)); - - final NodeKindLayout booleanLayout = NodeKindLayouts.layoutFor(NodeKind.BOOLEAN_VALUE); - assertTrue(FixedSlotRecordProjector.hasSupportedPayloads(booleanLayout)); - } - - @Test - void computeInlinePayloadLengthReturnsZeroForNonPayloadNodes() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.BOOLEAN_VALUE); - final BooleanNode node = new BooleanNode(1L, 2L, 3, 4, 5L, 6L, 7L, false, LongHashFunction.xx3(), (byte[]) null); - assertEquals(0, FixedSlotRecordProjector.computeInlinePayloadLength(node, layout)); - } - - @Test - void projectTextNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.TEXT); - final byte[] value = "hello xml text".getBytes(StandardCharsets.UTF_8); - final TextNode node = - new TextNode(42L, 7L, 11, 13, 17L, 19L, 23L, value, false, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - final boolean projected = FixedSlotRecordProjector.project(node, layout, slot, 0L); - - assertTrue(projected); - - // Verify structural fields - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - - // Verify PayloadRef metadata - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final int flags = SlotLayoutAccessors.readPayloadFlags(slot, layout, 0); - - assertEquals(layout.fixedSlotSizeInBytes(), (int) pointer); - assertEquals(value.length, length); - assertEquals(0, flags); // not compressed - - // Verify inline payload bytes - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void projectTextNodeCompressedRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.TEXT); - final byte[] rawCompressed = {0x01, 0x02, 0x03, 0x04}; - final TextNode node = - new TextNode(42L, 7L, 11, 13, 17L, 19L, 23L, rawCompressed, true, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - final int flags = SlotLayoutAccessors.readPayloadFlags(slot, layout, 0); - assertEquals(1, flags); // compressed flag set - } - } - - @Test - void projectCommentNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.COMMENT); - final byte[] value = "a comment".getBytes(StandardCharsets.UTF_8); - final CommentNode node = - new CommentNode(42L, 7L, 11, 13, 17L, 19L, 23L, value, false, LongHashFunction.xx3(), (byte[]) null); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void projectAttributeNodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.ATTRIBUTE); - final byte[] value = "attr-value".getBytes(StandardCharsets.UTF_8); - final QNm qNm = new QNm("ns", "prefix", "local"); - final AttributeNode node = - new AttributeNode(42L, 7L, 11, 13, 5L, 100, 200, 300, 23L, value, LongHashFunction.xx3(), (byte[]) null, qNm); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(5L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PATH_NODE_KEY)); - assertEquals(100, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREFIX_KEY)); - assertEquals(200, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LOCAL_NAME_KEY)); - assertEquals(300, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.URI_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - - final int flags = SlotLayoutAccessors.readPayloadFlags(slot, layout, 0); - assertEquals(0, flags); // attributes never compressed - - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void projectPINodeRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.PROCESSING_INSTRUCTION); - final byte[] value = "pi-target data".getBytes(StandardCharsets.UTF_8); - final QNm qNm = new QNm("", "", "xml-stylesheet"); - final PINode node = new PINode(42L, 7L, 11, 13, 17L, 19L, 31L, 37L, 3, 5, 23L, 41L, 100, 200, 300, value, false, - LongHashFunction.xx3(), (byte[]) null, qNm); - - final int inlinePayload = FixedSlotRecordProjector.computeInlinePayloadLength(node, layout); - assertEquals(value.length, inlinePayload); - - final int totalSize = layout.fixedSlotSizeInBytes() + inlinePayload; - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - // Verify structural fields - assertEquals(7L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(17L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(19L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(31L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.FIRST_CHILD_KEY)); - assertEquals(37L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LAST_CHILD_KEY)); - assertEquals(3L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.CHILD_COUNT)); - assertEquals(5L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.DESCENDANT_COUNT)); - - // Verify name fields - assertEquals(41L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PATH_NODE_KEY)); - assertEquals(100, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREFIX_KEY)); - assertEquals(200, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LOCAL_NAME_KEY)); - assertEquals(300, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.URI_KEY)); - - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(13, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - - // Verify inline payload bytes - final int length = SlotLayoutAccessors.readPayloadLength(slot, layout, 0); - final long pointer = SlotLayoutAccessors.readPayloadPointer(slot, layout, 0); - final byte[] readValue = new byte[length]; - MemorySegment.copy(slot, pointer, MemorySegment.ofArray(readValue), 0, length); - assertArrayEquals(value, readValue); - } - } - - @Test - void hasSupportedPayloadsAcceptsVectorPayloadRefs() { - final NodeKindLayout layoutWithAttrs = NodeKindLayout.builder(NodeKind.ELEMENT) - .addField(StructuralField.PARENT_KEY) - .addField(StructuralField.HASH) - .addPayloadRef("attrs", PayloadRefKind.ATTRIBUTE_VECTOR) - .build(); - - assertTrue(FixedSlotRecordProjector.hasSupportedPayloads(layoutWithAttrs)); - - // project() should reject a type mismatch (BooleanNode for ELEMENT layout) - final BooleanNode node = new BooleanNode(1L, 2L, 3, 4, 5L, 6L, 7L, false, LongHashFunction.xx3(), (byte[]) null); - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(Math.max(layoutWithAttrs.fixedSlotSizeInBytes(), 128)); - assertFalse(FixedSlotRecordProjector.project(node, layoutWithAttrs, slot, 0L)); - } - } - - @Test - void elementNodeRoundTripWithAttributesAndNamespaces() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.ELEMENT); - final LongHashFunction hashFunction = LongHashFunction.xx3(); - final QNm qNm = new QNm("ns", "pfx", "local"); - - final LongArrayList attrKeys = new LongArrayList(new long[] {100L, 200L, 300L}); - final LongArrayList nsKeys = new LongArrayList(new long[] {400L, 500L}); - - final ElementNode node = new ElementNode(42L, 10L, 3, 7, 20L, 30L, 40L, 50L, 5L, 100L, 999L, 60L, 11, 22, 33, - hashFunction, (byte[]) null, attrKeys, nsKeys, qNm); - - try (final Arena arena = Arena.ofConfined()) { - final int totalSize = - layout.fixedSlotSizeInBytes() + node.getAttributeCount() * Long.BYTES + node.getNamespaceCount() * Long.BYTES; - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - // Verify structural fields - assertEquals(10L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(20L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.RIGHT_SIBLING_KEY)); - assertEquals(30L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LEFT_SIBLING_KEY)); - assertEquals(40L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.FIRST_CHILD_KEY)); - assertEquals(50L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.LAST_CHILD_KEY)); - assertEquals(60L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PATH_NODE_KEY)); - assertEquals(11, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREFIX_KEY)); - assertEquals(22, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LOCAL_NAME_KEY)); - assertEquals(33, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.URI_KEY)); - assertEquals(3, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - assertEquals(7, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.LAST_MODIFIED_REVISION)); - assertEquals(999L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.HASH)); - assertEquals(5L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.CHILD_COUNT)); - assertEquals(100L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.DESCENDANT_COUNT)); - - // Verify attribute vector payload - assertEquals(3 * Long.BYTES, SlotLayoutAccessors.readPayloadLength(slot, layout, 0)); - // Verify namespace vector payload - assertEquals(2 * Long.BYTES, SlotLayoutAccessors.readPayloadLength(slot, layout, 1)); - - // Populate existing singleton from projected slot - final ElementNode populated = new ElementNode(0L, 0L, 0, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0, 0, 0, hashFunction, - (byte[]) null, new LongArrayList(), new LongArrayList(), new QNm("")); - assertTrue(FixedSlotRecordMaterializer.populateExisting(populated, NodeKind.ELEMENT, 42L, slot, 0L, - (int) slot.byteSize(), null, null)); - assertEquals(42L, populated.getNodeKey()); - assertEquals(10L, populated.getParentKey()); - assertEquals(20L, populated.getRightSiblingKey()); - assertEquals(30L, populated.getLeftSiblingKey()); - assertEquals(40L, populated.getFirstChildKey()); - assertEquals(50L, populated.getLastChildKey()); - assertEquals(60L, populated.getPathNodeKey()); - assertEquals(11, populated.getPrefixKey()); - assertEquals(22, populated.getLocalNameKey()); - assertEquals(33, populated.getURIKey()); - assertEquals(3, populated.getPreviousRevisionNumber()); - assertEquals(7, populated.getLastModifiedRevisionNumber()); - assertEquals(999L, populated.getHash()); - assertEquals(5L, populated.getChildCount()); - assertEquals(100L, populated.getDescendantCount()); - assertEquals(3, populated.getAttributeCount()); - assertEquals(100L, populated.getAttributeKey(0)); - assertEquals(200L, populated.getAttributeKey(1)); - assertEquals(300L, populated.getAttributeKey(2)); - assertEquals(2, populated.getNamespaceCount()); - assertEquals(400L, populated.getNamespaceKey(0)); - assertEquals(500L, populated.getNamespaceKey(1)); - } - } - - @Test - void elementNodeRoundTripWithEmptyVectors() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.ELEMENT); - final LongHashFunction hashFunction = LongHashFunction.xx3(); - - final ElementNode node = new ElementNode(1L, 2L, 1, 1, 3L, 4L, 5L, 6L, 0L, 0L, 0L, 7L, 0, 0, 0, hashFunction, - (byte[]) null, new LongArrayList(), new LongArrayList(), new QNm("")); - - try (final Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - assertTrue(FixedSlotRecordProjector.project(node, layout, slot, 0L)); - - assertEquals(0, SlotLayoutAccessors.readPayloadLength(slot, layout, 0)); - assertEquals(0, SlotLayoutAccessors.readPayloadLength(slot, layout, 1)); - - final ElementNode populated = new ElementNode(0L, 0L, 0, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0, 0, 0, hashFunction, - (byte[]) null, new LongArrayList(), new LongArrayList(), new QNm("")); - assertTrue(FixedSlotRecordMaterializer.populateExisting(populated, NodeKind.ELEMENT, 1L, slot, 0L, - (int) slot.byteSize(), null, null)); - assertEquals(0, populated.getAttributeCount()); - assertEquals(0, populated.getNamespaceCount()); - assertEquals(2L, populated.getParentKey()); - } - } - - @Test - void elementNodePopulateExistingRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.ELEMENT); - final LongHashFunction hashFunction = LongHashFunction.xx3(); - - final LongArrayList attrKeys = new LongArrayList(new long[] {10L, 20L}); - final LongArrayList nsKeys = new LongArrayList(new long[] {30L}); - - final ElementNode original = new ElementNode(5L, 100L, 2, 4, 101L, 102L, 103L, 104L, 3L, 50L, 777L, 105L, 8, 9, 10, - hashFunction, (byte[]) null, attrKeys, nsKeys, new QNm("")); - - try (final Arena arena = Arena.ofConfined()) { - final int totalSize = layout.fixedSlotSizeInBytes() + original.getAttributeCount() * Long.BYTES - + original.getNamespaceCount() * Long.BYTES; - final MemorySegment slot = arena.allocate(totalSize); - assertTrue(FixedSlotRecordProjector.project(original, layout, slot, 0L)); - - // Populate into an existing singleton - final ElementNode singleton = new ElementNode(0L, 0L, 0, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0, 0, 0, hashFunction, - (byte[]) null, new LongArrayList(), new LongArrayList(), new QNm("")); - - assertTrue(FixedSlotRecordMaterializer.populateExisting(singleton, NodeKind.ELEMENT, 5L, slot, 0L, - (int) slot.byteSize(), null, null)); - - assertEquals(5L, singleton.getNodeKey()); - assertEquals(100L, singleton.getParentKey()); - assertEquals(101L, singleton.getRightSiblingKey()); - assertEquals(102L, singleton.getLeftSiblingKey()); - assertEquals(103L, singleton.getFirstChildKey()); - assertEquals(104L, singleton.getLastChildKey()); - assertEquals(777L, singleton.getHash()); - assertEquals(2, singleton.getAttributeCount()); - assertEquals(10L, singleton.getAttributeKey(0)); - assertEquals(20L, singleton.getAttributeKey(1)); - assertEquals(1, singleton.getNamespaceCount()); - assertEquals(30L, singleton.getNamespaceKey(0)); - } - } -} diff --git a/bundles/sirix-core/src/test/java/io/sirix/node/layout/NodeKindLayoutsTest.java b/bundles/sirix-core/src/test/java/io/sirix/node/layout/NodeKindLayoutsTest.java deleted file mode 100644 index 470cb360f..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/node/layout/NodeKindLayoutsTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.NodeKind; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class NodeKindLayoutsTest { - - @Test - void xmlAndJsonCoreKindsExposeFixedLayouts() { - final NodeKind[] fixedKinds = {NodeKind.XML_DOCUMENT, NodeKind.ELEMENT, NodeKind.ATTRIBUTE, NodeKind.NAMESPACE, - NodeKind.TEXT, NodeKind.COMMENT, NodeKind.PROCESSING_INSTRUCTION, NodeKind.JSON_DOCUMENT, NodeKind.OBJECT, - NodeKind.ARRAY, NodeKind.OBJECT_KEY, NodeKind.STRING_VALUE, NodeKind.NUMBER_VALUE, NodeKind.BOOLEAN_VALUE, - NodeKind.NULL_VALUE, NodeKind.OBJECT_STRING_VALUE, NodeKind.OBJECT_NUMBER_VALUE, NodeKind.OBJECT_BOOLEAN_VALUE, - NodeKind.OBJECT_NULL_VALUE}; - - for (final NodeKind kind : fixedKinds) { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(kind); - assertTrue(layout.isFixedSlotSupported(), "Expected fixed-slot support for: " + kind); - assertTrue(layout.fixedSlotSizeInBytes() > 0, "Expected non-zero slot size for: " + kind); - assertSame(layout, kind.layoutDescriptor(), "NodeKind should delegate to registry for: " + kind); - assertTrue(kind.hasFixedSlotLayout(), "NodeKind should expose fixed-slot support for: " + kind); - assertEquals(layout.fixedSlotSizeInBytes(), kind.fixedSlotSizeInBytes(), - "NodeKind should expose fixed-slot size for: " + kind); - } - } - - @Test - void unsupportedKindsRemainMarkedUnsupported() { - final NodeKindLayout pathLayout = NodeKindLayouts.layoutFor(NodeKind.PATH); - assertFalse(pathLayout.isFixedSlotSupported()); - assertTrue(pathLayout.fixedSlotSizeInBytes() == 0); - assertTrue(pathLayout.payloadRefCount() == 0); - assertFalse(NodeKind.PATH.hasFixedSlotLayout()); - assertEquals(0, NodeKind.PATH.fixedSlotSizeInBytes()); - } - - @Test - void fixedSlotContractsHaveNoOverlappingRanges() { - for (final NodeKind kind : NodeKind.values()) { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(kind); - if (!layout.isFixedSlotSupported()) { - continue; - } - - final int slotSize = layout.fixedSlotSizeInBytes(); - final boolean[] occupied = new boolean[slotSize]; - - for (final StructuralField field : StructuralField.values()) { - final int offset = layout.offsetOfOrMinusOne(field); - if (offset < 0) { - continue; - } - assertTrue(offset + field.widthInBytes() <= slotSize, "Field exceeds slot boundary: " + kind + "/" + field); - markRangeFreeThenOccupy(occupied, offset, field.widthInBytes(), kind + "/" + field); - } - - for (int i = 0; i < layout.payloadRefCount(); i++) { - final PayloadRef payloadRef = layout.payloadRef(i); - assertNotNull(payloadRef); - markRangeFreeThenOccupy(occupied, payloadRef.pointerOffset(), PayloadRef.POINTER_WIDTH_BYTES, - kind + "/" + payloadRef.name() + "#ptr"); - markRangeFreeThenOccupy(occupied, payloadRef.lengthOffset(), PayloadRef.LENGTH_WIDTH_BYTES, - kind + "/" + payloadRef.name() + "#len"); - markRangeFreeThenOccupy(occupied, payloadRef.flagsOffset(), PayloadRef.FLAGS_WIDTH_BYTES, - kind + "/" + payloadRef.name() + "#flags"); - } - } - } - - private static void markRangeFreeThenOccupy(final boolean[] occupied, final int offset, final int width, - final String label) { - assertTrue(offset >= 0, "Negative offset for: " + label); - assertTrue(offset + width <= occupied.length, "Out-of-bounds range for: " + label); - for (int i = offset; i < offset + width; i++) { - assertFalse(occupied[i], "Overlapping byte in slot layout for: " + label); - occupied[i] = true; - } - } -} diff --git a/bundles/sirix-core/src/test/java/io/sirix/node/layout/SlotLayoutAccessorsTest.java b/bundles/sirix-core/src/test/java/io/sirix/node/layout/SlotLayoutAccessorsTest.java deleted file mode 100644 index bddb5bbcf..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/node/layout/SlotLayoutAccessorsTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.sirix.node.layout; - -import io.sirix.node.NodeKind; -import org.junit.jupiter.api.Test; - -import java.lang.foreign.Arena; -import java.lang.foreign.MemorySegment; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class SlotLayoutAccessorsTest { - - @Test - void longAndIntFieldRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.OBJECT); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - - SlotLayoutAccessors.writeLongField(slot, layout, StructuralField.PARENT_KEY, 100L); - SlotLayoutAccessors.writeLongField(slot, layout, StructuralField.FIRST_CHILD_KEY, 101L); - SlotLayoutAccessors.writeIntField(slot, layout, StructuralField.PREVIOUS_REVISION, 7); - - assertEquals(100L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.PARENT_KEY)); - assertEquals(101L, SlotLayoutAccessors.readLongField(slot, layout, StructuralField.FIRST_CHILD_KEY)); - assertEquals(7, SlotLayoutAccessors.readIntField(slot, layout, StructuralField.PREVIOUS_REVISION)); - } - } - - @Test - void booleanFieldRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.BOOLEAN_VALUE); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - - SlotLayoutAccessors.writeBooleanField(slot, layout, StructuralField.BOOLEAN_VALUE, true); - assertTrue(SlotLayoutAccessors.readBooleanField(slot, layout, StructuralField.BOOLEAN_VALUE)); - - SlotLayoutAccessors.writeBooleanField(slot, layout, StructuralField.BOOLEAN_VALUE, false); - assertFalse(SlotLayoutAccessors.readBooleanField(slot, layout, StructuralField.BOOLEAN_VALUE)); - } - } - - @Test - void payloadRefRoundTrip() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - SlotLayoutAccessors.writePayloadRef(slot, layout, 0, 4096L, 128, 3); - - assertEquals(4096L, SlotLayoutAccessors.readPayloadPointer(slot, layout, 0)); - assertEquals(128, SlotLayoutAccessors.readPayloadLength(slot, layout, 0)); - assertEquals(3, SlotLayoutAccessors.readPayloadFlags(slot, layout, 0)); - } - } - - @Test - void missingFieldAccessFailsFast() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.NULL_VALUE); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - assertThrows(IllegalArgumentException.class, - () -> SlotLayoutAccessors.readLongField(slot, layout, StructuralField.FIRST_CHILD_KEY)); - } - } - - @Test - void invalidPayloadIndexFailsFast() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - assertThrows(IllegalArgumentException.class, () -> SlotLayoutAccessors.readPayloadPointer(slot, layout, 1)); - } - } - - @Test - void payloadRefRejectsNegativeLengthOrFlags() { - final NodeKindLayout layout = NodeKindLayouts.layoutFor(NodeKind.STRING_VALUE); - try (Arena arena = Arena.ofConfined()) { - final MemorySegment slot = arena.allocate(layout.fixedSlotSizeInBytes()); - assertThrows(IllegalArgumentException.class, - () -> SlotLayoutAccessors.writePayloadRef(slot, layout, 0, 1L, -1, 0)); - assertThrows(IllegalArgumentException.class, - () -> SlotLayoutAccessors.writePayloadRef(slot, layout, 0, 1L, 1, -1)); - } - } -} diff --git a/bundles/sirix-core/src/test/java/io/sirix/page/KeyValueLeafTest.java b/bundles/sirix-core/src/test/java/io/sirix/page/KeyValueLeafTest.java index 7d2b2b809..64a8d8c5f 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/page/KeyValueLeafTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/page/KeyValueLeafTest.java @@ -2,7 +2,6 @@ import io.sirix.access.ResourceConfiguration; import io.sirix.index.IndexType; -import io.sirix.node.NodeKind; import io.sirix.node.json.BooleanNode; import io.sirix.settings.Constants; import net.openhft.hashing.LongHashFunction; @@ -13,7 +12,6 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import java.util.Arrays; import static io.sirix.cache.LinuxMemorySegmentAllocator.SIXTYFOUR_KB; import static org.junit.jupiter.api.Assertions.*; @@ -322,126 +320,6 @@ void testSetSlotMemorySegmentResizing() { assertEquals(largeData.byteSize(), slot.byteSize()); } - @Test - void testHasEnoughSpaceWithSufficientSpace() { - // Simulate a situation where there is enough space for the new data. - byte[] recordData = new byte[50]; - int[] offsets = new int[5]; - Arrays.fill(offsets, -1); // Initially, no slots are occupied. - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(200); // Create a memory segment of 200 bytes. - - // Set some initial data. - keyValueLeafPage.setSlotMemory(memory); - keyValueLeafPage.setSlot(recordData, 0); - assertTrue(keyValueLeafPage.hasEnoughSpace(offsets, memory, 50), - "There should be enough space for the new data."); - } - } - - @Test - void testHasEnoughSpaceWithExactSpace() { - // Simulate a situation where there is exactly enough space. - byte[] recordData = new byte[50]; - int[] offsets = new int[5]; - Arrays.fill(offsets, -1); // Initially, no slots are occupied. - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(106); // Create a memory segment of 106 bytes (100 + 4(size) + 2(aligned). - - // Set some initial data. - keyValueLeafPage.setSlotMemory(memory); - keyValueLeafPage.setSlot(recordData, 0); - - // Check that there's exactly enough space left. - assertTrue(keyValueLeafPage.hasEnoughSpace(offsets, memory, 50), - "There should be exactly enough space for the new data."); - } - } - - @Test - void testHasEnoughSpaceWithInsufficientSpace() { - // Simulate a situation where there is not enough space for the new data. - byte[] recordData = new byte[50]; - int[] offsets = new int[5]; - Arrays.fill(offsets, -1); // Initially, no slots are occupied. - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(80); // Create a memory segment of 80 bytes. - - // Set some initial data. - keyValueLeafPage.setSlotMemory(memory); - keyValueLeafPage.setSlot(recordData, 0); - - // Check that there's not enough space for new data. - assertFalse(keyValueLeafPage.hasEnoughSpace(offsets, memory, 100), - "There should not be enough space for the new data."); - } - } - - @Test - void testHasEnoughSpaceWithLargeOffset() { - // Simulate a situation with large offset values and small memory. - byte[] recordData = new byte[20]; - int[] offsets = new int[] {0, 24, -1}; - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(50); // Small memory segment. - - keyValueLeafPage.setSlotMemory(memory); - - // Set some initial data. - keyValueLeafPage.setSlot(recordData, 0); - keyValueLeafPage.setSlot(recordData, 1); - - // Check that there's not enough space. - assertFalse(keyValueLeafPage.hasEnoughSpace(offsets, memory, 50), - "There should not be enough space due to the small memory segment and large offsets."); - } - } - - @Test - void testHasEnoughSpaceWithEmptyMemory() { - // Simulate a situation with an empty memory segment. - int[] offsets = new int[5]; - Arrays.fill(offsets, -1); // Initially, no slots are occupied. - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(0); // Empty memory segment. - - keyValueLeafPage.setSlotMemory(memory); - // Check that there's not enough space. - assertFalse(keyValueLeafPage.hasEnoughSpace(offsets, memory, 1), - "There should not be enough space with an empty memory segment."); - } - } - - @Test - void testHasEnoughSpaceAfterResizing() { - // Simulate a situation where the memory is resized. - byte[] recordData = new byte[50]; - int[] offsets = new int[5]; - Arrays.fill(offsets, -1); // Initially, no slots are occupied. - - try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(54); // Small memory segment. - - // Set some initial data. - keyValueLeafPage.setSlotMemory(memory); - keyValueLeafPage.setSlot(recordData, 0); - - // Resize the memory segment. - MemorySegment resizedMemory = arena.allocate(100); - - keyValueLeafPage.setSlotMemory(resizedMemory); - - // Check that there's enough space after resizing. - assertTrue(keyValueLeafPage.hasEnoughSpace(offsets, resizedMemory, 50), - "There should be enough space after resizing the memory segment."); - } - } - @Test void testAddReferencesCopiesOnlyMarkedPreservationSlots() { byte[] preserved = new byte[] {42, 43, 44}; @@ -530,7 +408,7 @@ void testAddReferencesCopiesPreservedSlotData() { } @Test - void testSetRecordDefersSlotMaterializationUntilAddReferences() { + void testSetRecordDefersSerializationForNonSingletonFlyweight() { final long nodeKey = 1L; final int offset = (int) (nodeKey - ((nodeKey >> Constants.NDP_NODE_COUNT_EXPONENT) << Constants.NDP_NODE_COUNT_EXPONENT)); @@ -539,11 +417,9 @@ void testSetRecordDefersSlotMaterializationUntilAddReferences() { keyValueLeafPage.setRecord(node); + // Non-singleton FlyweightNode: stored in records[] for processEntries serialization. + // Only write singletons are serialized directly to heap. assertSame(node, keyValueLeafPage.getRecord(offset)); - assertNull(keyValueLeafPage.getSlot(offset)); - - keyValueLeafPage.addReferences(new ResourceConfiguration.Builder("testResource").build()); - assertNotNull(keyValueLeafPage.getSlot(offset)); } @Test @@ -564,35 +440,4 @@ void testRawSlotWriteWithoutMaterializedRecord() { assertArrayEquals(new byte[] {1, 2, 3}, slot.toArray(ValueLayout.JAVA_BYTE)); } - @Test - void testFixedSlotFormatMarkersRoundTrip() { - final int slot = 13; - keyValueLeafPage.setSlot(new byte[] {7, 8, 9}, slot); - - assertFalse(keyValueLeafPage.isFixedSlotFormat(slot)); - assertNull(keyValueLeafPage.getFixedSlotNodeKind(slot)); - - keyValueLeafPage.markSlotAsFixedFormat(slot, NodeKind.BOOLEAN_VALUE); - assertTrue(keyValueLeafPage.isFixedSlotFormat(slot)); - assertEquals(NodeKind.BOOLEAN_VALUE, keyValueLeafPage.getFixedSlotNodeKind(slot)); - - keyValueLeafPage.markSlotAsCompactFormat(slot); - assertFalse(keyValueLeafPage.isFixedSlotFormat(slot)); - assertNull(keyValueLeafPage.getFixedSlotNodeKind(slot)); - } - - @Test - void testResetClearsFixedSlotFormatMetadata() { - final int slot = 17; - keyValueLeafPage.setSlot(new byte[] {1, 2}, slot); - keyValueLeafPage.markSlotAsFixedFormat(slot, NodeKind.OBJECT); - - assertTrue(keyValueLeafPage.isFixedSlotFormat(slot)); - assertEquals(NodeKind.OBJECT, keyValueLeafPage.getFixedSlotNodeKind(slot)); - - keyValueLeafPage.reset(); - - assertFalse(keyValueLeafPage.isFixedSlotFormat(slot)); - assertNull(keyValueLeafPage.getFixedSlotNodeKind(slot)); - } } diff --git a/bundles/sirix-core/src/test/java/io/sirix/page/ResizeFieldCorrectnessTest.java b/bundles/sirix-core/src/test/java/io/sirix/page/ResizeFieldCorrectnessTest.java new file mode 100644 index 000000000..a197efdf3 --- /dev/null +++ b/bundles/sirix-core/src/test/java/io/sirix/page/ResizeFieldCorrectnessTest.java @@ -0,0 +1,422 @@ +package io.sirix.page; + +import io.sirix.access.ResourceConfiguration; +import io.sirix.index.IndexType; +import io.sirix.node.DeltaVarIntCodec; +import io.sirix.node.json.ObjectNode; +import io.sirix.settings.Fixed; +import net.openhft.hashing.LongHashFunction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.stream.Stream; + +import static io.sirix.cache.LinuxMemorySegmentAllocator.SIXTYFOUR_KB; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link DeltaVarIntCodec#resizeField} and {@link KeyValueLeafPage#resizeRecordField}. + * + *

Verifies INV-4 (offset table consistency) and INV-5 (raw-copy resize correctness) + * from the LeanStore write path plan. + */ +class ResizeFieldCorrectnessTest { + + private static final long NODE_KEY = 1000L; + private static final long PARENT_KEY = 999L; + private static final long RIGHT_SIB_KEY = 1001L; + private static final long LEFT_SIB_KEY = Fixed.NULL_NODE_KEY.getStandardProperty(); + private static final long FIRST_CHILD_KEY = 1002L; + private static final long LAST_CHILD_KEY = 1005L; + private static final int PREV_REVISION = 0; + private static final int LAST_MOD_REVISION = 1; + private static final long HASH = 0xDEADBEEFCAFEL; + private static final long CHILD_COUNT = 4L; + private static final long DESCENDANT_COUNT = 42L; + + private KeyValueLeafPage page; + private Arena arena; + private ObjectNode objectNode; + + @BeforeEach + void setUp() { + arena = Arena.ofConfined(); + page = new KeyValueLeafPage(0L, IndexType.DOCUMENT, + new ResourceConfiguration.Builder("testResource").build(), 1, + arena.allocate(SIXTYFOUR_KB), null); + + objectNode = new ObjectNode(NODE_KEY, PARENT_KEY, PREV_REVISION, LAST_MOD_REVISION, + RIGHT_SIB_KEY, LEFT_SIB_KEY, FIRST_CHILD_KEY, LAST_CHILD_KEY, + CHILD_COUNT, DESCENDANT_COUNT, HASH, + LongHashFunction.xx3(), (byte[]) null); + objectNode.setWriteSingleton(true); + } + + @AfterEach + void tearDown() { + if (page != null) { + page.close(); + } + if (arena != null) { + arena.close(); + } + } + + /** + * Helper: serialize ObjectNode to the page heap at slot 0, then bind it. + */ + private void serializeAndBind() { + page.serializeNewRecord(objectNode, NODE_KEY, 0); + // serializeNewRecord calls clearBinding, so re-bind manually + final MemorySegment sp = page.getSlottedPage(); + final int heapOff = PageLayout.getDirHeapOffset(sp, 0); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOff); + objectNode.bind(sp, recordBase, NODE_KEY, 0); + objectNode.setOwnerPage(page); + } + + /** + * Verify all 10 fields of the bound ObjectNode match expected values. + */ + private void assertAllFields(final long expectedParent, final long expectedRightSib, + final long expectedLeftSib, final long expectedFirstChild, final long expectedLastChild, + final int expectedPrevRev, final int expectedLastModRev, final long expectedHash, + final long expectedChildCount, final long expectedDescCount) { + assertEquals(expectedParent, objectNode.getParentKey(), "parentKey"); + assertEquals(expectedRightSib, objectNode.getRightSiblingKey(), "rightSiblingKey"); + assertEquals(expectedLeftSib, objectNode.getLeftSiblingKey(), "leftSiblingKey"); + assertEquals(expectedFirstChild, objectNode.getFirstChildKey(), "firstChildKey"); + assertEquals(expectedLastChild, objectNode.getLastChildKey(), "lastChildKey"); + assertEquals(expectedPrevRev, objectNode.getPreviousRevisionNumber(), "previousRevision"); + assertEquals(expectedLastModRev, objectNode.getLastModifiedRevisionNumber(), "lastModifiedRevision"); + assertEquals(expectedHash, objectNode.getHash(), "hash"); + assertEquals(expectedChildCount, objectNode.getChildCount(), "childCount"); + assertEquals(expectedDescCount, objectNode.getDescendantCount(), "descendantCount"); + } + + // ==================== INV-5: Raw-Copy Resize Correctness ==================== + + /** + * Provides (fieldIndex, newValue) pairs for each of the 10 ObjectNode fields. + * Each new value is chosen to require a different varint width than the original. + */ + static Stream fieldResizeParameters() { + // Original values produce small deltas (1-2 byte varints). + // New values produce large deltas (3+ byte varints) to force width change. + final long farKey = NODE_KEY + 100_000L; // large delta → 3-byte varint + return Stream.of( + // fieldIndex, newValue (key or int/long), description + Arguments.of(NodeFieldLayout.OBJECT_PARENT_KEY, farKey, "parentKey"), + Arguments.of(NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, farKey, "rightSiblingKey"), + Arguments.of(NodeFieldLayout.OBJECT_LEFT_SIB_KEY, farKey, "leftSiblingKey"), + Arguments.of(NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, farKey, "firstChildKey"), + Arguments.of(NodeFieldLayout.OBJECT_LAST_CHILD_KEY, farKey, "lastChildKey"), + // Revision fields: small → large signed varint + Arguments.of(NodeFieldLayout.OBJECT_PREV_REVISION, 100_000L, "previousRevision"), + Arguments.of(NodeFieldLayout.OBJECT_LAST_MOD_REVISION, 100_000L, "lastModifiedRevision"), + // Hash: fixed 8 bytes, no width change — test that it survives resize of OTHER field + // childCount/descendantCount: signed long varint + Arguments.of(NodeFieldLayout.OBJECT_CHILD_COUNT, 100_000L, "childCount"), + Arguments.of(NodeFieldLayout.OBJECT_DESCENDANT_COUNT, 100_000L, "descendantCount") + ); + } + + @ParameterizedTest(name = "resize field {2} (index={0})") + @MethodSource("fieldResizeParameters") + void resizeRecordField_preservesUnchangedFields(final int fieldIndex, final long newValue, + final String fieldName) { + serializeAndBind(); + + // Record heap state before resize + final MemorySegment sp = page.getSlottedPage(); + final int heapEndBefore = PageLayout.getHeapEnd(sp); + + // Perform raw-copy resize via KVL + final DeltaVarIntCodec.FieldEncoder encoder = createEncoder(fieldIndex, newValue); + page.resizeRecordField(objectNode, NODE_KEY, 0, fieldIndex, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + // INV-2: Heap monotonicity + final int heapEndAfter = PageLayout.getHeapEnd(page.getSlottedPage()); + assertTrue(heapEndAfter > heapEndBefore, "heapEnd must advance after resize"); + + // INV-5: All unchanged fields preserved, changed field updated + final long expectedParent = fieldIndex == NodeFieldLayout.OBJECT_PARENT_KEY ? newValue : PARENT_KEY; + final long expectedRightSib = fieldIndex == NodeFieldLayout.OBJECT_RIGHT_SIB_KEY ? newValue : RIGHT_SIB_KEY; + final long expectedLeftSib = fieldIndex == NodeFieldLayout.OBJECT_LEFT_SIB_KEY ? newValue : LEFT_SIB_KEY; + final long expectedFirstChild = fieldIndex == NodeFieldLayout.OBJECT_FIRST_CHILD_KEY ? newValue : FIRST_CHILD_KEY; + final long expectedLastChild = fieldIndex == NodeFieldLayout.OBJECT_LAST_CHILD_KEY ? newValue : LAST_CHILD_KEY; + final int expectedPrevRev = fieldIndex == NodeFieldLayout.OBJECT_PREV_REVISION ? (int) newValue : PREV_REVISION; + final int expectedLastModRev = fieldIndex == NodeFieldLayout.OBJECT_LAST_MOD_REVISION ? (int) newValue : LAST_MOD_REVISION; + final long expectedChildCount = fieldIndex == NodeFieldLayout.OBJECT_CHILD_COUNT ? newValue : CHILD_COUNT; + final long expectedDescCount = fieldIndex == NodeFieldLayout.OBJECT_DESCENDANT_COUNT ? newValue : DESCENDANT_COUNT; + + assertAllFields(expectedParent, expectedRightSib, expectedLeftSib, + expectedFirstChild, expectedLastChild, + expectedPrevRev, expectedLastModRev, + HASH, // hash is always fixed 8 bytes, never changed in this test + expectedChildCount, expectedDescCount); + } + + /** + * Test resizing a field that shrinks (large varint → small varint). + * This exercises the negative widthDelta path. + */ + @Test + void resizeRecordField_shrinkField() { + // Create node with a far-away parent (large delta → multi-byte varint) + final long farParent = NODE_KEY + 100_000L; + objectNode = new ObjectNode(NODE_KEY, farParent, PREV_REVISION, LAST_MOD_REVISION, + RIGHT_SIB_KEY, LEFT_SIB_KEY, FIRST_CHILD_KEY, LAST_CHILD_KEY, + CHILD_COUNT, DESCENDANT_COUNT, HASH, + LongHashFunction.xx3(), (byte[]) null); + objectNode.setWriteSingleton(true); + serializeAndBind(); + + // Now resize parentKey back to a nearby value (small delta → 1-byte varint) + final long nearParent = NODE_KEY - 1; + final DeltaVarIntCodec.FieldEncoder encoder = (target, offset) -> + DeltaVarIntCodec.writeDeltaToSegment(target, offset, nearParent, NODE_KEY); + page.resizeRecordField(objectNode, NODE_KEY, 0, NodeFieldLayout.OBJECT_PARENT_KEY, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + assertAllFields(nearParent, RIGHT_SIB_KEY, LEFT_SIB_KEY, + FIRST_CHILD_KEY, LAST_CHILD_KEY, + PREV_REVISION, LAST_MOD_REVISION, HASH, + CHILD_COUNT, DESCENDANT_COUNT); + } + + /** + * Test resizing NULL_NODE_KEY field to a real value and back. + */ + @Test + void resizeRecordField_nullToReal_and_realToNull() { + serializeAndBind(); + + // leftSiblingKey starts as NULL (-1). Resize to a real value. + final long realSib = NODE_KEY + 50_000L; + DeltaVarIntCodec.FieldEncoder encoder = (target, offset) -> + DeltaVarIntCodec.writeDeltaToSegment(target, offset, realSib, NODE_KEY); + page.resizeRecordField(objectNode, NODE_KEY, 0, NodeFieldLayout.OBJECT_LEFT_SIB_KEY, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + assertEquals(realSib, objectNode.getLeftSiblingKey()); + // Other fields preserved + assertEquals(PARENT_KEY, objectNode.getParentKey()); + assertEquals(HASH, objectNode.getHash()); + + // Now resize back to NULL + final long nullKey = Fixed.NULL_NODE_KEY.getStandardProperty(); + encoder = (target, offset) -> + DeltaVarIntCodec.writeDeltaToSegment(target, offset, nullKey, NODE_KEY); + page.resizeRecordField(objectNode, NODE_KEY, 0, NodeFieldLayout.OBJECT_LEFT_SIB_KEY, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + assertEquals(nullKey, objectNode.getLeftSiblingKey()); + assertEquals(PARENT_KEY, objectNode.getParentKey()); + assertEquals(HASH, objectNode.getHash()); + } + + /** + * Test multiple consecutive resizes on the same node (fragmentation test). + * INV-2: heap must grow monotonically. INV-8: fragmentation tracked. + */ + @Test + void resizeRecordField_multipleResizes_heapMonotonic() { + serializeAndBind(); + + final MemorySegment sp = page.getSlottedPage(); + int prevHeapEnd = PageLayout.getHeapEnd(sp); + + // Resize 5 different fields consecutively + final long farKey = NODE_KEY + 200_000L; + final int[] fields = { + NodeFieldLayout.OBJECT_PARENT_KEY, + NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, + NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, + NodeFieldLayout.OBJECT_CHILD_COUNT, + NodeFieldLayout.OBJECT_DESCENDANT_COUNT + }; + + for (final int field : fields) { + final DeltaVarIntCodec.FieldEncoder encoder = createEncoder(field, farKey); + page.resizeRecordField(objectNode, NODE_KEY, 0, field, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + final int currentHeapEnd = PageLayout.getHeapEnd(page.getSlottedPage()); + assertTrue(currentHeapEnd > prevHeapEnd, + "heapEnd must advance after resize of field " + field); + prevHeapEnd = currentHeapEnd; + } + + // Verify all resized fields have the new value + assertEquals(farKey, objectNode.getParentKey()); + assertEquals(farKey, objectNode.getRightSiblingKey()); + assertEquals(farKey, objectNode.getFirstChildKey()); + assertEquals(farKey, objectNode.getChildCount()); + assertEquals(farKey, objectNode.getDescendantCount()); + + // Unchanged fields preserved + assertEquals(LEFT_SIB_KEY, objectNode.getLeftSiblingKey()); + assertEquals(LAST_CHILD_KEY, objectNode.getLastChildKey()); + assertEquals(HASH, objectNode.getHash()); + } + + // ==================== INV-4: Offset Table Consistency ==================== + + @Test + void resizeRecordField_offsetTableMonotonic() { + serializeAndBind(); + + // Resize a middle field to force offset shifts + final long farKey = NODE_KEY + 100_000L; + final DeltaVarIntCodec.FieldEncoder encoder = (target, offset) -> + DeltaVarIntCodec.writeDeltaToSegment(target, offset, farKey, NODE_KEY); + page.resizeRecordField(objectNode, NODE_KEY, 0, + NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + // Read offset table and verify monotonicity + final MemorySegment sp = page.getSlottedPage(); + final int heapOffset = PageLayout.getDirHeapOffset(sp, 0); + final long recordBase = PageLayout.heapAbsoluteOffset(heapOffset); + + int prevOffset = -1; + for (int i = 0; i < NodeFieldLayout.OBJECT_FIELD_COUNT; i++) { + final int fieldOff = sp.get(ValueLayout.JAVA_BYTE, recordBase + 1 + i) & 0xFF; + assertTrue(fieldOff > prevOffset || (i == 0 && fieldOff == 0), + "offset table must be monotonically increasing: field " + i + + " offset=" + fieldOff + " prev=" + prevOffset); + prevOffset = fieldOff; + } + } + + // ==================== INV-3: Directory-Heap Consistency ==================== + + @Test + void resizeRecordField_directoryPointsWithinHeap() { + serializeAndBind(); + + // Resize + final long farKey = NODE_KEY + 100_000L; + final DeltaVarIntCodec.FieldEncoder encoder = (target, offset) -> + DeltaVarIntCodec.writeDeltaToSegment(target, offset, farKey, NODE_KEY); + page.resizeRecordField(objectNode, NODE_KEY, 0, + NodeFieldLayout.OBJECT_PARENT_KEY, + NodeFieldLayout.OBJECT_FIELD_COUNT, encoder); + + final MemorySegment sp = page.getSlottedPage(); + final int dirHeapOff = PageLayout.getDirHeapOffset(sp, 0); + final int dirDataLen = PageLayout.getDirDataLength(sp, 0); + final int heapEnd = PageLayout.getHeapEnd(sp); + + assertTrue(dirHeapOff + dirDataLen <= heapEnd, + "directory entry must point within heap bounds"); + assertTrue(PageLayout.isSlotPopulated(sp, 0), "slot must remain populated"); + assertTrue(PageLayout.getDirNodeKindId(sp, 0) > 0, "nodeKindId must be > 0"); + } + + // ==================== DeltaVarIntCodec.resizeField unit test ==================== + + @Test + void resizeField_directCodecCall() { + // Create a small MemorySegment with a mock record: + // [kindByte:1][offsets:3][field0:1byte][field1:1byte][field2:1byte] + final int fieldCount = 3; + final byte[] src = new byte[64]; + final MemorySegment srcSeg = MemorySegment.ofArray(src); + + // Write mock record + srcSeg.set(ValueLayout.JAVA_BYTE, 0, (byte) 42); // nodeKind + // Offset table: field0 at 0, field1 at 2, field2 at 4 + srcSeg.set(ValueLayout.JAVA_BYTE, 1, (byte) 0); + srcSeg.set(ValueLayout.JAVA_BYTE, 2, (byte) 2); + srcSeg.set(ValueLayout.JAVA_BYTE, 3, (byte) 4); + // Data region starts at offset 4 (1 + 3) + // field0: 2 bytes [0xAA, 0xBB] + srcSeg.set(ValueLayout.JAVA_BYTE, 4, (byte) 0xAA); + srcSeg.set(ValueLayout.JAVA_BYTE, 5, (byte) 0xBB); + // field1: 2 bytes [0xCC, 0xDD] + srcSeg.set(ValueLayout.JAVA_BYTE, 6, (byte) 0xCC); + srcSeg.set(ValueLayout.JAVA_BYTE, 7, (byte) 0xDD); + // field2: 2 bytes [0xEE, 0xFF] + srcSeg.set(ValueLayout.JAVA_BYTE, 8, (byte) 0xEE); + srcSeg.set(ValueLayout.JAVA_BYTE, 9, (byte) 0xFF); + + final int srcRecordLen = 10; // 1 + 3 + 6 + + // Resize field1 from 2 bytes to 3 bytes [0x11, 0x22, 0x33] + final byte[] dst = new byte[64]; + final MemorySegment dstSeg = MemorySegment.ofArray(dst); + + final int newRecordLen = DeltaVarIntCodec.resizeField( + srcSeg, 0, srcRecordLen, + fieldCount, 1, + dstSeg, 0, + (target, offset) -> { + target.set(ValueLayout.JAVA_BYTE, offset, (byte) 0x11); + target.set(ValueLayout.JAVA_BYTE, offset + 1, (byte) 0x22); + target.set(ValueLayout.JAVA_BYTE, offset + 2, (byte) 0x33); + return 3; + }); + + // Expected: 1 + 3 + (2 + 3 + 2) = 11 bytes + assertEquals(11, newRecordLen); + + // NodeKind preserved + assertEquals(42, dstSeg.get(ValueLayout.JAVA_BYTE, 0)); + + // Offset table: field0=0, field1=2 (unchanged), field2=5 (shifted by +1) + assertEquals(0, dstSeg.get(ValueLayout.JAVA_BYTE, 1) & 0xFF); + assertEquals(2, dstSeg.get(ValueLayout.JAVA_BYTE, 2) & 0xFF); + assertEquals(5, dstSeg.get(ValueLayout.JAVA_BYTE, 3) & 0xFF); + + // Data region at offset 4: + // field0: [0xAA, 0xBB] (unchanged) + assertEquals((byte) 0xAA, dstSeg.get(ValueLayout.JAVA_BYTE, 4)); + assertEquals((byte) 0xBB, dstSeg.get(ValueLayout.JAVA_BYTE, 5)); + // field1: [0x11, 0x22, 0x33] (new value) + assertEquals((byte) 0x11, dstSeg.get(ValueLayout.JAVA_BYTE, 6)); + assertEquals((byte) 0x22, dstSeg.get(ValueLayout.JAVA_BYTE, 7)); + assertEquals((byte) 0x33, dstSeg.get(ValueLayout.JAVA_BYTE, 8)); + // field2: [0xEE, 0xFF] (unchanged, shifted) + assertEquals((byte) 0xEE, dstSeg.get(ValueLayout.JAVA_BYTE, 9)); + assertEquals((byte) 0xFF, dstSeg.get(ValueLayout.JAVA_BYTE, 10)); + } + + // ==================== Helpers ==================== + + /** + * Create a FieldEncoder for the given ObjectNode field index and new value. + */ + private DeltaVarIntCodec.FieldEncoder createEncoder(final int fieldIndex, final long newValue) { + return switch (fieldIndex) { + case NodeFieldLayout.OBJECT_PARENT_KEY, + NodeFieldLayout.OBJECT_RIGHT_SIB_KEY, + NodeFieldLayout.OBJECT_LEFT_SIB_KEY, + NodeFieldLayout.OBJECT_FIRST_CHILD_KEY, + NodeFieldLayout.OBJECT_LAST_CHILD_KEY -> + (target, offset) -> DeltaVarIntCodec.writeDeltaToSegment(target, offset, newValue, NODE_KEY); + case NodeFieldLayout.OBJECT_PREV_REVISION, + NodeFieldLayout.OBJECT_LAST_MOD_REVISION -> + (target, offset) -> DeltaVarIntCodec.writeSignedToSegment(target, offset, (int) newValue); + case NodeFieldLayout.OBJECT_HASH -> + (target, offset) -> { + DeltaVarIntCodec.writeLongToSegment(target, offset, newValue); + return Long.BYTES; + }; + case NodeFieldLayout.OBJECT_CHILD_COUNT, + NodeFieldLayout.OBJECT_DESCENDANT_COUNT -> + (target, offset) -> DeltaVarIntCodec.writeSignedLongToSegment(target, offset, newValue); + default -> throw new IllegalArgumentException("Unknown field index: " + fieldIndex); + }; + } +} diff --git a/bundles/sirix-core/src/test/java/io/sirix/page/SlotOffsetCodecTest.java b/bundles/sirix-core/src/test/java/io/sirix/page/SlotOffsetCodecTest.java deleted file mode 100644 index ee5ccc167..000000000 --- a/bundles/sirix-core/src/test/java/io/sirix/page/SlotOffsetCodecTest.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (c) 2023, Sirix Contributors - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * * Neither the name of the nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.sirix.page; - -import io.sirix.node.Bytes; -import io.sirix.node.BytesIn; -import io.sirix.node.BytesOut; -import io.sirix.settings.Constants; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.Random; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Unit tests for {@link SlotOffsetCodec}. - *

- * Tests cover: - Empty pages - Single slot - Fully populated pages - Sparse pages - Large offsets - * (requiring 16+ bits) - Round-trip serialization/deserialization - Bit-packing edge cases - */ -public final class SlotOffsetCodecTest { - - @Test - public void testEmptyPage() { - // All slots empty (-1) - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, -1); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Empty page should decode to all -1"); - } - - @Test - public void testSingleSlot() { - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - slotOffsets[0] = 0; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 0); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Single slot at index 0 should round-trip correctly"); - } - - @Test - public void testSingleSlotMiddle() { - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - slotOffsets[512] = 1000; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 512); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Single slot in middle should round-trip correctly"); - } - - @Test - public void testFullyPopulated() { - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - int offset = 0; - for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { - slotOffsets[i] = offset; - offset += 50; // Each record ~50 bytes - } - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, Constants.NDP_NODE_COUNT - 1); - - long compressedSize = sink.writePosition(); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Fully populated page should round-trip correctly"); - - // Verify compression - should be much smaller than 4096 bytes - int rawSize = Constants.NDP_NODE_COUNT * 4; // 4096 bytes - System.out.printf("Full page compression: raw=%d, compressed=%d, ratio=%.1f%%%n", rawSize, compressedSize, - (100.0 * compressedSize / rawSize)); - } - - @Test - public void testSparsePageRandom() { - Random random = new Random(42); // Fixed seed for reproducibility - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - // Populate ~10% of slots - int offset = 0; - int lastSlot = -1; - for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { - if (random.nextDouble() < 0.1) { - slotOffsets[i] = offset; - offset += random.nextInt(100) + 10; - lastSlot = i; - } - } - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, lastSlot); - - long compressedSize = sink.writePosition(); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Sparse page should round-trip correctly"); - - int rawSize = Constants.NDP_NODE_COUNT * 4; - System.out.printf("Sparse page compression: raw=%d, compressed=%d, ratio=%.1f%%%n", rawSize, compressedSize, - (100.0 * compressedSize / rawSize)); - } - - @Test - public void testLargeOffsets() { - // Test with offsets requiring 16+ bits - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - slotOffsets[0] = 0; - slotOffsets[1] = 65536; // 2^16 - slotOffsets[2] = 131072; // 2^17 - slotOffsets[3] = 200000; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 3); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Large offsets should round-trip correctly"); - } - - @Test - public void testMaxIntOffsets() { - // Test with maximum integer offsets - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - slotOffsets[0] = 0; - slotOffsets[1] = Integer.MAX_VALUE / 2; - slotOffsets[2] = Integer.MAX_VALUE; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 2); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Max int offsets should round-trip correctly"); - } - - @Test - public void testConsecutiveSlots() { - // Slots 0-99 with sequential offsets - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - for (int i = 0; i < 100; i++) { - slotOffsets[i] = i * 32; - } - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 99); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Consecutive slots should round-trip correctly"); - } - - @Test - public void testOffsetsWithGaps() { - // Test offsets with varying gaps between them (all still monotonic) - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - // Offsets with varying gaps - slotOffsets[0] = 0; - slotOffsets[1] = 10; // Small gap - slotOffsets[2] = 1000; // Large gap - slotOffsets[3] = 1001; // Tiny gap - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 3); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Varying gap offsets should round-trip"); - } - - @Test - public void testBitPackingWithVariousBitWidths() { - // Test bit widths that are commonly used (1-16 bits covers typical offset deltas) - int[] bitWidths = {1, 2, 3, 4, 5, 7, 8, 15, 16}; - - for (int bitWidth : bitWidths) { - // Create values that require exactly bitWidth bits - int maxValue = (1 << bitWidth) - 1; - int[] values = new int[100]; - Random random = new Random(bitWidth); - for (int i = 0; i < values.length; i++) { - values[i] = random.nextInt(maxValue + 1); - } - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.writeBitPacked(sink, values, bitWidth); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.readBitPacked(source, values.length, bitWidth); - - assertArrayEquals(values, decoded, "Bit-packing with " + bitWidth + " bits should round-trip correctly"); - } - } - - @Test - public void testBitPackingWith32Bits() { - // Special test for 32-bit values which need careful handling - int[] values = {0, 1, Integer.MAX_VALUE / 2, Integer.MAX_VALUE - 1, Integer.MAX_VALUE}; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.writeBitPacked(sink, values, 32); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.readBitPacked(source, values.length, 32); - - assertArrayEquals(values, decoded, "32-bit packing should round-trip correctly"); - } - - @Test - public void testBitPackingEdgeCaseZeroValues() { - int[] values = new int[100]; - Arrays.fill(values, 0); - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.writeBitPacked(sink, values, 1); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.readBitPacked(source, values.length, 1); - - assertArrayEquals(values, decoded, "All-zero values should round-trip"); - } - - @Test - public void testBitPackingEmptyArray() { - int[] values = new int[0]; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.writeBitPacked(sink, values, 8); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.readBitPacked(source, 0, 8); - - assertArrayEquals(values, decoded, "Empty array should round-trip"); - } - - @Test - public void testCompressionRatioRealistic() { - // Simulate realistic page with varying record sizes - Random random = new Random(12345); - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - - int offset = 0; - int lastSlot = -1; - - // Populate 80% of slots with realistic record sizes (30-200 bytes) - for (int i = 0; i < Constants.NDP_NODE_COUNT; i++) { - if (random.nextDouble() < 0.8) { - slotOffsets[i] = offset; - offset += 30 + random.nextInt(170); // 30-200 byte records - lastSlot = i; - } - } - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, lastSlot); - - long compressedSize = sink.writePosition(); - int rawSize = Constants.NDP_NODE_COUNT * 4; - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertArrayEquals(slotOffsets, decoded, "Realistic page should round-trip correctly"); - - double ratio = (100.0 * compressedSize / rawSize); - System.out.printf("Realistic page (80%% full): raw=%d, compressed=%d, ratio=%.1f%%, savings=%.1f%%%n", rawSize, - compressedSize, ratio, 100 - ratio); - - // Expect at least 50% compression - assert compressedSize < rawSize * 0.5 : "Expected at least 50% compression but got " + ratio + "%"; - } - - @Test - public void testNullSinkThrows() { - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - assertThrows(NullPointerException.class, () -> SlotOffsetCodec.encode(null, slotOffsets, -1)); - } - - @Test - public void testNullOffsetsThrows() { - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - assertThrows(NullPointerException.class, () -> SlotOffsetCodec.encode(sink, null, -1)); - } - - @Test - public void testWrongArrayLengthThrows() { - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - int[] wrongSize = new int[100]; // Not 1024 - assertThrows(IllegalArgumentException.class, () -> SlotOffsetCodec.encode(sink, wrongSize, -1)); - } - - @Test - public void testNullSourceThrows() { - assertThrows(NullPointerException.class, () -> SlotOffsetCodec.decode(null)); - } - - @Test - public void testFirstSlotOnly() { - // Edge case: only slot 0 is populated - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - slotOffsets[0] = 0; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, 0); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - assertEquals(0, decoded[0]); - for (int i = 1; i < Constants.NDP_NODE_COUNT; i++) { - assertEquals(-1, decoded[i], "Slot " + i + " should be -1"); - } - } - - @Test - public void testLastSlotOnly() { - // Edge case: only the last slot is populated - int[] slotOffsets = new int[Constants.NDP_NODE_COUNT]; - Arrays.fill(slotOffsets, -1); - slotOffsets[Constants.NDP_NODE_COUNT - 1] = 65535; - - BytesOut sink = Bytes.elasticOffHeapByteBuffer(); - SlotOffsetCodec.encode(sink, slotOffsets, Constants.NDP_NODE_COUNT - 1); - - BytesIn source = sink.bytesForRead(); - int[] decoded = SlotOffsetCodec.decode(source); - - for (int i = 0; i < Constants.NDP_NODE_COUNT - 1; i++) { - assertEquals(-1, decoded[i], "Slot " + i + " should be -1"); - } - assertEquals(65535, decoded[Constants.NDP_NODE_COUNT - 1]); - } -} - - diff --git a/bundles/sirix-core/src/test/java/io/sirix/service/json/shredder/JsonShredderTest.java b/bundles/sirix-core/src/test/java/io/sirix/service/json/shredder/JsonShredderTest.java index cdb2b7b01..c8c223fc5 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/service/json/shredder/JsonShredderTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/service/json/shredder/JsonShredderTest.java @@ -10,6 +10,7 @@ import io.sirix.api.Axis; import io.sirix.api.Database; import io.sirix.api.json.JsonResourceSession; +import io.sirix.settings.Fixed; import io.sirix.axis.DescendantAxis; import io.sirix.axis.PostOrderAxis; import io.sirix.io.StorageType; @@ -212,7 +213,7 @@ public void testChicagoDescendantAxis() { // JVM flags: -XX:+UseShenandoahGC -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+AlwaysPreTouch // -XX:+UseLargePages -XX:+DisableExplicitGC -XX:+PrintCompilation -XX:ReservedCodeCacheSize=1000m // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:EliminateAllocationArraySizeLimit=1024 - @Disabled + //@Disabled @Test public void testShredderAndTraverseChicago() { logger.info("start"); diff --git a/bundles/sirix-core/src/test/java/io/sirix/settings/VersioningTest.java b/bundles/sirix-core/src/test/java/io/sirix/settings/VersioningTest.java index 27e4a60bc..f42f466ea 100644 --- a/bundles/sirix-core/src/test/java/io/sirix/settings/VersioningTest.java +++ b/bundles/sirix-core/src/test/java/io/sirix/settings/VersioningTest.java @@ -323,7 +323,8 @@ public void test() { assertEquals((Constants.NDP_NODE_COUNT * 10) - 1, wtx.getNodeKey()); try (final XmlNodeReadOnlyTrx rtx = manager.beginNodeReadOnlyTrx()) { for (int i = 0; i < Constants.NDP_NODE_COUNT - 1; i++) { - assertTrue(rtx.moveToFirstChild()); + final boolean moved = rtx.moveToFirstChild(); + assertTrue(moved); } move(rtx); move(rtx);