aedes 0.51.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -0
- package/.github/actions/sticky-pr-comment/action.yml +55 -0
- package/.github/workflows/benchmark-compare-serial.yml +60 -0
- package/.github/workflows/ci.yml +12 -17
- package/.release-it.json +18 -0
- package/.taprc +15 -6
- package/README.md +6 -4
- package/aedes.d.ts +0 -6
- package/aedes.js +270 -242
- package/benchmarks/README.md +33 -0
- package/benchmarks/pingpong.js +94 -25
- package/benchmarks/receiver.js +77 -0
- package/benchmarks/report.js +150 -0
- package/benchmarks/runBenchmarks.js +118 -0
- package/benchmarks/sender.js +86 -0
- package/benchmarks/server.js +19 -18
- package/checkVersion.js +20 -0
- package/docs/Aedes.md +66 -8
- package/docs/Client.md +3 -4
- package/docs/Examples.md +39 -22
- package/docs/MIGRATION.md +50 -0
- package/eslint.config.js +8 -0
- package/example.js +51 -40
- package/examples/clusters/index.js +28 -23
- package/examples/clusters/package.json +10 -6
- package/lib/client.js +405 -306
- package/lib/handlers/connect.js +42 -38
- package/lib/handlers/index.js +9 -11
- package/lib/handlers/ping.js +2 -3
- package/lib/handlers/puback.js +5 -5
- package/lib/handlers/publish.js +29 -14
- package/lib/handlers/pubrec.js +9 -17
- package/lib/handlers/pubrel.js +34 -25
- package/lib/handlers/subscribe.js +47 -43
- package/lib/handlers/unsubscribe.js +16 -19
- package/lib/qos-packet.js +14 -17
- package/lib/utils.js +5 -12
- package/lib/write.js +4 -5
- package/package.json +139 -136
- package/test/auth.js +468 -804
- package/test/basic.js +613 -575
- package/test/bridge.js +44 -40
- package/test/client-pub-sub.js +531 -504
- package/test/close_socket_by_other_party.js +137 -102
- package/test/connect.js +487 -484
- package/test/drain-timeout.js +593 -0
- package/test/drain-toxiproxy.js +620 -0
- package/test/events.js +173 -145
- package/test/helper.js +351 -73
- package/test/keep-alive.js +40 -67
- package/test/meta.js +257 -210
- package/test/not-blocking.js +93 -197
- package/test/qos1.js +464 -554
- package/test/qos2.js +308 -393
- package/test/regr-21.js +39 -21
- package/test/require.cjs +22 -0
- package/test/retain.js +349 -398
- package/test/topics.js +176 -183
- package/test/types/aedes.test-d.ts +4 -8
- package/test/will.js +310 -428
- package/types/instance.d.ts +40 -35
- package/types/packet.d.ts +10 -10
- package/.coveralls.yml +0 -1
- package/benchmarks/bombing.js +0 -34
- package/benchmarks/bombingQoS1.js +0 -36
- package/benchmarks/throughputCounter.js +0 -23
- package/benchmarks/throughputCounterQoS1.js +0 -33
- package/types/.eslintrc.json +0 -47
package/benchmarks/pingpong.js
CHANGED
|
@@ -1,36 +1,115 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const mode = require('compute-mode')
|
|
6
|
-
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 })
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import { hrtime } from 'node:process'
|
|
4
|
+
import mqtt from 'mqtt'
|
|
7
5
|
const interval = 5000
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
function processsLatencies (latencies, counter) {
|
|
8
|
+
let total = 0
|
|
9
|
+
let count = 0
|
|
10
|
+
let median
|
|
11
|
+
let perc95
|
|
12
|
+
let perc99
|
|
13
|
+
const posMedian = Math.floor(counter / 2)
|
|
14
|
+
const pos95 = Math.floor(counter * 0.95)
|
|
15
|
+
const pos99 = Math.floor(counter * 0.99)
|
|
16
|
+
// sort keys from smallest to largest
|
|
17
|
+
const keys = Object.keys(latencies).sort((a, b) => a - b)
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const value = latencies[key]
|
|
20
|
+
total += value * key
|
|
21
|
+
count += value
|
|
22
|
+
|
|
23
|
+
if (count >= posMedian && median === undefined) {
|
|
24
|
+
median = key
|
|
25
|
+
}
|
|
26
|
+
if (count >= pos95 && perc95 === undefined) {
|
|
27
|
+
perc95 = key
|
|
28
|
+
}
|
|
29
|
+
if (count >= pos99 && perc99 === undefined) {
|
|
30
|
+
perc99 = key
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
buckets: keys.length,
|
|
35
|
+
mean: Math.floor(total / counter),
|
|
36
|
+
minimum: keys[0],
|
|
37
|
+
maximum: keys.pop(),
|
|
38
|
+
median,
|
|
39
|
+
perc95,
|
|
40
|
+
perc99,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let counter = 0
|
|
45
|
+
let latencies = {}
|
|
46
|
+
|
|
47
|
+
const { values } = parseArgs({
|
|
48
|
+
options: {
|
|
49
|
+
qos: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: '0',
|
|
52
|
+
choices: ['0', '1'],
|
|
53
|
+
description: 'QoS level to use for publishing messages',
|
|
54
|
+
short: 'q'
|
|
55
|
+
},
|
|
56
|
+
help: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
default: false,
|
|
59
|
+
description: 'Show this help message',
|
|
60
|
+
short: 'h'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (values.help) {
|
|
66
|
+
console.log('Usage: node pingpong.js [options]')
|
|
67
|
+
console.log('Options:')
|
|
68
|
+
console.log(' -q, --qos <0|1> QoS level to use for publishing messages (default: 0)')
|
|
69
|
+
console.log(' -h, --help Show this help message')
|
|
70
|
+
process.exit(0)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!process.send) {
|
|
74
|
+
console.error(`Starting pingpong with options: qos=${values.qos}`)
|
|
75
|
+
}
|
|
76
|
+
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 })
|
|
77
|
+
|
|
78
|
+
const qosOpts = {
|
|
79
|
+
qos: parseInt(values.qos, 10),
|
|
80
|
+
}
|
|
11
81
|
|
|
12
82
|
function count () {
|
|
13
|
-
|
|
14
|
-
|
|
83
|
+
// reset latencies while keeping the counts
|
|
84
|
+
|
|
85
|
+
const latencyResult = processsLatencies(latencies, counter)
|
|
86
|
+
latencies = {}
|
|
87
|
+
if (process.send) {
|
|
88
|
+
process.send({ type: 'latency', data: latencyResult })
|
|
89
|
+
} else {
|
|
90
|
+
console.log('latencies', latencyResult)
|
|
91
|
+
}
|
|
92
|
+
counter = 0
|
|
15
93
|
}
|
|
16
94
|
|
|
17
95
|
setInterval(count, interval)
|
|
18
96
|
|
|
19
97
|
function publish () {
|
|
20
|
-
|
|
21
|
-
client.publish('test',
|
|
98
|
+
counter++
|
|
99
|
+
client.publish('test', process.hrtime.bigint().toString(), qosOpts)
|
|
22
100
|
}
|
|
23
101
|
|
|
24
102
|
function subscribe () {
|
|
25
|
-
client.subscribe('test',
|
|
103
|
+
client.subscribe('test', qosOpts, publish)
|
|
26
104
|
}
|
|
27
105
|
|
|
28
106
|
client.on('connect', subscribe)
|
|
29
107
|
client.on('message', publish)
|
|
30
108
|
client.on('message', function (topic, payload) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
109
|
+
const receivedAt = hrtime.bigint()
|
|
110
|
+
const sentAt = BigInt(payload.toString())
|
|
111
|
+
const msDiff = Math.floor(Number(receivedAt - sentAt) / 1e6) // Convert from nanoseconds to milliseconds
|
|
112
|
+
latencies[msDiff] = (latencies[msDiff] || 0) + 1
|
|
34
113
|
})
|
|
35
114
|
|
|
36
115
|
client.on('offline', function () {
|
|
@@ -41,13 +120,3 @@ client.on('error', function () {
|
|
|
41
120
|
console.log('reconnect!')
|
|
42
121
|
client.stream.end()
|
|
43
122
|
})
|
|
44
|
-
|
|
45
|
-
process.on('SIGINT', function () {
|
|
46
|
-
const total = latencies.reduce(function (acc, num) {
|
|
47
|
-
return acc + num
|
|
48
|
-
})
|
|
49
|
-
console.log('total', total)
|
|
50
|
-
console.log('average', total / latencies.length)
|
|
51
|
-
console.log('mode', mode(latencies))
|
|
52
|
-
process.exit(0)
|
|
53
|
-
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import mqtt from 'mqtt'
|
|
4
|
+
const interval = 5000
|
|
5
|
+
|
|
6
|
+
let counter = 0
|
|
7
|
+
let previousSerial
|
|
8
|
+
|
|
9
|
+
const { values } = parseArgs({
|
|
10
|
+
options: {
|
|
11
|
+
qos: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
default: '0',
|
|
14
|
+
choices: ['0', '1'],
|
|
15
|
+
description: 'QoS level to use for publishing messages',
|
|
16
|
+
short: 'q'
|
|
17
|
+
},
|
|
18
|
+
help: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
default: false,
|
|
21
|
+
description: 'Show this help message',
|
|
22
|
+
short: 'h'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (values.help) {
|
|
28
|
+
console.log('Usage: node receiver.js [options]')
|
|
29
|
+
console.log('Options:')
|
|
30
|
+
console.log(' -q, --qos <0|1> QoS level to use for publishing messages (default: 0)')
|
|
31
|
+
console.log(' -h, --help Show this help message')
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 })
|
|
36
|
+
|
|
37
|
+
if (!process.send) {
|
|
38
|
+
console.error(`Starting receiver with options: qos=${values.qos}`)
|
|
39
|
+
}
|
|
40
|
+
const subscribeOpts = {
|
|
41
|
+
qos: parseInt(values.qos, 10),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function count () {
|
|
45
|
+
if (process.send) {
|
|
46
|
+
const rate = Math.floor(counter / interval * 1000)
|
|
47
|
+
process.send({ type: 'rate', data: rate })
|
|
48
|
+
} else {
|
|
49
|
+
console.log('received/s', counter / interval * 1000)
|
|
50
|
+
}
|
|
51
|
+
counter = 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setInterval(count, interval)
|
|
55
|
+
|
|
56
|
+
client.on('connect', function () {
|
|
57
|
+
this.subscribe('test', subscribeOpts)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
client.handleMessage = function (packet, done) {
|
|
61
|
+
const serial = BigInt(packet.payload.toString())
|
|
62
|
+
if (previousSerial !== undefined && (serial !== (previousSerial + 1n))) {
|
|
63
|
+
console.error(`Received out of order message: expected ${previousSerial}, got ${serial}`)
|
|
64
|
+
}
|
|
65
|
+
previousSerial = serial
|
|
66
|
+
counter++
|
|
67
|
+
done()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
client.on('offline', function () {
|
|
71
|
+
console.log('offline')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
client.on('error', function () {
|
|
75
|
+
console.log('reconnect!')
|
|
76
|
+
client.stream.end()
|
|
77
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'readline'
|
|
4
|
+
// if any average is more than threshold% off we exit with 1
|
|
5
|
+
const threshold = 10
|
|
6
|
+
let failed = false
|
|
7
|
+
|
|
8
|
+
const defaultUnit = 'msg/s' // default unit for the results
|
|
9
|
+
const units = {
|
|
10
|
+
'pingpong.js': 'ms'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseConfig (config) {
|
|
14
|
+
// parse config string like "QoS=0, Cores=2" into an object
|
|
15
|
+
const configObj = {}
|
|
16
|
+
const parts = config.split(',').map(part => part.trim())
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
const [key, value] = part.split('=').map(s => s.trim())
|
|
19
|
+
if (key && value) {
|
|
20
|
+
configObj[key] = value
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return configObj
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function gatherData () {
|
|
27
|
+
// read CSV data from STDIN
|
|
28
|
+
const results = {}
|
|
29
|
+
let maxCounts = 0
|
|
30
|
+
const rl = readline.createInterface({
|
|
31
|
+
input: process.stdin
|
|
32
|
+
})
|
|
33
|
+
// split each line by comma but retain commas inside quotes
|
|
34
|
+
for await (const line of rl) {
|
|
35
|
+
const fields = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g).map(s => s.replace(/^"|"$/g, ''))
|
|
36
|
+
const label = fields[0]
|
|
37
|
+
if (!label) {
|
|
38
|
+
continue // skips empty lines
|
|
39
|
+
}
|
|
40
|
+
const benchmark = fields[1]
|
|
41
|
+
const config = fields[2]
|
|
42
|
+
const parsedConfig = parseConfig(config)
|
|
43
|
+
const value = Number(fields[3])
|
|
44
|
+
const key = `${benchmark} QoS${parsedConfig.QoS}`
|
|
45
|
+
if (!results[label]) {
|
|
46
|
+
results[label] = {}
|
|
47
|
+
}
|
|
48
|
+
const resultsL2 = results[label]
|
|
49
|
+
if (!resultsL2[key]) {
|
|
50
|
+
resultsL2[key] = {
|
|
51
|
+
values: [],
|
|
52
|
+
benchmark,
|
|
53
|
+
config,
|
|
54
|
+
unit: units[benchmark] || defaultUnit
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const resultsL3 = resultsL2[key]
|
|
58
|
+
resultsL3.values.push(value)
|
|
59
|
+
if (resultsL3.values.length > maxCounts) {
|
|
60
|
+
maxCounts = resultsL3.values.length
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { results, maxCounts }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function reportPerLabel (label, results, maxCounts, avg) {
|
|
67
|
+
const roundLabels = []
|
|
68
|
+
for (let i = 0; i < maxCounts; i++) {
|
|
69
|
+
roundLabels.push(`Round ${i + 1}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`\n # Benchmark Results for ${label}`)
|
|
73
|
+
console.log(`|Benchmark | Config | Units | ${roundLabels.join(' |')}`)
|
|
74
|
+
console.log(`|----------|--------|-------|${roundLabels.map(() => '---').join('|')}`)
|
|
75
|
+
for (const key in results) {
|
|
76
|
+
const { unit, values, benchmark, config } = results[key]
|
|
77
|
+
console.log(`| ${benchmark} | ${config} | ${unit}| ${values.join(' |')}`)
|
|
78
|
+
}
|
|
79
|
+
console.log('\n')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function calculateAverages (results) {
|
|
83
|
+
const avg = {}
|
|
84
|
+
for (const label in results) {
|
|
85
|
+
const resultsL2 = results[label]
|
|
86
|
+
for (const key in resultsL2) {
|
|
87
|
+
const { unit, values, benchmark, config } = resultsL2[key]
|
|
88
|
+
if (!avg[key]) {
|
|
89
|
+
avg[key] = {}
|
|
90
|
+
}
|
|
91
|
+
avg[key][label] = {
|
|
92
|
+
value: values.reduce((acc, num) => acc + num, 0) / values.length,
|
|
93
|
+
benchmark,
|
|
94
|
+
unit,
|
|
95
|
+
config
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return avg
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function calculatePercentage (ref, value) {
|
|
103
|
+
if (ref === undefined) {
|
|
104
|
+
return { ref: value, diff: 0 }
|
|
105
|
+
}
|
|
106
|
+
const perc = ((value / ref) * 100)
|
|
107
|
+
if (perc > 100) {
|
|
108
|
+
const diff = perc - 100
|
|
109
|
+
return { ref, diff }
|
|
110
|
+
}
|
|
111
|
+
const diff = (100 - perc) * -1
|
|
112
|
+
return { ref, diff }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function reportAverages (avg) {
|
|
116
|
+
console.log('\n # Overall Benchmark Results')
|
|
117
|
+
console.log(`\n +x% is better, -x% is worse, current threshold to fail at -${threshold}%\n\n`)
|
|
118
|
+
console.log('| Label | Benchmark | Config | Average | Units | Percentage')
|
|
119
|
+
console.log('|-------|-----------|--------|---------|-------|-----------')
|
|
120
|
+
for (const key in avg) {
|
|
121
|
+
let oldRef
|
|
122
|
+
for (const label in avg[key]) {
|
|
123
|
+
const { value, unit, benchmark, config } = avg[key][label]
|
|
124
|
+
const { ref, diff } = calculatePercentage(oldRef, value)
|
|
125
|
+
oldRef = ref
|
|
126
|
+
// for unit = ms lower is better
|
|
127
|
+
const correctedDiff = unit === 'ms' ? diff * -1 : diff
|
|
128
|
+
const sign = correctedDiff > 0 ? '+' : ''
|
|
129
|
+
const perc = correctedDiff === 0 ? 100 : `${sign}${correctedDiff.toFixed(2)}`
|
|
130
|
+
if ((-1 * correctedDiff) > threshold) {
|
|
131
|
+
console.error(`\n\nError: ${key} is ${sign}${correctedDiff.toFixed(2)}% off the reference (${ref} ${unit}) which is more than the threshold of ${threshold}% `)
|
|
132
|
+
failed = true
|
|
133
|
+
}
|
|
134
|
+
console.log(`| ${label} | ${benchmark} | ${config} | ${value.toFixed(0)} | ${unit} | ${perc}%`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function report () {
|
|
140
|
+
const { results, maxCounts } = await gatherData()
|
|
141
|
+
const avg = calculateAverages(results)
|
|
142
|
+
reportAverages(avg)
|
|
143
|
+
for (const label in results) {
|
|
144
|
+
reportPerLabel(label, results[label], maxCounts)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
report().then(() => {
|
|
149
|
+
process.exit(failed ? 1 : 0)
|
|
150
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { fork, execSync } from 'node:child_process'
|
|
2
|
+
import { cpus } from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
const gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
|
|
5
|
+
const numCores = cpus().length
|
|
6
|
+
const cpuType = cpus()[0].model.trim()
|
|
7
|
+
const dirname = import.meta.dirname
|
|
8
|
+
|
|
9
|
+
const scripts = {
|
|
10
|
+
server: path.join(dirname, 'server.js'),
|
|
11
|
+
sender: path.join(dirname, 'sender.js'),
|
|
12
|
+
receiver: path.join(dirname, 'receiver.js'),
|
|
13
|
+
pingpong: path.join(dirname, 'pingpong.js'),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function spawn (script, cmdArgs = [], msgType = 'rate', args = {}) {
|
|
17
|
+
const child = fork(script, cmdArgs, { env: { ...process.env, ...args } })
|
|
18
|
+
const results = []
|
|
19
|
+
child.on('message', msg => {
|
|
20
|
+
if (msg.type === msgType) results.push(msg.data)
|
|
21
|
+
})
|
|
22
|
+
return { child, results }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function throughputTest (scripts, qos, warmupCount, maxCount) {
|
|
26
|
+
console.error('Starting test for QoS', qos)
|
|
27
|
+
await new Promise(resolve => {
|
|
28
|
+
console.error(`Starting warmup for QoS${qos}`)
|
|
29
|
+
// 1. Start server (no rates to collect)
|
|
30
|
+
const server = fork(scripts.server)
|
|
31
|
+
|
|
32
|
+
// 2. Start throughput receiver
|
|
33
|
+
const receiver = spawn(scripts.receiver, [`-q ${qos}`], 'rate')
|
|
34
|
+
|
|
35
|
+
// 3. Start throughput sender
|
|
36
|
+
const sender = spawn(scripts.sender, [`-q ${qos}`], 'rate')
|
|
37
|
+
const config = `"QoS=${qos}, Cores=${numCores}"`
|
|
38
|
+
sender.child.on('message', checkDone)
|
|
39
|
+
|
|
40
|
+
// 4. Collect and print rates
|
|
41
|
+
let counter = 0
|
|
42
|
+
function checkDone () {
|
|
43
|
+
counter++
|
|
44
|
+
process.stderr.write('.')
|
|
45
|
+
if (counter === warmupCount) {
|
|
46
|
+
console.error('\n starting measurement')
|
|
47
|
+
receiver.results.length = 0
|
|
48
|
+
sender.results.length = 0
|
|
49
|
+
}
|
|
50
|
+
if (counter === (maxCount + warmupCount)) {
|
|
51
|
+
sender.child.kill()
|
|
52
|
+
receiver.child.kill()
|
|
53
|
+
server.kill()
|
|
54
|
+
for (const result of sender.results) {
|
|
55
|
+
console.log(`${gitBranch}, sender.js,${config} , ${result}`)
|
|
56
|
+
}
|
|
57
|
+
for (const result of receiver.results) {
|
|
58
|
+
console.log(`${gitBranch}, receiver.js,${config} , ${result}`)
|
|
59
|
+
}
|
|
60
|
+
resolve()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
async function latencyTest (scripts, qos, warmupCount, maxCount, score) {
|
|
66
|
+
console.error('Starting latency test for QoS', qos)
|
|
67
|
+
await new Promise(resolve => {
|
|
68
|
+
console.error(`Starting warmup for QoS${qos}`)
|
|
69
|
+
// 1. Start server (no rates to collect)
|
|
70
|
+
const server = fork(scripts.server)
|
|
71
|
+
|
|
72
|
+
// 2. Start latency measurement
|
|
73
|
+
const pingpong = spawn(scripts.pingpong, [`-q ${qos}`], 'latency')
|
|
74
|
+
const config = `"QoS=${qos}, Cores=${numCores}, Score='${score}'"`
|
|
75
|
+
pingpong.child.on('message', checkDone)
|
|
76
|
+
|
|
77
|
+
// 4. Collect and print latency
|
|
78
|
+
let counter = 0
|
|
79
|
+
function checkDone () {
|
|
80
|
+
counter++
|
|
81
|
+
process.stderr.write('.')
|
|
82
|
+
if (counter === warmupCount) {
|
|
83
|
+
console.error('\n starting measurement')
|
|
84
|
+
pingpong.results.length = 0
|
|
85
|
+
}
|
|
86
|
+
if (counter === (maxCount + warmupCount)) {
|
|
87
|
+
pingpong.child.kill()
|
|
88
|
+
server.kill()
|
|
89
|
+
for (const result of pingpong.results) {
|
|
90
|
+
console.log(`${gitBranch}, pingpong.js ,${config} , ${result[score]}`)
|
|
91
|
+
}
|
|
92
|
+
resolve()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function main (opts) {
|
|
99
|
+
console.error(`Running benchmarks on branch: ${gitBranch} using ${cpuType} with ${numCores} cores`)
|
|
100
|
+
if (numCores < 3) {
|
|
101
|
+
console.error('WARNING: Not enough CPU cores to run proper benchmarks, at least 4 cores are recommended')
|
|
102
|
+
}
|
|
103
|
+
await throughputTest(scripts, 0, opts.warmupCount, opts.maxCount)
|
|
104
|
+
await throughputTest(scripts, 1, opts.warmupCount, opts.maxCount)
|
|
105
|
+
await latencyTest(scripts, 1, opts.warmupCount, opts.maxCount, opts.latencyScore)
|
|
106
|
+
console.error('Tests done')
|
|
107
|
+
process.exit(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const defaultopts = {
|
|
111
|
+
warmupCount: 5,
|
|
112
|
+
maxCount: 10,
|
|
113
|
+
latencyScore: 'perc95' // valid score names are mean, median, perc95 and perc99
|
|
114
|
+
}
|
|
115
|
+
main(defaultopts).catch(err => {
|
|
116
|
+
console.error('Error in main:', err)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import mqtt from 'mqtt'
|
|
4
|
+
import { parseArgs } from 'node:util'
|
|
5
|
+
const interval = 5000
|
|
6
|
+
|
|
7
|
+
let sent = 0
|
|
8
|
+
let serial = 0n
|
|
9
|
+
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
options: {
|
|
12
|
+
qos: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: '0',
|
|
15
|
+
choices: ['0', '1'],
|
|
16
|
+
description: 'QoS level to use for publishing messages',
|
|
17
|
+
short: 'q'
|
|
18
|
+
},
|
|
19
|
+
help: {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
default: false,
|
|
22
|
+
description: 'Show this help message',
|
|
23
|
+
short: 'h'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (values.help) {
|
|
29
|
+
console.log('Usage: node sender.js [options]')
|
|
30
|
+
console.log('Options:')
|
|
31
|
+
console.log(' -q, --qos <0|1> QoS level to use for publishing messages (default: 0)')
|
|
32
|
+
console.log(' -h, --help Show this help message')
|
|
33
|
+
process.exit(0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!process.send) {
|
|
37
|
+
console.error(`Starting sender with options: qos=${values.qos}`)
|
|
38
|
+
}
|
|
39
|
+
const publishOpts = {
|
|
40
|
+
qos: parseInt(values.qos, 10),
|
|
41
|
+
retain: false,
|
|
42
|
+
dup: false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 })
|
|
46
|
+
|
|
47
|
+
function count () {
|
|
48
|
+
if (process.send) {
|
|
49
|
+
const rate = Math.floor(sent / interval * 1000)
|
|
50
|
+
process.send({ type: 'rate', data: rate })
|
|
51
|
+
} else {
|
|
52
|
+
console.log('sent/s', sent / interval * 1000)
|
|
53
|
+
}
|
|
54
|
+
sent = 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setInterval(count, interval)
|
|
58
|
+
|
|
59
|
+
function immediatePublish () {
|
|
60
|
+
setImmediate(publish)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function publish () {
|
|
64
|
+
sent++
|
|
65
|
+
serial++
|
|
66
|
+
const payload = serial.toString()
|
|
67
|
+
|
|
68
|
+
client.publish('test', payload, publishOpts, immediatePublish)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
client.setMaxListeners(100)
|
|
72
|
+
|
|
73
|
+
client.on('connect', function () {
|
|
74
|
+
for (let i = 0; i < 50; i++) {
|
|
75
|
+
publish()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
client.on('offline', function () {
|
|
80
|
+
console.log('offline')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
client.on('error', function () {
|
|
84
|
+
console.log('reconnect!')
|
|
85
|
+
client.stream.end()
|
|
86
|
+
})
|
package/benchmarks/server.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import { Aedes } from '../aedes.js'
|
|
2
|
+
import { createServer } from 'net'
|
|
2
3
|
|
|
3
4
|
// To be used with cpuprofilify http://npm.im/cpuprofilify
|
|
5
|
+
Aedes.createBroker().then(aedes => {
|
|
6
|
+
const server = createServer(aedes.handle)
|
|
7
|
+
const port = 1883
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
server.listen(port, function () {
|
|
10
|
+
console.error('server listening on port', port, 'pid', process.pid)
|
|
11
|
+
})
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
aedes.on('clientError', function (client, err) {
|
|
14
|
-
console.error('client error', client.id, err.message)
|
|
15
|
-
})
|
|
13
|
+
aedes.on('clientError', function (client, err) {
|
|
14
|
+
console.error('client error', client.id, err.message)
|
|
15
|
+
})
|
|
16
16
|
|
|
17
|
-
// Cleanly shut down process on SIGTERM to ensure that perf-<pid>.map gets flushed
|
|
18
|
-
process.on('SIGINT', onSIGINT)
|
|
17
|
+
// Cleanly shut down process on SIGTERM to ensure that perf-<pid>.map gets flushed
|
|
18
|
+
process.on('SIGINT', onSIGINT)
|
|
19
19
|
|
|
20
|
-
function onSIGINT () {
|
|
20
|
+
function onSIGINT () {
|
|
21
21
|
// IMPORTANT to log on stderr, to not clutter stdout which is purely for data, i.e. dtrace stacks
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
22
|
+
console.error('Caught SIGTERM, shutting down.')
|
|
23
|
+
server.close()
|
|
24
|
+
process.exit(0)
|
|
25
|
+
}
|
|
26
|
+
})
|
package/checkVersion.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// node 20 on windows has issues with node:test wildcard
|
|
2
|
+
// this script is only used by package.json
|
|
3
|
+
// example of usage in package.json:
|
|
4
|
+
// "unit": "node checkVersion.js && npm run unit:v20win32 || npm run unit:other",
|
|
5
|
+
// "unit:v20win32": "node --test --test-timeout=180000",
|
|
6
|
+
// "unit:other": "node --test --test-timeout=180000 test/*.js test/*.cjs",
|
|
7
|
+
|
|
8
|
+
/* c8 ignore start */
|
|
9
|
+
import { platform, version, exit } from 'node:process'
|
|
10
|
+
|
|
11
|
+
const major = version.split('.')[0]
|
|
12
|
+
let exitCode = 1
|
|
13
|
+
// node 20 on windows returns 0
|
|
14
|
+
if ((major === 'v20') && platform === 'win32') {
|
|
15
|
+
exitCode = 0
|
|
16
|
+
}
|
|
17
|
+
// all the others 1
|
|
18
|
+
console.log(`Running ${major} on ${platform} returning exitCode ${exitCode}`)
|
|
19
|
+
exit(exitCode)
|
|
20
|
+
/* c8 ignore stop */
|