Skip to content

Commit 6619c5d

Browse files
committed
Add uasyncio monitor v3/as_demos/monitor.
1 parent 3c8817d commit 6619c5d

File tree

6 files changed

+447
-0
lines changed

6 files changed

+447
-0
lines changed

v3/as_demos/monitor/README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# 1. A uasyncio monitor
2+
3+
This library provides a means of examining the behaviour of a running
4+
`uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The
5+
latter displays the behaviour of the host by pin changes and/or optional print
6+
statements. Communication with the Pico is uni-directional via a UART so only a
7+
single GPIO pin is used - at last a use for the ESP8266 transmit-only UART(1).
8+
9+
A logic analyser or scope provides an insight into the way an asynchronous
10+
application is working.
11+
12+
Where an application runs multiple concurrent tasks it can be difficult to
13+
locate a task which is hogging CPU time. Long blocking periods can also result
14+
from several tasks each of which can block for a period. If, on occasion, these
15+
are scheduled in succession, the times can add. The monitor issues a trigger
16+
when the blocking period exceeds a threshold. With a logic analyser the system
17+
state at the time of the transient event may be examined.
18+
19+
The following image shows the `quick_test.py` code being monitored at the point
20+
when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line
21+
02 shows the fast running `hog_detect` task which cannot run at the time of the
22+
trigger. Lines 01 and 03 show the `foo` and `bar` tasks.
23+
![Image](/.monitor.jpg)
24+
25+
## 1.1 Pre-requisites
26+
27+
The device being monitored must run firmware V1.17 or later. The `uasyncio`
28+
version should be V3 (as included in the firmware).
29+
30+
## 1.2 Usage
31+
32+
Example script `quick_test.py` provides a usage example.
33+
34+
An application to be monitored typically has the following setup code:
35+
```python
36+
from monitor import monitor, hog_detect, set_uart
37+
set_uart(2) # Define device under test UART no.
38+
```
39+
40+
Coroutines to be monitored are prefixed with the `@monitor` decorator:
41+
```python
42+
@monitor(2, 3)
43+
async def my_coro():
44+
# code
45+
```
46+
The decorator args are as follows:
47+
1. A unique `ident` for the code being monitored. Determines the pin number on
48+
the Pico. See [Pico Pin mapping](./README.md#3-pico-pin-mapping).
49+
2. An optional arg defining the maximum number of concurrent instances of the
50+
task to be independently monitored (default 1).
51+
52+
Whenever the code runs, a pin on the Pico will go high, and when the code
53+
terminates it will go low. This enables the behaviour of the system to be
54+
viewed on a logic analyser or via console output on the Pico. This behavior
55+
works whether the code terminates normally, is cancelled or has a timeout.
56+
57+
In the example above, when `my_coro` starts, the pin defined by `ident==2`
58+
(GPIO 4) will go high. When it ends, the pin will go low. If, while it is
59+
running, a second instance of `my_coro` is launched, the next pin (GPIO 5) will
60+
go high. Pins will go low when the relevant instance terminates, is cancelled,
61+
or times out. If more instances are started than were specified to the
62+
decorator, a warning will be printed on the host. All excess instances will be
63+
associated with the final pin (`pins[ident + max_instances - 1]`) which will
64+
only go low when all instances associated with that pin have terminated.
65+
66+
Consequently if `max_instances=1` and multiple instances are launched, a
67+
warning will appear on the host; the pin will go high when the first instance
68+
starts and will not go low until all have ended.
69+
70+
## 1.3 Detecting CPU hogging
71+
72+
A common cause of problems in asynchronous code is the case where a task blocks
73+
for a period, hogging the CPU, stalling the scheduler and preventing other
74+
tasks from running. Determining the task responsible can be difficult.
75+
76+
The pin state only indicates that the task is running. A pin state of 1 does
77+
not imply CPU hogging. Thus
78+
```python
79+
@monitor(3)
80+
async def long_time():
81+
await asyncio.sleep(30)
82+
```
83+
will cause the pin to go high for 30s, even though the task is consuming no
84+
resources for that period.
85+
86+
To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This
87+
has `ident=0` and, if used, is monitored on GPIO 2. It loops, yielding to the
88+
scheduler. It will therefore be scheduled in round-robin fashion at speed. If
89+
long gaps appear in the pulses on GPIO 2, other tasks are hogging the CPU.
90+
Usage of this is optional. To use, issue
91+
```python
92+
import uasyncio as asyncio
93+
from monitor import monitor, hog_detect
94+
# code omitted
95+
asyncio.create_task(hog_detect())
96+
# code omitted
97+
```
98+
To aid in detecting the gaps in execution, the Pico code implements a timer.
99+
This is retriggered by activity on `ident=0`. If it times out, a brief high
100+
going pulse is produced on pin 28, along with the console message "Hog". The
101+
pulse can be used to trigger a scope or logic analyser. The duration of the
102+
timer may be adjusted - see [section 4](./README.md~4-the-pico-code).
103+
104+
# 2. Monitoring synchronous code
105+
106+
In general there are easier ways to debug synchronous code. However in the
107+
context of a monitored asynchronous application there may be a need to view the
108+
timing of synchronous code. Functions and methods may be monitored either in
109+
the declaration via a decorator or when called via a context manager.
110+
111+
## 2.1 The mon_func decorator
112+
113+
This works as per the asynchronous decorator, but without the `max_instances`
114+
arg. This will activate the GPIO associated with ident 20 for the duration of
115+
every call to `sync_func()`:
116+
```python
117+
@mon_func(20)
118+
def sync_func():
119+
pass
120+
```
121+
122+
## 2.2 The mon_call context manager
123+
124+
This may be used to monitor a function only when called from specific points in
125+
the code.
126+
```python
127+
def another_sync_func():
128+
pass
129+
130+
with mon_call(22):
131+
another_sync_func()
132+
```
133+
134+
It is advisable not to use the context manager with a function having the
135+
`mon_func` decorator. The pin and report behaviour is confusing.
136+
137+
# 3. Pico Pin mapping
138+
139+
The Pico GPIO numbers start at 2 to allow for UART(0) and also have a gap where
140+
GPIO's are used for particular purposes. This is the mapping between `ident`
141+
GPIO no. and Pico PCB pin, with the pins for the timer and the UART link also
142+
identified:
143+
144+
| ident | GPIO | pin |
145+
|:-----:|:----:|:----:|
146+
| uart | 1 | 2 |
147+
| 0 | 2 | 4 |
148+
| 1 | 3 | 5 |
149+
| 2 | 4 | 6 |
150+
| 3 | 5 | 7 |
151+
| 4 | 6 | 9 |
152+
| 5 | 7 | 10 |
153+
| 6 | 8 | 11 |
154+
| 7 | 9 | 12 |
155+
| 8 | 10 | 14 |
156+
| 9 | 11 | 15 |
157+
| 10 | 12 | 16 |
158+
| 11 | 13 | 17 |
159+
| 12 | 14 | 19 |
160+
| 13 | 15 | 20 |
161+
| 14 | 16 | 21 |
162+
| 15 | 17 | 22 |
163+
| 16 | 18 | 24 |
164+
| 17 | 19 | 25 |
165+
| 18 | 20 | 26 |
166+
| 19 | 21 | 27 |
167+
| 20 | 22 | 29 |
168+
| 21 | 26 | 31 |
169+
| 22 | 27 | 32 |
170+
| timer | 28 | 34 |
171+
172+
The host's UART `txd` pin should be connected to Pico GPIO 1 (pin 2). There
173+
must be a link between `Gnd` pins on the host and Pico.
174+
175+
# 4. The Pico code
176+
177+
Monitoring of the UART with default behaviour is started as follows:
178+
```python
179+
from monitor_pico import run
180+
run()
181+
```
182+
By default the Pico does not produce console output and the timer has a period
183+
of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These
184+
behaviours can be modified by the following `run` args:
185+
1. `period=100` Define the timer period in ms.
186+
2. `verbose=()` Determines which `ident` values should produce console output.
187+
188+
Thus to run such that idents 4 and 7 produce console output, with hogging
189+
reported if blocking is for more than 60ms, issue
190+
```python
191+
from monitor_pico import run
192+
run(60, (4, 7))
193+
```
194+
195+
# 5. Design notes
196+
197+
The use of decorators is intended to ease debugging: they are readily turned on
198+
and off by commenting out.
199+
200+
The Pico was chosen for extremely low cost. It has plenty of GPIO pins and no
201+
underlying OS to introduce timing uncertainties.
202+
203+
Symbols transmitted by the UART are printable ASCII characters to ease
204+
debugging. A single byte protocol simplifies and speeds the Pico code.
205+
206+
The baudrate of 1Mbps was chosen to minimise latency (10μs per character is
207+
fast in the context of uasyncio). It also ensures that tasks like `hog_detect`,
208+
which can be scheduled at a high rate, can't overflow the UART buffer. The
209+
1Mbps rate seems widely supported.

v3/as_demos/monitor/monitor.jpg

49.5 KB
Loading

v3/as_demos/monitor/monitor.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# monitor.py
2+
# Monitor an asynchronous program by sending single bytes down an interface.
3+
4+
# Copyright (c) 2021 Peter Hinch
5+
# Released under the MIT License (MIT) - see LICENSE file
6+
7+
import uasyncio as asyncio
8+
from machine import UART
9+
10+
uart = None
11+
def set_uart(n): # Monitored app defines interface
12+
global uart
13+
uart = UART(n, 1_000_000)
14+
15+
_available = set(range(0, 23)) # Valid idents are 0..22
16+
17+
def _validate(ident, num=1):
18+
if ident >= 0 and ident + num <= 23:
19+
try:
20+
for x in range(ident, ident + num):
21+
_available.remove(x)
22+
except KeyError:
23+
raise ValueError(f'Monitor error - ident {x:02} already allocated.')
24+
else:
25+
raise ValueError(f'Monitor error - ident {ident:02} out of range.')
26+
27+
28+
def monitor(n, max_instances=1):
29+
def decorator(coro):
30+
# This code runs before asyncio.run()
31+
_validate(n, max_instances)
32+
instance = 0
33+
async def wrapped_coro(*args, **kwargs):
34+
# realtime
35+
nonlocal instance
36+
d = 0x40 + n + min(instance, max_instances - 1)
37+
v = bytes(chr(d), 'utf8')
38+
instance += 1
39+
if instance > max_instances:
40+
print(f'Monitor {n:02} max_instances reached')
41+
uart.write(v)
42+
try:
43+
res = await coro(*args, **kwargs)
44+
except asyncio.CancelledError:
45+
raise
46+
finally:
47+
d |= 0x20
48+
v = bytes(chr(d), 'utf8')
49+
uart.write(v)
50+
instance -= 1
51+
return res
52+
return wrapped_coro
53+
return decorator
54+
55+
# Optionally run this to show up periods of blocking behaviour
56+
@monitor(0)
57+
async def _do_nowt():
58+
await asyncio.sleep_ms(0)
59+
60+
async def hog_detect():
61+
while True:
62+
await _do_nowt()
63+
64+
# Monitor a synchronous function definition
65+
def mon_func(n):
66+
def decorator(func):
67+
_validate(n)
68+
dstart = 0x40 + n
69+
vstart = bytes(chr(dstart), 'utf8')
70+
dend = 0x60 + n
71+
vend = bytes(chr(dend), 'utf8')
72+
def wrapped_func(*args, **kwargs):
73+
uart.write(vstart)
74+
res = func(*args, **kwargs)
75+
uart.write(vend)
76+
return res
77+
return wrapped_func
78+
return decorator
79+
80+
81+
# Monitor a synchronous function call
82+
class mon_call:
83+
def __init__(self, n):
84+
_validate(n)
85+
self.n = n
86+
self.dstart = 0x40 + n
87+
self.vstart = bytes(chr(self.dstart), 'utf8')
88+
self.dend = 0x60 + n
89+
self.vend = bytes(chr(self.dend), 'utf8')
90+
91+
def __enter__(self):
92+
uart.write(self.vstart)
93+
return self
94+
95+
def __exit__(self, type, value, traceback):
96+
uart.write(self.vend)
97+
return False # Don't silence exceptions

v3/as_demos/monitor/monitor_pico.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# monitor_pico.py
2+
# Runs on a Raspberry Pico board to receive data from monitor.py
3+
4+
# Copyright (c) 2021 Peter Hinch
5+
# Released under the MIT License (MIT) - see LICENSE file
6+
7+
# UART gets a single ASCII byte defining the pin number and whether
8+
# to increment (uppercase) or decrement (lowercase) the use count.
9+
# Pin goes high if use count > 0 else low.
10+
# incoming numbers are 0..22 which map onto 23 GPIO pins
11+
12+
from machine import UART, Pin, Timer
13+
14+
# Valid GPIO pins
15+
# GP0,1 are UART 0 so pins are 2..22, 26..27
16+
PIN_NOS = list(range(2,23)) + list(range(26, 28))
17+
uart = UART(0, 1_000_000) # rx on GP1
18+
19+
pin_t = Pin(28, Pin.OUT)
20+
def _cb(_):
21+
pin_t(1)
22+
print('Hog')
23+
pin_t(0)
24+
25+
tim = Timer()
26+
t_ms = 100
27+
# Index is incoming ID
28+
# contents [Pin, instance_count, verbose]
29+
pins = []
30+
for pin_no in PIN_NOS:
31+
pins.append([Pin(pin_no, Pin.OUT), 0, False])
32+
33+
def run(period=100, verbose=[]):
34+
global t_ms
35+
t_ms = period
36+
for x in verbose:
37+
pins[x][2] = True
38+
while True:
39+
while not uart.any():
40+
pass
41+
x = ord(uart.read(1))
42+
#print('got', chr(x)) gets CcAa
43+
if not 0x40 <= x <= 0x7f: # Get an initial 0
44+
continue
45+
if x == 0x40:
46+
tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb)
47+
i = x & 0x1f # Key: 0x40 (ord('@')) is pin ID 0
48+
d = -1 if x & 0x20 else 1
49+
pins[i][1] += d
50+
if pins[i][1]: # Count > 0 turn pin on
51+
pins[i][0](1)
52+
else:
53+
pins[i][0](0)
54+
if pins[i][2]:
55+
print(f'ident {i} count {pins[i][1]}')

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy