Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce overhead of creating / parsing / serializing trace IDs. #212

Merged
merged 2 commits into from
Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ public class IdsBenchmark {

private static final SecureRandom SECURE_RANDOM = new SecureRandom();

private static final TraceID TRACE_ID = TraceID.create();

@Benchmark
public TraceID traceId_create() {
return TraceID.create();
}

@Benchmark
public TraceID traceId_parse() {
return TraceID.fromString("1-57ff426a-80c11c39b0c928905eb0828d");
}

@Benchmark
public String traceId_serialize() {
return TRACE_ID.toString();
}

@Benchmark
public BigInteger traceId_secureRandom() {
return new BigInteger(96, SECURE_RANDOM);
Expand Down Expand Up @@ -77,7 +94,7 @@ public String segmentId_threadLocalRandom() {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.addProfiler("gc")
.include(".*" + IdsBenchmark.class.getSimpleName())
.include(".*" + IdsBenchmark.class.getSimpleName() + ".*_(create|parse|serialize)")
.build();

new Runner(opt).run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@
package com.amazonaws.xray.entities;

import com.amazonaws.xray.ThreadLocalStorage;
import com.amazonaws.xray.internal.RecyclableBuffers;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.Instant;
import org.checkerframework.checker.nullness.qual.Nullable;

public class TraceID {

private static final TraceID INVALID = new TraceID(0, BigInteger.ZERO);
private static final String INVALID_START_TIME = "00000000";
private static final String INVALID_NUMBER = "000000000000000000000000";

private static final TraceID INVALID = new TraceID(INVALID_START_TIME, INVALID_NUMBER);

/**
* Returns a new {@link TraceID} which represents the start of a new trace.
Expand Down Expand Up @@ -54,15 +59,15 @@ public static TraceID fromString(String xrayTraceId) {
}

String startTimePart = xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_1 + 1, TRACE_ID_DELIMITER_INDEX_2);
if (!isHex(startTimePart)) {
return TraceID.create();
}
String randomPart = xrayTraceId.substring(TRACE_ID_DELIMITER_INDEX_2 + 1, TRACE_ID_LENGTH);

final TraceID result;
try {
result = new TraceID(Long.valueOf(startTimePart, 16), new BigInteger(randomPart, 16));
} catch (NumberFormatException e) {
if (!isHex(randomPart)) {
return TraceID.create();
}
return result;

return new TraceID(startTimePart, randomPart);
}

/**
Expand All @@ -80,8 +85,8 @@ public static TraceID invalid() {
private static final char VERSION = '1';
private static final char DELIMITER = '-';

private BigInteger number;
private long startTime;
private String numberHex;
private String startTimeHex;

/**
* @deprecated Use {@link #create()}.
Expand All @@ -96,39 +101,40 @@ public TraceID() {
*/
@Deprecated
public TraceID(long startTime) {
number = new BigInteger(96, ThreadLocalStorage.getRandom());
this.startTime = startTime;
SecureRandom random = ThreadLocalStorage.getRandom();

// nextBytes much faster than calling nextInt multiple times when using SecureRandom
byte[] randomBytes = RecyclableBuffers.bytes(12);
random.nextBytes(randomBytes);
numberHex = bytesToBase16String(randomBytes);
this.startTimeHex = intToBase16String((int) startTime);
}

private TraceID(long startTime, BigInteger number) {
this.startTime = startTime;
this.number = number;
private TraceID(String startTimeHex, String numberHex) {
this.startTimeHex = startTimeHex;
this.numberHex = numberHex;
}

@Override
public String toString() {
String paddedNumber = padLeft(number.toString(16), 24);
String startTime = padLeft(Long.toHexString(this.startTime), 8);
return "" + VERSION + DELIMITER + startTime + DELIMITER + paddedNumber;
}

private static String padLeft(String str, int size) {
if (str.length() == size) {
return str;
}
StringBuilder padded = new StringBuilder(size);
for (int i = str.length(); i < size; i++) {
padded.append('0');
}
padded.append(str);
return padded.toString();
return "" + VERSION + DELIMITER + startTimeHex + DELIMITER + numberHex;
}

/**
* @return the number
*
* @deprecated use {@link #getNumberAsHex()}.
*/
@Deprecated
public BigInteger getNumber() {
return number;
return new BigInteger(numberHex, 16);
}

/**
* Returns the number component of this {@link TraceID} as a hexadecimal string.
*/
public String getNumberAsHex() {
return numberHex;
}

/**
Expand All @@ -139,15 +145,25 @@ public BigInteger getNumber() {
@Deprecated
public void setNumber(@Nullable BigInteger number) {
if (number != null) {
this.number = number;
this.numberHex = numberToBase16String(number.shiftRight(64).intValue(), number.longValue());
}
}

/**
* @return the startTime
*
* @deprecated Use {@link #getStartTimeAsHex()}.
*/
public long getStartTime() {
return startTime;
return Long.parseLong(startTimeHex, 16);
}

/**
* Returns the start time of this {@link TraceID} as a hexadecimal string representing the number of seconds since
* the epoch.
*/
public String getStartTimeAsHex() {
return startTimeHex;
}

/**
Expand All @@ -157,15 +173,12 @@ public long getStartTime() {
*/
@Deprecated
public void setStartTime(long startTime) {
this.startTime = startTime;
this.startTimeHex = intToBase16String(startTime);
}

@Override
public int hashCode() {
int result = 1;
result = 31 * result + ((number == null) ? 0 : number.hashCode());
result = 31 * result + (int) (startTime ^ (startTime >>> 32));
return result;
return 31 * numberHex.hashCode() + startTimeHex.hashCode();
}

@Override
Expand All @@ -177,6 +190,83 @@ public boolean equals(@Nullable Object obj) {
return false;
}
TraceID other = (TraceID) obj;
return number.equals(other.number) && startTime == other.startTime;
return numberHex.equals(other.numberHex) && startTimeHex.equals(other.startTimeHex);
}

private static final int BYTE_BASE16 = 2;
private static final String ALPHABET = "0123456789abcdef";
private static final char[] ENCODING = buildEncodingArray();

private static char[] buildEncodingArray() {
char[] encoding = new char[512];
for (int i = 0; i < 256; ++i) {
encoding[i] = ALPHABET.charAt(i >>> 4);
encoding[i | 0x100] = ALPHABET.charAt(i & 0xF);
}
return encoding;
}

private static String bytesToBase16String(byte[] bytes) {
char[] dest = RecyclableBuffers.chars(24);
for (int i = 0; i < 12; i++) {
byteToBase16(bytes[i], dest, i * BYTE_BASE16);
}

return new String(dest, 0, 24);
}

private static String numberToBase16String(int hi, long lo) {
char[] dest = RecyclableBuffers.chars(24);

byteToBase16((byte) (hi >> 24 & 0xFFL), dest, 0);
byteToBase16((byte) (hi >> 16 & 0xFFL), dest, BYTE_BASE16);
byteToBase16((byte) (hi >> 8 & 0xFFL), dest, 2 * BYTE_BASE16);
byteToBase16((byte) (hi & 0xFFL), dest, 3 * BYTE_BASE16);

byteToBase16((byte) (lo >> 56 & 0xFFL), dest, 4 * BYTE_BASE16);
byteToBase16((byte) (lo >> 48 & 0xFFL), dest, 5 * BYTE_BASE16);
byteToBase16((byte) (lo >> 40 & 0xFFL), dest, 6 * BYTE_BASE16);
byteToBase16((byte) (lo >> 32 & 0xFFL), dest, 7 * BYTE_BASE16);
byteToBase16((byte) (lo >> 24 & 0xFFL), dest, 8 * BYTE_BASE16);
byteToBase16((byte) (lo >> 16 & 0xFFL), dest, 9 * BYTE_BASE16);
byteToBase16((byte) (lo >> 8 & 0xFFL), dest, 10 * BYTE_BASE16);
byteToBase16((byte) (lo & 0xFFL), dest, 11 * BYTE_BASE16);

return new String(dest, 0, 24);
}


private static String intToBase16String(long value) {
char[] dest = RecyclableBuffers.chars(8);
byteToBase16((byte) (value >> 24 & 0xFFL), dest, 0);
byteToBase16((byte) (value >> 16 & 0xFFL), dest, BYTE_BASE16);
byteToBase16((byte) (value >> 8 & 0xFFL), dest, 2 * BYTE_BASE16);
byteToBase16((byte) (value & 0xFFL), dest, 3 * BYTE_BASE16);
return new String(dest, 0, 8);
}

private static void byteToBase16(byte value, char[] dest, int destOffset) {
int b = value & 0xFF;
dest[destOffset] = ENCODING[b];
dest[destOffset + 1] = ENCODING[b | 0x100];
}

// Visible for testing
static boolean isHex(String value) {
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (!isDigit(c) && !isLowercaseHexCharacter(c)) {
return false;
}
}
return true;
}

private static boolean isLowercaseHexCharacter(char b) {
return 'a' <= b && b <= 'f';
}

private static boolean isDigit(char b) {
return '0' <= b && b <= '9';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public final class RecyclableBuffers {

private static final ThreadLocal<@Nullable StringBuilder> STRING_BUILDER = new ThreadLocal<>();

@SuppressWarnings("nullness:type.argument.type.incompatible")
private static final ThreadLocal<char[]> CHARS = new ThreadLocal<>();

@SuppressWarnings("nullness:type.argument.type.incompatible")
private static final ThreadLocal<byte[]> BYTES = new ThreadLocal<>();

/**
* A {@link ThreadLocal} {@link StringBuilder}. Take care when filling a large value into this buffer
* because the memory will remain for the lifetime of the thread.
Expand All @@ -42,6 +48,32 @@ public static StringBuilder stringBuilder() {
return buffer;
}

/**
* A {@link ThreadLocal} {@code char[]} of length {@code length}. The array is not zeroed in any way - every character of
* a resulting {@link String} must be set explicitly. The array returned my be longer than {@code length} - always explicitly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: may be longer

anuraaga marked this conversation as resolved.
Show resolved Hide resolved
* set the length when using the result, for example by calling {@link String#valueOf(char[], int, int)}.
*/
public static char[] chars(int length) {
char[] buffer = CHARS.get();
if (buffer == null || buffer.length < length) {
buffer = new char[length];
CHARS.set(buffer);
}
return buffer;
}

/**
* A {@link ThreadLocal} {@code byte[]} of length {@code length}. The array is not zeroed in any way.
*/
public static byte[] bytes(int length) {
byte[] buffer = BYTES.get();
if (buffer == null || buffer.length < length) {
buffer = new byte[length];
BYTES.set(buffer);
}
return buffer;
}

private RecyclableBuffers() {
}
}
Loading