Skip to content

Commit

Permalink
Merge pull request #82 from m-lab/sandbox-roberto-latency-on-upload
Browse files Browse the repository at this point in the history
Show latency on upload-only measurements
  • Loading branch information
robertodauria committed Aug 29, 2022
2 parents 595c7d6 + 67b3641 commit f2d6e9b
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 130 deletions.
43 changes: 34 additions & 9 deletions internal/emitter/humanreadable.go
Expand Up @@ -91,23 +91,48 @@ func (h HumanReadable) OnComplete(test spec.TestKind) error {

// OnSummary handles the summary event.
func (h HumanReadable) OnSummary(s *Summary) error {
const summaryFormat = `%15s: %s
%15s: %s
const summaryHeaderFormat = `
Test results
%10s: %s
%10s: %s
`
const downloadFormat = `
%22s
%15s: %7.1f %s
%15s: %7.1f %s
%15s: %7.1f %s
%15s: %7.2f %s
`
_, err := fmt.Fprintf(h.out, summaryFormat,
const uploadFormat = `
%20s
%15s: %7.1f %s
%15s: %7.1f %s
`
_, err := fmt.Fprintf(h.out, summaryHeaderFormat,
"Server", s.ServerFQDN,
"Client", s.ClientIP,
"Latency", s.MinRTT.Value, s.MinRTT.Unit,
"Download", s.Download.Value, s.Upload.Unit,
"Upload", s.Upload.Value, s.Upload.Unit,
"Retransmission", s.DownloadRetrans.Value, s.DownloadRetrans.Unit)
"Client", s.ClientIP)
if err != nil {
return err
}

if s.Download != nil {
_, err := fmt.Fprintf(h.out, downloadFormat, "Download",
"Throughput", s.Download.Throughput.Value, s.Download.Throughput.Unit,
"Latency", s.Download.Latency.Value, s.Download.Latency.Unit,
"Retransmission", s.Download.Retransmission.Value, s.Download.Retransmission.Unit)
if err != nil {
return err
}
}

if s.Upload != nil {
_, err := fmt.Fprintf(h.out, uploadFormat, "Upload",
"Throughput", s.Upload.Throughput.Value, s.Upload.Throughput.Unit,
"Latency", s.Upload.Latency.Value, s.Upload.Latency.Unit)
if err != nil {
return err
}
}

return nil
}
68 changes: 43 additions & 25 deletions internal/emitter/humanreadable_test.go
Expand Up @@ -2,7 +2,6 @@ package emitter

import (
"errors"
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -227,31 +226,49 @@ func TestHumanReadableOnCompleteFailure(t *testing.T) {
}

func TestHumanReadableOnSummary(t *testing.T) {
expected := ` Server: test
Client: test
expectedHeader := `
Test results
Server: test
Client: test
`
expectedUpload := `
Upload
Throughput: 100.0 Mbit/s
Latency: 10.0 ms
Download: 100.0 Mbit/s
Upload: 100.0 Mbit/s
Retransmission: 1.00 %
`
expectedDownload := `
Download
Throughput: 100.0 Mbit/s
Latency: 10.0 ms
Retransmission: 1.0 %
`
summary := &Summary{
ClientIP: "test",
ServerFQDN: "test",
Download: ValueUnitPair{
Value: 100.0,
Unit: "Mbit/s",
},
Upload: ValueUnitPair{
Value: 100.0,
Unit: "Mbit/s",
Download: &SubtestSummary{
Throughput: ValueUnitPair{
Value: 100.0,
Unit: "Mbit/s",
},
Latency: ValueUnitPair{
Value: 10.0,
Unit: "ms",
},
Retransmission: ValueUnitPair{
Value: 1.0,
Unit: "%",
},
},
DownloadRetrans: ValueUnitPair{
Value: 1.0,
Unit: "%",
},
MinRTT: ValueUnitPair{
Value: 10.0,
Unit: "ms",
Upload: &SubtestSummary{
Throughput: ValueUnitPair{
Value: 100.0,
Unit: "Mbit/s",
},
Latency: ValueUnitPair{
Value: 10.0,
Unit: "ms",
},
},
}
sw := &mocks.SavingWriter{}
Expand All @@ -261,12 +278,13 @@ func TestHumanReadableOnSummary(t *testing.T) {
t.Fatal(err)
}

if len(sw.Data) != 1 {
t.Fatal("invalid length")
if len(sw.Data) == 0 {
t.Fatal("no data written")
}
if string(sw.Data[0]) != expected {
fmt.Println(string(sw.Data[0]))
fmt.Println(expected)

if string(sw.Data[0]) != expectedHeader ||
string(sw.Data[1]) != expectedDownload ||
string(sw.Data[2]) != expectedUpload {
t.Fatal("OnSummary(): unexpected data")
}
}
Expand Down
5 changes: 1 addition & 4 deletions internal/emitter/json_test.go
Expand Up @@ -317,11 +317,8 @@ func TestJSONOnSummary(t *testing.T) {
if output.ClientIP != summary.ClientIP ||
output.ServerFQDN != summary.ServerFQDN ||
output.ServerIP != summary.ServerIP ||
output.DownloadUUID != summary.DownloadUUID ||
output.Download != summary.Download ||
output.Upload != summary.Upload ||
output.DownloadRetrans != summary.DownloadRetrans ||
output.MinRTT != summary.MinRTT {
output.Upload != summary.Upload {
t.Fatal("OnSummary(): unexpected output")
}

Expand Down
35 changes: 18 additions & 17 deletions internal/emitter/summary.go
Expand Up @@ -6,6 +6,20 @@ type ValueUnitPair struct {
Unit string
}

// SubtestSummary contains all the results of a single subtest (download or
// upload). All the values are from the server's perspective, except for the
// download throughput.
type SubtestSummary struct {
// UUID is the unique identified of this subtest.
UUID string
// Throughput is the measured throughput during this subtest.
Throughput ValueUnitPair
// Latency is the MinRTT value of the latest measurement, in milliseconds.
Latency ValueUnitPair
// Retransmission is BytesRetrans / BytesSent from TCPInfo
Retransmission ValueUnitPair
}

// Summary is a struct containing the values displayed to the user at
// the end of an ndt7 test.
type Summary struct {
Expand All @@ -18,24 +32,11 @@ type Summary struct {
// ClientIP is the (v4 or v6) IP address of the client.
ClientIP string

// DownloadUUID is the UUID of the download test.
// TODO: add UploadUUID after we start processing counterflow messages.
DownloadUUID string

// Download is the download speed, in Mbit/s. This is measured at the
// receiver.
Download ValueUnitPair

// Upload is the upload speed, in Mbit/s. This is measured at the sender.
Upload ValueUnitPair

// DownloadRetrans is the retransmission rate. This is based on the TCPInfo
// values provided by the server during a download test.
DownloadRetrans ValueUnitPair
// Download is a summary of the download subtest.
Download *SubtestSummary

// RTT is the round-trip time of the latest measurement, in milliseconds.
// This is provided by the server during a download test.
MinRTT ValueUnitPair
// Upload is a summary of the upload subtest.
Upload *SubtestSummary
}

// NewSummary returns a new Summary struct for a given FQDN.
Expand Down
4 changes: 3 additions & 1 deletion internal/mocks/writer.go
Expand Up @@ -23,6 +23,8 @@ type SavingWriter struct {

// Write appends data to sw.Data. It never fails.
func (sw *SavingWriter) Write(data []byte) (int, error) {
sw.Data = append(sw.Data, data)
d := make([]byte, len(data))
copy(d, data)
sw.Data = append(sw.Data, d)
return len(data), nil
}
108 changes: 64 additions & 44 deletions internal/runner/runner.go
Expand Up @@ -14,8 +14,8 @@ import (

type RunnerOptions struct {
Download, Upload bool
Timeout time.Duration
ClientFactory func() *ndt7.Client
Timeout time.Duration
ClientFactory func() *ndt7.Client
}

type Runner struct {
Expand All @@ -27,9 +27,9 @@ type Runner struct {

func New(opt RunnerOptions, emitter emitter.Emitter, ticker *memoryless.Ticker) *Runner {
return &Runner{
opt: opt,
opt: opt,
emitter: emitter,
ticker: ticker,
ticker: ticker,
}
}

Expand Down Expand Up @@ -89,7 +89,7 @@ func (r Runner) runUpload(ctx context.Context) error {

func (r Runner) RunTestsOnce() []error {
errs := make([]error, 0)

ctx, cancel := context.WithTimeout(context.Background(), r.opt.Timeout)
defer cancel()

Expand Down Expand Up @@ -122,69 +122,89 @@ func (r Runner) RunTestsInLoop() {
_ = r.RunTestsOnce()

// Wait
<- r.ticker.C
<-r.ticker.C
}
}

func makeSummary(FQDN string, results map[spec.TestKind]*ndt7.LatestMeasurements) *emitter.Summary {

s := emitter.NewSummary(FQDN)

if results[spec.TestDownload] != nil &&
results[spec.TestDownload].ConnectionInfo != nil {
// Get UUID, ClientIP and ServerIP from ConnectionInfo.
s.DownloadUUID = results[spec.TestDownload].ConnectionInfo.UUID

clientIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Client)
if err == nil {
s.ClientIP = clientIP
}
var server, client string

serverIP, _, err := net.SplitHostPort(results[spec.TestDownload].ConnectionInfo.Server)
if err == nil {
s.ServerIP = serverIP
}
}

// Download comes from the client-side Measurement during the download
// test. DownloadRetrans and MinRTT come from the server-side Measurement,
// if it includes a TCPInfo object.
// If there is a download result, populate the summary.
if dl, ok := results[spec.TestDownload]; ok {
if dl.Client.AppInfo != nil && dl.Client.AppInfo.ElapsedTime > 0 {
elapsed := float64(dl.Client.AppInfo.ElapsedTime) / 1e06
s.Download = emitter.ValueUnitPair{
Value: (8.0 * float64(dl.Client.AppInfo.NumBytes)) /
s.Download = &emitter.SubtestSummary{}
if dl.ConnectionInfo != nil {
connInfo := dl.ConnectionInfo
s.Download.UUID = connInfo.UUID
client = connInfo.Client
server = connInfo.Server
}
// Read the throughput at the receiver (i.e. the client).
if dl.Client.AppInfo != nil &&
dl.Client.AppInfo.ElapsedTime > 0 {
appInfo := dl.Client.AppInfo
elapsed := float64(appInfo.ElapsedTime) / 1e06
s.Download.Throughput = emitter.ValueUnitPair{
Value: (8.0 * float64(appInfo.NumBytes)) /
elapsed / (1000.0 * 1000.0),
Unit: "Mbit/s",
}
}
if dl.Server.TCPInfo != nil {
if dl.Server.TCPInfo.BytesSent > 0 {
s.DownloadRetrans = emitter.ValueUnitPair{
Value: float64(dl.Server.TCPInfo.BytesRetrans) / float64(dl.Server.TCPInfo.BytesSent) * 100,
Unit: "%",
tcpInfo := dl.Server.TCPInfo
// Read the retransmission rate at the sender.
if tcpInfo.BytesSent > 0 {
s.Download.Retransmission = emitter.ValueUnitPair{
Value: float64(tcpInfo.BytesRetrans) /
float64(tcpInfo.BytesSent) * 100,
Unit: "%",
}
}
s.MinRTT = emitter.ValueUnitPair{
Value: float64(dl.Server.TCPInfo.MinRTT) / 1000,
// Read the latency at the sender.
s.Download.Latency = emitter.ValueUnitPair{
Value: float64(tcpInfo.MinRTT) / 1000,
Unit: "ms",
}
}
}
// The upload rate comes from the receiver (the server). Currently
// ndt-server only provides network-level throughput via TCPInfo.
// TODO: Use AppInfo for application-level measurements when available.

if ul, ok := results[spec.TestUpload]; ok {
if ul.Server.TCPInfo != nil && ul.Server.TCPInfo.BytesReceived > 0 {
elapsed := float64(ul.Server.TCPInfo.ElapsedTime) / 1e06
s.Upload = emitter.ValueUnitPair{
Value: (8.0 * float64(ul.Server.TCPInfo.BytesReceived)) /
elapsed / (1000.0 * 1000.0),
Unit: "Mbit/s",
s.Upload = &emitter.SubtestSummary{}
if ul.ConnectionInfo != nil {
connInfo := ul.ConnectionInfo
s.Upload.UUID = connInfo.UUID
client = connInfo.Client
server = connInfo.Server
}
if ul.Server.TCPInfo != nil {
tcpInfo := ul.Server.TCPInfo
// Read the throughput at the receiver (i.e. the server).
if tcpInfo.ElapsedTime > 0 {
elapsed := float64(tcpInfo.ElapsedTime) / 1e06
s.Upload.Throughput = emitter.ValueUnitPair{
Value: (8.0 * float64(tcpInfo.BytesReceived)) /
elapsed / (1000.0 * 1000.0),
Unit: "Mbit/s",
}
}
// Read the latency at the receiver.
s.Upload.Latency = emitter.ValueUnitPair{
Value: float64(tcpInfo.MinRTT) / 1000,
Unit: "ms",
}
}
}

clientIP, _, err := net.SplitHostPort(client)
if err == nil {
s.ClientIP = clientIP
}
serverIP, _, err := net.SplitHostPort(server)
if err == nil {
s.ServerIP = serverIP
}

return s
}

0 comments on commit f2d6e9b

Please sign in to comment.