| /******************************************************************************* |
| * Copyright (c) 2012, 2019 Ericsson, École Polytechnique de Montréal |
| * Copyright (c) 2010, 2011 Alexandre Montplaisir <alexandre.montplaisir@gmail.com> |
| * |
| * All rights reserved. This program and the accompanying materials are |
| * made available under the terms of the Eclipse Public License 2.0 which |
| * accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Alexandre Montplaisir - Initial API and implementation |
| * Florian Wininger - Allow to change the size of a interval |
| * Patrick Tasse - Add message to exceptions |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.statesystem.core.backend.historytree; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.Charset; |
| import java.util.Objects; |
| |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.tracecompass.datastore.core.encoding.HTVarInt; |
| import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferReader; |
| import org.eclipse.tracecompass.datastore.core.serialization.ISafeByteBufferWriter; |
| import org.eclipse.tracecompass.datastore.core.serialization.SafeByteBufferFactory; |
| import org.eclipse.tracecompass.internal.provisional.statesystem.core.statevalue.CustomStateValue; |
| import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException; |
| import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval; |
| import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue; |
| import org.eclipse.tracecompass.statesystem.core.statevalue.TmfStateValue; |
| |
| /** |
| * The interval component, which will be contained in a node of the History |
| * Tree. |
| * |
| * @author Alexandre Montplaisir |
| */ |
| public final class HTInterval implements ITmfStateInterval { |
| |
| private static final Charset CHARSET = Charset.forName("UTF-8"); //$NON-NLS-1$ |
| |
| private static final String errMsg = "Invalid interval data. Maybe your file is corrupt?"; //$NON-NLS-1$ |
| |
| /* 'Byte' equivalent for state values types */ |
| private static final byte TYPE_NULL = -1; |
| private static final byte TYPE_INTEGER = 0; |
| private static final byte TYPE_STRING = 1; |
| private static final byte TYPE_LONG = 2; |
| private static final byte TYPE_DOUBLE = 3; |
| private static final byte TYPE_CUSTOM = 20; |
| |
| private final long fStart; |
| private final long fDuration; |
| private final int fAttribute; |
| private final @Nullable Object fStateValue; |
| |
| /** Number of bytes used by this interval when it is written to disk */ |
| private int fSizeOnDisk; |
| |
| /** |
| * Standard constructor |
| * |
| * @param intervalStart |
| * Start time of the interval |
| * @param intervalEnd |
| * End time of the interval |
| * @param attribute |
| * Attribute (quark) to which the state represented by this |
| * interval belongs |
| * @param value |
| * State value represented by this interval |
| * @throws TimeRangeException |
| * If the start time or end time are invalid |
| */ |
| public HTInterval(long intervalStart, long intervalEnd, int attribute, |
| Object value) throws TimeRangeException { |
| if (intervalStart > intervalEnd) { |
| throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| fStart = intervalStart; |
| fDuration = intervalEnd - intervalStart; |
| fAttribute = attribute; |
| fStateValue = (value instanceof TmfStateValue) ? ((ITmfStateValue) value).unboxValue() : value; |
| fSizeOnDisk = computeSizeOnDisk(fStateValue, 0); |
| } |
| |
| /** |
| * Compute how much space (in bytes) an interval will take in its serialized |
| * form on disk. This is dependent on its state value and the start time of |
| * the node it is linked to. |
| * |
| * @param stateValue |
| * The state value |
| * @param nodeStart |
| * The start time of the node the interval is linked to |
| * @return The computed size on disk (with HTVarInt encoding) |
| */ |
| private int computeSizeOnDisk(Object stateValue, long nodeStart) { |
| /* |
| * Minimum size is a 2x bytes (start), 2x bytes (duration), 1x int (attribute) and 1x |
| * byte (value type). |
| */ |
| int minSize = HTVarInt.getEncodedLengthLong(fStart - nodeStart) + HTVarInt.getEncodedLengthLong(fDuration) + Integer.BYTES + Byte.BYTES; |
| |
| if (stateValue == null) { |
| return minSize; |
| } else if (stateValue instanceof Integer) { |
| return (minSize + Integer.BYTES); |
| } else if (stateValue instanceof Long) { |
| return (minSize + Long.BYTES); |
| } else if (stateValue instanceof Double) { |
| return (minSize + Double.BYTES); |
| } else if (stateValue instanceof String) { |
| String str = (String) stateValue; |
| int strLength = str.getBytes(CHARSET).length; |
| |
| if (strLength > Short.MAX_VALUE) { |
| throw new IllegalArgumentException("String is too long to be stored in state system: " + str); //$NON-NLS-1$ |
| } |
| |
| /* |
| * String's length + 3 (2 bytes for size, 1 byte for \0 at the end) |
| */ |
| return (minSize + strLength + 3); |
| } else if (stateValue instanceof CustomStateValue) { |
| /* Length of serialized value (short) + state value */ |
| return (minSize + Short.BYTES + ((CustomStateValue) stateValue).getSerializedSize()); |
| } |
| /* |
| * It's very important that we know how to write the state value in the |
| * file!! |
| */ |
| throw new IllegalStateException(); |
| } |
| |
| /** |
| * "Faster" constructor for inner use only. When we build an interval when |
| * reading it from disk (with {@link #readFrom}), we already know the size |
| * of the strings entry, so there is no need to call |
| * {@link #computeStringsEntrySize()} and do an extra copy. |
| */ |
| private HTInterval(long intervalStart, long intervalEnd, int attribute, |
| Object value, int size) throws TimeRangeException { |
| if (intervalStart > intervalEnd) { |
| throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| fStart = intervalStart; |
| fDuration = intervalEnd - intervalStart; |
| fAttribute = attribute; |
| fStateValue = value; |
| fSizeOnDisk = size; |
| } |
| |
| /** |
| * Reader factory method. Builds the interval using an already-allocated |
| * ByteBuffer, which normally comes from a NIO FileChannel. |
| * |
| * The interval is just a start, end, attribute and value, this is the |
| * layout of the HTInterval on disk |
| * <ul> |
| * <li>start (2-9 bytes)</li> |
| * <li>end (2-9 bytes)</li> |
| * <li>attribute (4 bytes)</li> |
| * <li>sv type (1 byte)</li> |
| * <li>sv ( 0 bytes for null, 4 for int , 8 for long and double, and the |
| * length of the string +2 for strings (it's variable))</li> |
| * </ul> |
| * |
| * @param buffer |
| * The ByteBuffer from which to read the information |
| * @param nodeStart |
| * The start time of the node this interval is linked to |
| * @return The interval object |
| * @throws IOException |
| * If there was an error reading from the buffer |
| */ |
| public static final HTInterval readFrom(ByteBuffer buffer, long nodeStart) throws IOException { |
| Object value; |
| |
| int posStart = buffer.position(); |
| /* Read the Data Section entry */ |
| long intervalStart = HTVarInt.readLong(buffer) + nodeStart; |
| long intervalEnd = HTVarInt.readLong(buffer) + intervalStart; |
| int attribute = buffer.getInt(); |
| |
| /* Read the 'type' of the value, then react accordingly */ |
| byte valueType = buffer.get(); |
| switch (valueType) { |
| |
| case TYPE_NULL: |
| value = null; |
| break; |
| |
| case TYPE_INTEGER: |
| value = buffer.getInt(); |
| break; |
| |
| case TYPE_STRING: { |
| /* the first short = the size to read */ |
| int valueSize = buffer.getShort(); |
| |
| byte[] array = new byte[valueSize]; |
| buffer.get(array); |
| value = new String(array, CHARSET); |
| |
| /* Confirm the 0'ed byte at the end */ |
| byte res = buffer.get(); |
| if (res != 0) { |
| throw new IOException(errMsg); |
| } |
| break; |
| } |
| |
| case TYPE_LONG: |
| /* Go read the matching entry in the Strings section of the block */ |
| value = buffer.getLong(); |
| break; |
| |
| case TYPE_DOUBLE: |
| /* Go read the matching entry in the Strings section of the block */ |
| value = buffer.getDouble(); |
| break; |
| |
| case TYPE_CUSTOM: { |
| short valueSize = buffer.getShort(); |
| ISafeByteBufferReader safeBuffer = SafeByteBufferFactory.wrapReader(buffer, valueSize); |
| value = CustomStateValue.readSerializedValue(safeBuffer); |
| break; |
| } |
| default: |
| /* Unknown data, better to not make anything up... */ |
| throw new IOException(errMsg); |
| } |
| |
| try { |
| return new HTInterval(intervalStart, intervalEnd, attribute, value, buffer.position() - posStart); |
| } catch (TimeRangeException e) { |
| throw new IOException(errMsg); |
| } |
| } |
| |
| /** |
| * Antagonist of the previous constructor, write the Data entry |
| * corresponding to this interval in a ByteBuffer (mapped to a block in the |
| * history-file, hopefully) |
| * |
| * The interval is just a start, end, attribute and value, this is the |
| * layout of the HTInterval on disk |
| * <ul> |
| * <li>start (2-9 bytes)</li> |
| * <li>end (2-9 bytes)</li> |
| * <li>attribute (4 bytes)</li> |
| * <li>sv type (1 byte)</li> |
| * <li>sv ( 0 bytes for null, 4 for int , 8 for long and double, and the |
| * length of the string +2 for strings (it's variable))</li> |
| * </ul> |
| * |
| * @param buffer |
| * The already-allocated ByteBuffer corresponding to a SHT Node |
| */ |
| public void writeInterval(ByteBuffer buffer, long nodeStart) { |
| HTVarInt.writeLong(buffer, fStart - nodeStart); |
| HTVarInt.writeLong(buffer, fDuration); |
| buffer.putInt(fAttribute); |
| |
| if (fStateValue != null) { |
| @NonNull Object value = fStateValue; |
| if (value instanceof Integer) { |
| buffer.put(TYPE_INTEGER); |
| buffer.putInt((int) value); |
| } else if (value instanceof Long) { |
| buffer.put(TYPE_LONG); |
| buffer.putLong((long) value); |
| } else if (value instanceof Double) { |
| buffer.put(TYPE_DOUBLE); |
| buffer.putDouble((double) value); |
| } else if (value instanceof String) { |
| buffer.put(TYPE_STRING); |
| String string = (String) value; |
| byte[] strArray = string.getBytes(CHARSET); |
| |
| /* |
| * Write the Strings entry (1st byte = size, then the bytes, then the 0). We |
| * have checked the string length at the constructor. |
| */ |
| buffer.putShort((short) strArray.length); |
| buffer.put(strArray); |
| buffer.put((byte) 0); |
| } else if (value instanceof CustomStateValue) { |
| buffer.put(TYPE_CUSTOM); |
| int size = ((CustomStateValue) value).getSerializedSize(); |
| buffer.putShort((short) size); |
| ISafeByteBufferWriter safeBuffer = SafeByteBufferFactory.wrapWriter(buffer, size); |
| ((CustomStateValue) value).serialize(safeBuffer); |
| } else { |
| throw new IllegalStateException("Type: " + value.getClass() + " is not implemented in the state system"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } else { |
| buffer.put(TYPE_NULL); |
| } |
| } |
| |
| @Override |
| public long getStartTime() { |
| return fStart; |
| } |
| |
| @Override |
| public long getEndTime() { |
| return fStart + fDuration; |
| } |
| |
| @Override |
| public int getAttribute() { |
| return fAttribute; |
| } |
| |
| @Override |
| public ITmfStateValue getStateValue() { |
| return TmfStateValue.newValue(fStateValue); |
| } |
| |
| @Override |
| public Object getValue() { |
| return fStateValue; |
| } |
| |
| @Override |
| public boolean intersects(long timestamp) { |
| return (fStart <= timestamp && (fStart + fDuration) >= timestamp); |
| } |
| |
| /** |
| * Total serialized size of this interval |
| * |
| * @return The interval size |
| */ |
| public int getSizeOnDisk() { |
| return fSizeOnDisk; |
| } |
| |
| /** |
| * Computes serialized size of this interval with linked node start time |
| * |
| * @param nodeStart |
| * The start time of the node the interval is linked to |
| * @return The size of the interval on disk using the HTVarInt encoding |
| */ |
| public int getSizeOnDisk(long nodeStart) { |
| return computeSizeOnDisk(fStateValue, nodeStart); |
| } |
| |
| /** |
| * @param sizeOnDisk the sizeOnDisk to set |
| */ |
| public void setSizeOnDisk(int sizeOnDisk) { |
| fSizeOnDisk = sizeOnDisk; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| HTInterval other = (HTInterval) obj; |
| return (fStart == other.fStart && |
| fDuration == other.fDuration && |
| fAttribute == other.fAttribute && |
| Objects.equals(fStateValue, other.fStateValue)); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(fStart, fDuration, fAttribute, fStateValue); |
| } |
| |
| @Override |
| public String toString() { |
| /* Only for debug, should not be externalized */ |
| StringBuilder sb = new StringBuilder(); |
| sb.append('['); |
| sb.append(fStart); |
| sb.append(", "); //$NON-NLS-1$ |
| sb.append(fStart + fDuration); |
| sb.append(']'); |
| |
| sb.append(", attribute = "); //$NON-NLS-1$ |
| sb.append(fAttribute); |
| |
| sb.append(", value = "); //$NON-NLS-1$ |
| sb.append(String.valueOf(fStateValue)); |
| |
| return sb.toString(); |
| } |
| } |