| /******************************************************************************* |
| * Copyright (c) 2015 École Polytechnique de Montréal |
| * |
| * 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: |
| * Francis Giraldeau - Initial implementation and API |
| * Geneviève Bastien - Fixes and improvements |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.tmf.core.synchronization; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.math.BigDecimal; |
| import java.math.MathContext; |
| |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.tracecompass.tmf.core.synchronization.ITmfTimestampTransform; |
| import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp; |
| import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp; |
| |
| import com.google.common.hash.HashFunction; |
| import com.google.common.hash.Hashing; |
| |
| /** |
| * Fast linear timestamp transform. |
| * |
| * Reduce the use of BigDecimal for an interval of time where the transform can |
| * be computed only with integer math. By rearranging the linear equation |
| * |
| * f(t) = fAlpha * t + fBeta |
| * |
| * to |
| * |
| * f(t) = (fAlphaLong * (t - ts)) / m + fBeta + c |
| * |
| * where fAlphaLong = fAlpha * m, and c is the constant part of the slope |
| * product. |
| * |
| * The slope applies to a relative time reference instead of absolute timestamp |
| * from epoch. The constant part of the slope for the interval is added to beta. |
| * It reduces the width of slope and timestamp to 32-bit integers, and the |
| * result fits a 64-bit value. Using standard integer arithmetic yield speedup |
| * compared to BigDecimal, while preserving precision. Depending of rounding, |
| * there may be a slight difference of +/- 3ns between the value computed by the |
| * fast transform compared to BigDecimal. The timestamps produced are indepotent |
| * (transforming the same timestamp will always produce the same result), and |
| * the timestamps are monotonic. |
| * |
| * The number of bits available for the cache range is variable. The variable |
| * alphaLong must be a 32-bit value. We reserve 30-bit for the decimal part to |
| * reach the nanosecond precision. If the slope is greater than 1.0, the shift |
| * is reduced to avoid overflow. It reduces the useful cache range, but the |
| * result is correct even for large (1e9) slope. |
| * |
| * @author Francis Giraldeau <francis.giraldeau@gmail.com> |
| * |
| */ |
| public class TmfTimestampTransformLinearFast implements ITmfTimestampTransformInvertible { |
| |
| private static final long serialVersionUID = 2398540405078949740L; |
| |
| private static final int INTEGER_BITS = 32; |
| private static final int DECIMAL_BITS = 30; |
| private static final HashFunction HASHER = Hashing.goodFastHash(32); |
| private static final MathContext MC = MathContext.DECIMAL128; |
| |
| private final @NonNull BigDecimal fAlpha; |
| private final @NonNull BigDecimal fBeta; |
| private final long fAlphaLong; |
| private final long fDeltaMax; |
| private final int fDeltaBits; |
| private final int fHashCode; |
| |
| private transient long fOffset; |
| private transient long fRangeStart; |
| private transient long fScaleMiss; |
| private transient long fScaleHit; |
| |
| /** |
| * Default constructor, equivalent to the identity. |
| */ |
| public TmfTimestampTransformLinearFast() { |
| this(BigDecimal.ONE, BigDecimal.ZERO); |
| } |
| |
| /** |
| * Constructor with alpha and beta |
| * |
| * @param alpha |
| * The slope of the linear transform |
| * @param beta |
| * The initial offset of the linear transform |
| */ |
| public TmfTimestampTransformLinearFast(final double alpha, final double beta) { |
| this(BigDecimal.valueOf(alpha), BigDecimal.valueOf(beta)); |
| } |
| |
| /** |
| * Constructor with alpha and beta as BigDecimal |
| * |
| * @param alpha |
| * The slope of the linear transform (must be in the range |
| * [1e-9, 1e9] |
| * @param beta |
| * The initial offset of the linear transform |
| */ |
| public TmfTimestampTransformLinearFast(final @NonNull BigDecimal alpha, final @NonNull BigDecimal beta) { |
| /* |
| * Validate the slope range: |
| * |
| * - Negative slope means timestamp goes backward wrt another computer, |
| * and this would violate the basic laws of physics. |
| * |
| * - A slope smaller than 1e-9 means the transform result will always be |
| * truncated to zero nanosecond. |
| * |
| * - A slope larger than Integer.MAX_VALUE is too large for the |
| * nanosecond scale. |
| * |
| * Therefore, a valid slope must be in the range [1e-9, 1e9] |
| */ |
| if (alpha.compareTo(BigDecimal.valueOf(1e-9)) < 0 || |
| alpha.compareTo(BigDecimal.valueOf(1e9)) > 0) { |
| throw new IllegalArgumentException("The slope alpha must in the range [1e-9, 1e9]"); //$NON-NLS-1$ |
| } |
| fAlpha = alpha; |
| fBeta = beta; |
| |
| /* |
| * The result of (fAlphaLong * delta) must be at most 64-bit value. |
| * Below, we compute the number of bits usable to represent the delta. |
| * Small fAlpha (close to one) have greater range left for delta (at |
| * most 30-bit). For large fAlpha, we reduce the delta range. If fAlpha |
| * is close to ~1e9, then the delta size will be zero, effectively |
| * recomputing the result using the BigDecimal for each transform. |
| * |
| * Long.numberOfLeadingZeros(fAlpha.longValue()) returns the number of |
| * zero bits of the integer part of the slope. Then, fIntegerBits is |
| * subtracted, which returns the number of bits usable for delta. This |
| * value is bounded in the interval of [0, 30], because the delta can't |
| * be negative, and we handle at most nanosecond precision, or 2^30. One |
| * bit for each operand is reserved for the sign (Java enforce signed |
| * arithmetics), such that |
| * |
| * bitsof(fDeltaBits) + bitsof(fAlphaLong) = 62 + 2 = 64 |
| */ |
| int width = Long.numberOfLeadingZeros(fAlpha.longValue()) - INTEGER_BITS; |
| fDeltaBits = Math.max(Math.min(width, DECIMAL_BITS), 0); |
| fDeltaMax = 1 << fDeltaBits; |
| fAlphaLong = fAlpha.multiply(BigDecimal.valueOf(fDeltaMax), MC).longValue(); |
| rescale(0); |
| fScaleMiss = 0; |
| fScaleHit = 0; |
| fHashCode = HASHER.newHasher() |
| .putDouble(fAlpha.doubleValue()) |
| .putDouble(fBeta.doubleValue()) |
| .putLong(fAlphaLong) |
| .hash() |
| .asInt(); |
| } |
| |
| //------------------------------------------------------------------------- |
| // Main timestamp computation |
| //------------------------------------------------------------------------- |
| |
| @Override |
| public ITmfTimestamp transform(ITmfTimestamp timestamp) { |
| return TmfTimestamp.create(transform(timestamp.getValue()), timestamp.getScale()); |
| } |
| |
| @Override |
| public long transform(long timestamp) { |
| long delta = timestamp - fRangeStart; |
| if (delta >= fDeltaMax || delta < 0) { |
| /* |
| * Rescale if we exceed the safe range. |
| * |
| * If the same timestamp is transform with two different fStart |
| * reference, they may not produce the same result. To avoid this |
| * problem, align fStart on a deterministic boundary. |
| * |
| * TODO: use exact math arithmetic to detect overflow when switching to Java 8 |
| */ |
| rescale(timestamp); |
| delta = Math.abs(timestamp - fRangeStart); |
| fScaleMiss++; |
| } else { |
| fScaleHit++; |
| } |
| return ((fAlphaLong * delta) >> fDeltaBits) + fOffset; |
| } |
| |
| private void rescale(long timestamp) { |
| fRangeStart = timestamp - (timestamp % fDeltaMax); |
| fOffset = BigDecimal.valueOf(fRangeStart).multiply(fAlpha, MC).add(fBeta, MC).longValue(); |
| } |
| |
| //------------------------------------------------------------------------- |
| // Transform composition |
| //------------------------------------------------------------------------- |
| |
| @Override |
| public ITmfTimestampTransform composeWith(ITmfTimestampTransform composeWith) { |
| if (composeWith.equals(TmfTimestampTransform.IDENTITY)) { |
| /* If composing with identity, just return this */ |
| return this; |
| } else if (composeWith instanceof TmfTimestampTransformLinearFast) { |
| /* If composeWith is a linear transform, add the two together */ |
| TmfTimestampTransformLinearFast ttl = (TmfTimestampTransformLinearFast) composeWith; |
| BigDecimal newAlpha = fAlpha.multiply(ttl.getAlpha(), MC); |
| BigDecimal newBeta = fAlpha.multiply(ttl.getBeta(), MC).add(fBeta); |
| /* Don't use the factory to make sure any further composition will |
| * be performed on the same object type */ |
| return new TmfTimestampTransformLinearFast(newAlpha, newBeta); |
| } else { |
| /* |
| * We do not know what to do with this kind of transform, just |
| * return this |
| */ |
| return this; |
| } |
| } |
| |
| @Override |
| public ITmfTimestampTransform inverse() { |
| return new TmfTimestampTransformLinearFast(BigDecimal.ONE.divide(fAlpha, MC), |
| BigDecimal.valueOf(-1).multiply(fBeta).divide(fAlpha, MC)); |
| } |
| |
| //------------------------------------------------------------------------- |
| // Getters and utility methods |
| //------------------------------------------------------------------------- |
| |
| /** |
| * A cache miss occurs when the timestamp is out of the range for integer |
| * computation, and therefore requires using BigDecimal for re-scaling. |
| * |
| * @return number of misses |
| */ |
| public long getCacheMisses() { |
| return fScaleMiss; |
| } |
| |
| /** |
| * A scale hit occurs if the timestamp is in the range for fast transform. |
| * |
| * @return number of hits |
| */ |
| public long getCacheHits() { |
| return fScaleHit; |
| } |
| |
| /** |
| * Reset scale misses to zero |
| */ |
| public void resetScaleStats() { |
| fScaleMiss = 0; |
| fScaleHit = 0; |
| } |
| |
| /** |
| * @return the slope alpha |
| */ |
| public BigDecimal getAlpha() { |
| return fAlpha; |
| } |
| |
| /** |
| * @return the offset beta |
| */ |
| public BigDecimal getBeta() { |
| return fBeta; |
| } |
| |
| /** |
| * The value delta max is the timestamp range where integer arithmetic is |
| * used. |
| * |
| * @return the maximum delta |
| */ |
| public long getDeltaMax() { |
| return fDeltaMax; |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s [ slope = %s, offset = %s ]", //$NON-NLS-1$ |
| getClass().getSimpleName(), |
| fAlpha.toString(), |
| fBeta.toString()); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj instanceof TmfTimestampTransformLinearFast) { |
| TmfTimestampTransformLinearFast other = (TmfTimestampTransformLinearFast) obj; |
| return this.getAlpha().equals(other.getAlpha()) && |
| this.getBeta().equals(other.getBeta()); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return fHashCode; |
| } |
| |
| // Deserialization method, make sure there is a first scaling |
| private void readObject(ObjectInputStream stream) |
| throws IOException, ClassNotFoundException { |
| stream.defaultReadObject(); |
| |
| rescale(0); |
| } |
| |
| } |