blob: f04429ae680b6ba051b89beaba8eaa6f929e2a86 [file] [log] [blame]
/*******************************************************************************
* 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);
}
}