Skip to content

Commit 2ed6dd0

Browse files
authored
Added monitor rate-limiting functionality (#1221)
* Added Null monitor for testing * Added monitor rate limiting * Added terminal example to exercise rate-limiting functionality * Removed leftover and removed useless call to fmt.Errorf
1 parent ab92281 commit 2ed6dd0

File tree

7 files changed

+655
-50
lines changed

7 files changed

+655
-50
lines changed

arduino/monitors/null.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2021 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package monitors
17+
18+
import (
19+
"log"
20+
"time"
21+
)
22+
23+
// NullMonitor outputs zeros at a constant rate and discards anything sent
24+
type NullMonitor struct {
25+
started time.Time
26+
sent int
27+
bps float64
28+
}
29+
30+
// OpenNullMonitor creates a monitor that outputs the same character at a fixed
31+
// rate.
32+
func OpenNullMonitor(bytesPerSecondRate float64) *NullMonitor {
33+
log.Printf("Started streaming at %f\n", bytesPerSecondRate)
34+
return &NullMonitor{
35+
started: time.Now(),
36+
bps: bytesPerSecondRate,
37+
}
38+
}
39+
40+
// Close the connection
41+
func (mon *NullMonitor) Close() error {
42+
return nil
43+
}
44+
45+
// Read bytes from the port
46+
func (mon *NullMonitor) Read(bytes []byte) (int, error) {
47+
for {
48+
elapsed := time.Now().Sub(mon.started).Seconds()
49+
n := int(elapsed*mon.bps) - mon.sent
50+
if n == 0 {
51+
// Delay until the next char...
52+
time.Sleep(time.Millisecond)
53+
continue
54+
}
55+
if len(bytes) < n {
56+
n = len(bytes)
57+
}
58+
mon.sent += n
59+
for i := 0; i < n; i++ {
60+
bytes[i] = 0
61+
}
62+
return n, nil
63+
}
64+
}
65+
66+
// Write bytes to the port
67+
func (mon *NullMonitor) Write(bytes []byte) (int, error) {
68+
// Discard all chars
69+
return len(bytes), nil
70+
}

commands/daemon/monitor.go

+78-20
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
package daemon
1717

1818
import (
19-
"fmt"
19+
"errors"
2020
"io"
21+
"sync/atomic"
2122

2223
"github.com/arduino/arduino-cli/arduino/monitors"
2324
rpc "github.com/arduino/arduino-cli/rpc/monitor"
@@ -39,7 +40,7 @@ func (s *MonitorService) StreamingOpen(stream rpc.Monitor_StreamingOpenServer) e
3940
// ensure it's a config message and not data
4041
config := msg.GetMonitorConfig()
4142
if config == nil {
42-
return fmt.Errorf("first message must contain monitor configuration, not data")
43+
return errors.New("first message must contain monitor configuration, not data")
4344
}
4445

4546
// select which type of monitor we need
@@ -61,13 +62,34 @@ func (s *MonitorService) StreamingOpen(stream rpc.Monitor_StreamingOpenServer) e
6162
if mon, err = monitors.OpenSerialMonitor(config.GetTarget(), int(baudRate)); err != nil {
6263
return err
6364
}
65+
66+
case rpc.MonitorConfig_NULL:
67+
if addCfg, ok := config.GetAdditionalConfig().AsMap()["OutputRate"]; !ok {
68+
mon = monitors.OpenNullMonitor(100.0) // 100 bytes per second as default
69+
} else if outputRate, ok := addCfg.(float64); !ok {
70+
return errors.New("OutputRate in Null monitor must be a float64")
71+
} else {
72+
// get the Monitor instance
73+
mon = monitors.OpenNullMonitor(outputRate)
74+
}
6475
}
6576

6677
// we'll use these channels to communicate with the goroutines
6778
// handling the stream and the target respectively
6879
streamClosed := make(chan error)
6980
targetClosed := make(chan error)
7081

82+
// set rate limiting window
83+
bufferSize := int(config.GetRecvRateLimitBuffer())
84+
rateLimitEnabled := (bufferSize > 0)
85+
if !rateLimitEnabled {
86+
bufferSize = 1024
87+
}
88+
buffer := make([]byte, bufferSize)
89+
bufferUsed := 0
90+
91+
var writeSlots int32
92+
7193
// now we can read the other messages and re-route to the monitor...
7294
go func() {
7395
for {
@@ -84,6 +106,11 @@ func (s *MonitorService) StreamingOpen(stream rpc.Monitor_StreamingOpenServer) e
84106
break
85107
}
86108

109+
if rateLimitEnabled {
110+
// Increase rate limiter write slots
111+
atomic.AddInt32(&writeSlots, msg.GetRecvAcknowledge())
112+
}
113+
87114
if _, err := mon.Write(msg.GetData()); err != nil {
88115
// error writing to target
89116
targetClosed <- err
@@ -94,27 +121,58 @@ func (s *MonitorService) StreamingOpen(stream rpc.Monitor_StreamingOpenServer) e
94121

95122
// ...and read from the monitor and forward to the output stream
96123
go func() {
97-
buf := make([]byte, 8)
124+
dropBuffer := make([]byte, 10240)
125+
dropped := 0
98126
for {
99-
n, err := mon.Read(buf)
100-
if err != nil {
101-
// error reading from target
102-
targetClosed <- err
103-
break
127+
if bufferUsed < bufferSize {
128+
if n, err := mon.Read(buffer[bufferUsed:]); err != nil {
129+
// error reading from target
130+
targetClosed <- err
131+
break
132+
} else if n == 0 {
133+
// target was closed
134+
targetClosed <- nil
135+
break
136+
} else {
137+
bufferUsed += n
138+
}
139+
} else {
140+
// FIXME: a very rare condition but still...
141+
// we may be waiting here while, in the meantime, a transmit slot is
142+
// freed: in this case the (filled) buffer will stay in the server
143+
// until the following Read exits (-> the next char arrives from the
144+
// monitor).
145+
146+
if n, err := mon.Read(dropBuffer); err != nil {
147+
// error reading from target
148+
targetClosed <- err
149+
break
150+
} else if n == 0 {
151+
// target was closed
152+
targetClosed <- nil
153+
break
154+
} else {
155+
dropped += n
156+
}
104157
}
105158

106-
if n == 0 {
107-
// target was closed
108-
targetClosed <- nil
109-
break
110-
}
111-
112-
if err = stream.Send(&rpc.StreamingOpenResp{
113-
Data: buf[:n],
114-
}); err != nil {
115-
// error sending to stream
116-
streamClosed <- err
117-
break
159+
slots := atomic.LoadInt32(&writeSlots)
160+
if !rateLimitEnabled || slots > 0 {
161+
if err = stream.Send(&rpc.StreamingOpenResp{
162+
Data: buffer[:bufferUsed],
163+
Dropped: int32(dropped),
164+
}); err != nil {
165+
// error sending to stream
166+
streamClosed <- err
167+
break
168+
}
169+
bufferUsed = 0
170+
dropped = 0
171+
172+
// Rate limit, filling all the available window
173+
if rateLimitEnabled {
174+
slots = atomic.AddInt32(&writeSlots, -1)
175+
}
118176
}
119177
}
120178
}()

commands/daemon/term_example/go.mod

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/arduino/arduino-cli/term_example
2+
3+
go 1.16
4+
5+
replace github.com/arduino/arduino-cli => ../../..
6+
7+
require (
8+
github.com/arduino/arduino-cli v0.0.0-20200109150215-ffa84fdaab21
9+
google.golang.org/grpc v1.27.0
10+
google.golang.org/protobuf v1.25.0
11+
)

0 commit comments

Comments
 (0)