Use nanotime for additional clock resolution

If a start or stop timestamp on a span is provided, the nanotime is ignored.
This commit is contained in:
Tyler Benson 2018-02-22 16:29:22 +10:00
parent 72481c160d
commit 4c66bc8ed2
4 changed files with 123 additions and 37 deletions

View File

@ -1,7 +1,7 @@
import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.AgentTestRunner
import spock.lang.Timeout import spock.lang.Timeout
@Timeout(1) @Timeout(5)
class JBossClassloadingTest extends AgentTestRunner { class JBossClassloadingTest extends AgentTestRunner {
def "delegation property set on module load"() { def "delegation property set on module load"() {
setup: setup:

View File

@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Represents an in-flight span in the opentracing system. * Represents a period of time. Associated information is stored in the SpanContext.
* *
* <p>Spans are created by the {@link DDTracer#buildSpan}. This implementation adds some features * <p>Spans are created by the {@link DDTracer#buildSpan}. This implementation adds some features
* according to the DD agent. * according to the DD agent.
@ -28,31 +28,39 @@ import lombok.extern.slf4j.Slf4j;
public class DDSpan implements Span { public class DDSpan implements Span {
/** The context attached to the span */ /** The context attached to the span */
protected final DDSpanContext context; private final DDSpanContext context;
/** StartTime stores the creation time of the span in milliseconds */
protected long startTimeMicro; /** Creation time of the span in microseconds. Must be greater than zero. */
/** StartTimeNano stores the only the nanoseconds for more accuracy */ private final long startTimeMicro;
protected long startTimeNano;
/** The duration in nanoseconds computed using the startTimeMicro and startTimeNano */ /** Creation time of span in system relative nanotime (may be negative) */
protected long durationNano; private final long startTimeNano;
/** /**
* A simple constructor. Currently, users have * The duration in nanoseconds computed using the startTimeMicro or startTimeNano. Span is
* * considered finished when this is set.
* @param timestampMicro if set, use this time instead of the auto-generated time
* @param context the context
*/ */
protected DDSpan(final long timestampMicro, final DDSpanContext context) { private volatile long durationNano;
/**
* Spans should be constructed using the builder, not by calling the constructor directly.
*
* @param timestampMicro if greater than zero, use this time instead of the current time
* @param context the context used for the span
*/
DDSpan(final long timestampMicro, final DDSpanContext context) {
this.context = context; this.context = context;
// record the start time in nano (current milli + nano delta) // record the start time in nano (current milli + nano delta)
if (timestampMicro == 0L) { if (timestampMicro <= 0L) {
this.startTimeMicro = Clock.currentMicroTime(); this.startTimeMicro = Clock.currentMicroTime();
this.startTimeNano = Clock.currentNanoTicks();
} else { } else {
this.startTimeMicro = timestampMicro; this.startTimeMicro = timestampMicro;
// timestamp might have come from an external clock, so don't bother with nanotime.
this.startTimeNano = 0;
} }
this.startTimeNano = Clock.currentNanoTicks();
// track each span of the trace // track each span of the trace
this.context.getTrace().add(this); this.context.getTrace().add(this);
@ -60,11 +68,23 @@ public class DDSpan implements Span {
@Override @Override
public final void finish() { public final void finish() {
if (startTimeNano != 0) {
if (durationNano != 0) {
log.debug("Span already finished: {}", this);
}
// no external clock was used, so we can rely on nanotime.
this.durationNano = Math.max(1, Clock.currentNanoTicks() - startTimeNano);
afterFinish();
} else {
finish(Clock.currentMicroTime()); finish(Clock.currentMicroTime());
} }
}
@Override @Override
public final void finish(final long stoptimeMicros) { public final void finish(final long stoptimeMicros) {
if (durationNano != 0) {
log.debug("Span already finished: {}", this);
}
// Ensure that duration is at least 1. Less than 1 is possible due to our use of system clock instead of nano time. // Ensure that duration is at least 1. Less than 1 is possible due to our use of system clock instead of nano time.
this.durationNano = this.durationNano =
Math.max(1, TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro)); Math.max(1, TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro));

View File

@ -1,11 +1,16 @@
package datadog.opentracing package datadog.opentracing
import datadog.trace.common.sampling.PrioritySampling import datadog.trace.common.sampling.PrioritySampling
import datadog.trace.common.writer.ListWriter
import spock.lang.Specification import spock.lang.Specification
import spock.lang.Timeout import spock.lang.Timeout
import java.util.concurrent.TimeUnit
@Timeout(1) @Timeout(1)
class DDSpanTest extends Specification { class DDSpanTest extends Specification {
def writer = new ListWriter()
def tracer = new DDTracer(writer)
def "getters and setters"() { def "getters and setters"() {
setup: setup:
@ -70,7 +75,7 @@ class DDSpanTest extends Specification {
DDSpan span DDSpan span
when: when:
span = new DDTracer().buildSpan(opName).startManual() span = tracer.buildSpan(opName).start()
then: then:
span.getResourceName() == opName span.getResourceName() == opName
span.getServiceName() == DDTracer.UNASSIGNED_DEFAULT_SERVICE_NAME span.getServiceName() == DDTracer.UNASSIGNED_DEFAULT_SERVICE_NAME
@ -82,9 +87,70 @@ class DDSpanTest extends Specification {
.buildSpan(opName) .buildSpan(opName)
.withResourceName(resourceName) .withResourceName(resourceName)
.withServiceName(serviceName) .withServiceName(serviceName)
.startManual() .start()
then: then:
span.getResourceName() == resourceName span.getResourceName() == resourceName
span.getServiceName() == serviceName span.getServiceName() == serviceName
} }
def "duration measured in nanoseconds"() {
setup:
def mod = TimeUnit.MILLISECONDS.toNanos(1)
def builder = tracer.buildSpan("test")
def start = System.nanoTime()
def span = builder.start()
def between = System.nanoTime()
def betweenDur = System.nanoTime() - between
span.finish()
def total = System.nanoTime() - start
expect:
span.durationNano > betweenDur
span.durationNano < total
span.durationNano % mod > 0 // Very slim chance of a false negative.
}
def "starting with a timestamp disables nanotime"() {
setup:
def mod = TimeUnit.MILLISECONDS.toNanos(1)
def start = System.currentTimeMillis()
def builder = tracer.buildSpan("test")
.withStartTimestamp(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()))
def span = builder.start()
def between = System.currentTimeMillis()
def betweenDur = System.currentTimeMillis() - between
span.finish()
def total = System.currentTimeMillis() - start
expect:
span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur)
span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total)
span.durationNano % mod == 0
}
def "stopping with a timestamp disables nanotime"() {
setup:
def mod = TimeUnit.MILLISECONDS.toNanos(1)
def builder = tracer.buildSpan("test")
def start = System.currentTimeMillis()
def span = builder.start()
def between = System.currentTimeMillis()
def betweenDur = System.currentTimeMillis() - between
span.finish(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() + 1))
def total = System.currentTimeMillis() - start + 1
expect:
span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur)
span.durationNano <= TimeUnit.MILLISECONDS.toNanos(total)
span.durationNano % mod == 0
}
def "stopping with a timestamp after start time yeilds a min duration of 1"() {
setup:
def span = tracer.buildSpan("test").start()
span.finish(span.startTimeMicro - 10)
expect:
span.durationNano == 1
}
} }

View File

@ -23,7 +23,7 @@ import static ratpack.groovy.test.embed.GroovyEmbeddedApp.ratpack
class DDApiTest extends Specification { class DDApiTest extends Specification {
static mapper = new ObjectMapper(new MessagePackFactory()) static mapper = new ObjectMapper(new MessagePackFactory())
@Timeout(5) @Timeout(10)
// first test takes longer // first test takes longer
def "sending an empty list of traces returns no errors"() { def "sending an empty list of traces returns no errors"() {
setup: setup: