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

Improve benchmark output from vespa-feed-client #33157

Merged
merged 6 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 24 additions & 5 deletions vespa-feed-client-api/abi-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,28 +302,47 @@
],
"fields" : [ ]
},
"ai.vespa.feed.client.OperationStats$Response" : {
"superClass" : "java.lang.Object",
"interfaces" : [ ],
"attributes" : [
"public"
],
"methods" : [
"public void <init>(long, long, long, long, long, long, double)",
"public long count()",
"public long averageLatencyMillis()",
"public long minLatencyMillis()",
"public long maxLatencyMillis()",
"public long bytesReceived()",
"public double rate()",
"public java.lang.String toString()"
],
"fields" : [ ]
},
"ai.vespa.feed.client.OperationStats" : {
"superClass" : "java.lang.Object",
"interfaces" : [ ],
"attributes" : [
"public"
],
"methods" : [
"public void <init>(double, long, java.util.Map, long, long, long, long, long, long, long, long)",
"public ai.vespa.feed.client.OperationStats since(ai.vespa.feed.client.OperationStats)",
"public void <init>(double, long, long, long, long, long, long, long, long, java.util.Map)",
"public long requests()",
"public long responses()",
"public long successes()",
"public java.util.Map responsesByCode()",
"public java.util.Map statsByCode()",
"public java.util.Optional response(int)",
"public long exceptions()",
"public long inflight()",
"public long averageLatencyMillis()",
"public long minLatencyMillis()",
"public long maxLatencyMillis()",
"public long bytesSent()",
"public long bytesReceived()",
"public boolean equals(java.lang.Object)",
"public int hashCode()",
"public long operationAverageLatencyMillis()",
"public long operationMinLatencyMillis()",
"public long operationMaxLatencyMillis()",
"public java.lang.String toString()"
],
"fields" : [ ]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,40 @@
package ai.vespa.feed.client;

import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Optional;

/**
* Statistics for feed operations over HTTP against a Vespa cluster.
*
* @author jonmv
* @author bjorncs
*/
public class OperationStats {

private final double duration;
private final long requests;
private final Map<Integer, Long> responsesByCode;
private final long inflight;
private final long targetInflight;
private final long exceptions;
private final long bytesSent;
private final long averageLatencyMillis;
private final long minLatencyMillis;
private final long maxLatencyMillis;
private final long bytesSent;
private final long bytesReceived;
private final Map<Integer, Response> statsByCode;

public OperationStats(double duration, long requests, Map<Integer, Long> responsesByCode, long exceptions,
long inflight, long targetInFlight, long averageLatencyMillis, long minLatencyMillis,
long maxLatencyMillis, long bytesSent, long bytesReceived) {
public OperationStats(double duration, long requests, long exceptions, long inflight, long targetInFlight, long bytesSent,
long averageLatencyMillis, long minLatencyMillis, long maxLatencyMillis,
Map<Integer, Response> statsByCode) {
this.duration = duration;
this.requests = requests;
this.responsesByCode = responsesByCode;
this.exceptions = exceptions;
this.inflight = inflight;
this.targetInflight = targetInFlight;
this.bytesSent = bytesSent;
this.averageLatencyMillis = averageLatencyMillis;
this.minLatencyMillis = minLatencyMillis;
this.maxLatencyMillis = maxLatencyMillis;
this.bytesSent = bytesSent;
this.bytesReceived = bytesReceived;
}

/** Returns the difference between this and the initial.
* Min and max latency, inflight and targetInflight are not modified.
*/
public OperationStats since(OperationStats initial) {
return new OperationStats(duration - initial.duration,
requests - initial.requests,
responsesByCode.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue() - initial.responsesByCode.getOrDefault(entry.getKey(), 0L))),
exceptions - initial.exceptions,
inflight,
targetInflight,
responsesByCode.size() == initial.responsesByCode.size() ? 0 :
(averageLatencyMillis * responsesByCode.size() - initial.averageLatencyMillis * initial.responsesByCode.size())
/ (responsesByCode.size() - initial.responsesByCode.size()),
minLatencyMillis,
maxLatencyMillis,
bytesSent - initial.bytesSent,
bytesReceived - initial.bytesReceived);
this.statsByCode = statsByCode;
}

/** Number of HTTP requests attempted. */
Expand All @@ -68,18 +45,21 @@ public long requests() {

/** Number of HTTP responses received. */
public long responses() {
return requests - inflight - exceptions;
return statsByCode.values().stream().mapToLong(r -> r.count).sum();
}

/** Number of 200 OK HTTP responses received. */
public long successes() {
return responsesByCode.getOrDefault(200, 0L);
var okStats = statsByCode.get(200);
if (okStats == null) return 0;
return okStats.count;
}

/** Number of HTTP responses by status code. */
public Map<Integer, Long> responsesByCode() {
return responsesByCode;
}
/** Statistics per response code. */
public Map<Integer, Response> statsByCode() { return statsByCode; }
bjorncs marked this conversation as resolved.
Show resolved Hide resolved

/** Statistics for the given code. */
public Optional<Response> response(int code) { return Optional.ofNullable(statsByCode.get(code)); }

/** Number of exceptions (instead of responses). */
public long exceptions() {
Expand All @@ -93,17 +73,20 @@ public long inflight() {

/** Average request-response latency, or -1. */
public long averageLatencyMillis() {
return averageLatencyMillis;
var responses = responses();
if (responses == 0) return -1;
var totalLatencyMillis = statsByCode.values().stream().mapToLong(r -> r.totalLatencyMillis).sum();
return totalLatencyMillis / responses;
}

/** Minimum request-response latency, or -1. */
public long minLatencyMillis() {
return minLatencyMillis;
return statsByCode.values().stream().mapToLong(r -> r.minLatencyMillis).min().orElse(-1L);
}

/** Maximum request-response latency, or -1. */
public long maxLatencyMillis() {
return maxLatencyMillis;
return statsByCode.values().stream().mapToLong(r -> r.maxLatencyMillis).max().orElse(-1L);
}

/** Number of bytes sent, for HTTP requests with a response. */
Expand All @@ -113,39 +96,85 @@ public long bytesSent() {

/** Number of bytes received in HTTP responses. */
public long bytesReceived() {
return bytesReceived;
return statsByCode.values().stream().mapToLong(r -> r.bytesReceived).sum();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OperationStats that = (OperationStats) o;
return requests == that.requests && inflight == that.inflight && exceptions == that.exceptions && averageLatencyMillis == that.averageLatencyMillis && minLatencyMillis == that.minLatencyMillis && maxLatencyMillis == that.maxLatencyMillis && bytesSent == that.bytesSent && bytesReceived == that.bytesReceived && responsesByCode.equals(that.responsesByCode);
}
/**
* Operation latency is the time from the initial HTTP request is sent until the operation was successfully completed
* as observed by the client. Time spent on retrying the request will be included. Operations that eventually failed are not included.
* @return average latency in milliseconds
*/
public long operationAverageLatencyMillis() { return averageLatencyMillis; }

@Override
public int hashCode() {
return Objects.hash(requests, responsesByCode, inflight, exceptions, averageLatencyMillis, minLatencyMillis, maxLatencyMillis, bytesSent, bytesReceived);
}
/**
* @see #operationAverageLatencyMillis()
* @return minimum latency as milliseconds
*/
public long operationMinLatencyMillis() { return minLatencyMillis; }

/**
* @see #operationAverageLatencyMillis()
* @return max latency as milliseconds
*/
public long operationMaxLatencyMillis() { return maxLatencyMillis; }

@Override
public String toString() {
Map<Integer, Double> rateByCode = responsesByCode.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()/duration));
return "Stats{" +
"requests=" + requests +
", responsesByCode=" + responsesByCode +
", responseRateByCode=" + rateByCode +
", exceptions=" + exceptions +
", inflight=" + inflight +
", targetInflight=" + targetInflight +
", averageLatencyMillis=" + averageLatencyMillis +
", minLatencyMillis=" + minLatencyMillis +
", maxLatencyMillis=" + maxLatencyMillis +
", bytesSent=" + bytesSent +
", bytesReceived=" + bytesReceived +
'}';
return "OperationStats{" +
"duration=" + duration +
", requests=" + requests +
", inflight=" + inflight +
", targetInflight=" + targetInflight +
", exceptions=" + exceptions +
", bytesSent=" + bytesSent +
", averageLatencyMillis=" + averageLatencyMillis +
", minLatencyMillis=" + minLatencyMillis +
", maxLatencyMillis=" + maxLatencyMillis +
", statsByCode=" + statsByCode +
'}';
}

public static class Response {
private final long count;
private final long totalLatencyMillis;
private final long averageLatencyMillis;
private final long minLatencyMillis;
private final long maxLatencyMillis;
private final long bytesReceived;
private final double rate;

public Response(
long count, long totalLatencyMillis, long averageLatencyMillis, long minLatencyMillis,
long maxLatencyMillis, long bytesReceived, double rate) {
this.count = count;
this.totalLatencyMillis = totalLatencyMillis;
this.averageLatencyMillis = averageLatencyMillis;
this.minLatencyMillis = minLatencyMillis;
this.maxLatencyMillis = maxLatencyMillis;
this.bytesReceived = bytesReceived;
this.rate = rate;
}

// Generate getters for all fields. Should have the same name as the field, and written as a single line
public long count() { return count; }
public long averageLatencyMillis() { return averageLatencyMillis; }
public long minLatencyMillis() { return minLatencyMillis; }
public long maxLatencyMillis() { return maxLatencyMillis; }
public long bytesReceived() { return bytesReceived; }
public double rate() { return rate; }

@Override
public String toString() {
return "Response{" +
"count=" + count +
", totalLatencyMillis=" + totalLatencyMillis +
", averageLatencyMillis=" + averageLatencyMillis +
", minLatencyMillis=" + minLatencyMillis +
", maxLatencyMillis=" + maxLatencyMillis +
", bytesReceived=" + bytesReceived +
", rate=" + rate +
'}';
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,32 @@ static void printBenchmarkResult(long durationNanos, long successes, long failur
writeFloatField(generator, "http.response.latency.millis.avg", stats.averageLatencyMillis(), 3);
writeFloatField(generator, "http.response.latency.millis.max", stats.maxLatencyMillis(), 3);

generator.writeObjectFieldStart("http.response.code.counts");
for (Map.Entry<Integer, Long> entry : stats.responsesByCode().entrySet())
generator.writeNumberField(Integer.toString(entry.getKey()), entry.getValue());
generator.writeEndObject();

// Hide new experimental output behind feature flag
if (System.getenv("VESPA_EXTENDED_STATS") != null) {
generator.writeObjectFieldStart("operation.latency");
generator.writeNumberField("min", stats.operationMinLatencyMillis());
generator.writeNumberField("avg", stats.operationAverageLatencyMillis());
generator.writeNumberField("max", stats.operationMaxLatencyMillis());
generator.writeEndObject();

generator.writeObjectFieldStart("http.response");
for (var e : stats.statsByCode().entrySet()) {
generator.writeObjectFieldStart(Integer.toString(e.getKey()));
generator.writeNumberField("count", e.getValue().count());
generator.writeObjectFieldStart("latency");
generator.writeNumberField("min", e.getValue().minLatencyMillis());
generator.writeNumberField("avg", e.getValue().averageLatencyMillis());
generator.writeNumberField("max", e.getValue().maxLatencyMillis());
generator.writeEndObject();
generator.writeEndObject();
}
generator.writeEndObject();
} else {
generator.writeObjectFieldStart("http.response.code.counts");
for (Map.Entry<Integer, OperationStats.Response> entry : stats.statsByCode().entrySet())
generator.writeNumberField(Integer.toString(entry.getKey()), entry.getValue().count());
generator.writeEndObject();
}
generator.writeEndObject();
}
}
Expand Down
5 changes: 5 additions & 0 deletions vespa-feed-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Loading
Loading