1 /*******************************************************************************
2 * Copyright (c) 2017 École Polytechnique de Montréal
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.historytree
;
12 import java
.io
.IOException
;
13 import java
.io
.PrintStream
;
14 import java
.nio
.ByteBuffer
;
15 import java
.nio
.ByteOrder
;
16 import java
.nio
.channels
.FileChannel
;
17 import java
.util
.ArrayList
;
18 import java
.util
.Arrays
;
19 import java
.util
.Collection
;
20 import java
.util
.Collections
;
21 import java
.util
.Comparator
;
22 import java
.util
.List
;
23 import java
.util
.Objects
;
24 import java
.util
.concurrent
.locks
.ReentrantReadWriteLock
;
25 import java
.util
.function
.Predicate
;
26 import java
.util
.stream
.Collectors
;
28 import org
.eclipse
.jdt
.annotation
.NonNull
;
29 import org
.eclipse
.jdt
.annotation
.Nullable
;
30 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.condition
.RangeCondition
;
31 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.exceptions
.RangeException
;
32 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.historytree
.AbstractHistoryTree
.IHTNodeFactory
;
33 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.interval
.IHTInterval
;
34 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.interval
.IHTIntervalReader
;
35 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.serialization
.ISafeByteBufferReader
;
36 import org
.eclipse
.tracecompass
.internal
.provisional
.datastore
.core
.serialization
.SafeByteBufferFactory
;
38 import com
.google
.common
.annotations
.VisibleForTesting
;
39 import com
.google
.common
.collect
.ImmutableList
;
40 import com
.google
.common
.collect
.Iterables
;
43 * The base class for all the types of nodes that go in the History Tree.
45 * @author Alexandre Montplaisir
46 * @author Geneviève Bastien
48 * The type of objects that will be saved in the tree
50 public class HTNode
<E
extends IHTInterval
> implements IHTNode
<E
> {
52 // ------------------------------------------------------------------------
54 // ------------------------------------------------------------------------
57 * Size of the basic node header. This is backward-compatible with previous
58 * state sytem history trees
61 * 1 - byte (the type of node)
62 * 16 - 2x long (start time, end time)
63 * 16 - 3x int (seq number, parent seq number, intervalcount)
64 * 1 - byte (done or not)
68 public static final int COMMON_HEADER_SIZE
= Byte
.BYTES
73 // ------------------------------------------------------------------------
75 // ------------------------------------------------------------------------
78 * Default implementation of the interval comparator, which sorts first by
79 * end times, then by start times
81 private final Comparator
<E
> fDefaultIntervalComparator
= Comparator
82 .comparingLong(E
::getEnd
)
83 .thenComparingLong(E
::getStart
);
85 /* Node configuration, defined by the history tree */
86 private final int fBlockSize
;
87 private final int fMaxChildren
;
89 /* Time range of this node */
90 private final long fNodeStart
;
91 private long fNodeEnd
;
93 /* Sequence number = position in the node section of the file */
94 private final int fSequenceNumber
;
95 private int fParentSequenceNumber
; /* = -1 if this node is the root node */
97 /* Sum of bytes of all objects in the node */
98 private int fSizeOfContentSection
;
101 * True if this node was saved on disk (meaning its end time is now fixed)
103 private volatile boolean fIsOnDisk
;
105 /* List containing all the intervals contained in this node */
106 private final List
<E
> fIntervals
;
108 /* Lock used to protect the accesses to objects, nodeEnd and such */
109 private final ReentrantReadWriteLock fRwl
= new ReentrantReadWriteLock(false);
111 /* Object containing extra data for core nodes */
112 private final @Nullable CoreNodeData fExtraData
;
115 * A class that encapsulates data about children of this node. This class
116 * will be constructed by the core node and contains the extra header data,
117 * methods to read/write the header data, etc.
119 * This base class for CORE nodes just saves the children, ie their sequence
122 * @author Geneviève Bastien
124 protected static class CoreNodeData
{
126 /** Back-reference to the node class */
127 private final HTNode
<?
> fNode
;
130 * Seq. numbers of the children nodes (size = max number of nodes,
131 * determined by configuration)
133 private final int[] fChildren
;
135 /** Nb. of children this node has */
136 private int fNbChildren
;
142 * The node containing this extra data.
144 public CoreNodeData(HTNode
<?
> node
) {
148 * We instantiate the following array at full size right away, since
149 * we want to reserve that space in the node's header. "fNbChildren"
150 * will tell us how many relevant entries there are in those tables.
152 fChildren
= new int[fNode
.fMaxChildren
];
156 * Return this core data's full node. To be used by subclasses.
160 protected HTNode
<?
> getNode() {
165 * Read the specific header for this extra node data
168 * The byte buffer in which to read
170 protected void readSpecificHeader(ByteBuffer buffer
) {
171 fNode
.takeWriteLock();
173 int size
= fNode
.getMaxChildren();
175 fNbChildren
= buffer
.getInt();
177 for (int i
= 0; i
< fNbChildren
; i
++) {
178 fChildren
[i
] = buffer
.getInt();
180 for (int i
= fNbChildren
; i
< size
; i
++) {
184 fNode
.releaseWriteLock();
189 * Write the specific header for this extra node data
192 * The byte buffer in which to write
194 protected void writeSpecificHeader(ByteBuffer buffer
) {
195 fNode
.takeReadLock();
197 buffer
.putInt(fNbChildren
);
199 /* Write the "children's seq number" array */
200 for (int i
= 0; i
< fNbChildren
; i
++) {
201 buffer
.putInt(fChildren
[i
]);
203 for (int i
= fNbChildren
; i
< fNode
.getMaxChildren(); i
++) {
207 fNode
.releaseReadLock();
212 * Get the number of children
214 * @return The number of children
216 public int getNbChildren() {
217 fNode
.takeReadLock();
221 fNode
.releaseReadLock();
226 * Get the child sequence number at an index
229 * The index of the child to get
230 * @return The sequence number of the child node
232 public int getChild(int index
) {
233 fNode
.takeReadLock();
235 if (index
>= fNbChildren
) {
236 throw new IndexOutOfBoundsException("The child at index " + index
+ " does not exist"); //$NON-NLS-1$ //$NON-NLS-2$
238 return fChildren
[index
];
240 fNode
.releaseReadLock();
245 * Get the sequence number of the last child node of this one
247 * @return The sequence number of the last child
249 public int getLatestChild() {
250 fNode
.takeReadLock();
252 return fChildren
[fNbChildren
- 1];
254 fNode
.releaseReadLock();
259 * Add a new child to this node's data
262 * The child node to add
264 public void linkNewChild(IHTNode
<?
> childNode
) {
265 fNode
.takeWriteLock();
267 if (fNbChildren
>= fNode
.getMaxChildren()) {
268 throw new IllegalStateException("Asked to link another child but parent already has maximum number of children"); //$NON-NLS-1$
271 fChildren
[fNbChildren
] = childNode
.getSequenceNumber();
275 fNode
.releaseWriteLock();
280 * Get the size of the extra header space necessary for this node's
283 * @return The extra header size
285 protected int getSpecificHeaderSize() {
286 int maxChildren
= fNode
.getMaxChildren();
287 int specificSize
= Integer
.BYTES
/* 1x int (nbChildren) */
289 /* MAX_NB * int ('children' table) */
290 + Integer
.BYTES
* maxChildren
;
296 public String
toString() {
297 /* Only used for debugging, shouldn't be externalized */
298 return String
.format("Core Node, %d children %s", //$NON-NLS-1$
299 fNbChildren
, Arrays
.toString(Arrays
.copyOf(fChildren
, fNbChildren
)));
303 * Inner method to select the sequence numbers for the children of the
304 * current node that intersect the given timestamp. Useful for moving
307 * @param timeCondition
308 * The time-based RangeCondition to choose which children
310 * @return Collection of sequence numbers of the child nodes that
311 * intersect t, non-null empty collection if this is a Leaf Node
313 public final Collection
<Integer
> selectNextChildren(RangeCondition
<Long
> timeCondition
) {
314 fNode
.takeReadLock();
316 return selectNextIndices(timeCondition
).stream()
317 .map(i
-> fChildren
[i
])
318 .collect(Collectors
.toList());
320 fNode
.releaseReadLock();
325 * Inner method to select the indices of the children of the current
326 * node that intersect the given timestamp. Useful for moving down the
329 * This is the method that children implementations of this node should
330 * override. They may call
331 * <code>super.selectNextIndices(timeCondition)</code> to get access to
332 * the subset of indices that match the parent's condition and add their
333 * own filters. When this method is called a read-lock is already taken
336 * @param timeCondition
337 * The time-based RangeCondition to choose which children
339 * @return Collection of the indices of the child nodes that intersect
342 protected Collection
<Integer
> selectNextIndices(RangeCondition
<Long
> timeCondition
) {
343 /* By default, all children are returned */
344 List
<Integer
> childList
= new ArrayList
<>();
345 for (int i
= 0; i
< fNbChildren
; i
++) {
353 public int hashCode() {
355 * Do not consider "fNode", since the node's equals/hashCode already
358 return Objects
.hash(fNbChildren
, fChildren
);
362 public boolean equals(@Nullable Object obj
) {
366 if (obj
.getClass() != getClass()) {
369 CoreNodeData other
= (CoreNodeData
) obj
;
370 return (fNbChildren
== other
.fNbChildren
371 && Arrays
.equals(fChildren
, other
.fChildren
));
380 * The type of this node
382 * The size (in bytes) of a serialized node on disk
384 * The maximum allowed number of children per node
386 * The (unique) sequence number assigned to this particular node
387 * @param parentSeqNumber
388 * The sequence number of this node's parent node
390 * The earliest timestamp stored in this node
392 public HTNode(NodeType type
, int blockSize
, int maxChildren
, int seqNumber
,
393 int parentSeqNumber
, long start
) {
394 fBlockSize
= blockSize
;
395 fMaxChildren
= maxChildren
;
398 fSequenceNumber
= seqNumber
;
399 fParentSequenceNumber
= parentSeqNumber
;
401 fSizeOfContentSection
= 0;
403 fIntervals
= new ArrayList
<>();
405 fExtraData
= createNodeExtraData(type
);
409 * Reader factory method. Build a Node object (of the right type) by reading
410 * a block in the file.
413 * The size (in bytes) of a serialized node on disk
415 * The maximum allowed number of children per node
417 * FileChannel to the history file, ALREADY SEEKED at the start
419 * @param objectReader
420 * The reader to read serialized node objects
422 * The factory to create the nodes for this tree
423 * @return The node object
424 * @throws IOException
425 * If there was an error reading from the file channel
427 public static final <E
extends IHTInterval
, N
extends HTNode
<E
>> N
readNode(
431 IHTIntervalReader
<E
> objectReader
,
432 IHTNodeFactory
<E
, N
> nodeFactory
) throws IOException
{
436 ByteBuffer buffer
= ByteBuffer
.allocate(blockSize
);
437 buffer
.order(ByteOrder
.LITTLE_ENDIAN
);
439 int res
= fc
.read(buffer
);
440 if (res
!= blockSize
) {
441 throw new IOException("The block for the HTNode is not the right size: " + res
); //$NON-NLS-1$
445 /* Read the common header part */
446 byte typeByte
= buffer
.get();
447 NodeType type
= NodeType
.fromByte(typeByte
);
448 long start
= buffer
.getLong();
449 long end
= buffer
.getLong();
450 int seqNb
= buffer
.getInt();
451 int parentSeqNb
= buffer
.getInt();
452 int intervalCount
= buffer
.getInt();
453 buffer
.get(); // TODO Used to be "isDone", to be removed from the header
455 /* Now the rest of the header depends on the node type */
459 newNode
= nodeFactory
.createNode(type
, blockSize
, maxChildren
, seqNb
, parentSeqNb
, start
);
460 newNode
.readSpecificHeader(buffer
);
464 /* Unrecognized node type */
465 throw new IOException();
469 * At this point, we should be done reading the header and 'buffer'
470 * should only have the intervals left
472 ISafeByteBufferReader readBuffer
= SafeByteBufferFactory
.wrapReader(buffer
, res
- buffer
.position());
473 for (int i
= 0; i
< intervalCount
; i
++) {
474 E interval
= objectReader
.readInterval(readBuffer
);
475 newNode
.add(interval
);
478 /* Assign the node's other information we have read previously */
479 newNode
.setNodeEnd(end
);
486 * Create a node's extra data object for the given node type
490 * @return The node's extra data object, or <code>null</code> if there is
493 protected @Nullable CoreNodeData
createNodeExtraData(NodeType type
) {
494 if (type
== NodeType
.CORE
) {
495 return new CoreNodeData(this);
501 public final void writeSelf(FileChannel fc
) throws IOException
{
503 * Yes, we are taking the *read* lock here, because we are reading the
504 * information in the node to write it to disk.
506 fRwl
.readLock().lock();
508 final int blockSize
= getBlockSize();
510 ByteBuffer buffer
= ByteBuffer
.allocate(blockSize
);
511 buffer
.order(ByteOrder
.LITTLE_ENDIAN
);
514 /* Write the common header part */
515 buffer
.put(getNodeType().toByte());
516 buffer
.putLong(fNodeStart
);
517 buffer
.putLong(fNodeEnd
);
518 buffer
.putInt(fSequenceNumber
);
519 buffer
.putInt(fParentSequenceNumber
);
520 buffer
.putInt(fIntervals
.size());
521 buffer
.put((byte) 1); // TODO Used to be "isDone", to be removed
524 /* Now call the inner method to write the specific header part */
525 writeSpecificHeader(buffer
);
527 /* Back to us, we write the intervals */
528 fIntervals
.forEach(i
-> i
.writeSegment(SafeByteBufferFactory
.wrapWriter(buffer
, i
.getSizeOnDisk())));
529 if (blockSize
- buffer
.position() != getNodeFreeSpace()) {
530 throw new IllegalStateException("Wrong free space: Actual: " + (blockSize
- buffer
.position()) + ", Expected: " + getNodeFreeSpace()); //$NON-NLS-1$ //$NON-NLS-2$
533 * Fill the rest with zeros
535 while (buffer
.position() < blockSize
) {
536 buffer
.put((byte) 0);
539 /* Finally, write everything in the Buffer to disk */
541 int res
= fc
.write(buffer
);
542 if (res
!= blockSize
) {
543 throw new IllegalStateException("Wrong size of block written: Actual: " + res
+ ", Expected: " + blockSize
); //$NON-NLS-1$ //$NON-NLS-2$
547 fRwl
.readLock().unlock();
552 // ------------------------------------------------------------------------
554 // ------------------------------------------------------------------------
557 * Get this node's block size.
559 * @return The block size
561 protected final int getBlockSize() {
566 * Get this node's maximum amount of children.
568 * @return The maximum amount of children
570 protected final int getMaxChildren() {
575 * Get the interval comparator. Intervals will always be stored sorted
576 * according to this comparator. This can be used by insertion or retrieval
579 * Sub-classes may override this to change or specify the interval
582 * @return The way intervals are to be sorted in this node
584 protected Comparator
<E
> getIntervalComparator() {
585 return fDefaultIntervalComparator
;
589 public long getNodeStart() {
594 public long getNodeEnd() {
598 return Long
.MAX_VALUE
;
602 public int getSequenceNumber() {
603 return fSequenceNumber
;
607 public int getParentSequenceNumber() {
608 return fParentSequenceNumber
;
612 public void setParentSequenceNumber(int newParent
) {
613 fParentSequenceNumber
= newParent
;
617 public boolean isOnDisk() {
622 * Get the node's extra data.
624 * @return The node extra data
626 protected @Nullable CoreNodeData
getCoreNodeData() {
631 * Get the list of objects in this node. This list is immutable. All objects
632 * must be inserted through the {@link #add(IHTInterval)} method
634 * @return The list of intervals in this node
636 protected List
<E
> getIntervals() {
637 return ImmutableList
.copyOf(fIntervals
);
641 * Set this node's end time. Called by the reader factory.
644 * The end time of the node
646 protected void setNodeEnd(long end
) {
651 * Set this node to be on disk. Called by the reader factory.
653 protected void setOnDisk() {
658 public void add(E newInterval
) {
659 fRwl
.writeLock().lock();
662 * Just in case, should be checked before even calling this function
664 int objSize
= newInterval
.getSizeOnDisk();
665 if (objSize
> getNodeFreeSpace()) {
666 throw new IllegalArgumentException("The interval to insert (" + objSize
+ ") is larger than available space (" + getNodeFreeSpace() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
669 int insertPoint
= Collections
.binarySearch(fIntervals
, newInterval
, getIntervalComparator());
670 insertPoint
= (insertPoint
>= 0 ? insertPoint
: -insertPoint
- 1);
671 fIntervals
.add(insertPoint
, newInterval
);
673 fSizeOfContentSection
+= objSize
;
676 fRwl
.writeLock().unlock();
681 public Iterable
<E
> getMatchingIntervals(RangeCondition
<Long
> timeCondition
,
682 Predicate
<E
> extraPredicate
) {
684 // TODO Use getIntervalComparator() to restrict the dataset further
685 // TODO Benchmark using/returning streams instead of iterables
688 @NonNull Iterable
<E
> ret
= fIntervals
;
689 ret
= Iterables
.filter(ret
, interval
-> timeCondition
.intersects(interval
.getStart(), interval
.getEnd()));
690 ret
= Iterables
.filter(ret
, interval
-> extraPredicate
.test(interval
));
696 return fIntervals
.stream()
697 .filter(interval
-> timeCondition
.intersects(interval
.getStart(), interval
.getEnd()))
698 .filter(extraPredicate
)
700 * Because this class works with read locks, we can't
701 * return a lazy stream unfortunately. Room for improvement?
703 .collect(Collectors
.toList());
711 public void closeThisNode(long endtime
) {
712 fRwl
.writeLock().lock();
715 * FIXME: was assert (endtime >= fNodeStart); but that exception is
716 * reached with an empty node that has start time endtime + 1
718 // if (endtime < fNodeStart) {
719 // throw new IllegalArgumentException("Endtime " + endtime + "
720 // cannot be lower than start time " + fNodeStart);
723 if (!fIntervals
.isEmpty()) {
725 * Make sure there are no intervals in this node with their
726 * EndTime > the one requested. Only need to check the last one
727 * since they are sorted
729 if (endtime
< fIntervals
.get(fIntervals
.size() - 1).getEnd()) {
730 throw new IllegalArgumentException("Closing end time should be greater than or equal to the end time of the objects of this node"); //$NON-NLS-1$
736 fRwl
.writeLock().unlock();
741 public final int getTotalHeaderSize() {
742 return COMMON_HEADER_SIZE
+ getSpecificHeaderSize();
746 * @return The offset, within the node, where the Data section ends
748 private int getDataSectionEndOffset() {
749 return getTotalHeaderSize() + fSizeOfContentSection
;
753 public int getNodeFreeSpace() {
754 fRwl
.readLock().lock();
756 int ret
= getBlockSize() - getDataSectionEndOffset();
759 fRwl
.readLock().unlock();
764 public long getNodeUsagePercent() {
765 fRwl
.readLock().lock();
767 final int blockSize
= getBlockSize();
768 float freePercent
= (float) getNodeFreeSpace()
769 / (float) (blockSize
- getTotalHeaderSize())
771 return (long) (100L - freePercent
);
774 fRwl
.readLock().unlock();
779 public NodeType
getNodeType() {
781 CoreNodeData extraData
= getCoreNodeData();
782 if (extraData
== null) {
783 return NodeType
.LEAF
;
785 return NodeType
.CORE
;
789 * Return the specific header size of this node. This means the size
790 * occupied by the type-specific section of the header (not counting the
793 * @return The specific header size
795 protected int getSpecificHeaderSize() {
796 CoreNodeData extraData
= fExtraData
;
797 if (extraData
!= null) {
798 return extraData
.getSpecificHeaderSize();
804 * Read the specific header for this node
807 * The buffer to read from
809 protected void readSpecificHeader(ByteBuffer buffer
) {
810 CoreNodeData extraData
= fExtraData
;
811 if (extraData
!= null) {
812 extraData
.readSpecificHeader(buffer
);
817 * Write the type-specific part of the header in a byte buffer.
820 * The buffer to write to. It should already be at the correct
823 protected void writeSpecificHeader(ByteBuffer buffer
) {
824 CoreNodeData extraData
= fExtraData
;
825 if (extraData
!= null) {
826 extraData
.writeSpecificHeader(buffer
);
831 * Node-type-specific toString method. Used for debugging.
833 * @return A string representing the node
835 protected String
toStringSpecific() {
836 return ""; //$NON-NLS-1$
840 public boolean isEmpty() {
841 return fIntervals
.isEmpty();
844 // -------------------------------------------
846 // -------------------------------------------
849 public int getNbChildren() {
850 CoreNodeData extraData
= fExtraData
;
851 if (extraData
!= null) {
852 return extraData
.getNbChildren();
854 return IHTNode
.super.getNbChildren();
858 public int getChild(int index
) {
859 CoreNodeData extraData
= fExtraData
;
860 if (extraData
!= null) {
861 return extraData
.getChild(index
);
863 return IHTNode
.super.getChild(index
);
867 public int getLatestChild() {
868 CoreNodeData extraData
= fExtraData
;
869 if (extraData
!= null) {
870 return extraData
.getLatestChild();
872 return IHTNode
.super.getLatestChild();
876 public void linkNewChild(@NonNull IHTNode
<E
> childNode
) {
877 CoreNodeData extraData
= fExtraData
;
878 if (extraData
!= null) {
879 extraData
.linkNewChild(childNode
);
882 IHTNode
.super.linkNewChild(childNode
);
886 public Collection
<Integer
> selectNextChildren(RangeCondition
<Long
> timeCondition
)
887 throws RangeException
{
888 CoreNodeData extraData
= fExtraData
;
889 if (extraData
!= null) {
890 return extraData
.selectNextChildren(timeCondition
);
892 return IHTNode
.super.selectNextChildren(timeCondition
);
895 // -----------------------------------------
897 // -----------------------------------------
900 * Takes a read lock on the fields of this class. Each call to this method
901 * should be followed by a {@link HTNode#releaseReadLock()}, in a
904 protected void takeReadLock() {
905 fRwl
.readLock().lock();
909 * Releases a read lock on the fields of this class. A call to this method
910 * should have been preceded by a call to {@link HTNode#takeReadLock()}
912 protected void releaseReadLock() {
913 fRwl
.readLock().unlock();
917 * Takes a write lock on the fields of this class. Each call to this method
918 * should be followed by a {@link HTNode#releaseWriteLock()}, in a
921 protected void takeWriteLock() {
922 fRwl
.writeLock().lock();
926 * Releases a write lock on the fields of this class. A call to this method
927 * should have been preceded by a call to {@link HTNode#takeWriteLock()}
929 protected void releaseWriteLock() {
930 fRwl
.writeLock().unlock();
933 // -----------------------------------------
935 // -----------------------------------------
937 @SuppressWarnings("nls")
939 public String
toString() {
940 /* Only used for debugging, shouldn't be externalized */
941 return String
.format("Node #%d, %s, %s, %d intervals (%d%% used), [%d - %s]",
943 (fParentSequenceNumber
== -1) ?
"Root" : "Parent #" + fParentSequenceNumber
,
946 getNodeUsagePercent(),
948 (fIsOnDisk
|| fNodeEnd
!= 0) ? fNodeEnd
: "...");
952 public int hashCode() {
953 return Objects
.hash(fBlockSize
, fMaxChildren
, fNodeStart
, fNodeEnd
, fSequenceNumber
, fParentSequenceNumber
, fExtraData
);
957 public boolean equals(@Nullable Object obj
) {
961 if (obj
.getClass() != getClass()) {
964 HTNode
<?
extends IHTInterval
> other
= (HTNode
<?
>) obj
;
965 return (fBlockSize
== other
.fBlockSize
&&
966 fMaxChildren
== other
.fMaxChildren
&&
967 fNodeStart
== other
.fNodeStart
&&
968 fNodeEnd
== other
.fNodeEnd
&&
969 fSequenceNumber
== other
.fSequenceNumber
&&
970 fParentSequenceNumber
== other
.fParentSequenceNumber
&&
971 Objects
.equals(fExtraData
, other
.fExtraData
));
974 // -----------------------------------------
975 // Test-specific methods
976 // -----------------------------------------
979 * Print all current intervals into the given writer.
982 * The writer to write to
985 public void debugPrintIntervals(PrintStream writer
) {
986 getIntervals().forEach(writer
::println
);