/******************************************************************************* * Copyright (c) 2012, 2015 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 v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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.tracecompass.internal.provisional.datastore.core.serialization.ISafeByteBufferReader; import org.eclipse.tracecompass.internal.provisional.datastore.core.serialization.ISafeByteBufferWriter; import org.eclipse.tracecompass.internal.provisional.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 start; private final long end; private final int attribute; private final @NonNull TmfStateValue sv; /** Number of bytes used by this interval when it is written to disk */ private final 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, @NonNull TmfStateValue value) throws TimeRangeException { if (intervalStart > intervalEnd) { throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$ } this.start = intervalStart; this.end = intervalEnd; this.attribute = attribute; this.sv = value; this.fSizeOnDisk = computeSizeOnDisk(sv); } /** * Compute how much space (in bytes) an interval will take in its serialized * form on disk. This is dependent on its state value. */ private static int computeSizeOnDisk(ITmfStateValue sv) { /* * Minimum size is 2x long (start and end), 1x int (attribute) and 1x * byte (value type). */ int minSize = Long.BYTES + Long.BYTES + Integer.BYTES + Byte.BYTES; switch (sv.getType()) { case NULL: return minSize; case INTEGER: return (minSize + Integer.BYTES); case LONG: return (minSize + Long.BYTES); case DOUBLE: return (minSize + Double.BYTES); case STRING: String str = sv.unboxStr(); 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); case CUSTOM: /* Length of serialized value (short) + state value */ return (minSize + Short.BYTES + ((CustomStateValue) sv).getSerializedSize()); default: /* * 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, @NonNull TmfStateValue value, int size) throws TimeRangeException { if (intervalStart > intervalEnd) { throw new TimeRangeException("Start:" + intervalStart + ", End:" + intervalEnd); //$NON-NLS-1$ //$NON-NLS-2$ } this.start = intervalStart; this.end = intervalEnd; this.attribute = attribute; this.sv = value; this.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 (8 bytes)</li> * <li>end (8 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 * @return The interval object * @throws IOException * If there was an error reading from the buffer */ public static final HTInterval readFrom(ByteBuffer buffer) throws IOException { TmfStateValue value; int posStart = buffer.position(); /* Read the Data Section entry */ long intervalStart = buffer.getLong(); long intervalEnd = buffer.getLong(); int attribute = buffer.getInt(); /* Read the 'type' of the value, then react accordingly */ byte valueType = buffer.get(); switch (valueType) { case TYPE_NULL: value = TmfStateValue.nullValue(); break; case TYPE_INTEGER: value = TmfStateValue.newValueInt(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 = TmfStateValue.newValueString(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 = TmfStateValue.newValueLong(buffer.getLong()); break; case TYPE_DOUBLE: /* Go read the matching entry in the Strings section of the block */ value = TmfStateValue.newValueDouble(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 (8 bytes)</li> * <li>end (8 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) { final byte byteFromType = getByteFromType(sv.getType()); buffer.putLong(start); buffer.putLong(end); buffer.putInt(attribute); buffer.put(byteFromType); switch (byteFromType) { case TYPE_NULL: break; case TYPE_INTEGER: buffer.putInt(sv.unboxInt()); break; case TYPE_STRING: { String string = sv.unboxStr(); 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); break; } case TYPE_LONG: buffer.putLong(sv.unboxLong()); break; case TYPE_DOUBLE: buffer.putDouble(sv.unboxDouble()); break; case TYPE_CUSTOM: { int size = ((CustomStateValue) sv).getSerializedSize(); buffer.putShort((short) size); ISafeByteBufferWriter safeBuffer = SafeByteBufferFactory.wrapWriter(buffer, size); ((CustomStateValue) sv).serialize(safeBuffer); break; } default: break; } } @Override public long getStartTime() { return start; } @Override public long getEndTime() { return end; } @Override public int getAttribute() { return attribute; } @Override public ITmfStateValue getStateValue() { return sv; } @Override public boolean intersects(long timestamp) { if (start <= timestamp) { if (end >= timestamp) { return true; } } return false; } /** * Total serialized size of this interval * * @return The interval size */ public int getSizeOnDisk() { return fSizeOnDisk; } @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 (start == other.start && end == other.end && attribute == other.attribute && sv.equals(other.sv)); } @Override public int hashCode() { return Objects.hash(start, end, attribute, sv); } @Override public String toString() { /* Only for debug, should not be externalized */ StringBuilder sb = new StringBuilder(); sb.append('['); sb.append(start); sb.append(", "); //$NON-NLS-1$ sb.append(end); sb.append(']'); sb.append(", attribute = "); //$NON-NLS-1$ sb.append(attribute); sb.append(", value = "); //$NON-NLS-1$ sb.append(sv.toString()); return sb.toString(); } /** * Here we determine how state values "types" are written in the 8-bit field * that indicates the value type in the file. */ private static byte getByteFromType(ITmfStateValue.Type type) { switch (type) { case NULL: return TYPE_NULL; case INTEGER: return TYPE_INTEGER; case STRING: return TYPE_STRING; case LONG: return TYPE_LONG; case DOUBLE: return TYPE_DOUBLE; case CUSTOM: return TYPE_CUSTOM; default: /* Should not happen if the switch is fully covered */ throw new IllegalStateException(); } } }