mirror of
https://github.com/hanzomsg/nats-server.git
synced 2026-10-30 20:27:34 +00:00
This is a Test- and CI-only PR. No server changes. See also https://github.com/ConnectEverything/mqtt-test/pull/2.
335 lines
8.3 KiB
Go
335 lines
8.3 KiB
Go
// Copyright 2024 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build !skip_mqtt_tests
|
|
// +build !skip_mqtt_tests
|
|
|
|
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
KB = 1024
|
|
)
|
|
|
|
type mqttBenchMatrix struct {
|
|
QOS []int
|
|
MessageSize []int
|
|
Topics []int
|
|
Publishers []int
|
|
Subscribers []int
|
|
}
|
|
|
|
type mqttBenchContext struct {
|
|
QOS int
|
|
MessageSize int
|
|
Topics int
|
|
Publishers int
|
|
Subscribers int
|
|
|
|
Host string
|
|
Port int
|
|
}
|
|
|
|
var mqttBenchDefaultMatrix = mqttBenchMatrix{
|
|
QOS: []int{0, 1, 2},
|
|
MessageSize: []int{100, 1 * KB, 10 * KB},
|
|
Topics: []int{100},
|
|
Publishers: []int{1},
|
|
Subscribers: []int{1},
|
|
}
|
|
|
|
type MQTTBenchmarkResult struct {
|
|
Ops int `json:"ops"`
|
|
NS map[string]time.Duration `json:"ns"`
|
|
Bytes int64 `json:"bytes"`
|
|
}
|
|
|
|
func BenchmarkXMQTT(b *testing.B) {
|
|
if mqttTestCommandPath == "" {
|
|
b.Skip(`"mqtt-test" command is not found in $PATH.`)
|
|
}
|
|
|
|
bc := mqttBenchContext{}
|
|
b.Run("Server", func(b *testing.B) {
|
|
b.Cleanup(bc.startServer(b, false))
|
|
bc.runAll(b)
|
|
})
|
|
|
|
b.Run("Cluster", func(b *testing.B) {
|
|
b.Cleanup(bc.startCluster(b, false))
|
|
bc.runAll(b)
|
|
})
|
|
|
|
b.Run("Server-no-RMSCache", func(b *testing.B) {
|
|
b.Cleanup(bc.startServer(b, true))
|
|
|
|
bc.benchmarkSubRet(b)
|
|
})
|
|
|
|
b.Run("Cluster-no-RMSCache", func(b *testing.B) {
|
|
b.Cleanup(bc.startCluster(b, true))
|
|
|
|
bc.benchmarkSubRet(b)
|
|
})
|
|
}
|
|
|
|
func (bc mqttBenchContext) runAll(b *testing.B) {
|
|
bc.benchmarkPub(b)
|
|
bc.benchmarkPubRetained(b)
|
|
bc.benchmarkPubSub(b)
|
|
bc.benchmarkSubRet(b)
|
|
}
|
|
|
|
// makes a copy of bc
|
|
func (bc mqttBenchContext) benchmarkPub(b *testing.B) {
|
|
m := mqttBenchDefaultMatrix.
|
|
NoSubscribers().
|
|
NoTopics()
|
|
|
|
b.Run("PUB", func(b *testing.B) {
|
|
m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) {
|
|
bc.runAndReport(b, "pub",
|
|
"--qos", strconv.Itoa(bc.QOS),
|
|
"--messages", strconv.Itoa(b.N),
|
|
"--size", strconv.Itoa(bc.MessageSize),
|
|
"--publishers", strconv.Itoa(bc.Publishers),
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
// makes a copy of bc
|
|
func (bc mqttBenchContext) benchmarkPubRetained(b *testing.B) {
|
|
// This bench is meaningless for QOS0 since the client considers the message
|
|
// sent as soon as it's written out. It is also useless for QOS2 since the
|
|
// flow takes a lot longer, and the difference of publishing as retained or
|
|
// not is lost in the noise.
|
|
m := mqttBenchDefaultMatrix.
|
|
NoSubscribers().
|
|
NoTopics().
|
|
QOS1Only()
|
|
|
|
b.Run("PUBRET", func(b *testing.B) {
|
|
m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) {
|
|
bc.runAndReport(b, "pub", "--retain",
|
|
"--qos", strconv.Itoa(bc.QOS),
|
|
"--messages", strconv.Itoa(b.N),
|
|
"--size", strconv.Itoa(bc.MessageSize),
|
|
"--publishers", strconv.Itoa(bc.Publishers),
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
// makes a copy of bc
|
|
func (bc mqttBenchContext) benchmarkPubSub(b *testing.B) {
|
|
// This test uses a single built-in topic, and a built-in publisher, so no
|
|
// reason to run it for topics and publishers.
|
|
m := mqttBenchDefaultMatrix.
|
|
NoTopics().
|
|
NoPublishers()
|
|
|
|
b.Run("PUBSUB", func(b *testing.B) {
|
|
m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) {
|
|
bc.runAndReport(b, "pubsub",
|
|
"--qos", strconv.Itoa(bc.QOS),
|
|
"--messages", strconv.Itoa(b.N),
|
|
"--size", strconv.Itoa(bc.MessageSize),
|
|
"--subscribers", strconv.Itoa(bc.Subscribers),
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
// makes a copy of bc
|
|
func (bc mqttBenchContext) benchmarkSubRet(b *testing.B) {
|
|
// This test uses a a built-in publisher, and it makes most sense to measure
|
|
// the retained message delivery "overhead" on a QoS0 subscription; without
|
|
// the extra time involved in actually subscribing.
|
|
m := mqttBenchDefaultMatrix.
|
|
NoPublishers().
|
|
QOS0Only()
|
|
|
|
b.Run("SUBRET", func(b *testing.B) {
|
|
m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) {
|
|
bc.runAndReport(b, "subret",
|
|
"--qos", strconv.Itoa(bc.QOS),
|
|
"--topics", strconv.Itoa(bc.Topics), // number of retained messages
|
|
"--subscribers", strconv.Itoa(bc.Subscribers),
|
|
"--size", strconv.Itoa(bc.MessageSize),
|
|
"--repeat", strconv.Itoa(b.N), // number of subscribe requests
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (bc mqttBenchContext) runAndReport(b *testing.B, name string, extraArgs ...string) {
|
|
b.Helper()
|
|
r := mqttRunExCommandTest(b, name, mqttNewDial("", "", bc.Host, bc.Port, ""), extraArgs...)
|
|
r.report(b)
|
|
}
|
|
|
|
func (bc *mqttBenchContext) startServer(b *testing.B, disableRMSCache bool) func() {
|
|
b.Helper()
|
|
b.StopTimer()
|
|
prevDisableRMSCache := testDisableRMSCache
|
|
testDisableRMSCache = disableRMSCache
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(b, o)
|
|
|
|
o = s.getOpts()
|
|
bc.Host = o.MQTT.Host
|
|
bc.Port = o.MQTT.Port
|
|
mqttInitTestServer(b, mqttNewDial("", "", bc.Host, bc.Port, ""))
|
|
return func() {
|
|
testMQTTShutdownServer(s)
|
|
testDisableRMSCache = prevDisableRMSCache
|
|
}
|
|
}
|
|
|
|
func (bc *mqttBenchContext) startCluster(b *testing.B, disableRMSCache bool) func() {
|
|
b.Helper()
|
|
b.StopTimer()
|
|
prevDisableRMSCache := testDisableRMSCache
|
|
testDisableRMSCache = disableRMSCache
|
|
conf := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
stream_replicas: 3
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
|
|
cl := createJetStreamClusterWithTemplate(b, conf, "MQTT", 3)
|
|
o := cl.randomNonLeader().getOpts()
|
|
bc.Host = o.MQTT.Host
|
|
bc.Port = o.MQTT.Port
|
|
mqttInitTestServer(b, mqttNewDial("", "", bc.Host, bc.Port, ""))
|
|
return func() {
|
|
cl.shutdown()
|
|
testDisableRMSCache = prevDisableRMSCache
|
|
}
|
|
}
|
|
|
|
func mqttBenchWrapForMatrixField(
|
|
vFieldPtr *int,
|
|
arr []int,
|
|
f func(b *testing.B, bc *mqttBenchContext),
|
|
namef func(int) string,
|
|
) func(b *testing.B, bc *mqttBenchContext) {
|
|
if len(arr) == 0 {
|
|
return f
|
|
}
|
|
return func(b *testing.B, bc *mqttBenchContext) {
|
|
for _, value := range arr {
|
|
*vFieldPtr = value
|
|
b.Run(namef(value), func(b *testing.B) {
|
|
f(b, bc)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m mqttBenchMatrix) runMatrix(b *testing.B, bc mqttBenchContext, f func(*testing.B, *mqttBenchContext)) {
|
|
b.Helper()
|
|
f = mqttBenchWrapForMatrixField(&bc.MessageSize, m.MessageSize, f, func(size int) string {
|
|
return sizeKB(size)
|
|
})
|
|
f = mqttBenchWrapForMatrixField(&bc.Topics, m.Topics, f, func(n int) string {
|
|
return fmt.Sprintf("%dtopics", n)
|
|
})
|
|
f = mqttBenchWrapForMatrixField(&bc.Publishers, m.Publishers, f, func(n int) string {
|
|
return fmt.Sprintf("%dpubc", n)
|
|
})
|
|
f = mqttBenchWrapForMatrixField(&bc.Subscribers, m.Subscribers, f, func(n int) string {
|
|
return fmt.Sprintf("%dsubc", n)
|
|
})
|
|
f = mqttBenchWrapForMatrixField(&bc.QOS, m.QOS, f, func(qos int) string {
|
|
return fmt.Sprintf("QOS%d", qos)
|
|
})
|
|
b.ResetTimer()
|
|
b.StartTimer()
|
|
f(b, &bc)
|
|
}
|
|
|
|
func (m mqttBenchMatrix) NoSubscribers() mqttBenchMatrix {
|
|
m.Subscribers = nil
|
|
return m
|
|
}
|
|
|
|
func (m mqttBenchMatrix) NoTopics() mqttBenchMatrix {
|
|
m.Topics = nil
|
|
return m
|
|
}
|
|
|
|
func (m mqttBenchMatrix) NoPublishers() mqttBenchMatrix {
|
|
m.Publishers = nil
|
|
return m
|
|
}
|
|
|
|
func (m mqttBenchMatrix) QOS0Only() mqttBenchMatrix {
|
|
m.QOS = []int{0}
|
|
return m
|
|
}
|
|
|
|
func (m mqttBenchMatrix) QOS1Only() mqttBenchMatrix {
|
|
m.QOS = []int{1}
|
|
return m
|
|
}
|
|
|
|
func sizeKB(size int) string {
|
|
unit := "B"
|
|
N := size
|
|
if size >= KB {
|
|
unit = "KB"
|
|
N = (N + KB/2) / KB
|
|
}
|
|
return fmt.Sprintf("%d%s", N, unit)
|
|
}
|
|
|
|
func (r MQTTBenchmarkResult) report(b *testing.B) {
|
|
// Disable the default ns metric in favor of custom X_ms/op.
|
|
b.ReportMetric(0, "ns/op")
|
|
|
|
// Disable MB/s since the github benchmarking action misinterprets the sign
|
|
// of the result (treats it as less is better).
|
|
b.SetBytes(0)
|
|
// b.SetBytes(r.Bytes)
|
|
|
|
for unit, ns := range r.NS {
|
|
nsOp := float64(ns) / float64(r.Ops)
|
|
b.ReportMetric(nsOp/1000000, unit+"_ms/op")
|
|
}
|
|
// Diable ReportAllocs() since it confuses the github benchmarking action
|
|
// with the noise.
|
|
// b.ReportAllocs()
|
|
}
|