Skip to content

Commit 83a83c4

Browse files
jiceathomeJean-Christophe Hugly
andauthored
testing: incremental improvements to the router benchmark tooling (scionproto#4518)
Summary of changes: * The packet size is now configurable. CI uses a constant while the benchmark command accepts an optional argument. * The benchmark command sets the mtu to 9000 on the host side and instructs the user to do the same on the router side. In the CI test variant: an mtu of 9000 (not 8000) is used on both sides. * The performance index formula gained a tentative value for the NIC_CONSTANT constant. * The benchmark's total number of packets (per testcase) is no longer tunable. Instead, the duration is what is configurable. Both the CI test and the benchmark command use 13 seconds per test case. * The benchmark's output includes a bit more information: bit rate and raw (not corrected for preemptions) packet rate. * A useless and noisy error message was silenced in brload: that which was output for every foreign packet received (turns out that some Realtek hardware sends proprietary management traffic as Ethernet frames). * Improved the relevance of mmbm by deliberately misaligning buffers. Also make sure to not use more buffers than can fit in L3 cache when probing for L2 cache + TLB misses. * Added zip as a dependency after finding it isn't part of Debian by default. * Added an option (via constant for now) to profile the router during the CI test. --------- Co-authored-by: Jean-Christophe Hugly <[email protected]>
1 parent 27f725a commit 83a83c4

File tree

13 files changed

+224
-163
lines changed

13 files changed

+224
-163
lines changed

acceptance/router_benchmark/benchmark.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class RouterBMTool(cli.Application, RouterBM):
9595
help="The coremark score of the subject machine.")
9696
mmbm = cli.SwitchAttr(["m", "mmbm"], int, default=0,
9797
help="The mmbm score of the subject machine.")
98+
packet_size = cli.SwitchAttr(["s", "size"], int, default=172,
99+
help="Test packet size (includes all headers - floored at 154).")
98100
intf_map: dict[str, Intf] = {}
99101
brload: LocalCommand = local["./bin/brload"]
100102
brload_cpus: list[int] = []
@@ -158,8 +160,7 @@ def config_interface(self, req: IntfReq):
158160
if i.name == host_intf:
159161
break
160162
else:
161-
# TODO: instructions/warning regarding inability to enable jumbo frames.
162-
# sudo("ip", "link", "set", host_intf, "mtu", "8000")
163+
sudo("ip", "link", "set", host_intf, "mtu", "9000")
163164

164165
# Do not assign the host addresses but create one link-local addr.
165166
# Brload needs some src IP to send arp requests. (This requires rp_filter
@@ -291,16 +292,16 @@ def instructions(self):
291292
bmtools includes two microbenchmarks: scion-coremark and scion-mmbm. Those will run
292293
automatically and the results will be used to improve the benchmark report.
293294
294-
Optinal: If you did not install bmtools.ipk, install and run those microbenchmark and make a
295+
Optional: If you did not install bmtools.ipk, install and run those microbenchmarks and make a
295296
note of the results: (scion-coremark; scion-mmbm).
296297
297298
2 - Configure the following interfaces on your router (The procedure depends on your router
298-
UI):
299+
UI) - All interfaces should have the mtu set to 9000:
299300
- One physical interface with addresses: {", ".join(multiplexed)}
300301
{nl.join([' - One physical interface with address: ' + s for s in exclusives])}
301302
302303
IMPORTANT: if you're using a partitioned network (eg. multiple switches or no switches),
303-
the "must reach" annotation matters. The 'h' number is the order in which the corresponding host
304+
the "must reach" annotation matters. The '#' number is the order in which the corresponding host
304305
interface must be given on the command line in step 7.
305306
306307
3 - Connect the corresponding ports into your test switch (best if dedicated for the test).

acceptance/router_benchmark/benchmarklib.py

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,11 @@
3333
# to retrieve a frame. That one is hardware dependent and must be found by a third benchmark, so
3434
# it is not theoretically a constant, but keeping it here to not forget. Until then, our performance
3535
# index isn't really valid cross-hardware. M_COEF=400 gives roughly consistent results with the
36-
# hardware we have. So, using that until we know more.
36+
# hardware we have. So, using that until we know more. NIC_CONSTANT seems to be around
37+
# 1 microsecond. Using that, provisionally.
3738

3839
M_COEF = 400
39-
NIC_CONSTANT = 0
40-
41-
# TODO(jiceatscion): get it from or give it to brload?
42-
BM_PACKET_LEN = 172
40+
NIC_CONSTANT = 1.0/1000000
4341

4442
# Intf: description of an interface configured for brload's use. Depending on context
4543
# mac and peermac may be unused. "mac" is the MAC address configured on the side of the subject
@@ -55,31 +53,36 @@ class Results:
5553
cores: int = 0
5654
coremark: int = 0
5755
mmbm: int = 0
56+
packet_size: int = 0
5857
cases: list[dict] = []
5958
failed: list[dict] = []
6059
checked: bool = False
6160

62-
def __init__(self, cores: int, coremark: int, mmbm: int):
61+
def __init__(self, cores: int, coremark: int, mmbm: int, packet_size: int):
6362
self.cores = cores
6463
self.coremark = coremark
6564
self.mmbm = mmbm
65+
self.packet_size = packet_size
6666

6767
def perf_index(self, rate: int) -> float:
6868
# TODO(jiceatscion): The perf index assumes that line speed isn't the bottleneck.
6969
# It almost never is, but ideally we'd need to run iperf3 to verify.
7070
# mmbm is in mebiBytes/s, rate is in pkt/s
7171
return rate * (1.0 / self.coremark +
72-
M_COEF * BM_PACKET_LEN / (self.mmbm * 1024 * 1024) +
72+
M_COEF * self.packet_size / (self.mmbm * 1024 * 1024) +
7373
NIC_CONSTANT)
7474

75-
def add_case(self, name: str, rate: int, droppage: int):
75+
def add_case(self, name: str, rate: int, droppage: int, raw_rate: int):
7676
dropRatio = round(float(droppage) / (rate + droppage), 2)
7777
saturated = dropRatio > 0.03
7878
perf = 0.0
7979
if self.cores == 3 and self.coremark and self.mmbm:
8080
perf = round(self.perf_index(rate), 1)
8181
self.cases.append({"case": name,
82-
"perf": perf, "rate": rate, "drop": dropRatio, "full": saturated})
82+
"perf": perf, "rate": rate, "drop": dropRatio,
83+
"bit_rate": rate * self.packet_size * 8,
84+
"raw_pkt_rate": raw_rate,
85+
"full": saturated})
8386

8487
def CI_check(self, expectations: dict[str, int]):
8588
self.checked = True
@@ -147,14 +150,15 @@ class RouterBM():
147150
This class is a Mixin that borrows the following attributes from the host class:
148151
* coremark: the coremark benchmark results.
149152
* mmbm: the mmbm benchmark results.
153+
* packet_size: the packet_size to use in the test cases.
150154
* intf_map: the map "label->actual_interface" map to be passed to brload.
151155
* brload: "localCmd" wraper for the brload executable (plumbum.machines.LocalCommand)
152156
* brload_cpus: [int] cpus where it is acceptable to run brload ([] means any)
153157
* artifacts: the data directory (passed to docker).
154158
* prom_address: the address of the prometheus API a string in the form "host:port"
155159
"""
156160

157-
def exec_br_load(self, case: str, map_args: list[str], count: int) -> str:
161+
def exec_br_load(self, case: str, map_args: list[str], duration: int) -> str:
158162
# For num-streams, attempt to distribute uniformly on many possible number of cores.
159163
# 840 is a multiple of 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 15, 20, 21, 24, 28, ...
160164
brload_args = [
@@ -163,8 +167,9 @@ def exec_br_load(self, case: str, map_args: list[str], count: int) -> str:
163167
"--artifacts", self.artifacts,
164168
*map_args,
165169
"--case", case,
166-
"--num-packets", str(count),
170+
"--duration", f"{duration}s",
167171
"--num-streams", "840",
172+
"--packet-size", f"{self.packet_size}",
168173
]
169174
if self.brload_cpus:
170175
brload_args = [
@@ -176,20 +181,20 @@ def exec_br_load(self, case: str, map_args: list[str], count: int) -> str:
176181
def run_test_case(self, case: str, map_args: list[str]) -> (int, int):
177182
logger.debug(f"==> Starting load {case}")
178183

179-
output = self.exec_br_load(case, map_args, 10000000)
180-
beg = "0"
184+
# We transmit for 13 seconds and then ignore the first 3.
185+
output = self.exec_br_load(case, map_args, 13)
181186
end = "0"
182187
for line in output.splitlines():
183188
if line.startswith("metricsBegin"):
184-
_, beg, _, end = line.split()
189+
end = line.split()[3] # "... metricsEnd: <end>"
185190

186191
logger.debug(f"==> Collecting {case} performance metrics...")
187192

188193
# The raw metrics are expressed in terms of core*seconds. We convert to machine*seconds
189194
# which allows us to provide a projected packet/s; ...more intuitive than packets/core*s.
190-
# We measure the rate over 10s. For best results we sample the end of the middle 10s of the
191-
# run. "beg" is the start time of the real action and "end" is the end time.
192-
sampleTime = (int(beg) + int(end) + 10) / 2
195+
# We measure the rate over 10s. For best results we only look at the last 10 seconds.
196+
# "end" reports a time when the transmission was still going on at maximum rate.
197+
sampleTime = int(end)
193198
prom_query = urlencode({
194199
'time': f'{sampleTime}',
195200
'query': (
@@ -218,6 +223,31 @@ def run_test_case(self, case: str, map_args: list[str]) -> (int, int):
218223
processed = int(float(val))
219224
break
220225

226+
# Collect the raw packet rate too. Just so we can discover if the cpu-availability
227+
# correction is bad.
228+
prom_query = urlencode({
229+
'time': f'{sampleTime}',
230+
'query': (
231+
'sum by (instance, job) ('
232+
f' rate(router_output_pkts_total{{job="BR", type="{case}"}}[10s])'
233+
')'
234+
)
235+
})
236+
conn = HTTPConnection(self.prom_address)
237+
conn.request("GET", f"/api/v1/query?{prom_query}")
238+
resp = conn.getresponse()
239+
if resp.status != 200:
240+
raise RuntimeError(f"Unexpected response: {resp.status} {resp.reason}")
241+
242+
# There's only one router, so whichever metric we get is the right one.
243+
pld = json.loads(resp.read().decode("utf-8"))
244+
raw = 0
245+
results = pld["data"]["result"]
246+
for result in results:
247+
ts, val = result["value"]
248+
raw = int(float(val))
249+
break
250+
221251
# Collect dropped packets metrics, so we can verify that the router was well saturated.
222252
# If not, the metrics aren't very useful.
223253
prom_query = urlencode({
@@ -248,7 +278,7 @@ def run_test_case(self, case: str, map_args: list[str]) -> (int, int):
248278
dropped = int(float(val))
249279
break
250280

251-
return processed, dropped
281+
return processed, dropped, raw
252282

253283
# Fetch and log the number of cores used by Go. This may inform performance
254284
# modeling later.
@@ -289,18 +319,18 @@ def run_bm(self, test_cases: [str]) -> Results:
289319
# Run one test (30% size) as warm-up to trigger any frequency scaling, else the first test
290320
# can get much lower performance.
291321
logger.debug("Warmup")
292-
self.exec_br_load(test_cases[0], map_args, 3000000)
322+
self.exec_br_load(test_cases[0], map_args, 5)
293323

294324
# Fetch the core count once. It doesn't change while the router is running.
295325
# We can't get it until the router has done some work, but the warmup is enough.
296326
cores = self.core_count()
297327

298328
# At long last, run the tests.
299-
results = Results(cores, self.coremark, self.mmbm)
329+
results = Results(cores, self.coremark, self.mmbm, self.packet_size)
300330
for test_case in test_cases:
301331
logger.info(f"Case: {test_case}")
302-
rate, droppage = self.run_test_case(test_case, map_args)
303-
results.add_case(test_case, rate, droppage)
332+
rate, droppage, raw = self.run_test_case(test_case, map_args)
333+
results.add_case(test_case, rate or 1, droppage, raw)
304334

305335
return results
306336
logger.info("Benchmarked")

acceptance/router_benchmark/brload/main.go

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package main
1616

1717
import (
18+
"bytes"
1819
"encoding/binary"
1920
"errors"
2021
"fmt"
@@ -37,7 +38,7 @@ import (
3738
"github.com/scionproto/scion/private/keyconf"
3839
)
3940

40-
type Case func(payload string, mac hash.Hash) (string, string, []byte)
41+
type Case func(packetSize int, mac hash.Hash) (string, string, []byte, []byte)
4142

4243
type caseChoice string
4344

@@ -70,12 +71,13 @@ var (
7071
"out_transit": cases.OutTransit,
7172
"br_transit": cases.BrTransit,
7273
}
73-
logConsole string
74-
dir string
75-
numPackets int
76-
numStreams uint16
77-
caseToRun caseChoice
78-
interfaces []string
74+
logConsole string
75+
dir string
76+
testDuration time.Duration
77+
packetSize int
78+
numStreams uint16
79+
caseToRun caseChoice
80+
interfaces []string
7981
)
8082

8183
func main() {
@@ -97,7 +99,8 @@ func main() {
9799
os.Exit(run(cmd))
98100
},
99101
}
100-
runCmd.Flags().IntVar(&numPackets, "num-packets", 10, "Number of packets to send")
102+
runCmd.Flags().DurationVar(&testDuration, "duration", time.Second*15, "Test duration")
103+
runCmd.Flags().IntVar(&packetSize, "packet-size", 172, "Total size of each packet sent")
101104
runCmd.Flags().Uint16Var(&numStreams, "num-streams", 4,
102105
"Number of independent streams (flowID) to use")
103106
runCmd.Flags().StringVar(&logConsole, "log.console", "error",
@@ -160,10 +163,8 @@ func run(cmd *cobra.Command) int {
160163
registerScionPorts()
161164

162165
log.Info("BRLoad acceptance tests:")
163-
164-
payloadString := "actualpayloadbytes"
165166
caseFunc := allCases[string(caseToRun)] // key already checked.
166-
caseDevIn, caseDevOut, rawPkt := caseFunc(payloadString, hfMAC)
167+
caseDevIn, caseDevOut, payload, rawPkt := caseFunc(packetSize, hfMAC)
167168

168169
writePktTo, ok := handles[caseDevIn]
169170
if !ok {
@@ -186,30 +187,40 @@ func run(cmd *cobra.Command) int {
186187
go func() {
187188
defer log.HandlePanic()
188189
defer close(listenerChan)
189-
listenerChan <- receivePackets(packetChan, payloadString)
190+
listenerChan <- receivePackets(packetChan, payload)
190191
}()
191192

192-
// We started everything that could be started. So the best window for perf mertics
193-
// opens somewhere around now.
194-
metricsBegin := time.Now().Unix()
195193
// Because we're using IPV4 only, the UDP checksum is optional, so we are allowed to
196194
// just set it to zero instead of recomputing it. The IP checksum does not cover the payload, so
197195
// we don't need to update it.
198196
binary.BigEndian.PutUint16(rawPkt[40:42], 0)
199197

200-
for i := 0; i < numPackets; i++ {
201-
// Rotate through flowIDs. We patch it directly into the SCION header of the packet. The
202-
// SCION header starts at offset 42. The flowID is the 20 least significant bits of the
203-
// first 32 bit field. To make our life simpler, we only use the last 16 bits (so no more
204-
// than 64K flows).
205-
binary.BigEndian.PutUint16(rawPkt[44:46], uint16(i%int(numStreams)))
206-
if err := writePktTo.WritePacketData(rawPkt); err != nil {
207-
log.Error("writing input packet", "case", string(caseToRun), "error", err)
208-
return 1
198+
// We started everything that could be started. So the best window for perf mertics
199+
// opens somewhere around now.
200+
begin := time.Now()
201+
metricsBegin := begin.Unix()
202+
203+
numPkt := 0
204+
for time.Since(begin) < testDuration {
205+
// Check the time only once every 10000 packets
206+
for i := 0; i < 10000; i++ {
207+
// Rotate through flowIDs. We patch it directly into the SCION header of the packet. The
208+
// SCION header starts at offset 42. The flowID is the 20 least significant bits of the
209+
// first 32 bit field. To make our life simpler, we only use the last 16 bits (so no
210+
// more than 64K flows).
211+
binary.BigEndian.PutUint16(rawPkt[44:46], uint16(numPkt%int(numStreams)))
212+
numPkt++
213+
if err := writePktTo.WritePacketData(rawPkt); err != nil {
214+
log.Error("writing input packet", "case", string(caseToRun), "error", err)
215+
return 1
216+
}
209217
}
210218
}
219+
211220
metricsEnd := time.Now().Unix()
212-
// The test harness looks for this output.
221+
222+
// The test harness looks for this output. [metricsBegin, metricsEnd] needs to be fully
223+
// contained in the period when we were actually transmitting, but can be a bit smaller.
213224
fmt.Printf("metricsBegin: %d metricsEnd: %d\n", metricsBegin, metricsEnd)
214225

215226
// Get the results from the packet listener.
@@ -240,7 +251,7 @@ func run(cmd *cobra.Command) int {
240251
// The number of consumed packets is returned.
241252
// Currently we are content with receiving a single correct packet and we terminate after
242253
// that.
243-
func receivePackets(packetChan chan gopacket.Packet, payload string) int {
254+
func receivePackets(packetChan chan gopacket.Packet, payload []byte) int {
244255
numRcv := 0
245256

246257
for {
@@ -251,15 +262,18 @@ func receivePackets(packetChan chan gopacket.Packet, payload string) int {
251262
return numRcv
252263
}
253264
if err := got.ErrorLayer(); err != nil {
254-
log.Error("error decoding packet", "err", err)
265+
// This isn't an error. There is all sort of traffic that we might not know about
266+
// and not be able to read.
267+
// log.Error("error decoding packet", "err", err)
255268
continue
256269
}
257270
layer := got.Layer(gopacket.LayerTypePayload)
258271
if layer == nil {
259-
log.Error("error fetching packet payload: no PayLoad")
272+
// Don't treat this as an error. This could be random traffic we don't know about.
260273
continue
261274
}
262-
if string(layer.LayerContents()) == payload {
275+
if bytes.Equal(layer.LayerContents(), payload) {
276+
// That's ours.
263277
// To return the count of all packets received, just remove the "return" below.
264278
// Return will occur once packetChan closes (which happens after a short timeout at
265279
// the end of the test).

acceptance/router_benchmark/cases/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go_library(
44
name = "go_default_library",
55
srcs = [
66
"br_transit.go",
7+
"helpers.go",
78
"in.go",
89
"in_transit.go",
910
"out.go",

0 commit comments

Comments
 (0)