Skip to content

Commit 96c501e

Browse files
committed
V0.24 Fast response to cancellation and timeout.
1 parent d88e48b commit 96c501e

File tree

14 files changed

+543
-439
lines changed

14 files changed

+543
-439
lines changed

FASTPOLL.md

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
This version is a "drop in" replacement for official `uasyncio`. Existing
44
applications should run under it unchanged and with essentially identical
5-
performance.
5+
performance except that task cancellation and timeouts are expedited "soon"
6+
rather than being deferred until the task is next scheduled.
7+
8+
"Priority" features are only enabled if the event loop is instantiated with
9+
specific arguments.
610

7-
This version has the following features:
11+
This version has the following features relative to official V2.0:
12+
* Timeouts and task cancellation are handled promptly, rather than being
13+
deferred until the coroutine is next scheduled.
814
* I/O can optionally be handled at a higher priority than other coroutines
915
[PR287](https://github.com/micropython/micropython-lib/pull/287).
1016
* Tasks can yield with low priority, running when nothing else is pending.
@@ -18,7 +24,7 @@ This version has the following features:
1824
is called with a generator function
1925
[PR292](https://github.com/micropython/micropython-lib/pull/292). This traps
2026
a common coding error which otherwise results in silent failure.
21-
* The presence of the `fast_io` version can be tested at runtime.
27+
* The presence and version of the `fast_io` version can be tested at runtime.
2228
* The presence of an event loop instance can be tested at runtime.
2329
* `run_until_complete(coro())` now returns the value returned by `coro()` as
2430
per CPython
@@ -31,6 +37,12 @@ adding just one line of code. This implies that if official `uasyncio` acquires
3137
a means of prioritising I/O other than that in this version, application code
3238
changes should be minimal.
3339

40+
#### Changes incompatible with prior versions
41+
42+
V0.24
43+
The `version` bound variable now retuens a 2-tuple.
44+
45+
Prior versions.
3446
The high priority mechanism formerly provided in `asyncio_priority.py` was a
3547
workround based on the view that stream I/O written in Python would remain
3648
unsupported. This is now available so `asyncio_priority.py` is obsolete and
@@ -63,7 +75,7 @@ formerly provided by `asyncio_priority.py` is now implemented.
6375
The basic approach is to install and test `uasyncio` on the target hardware.
6476
Replace `core.py` and `__init__.py` with the files in the `fast_io` directory.
6577

66-
The current MicroPython release build (1.9.4) has `uasyncio` implemented as a
78+
The current MicroPython release build (1.10) has `uasyncio` implemented as a
6779
frozen module. The following options for installing `fast_io` exist:
6880

6981
1. Use a daily build, install `uasyncio` as per the tutorial then replace the
@@ -75,33 +87,43 @@ frozen module. The following options for installing `fast_io` exist:
7587
bytecode. If this is deleted and appended to the end, frozen files will only
7688
be found if there is no match in the filesystem.
7789

90+
```python
91+
import sys
92+
sys.path.append(sys.path.pop(0)) # Prefer modules in filesystem
93+
```
94+
7895
See [ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the
7996
suitability of ESP platforms for systems requiring fast response.
8097

8198
## 1.1 Benchmarks
8299

83-
The benchmarks directory contains files demonstrating the performance gains
84-
offered by prioritisation. They also offer illustrations of the use of these
85-
features. Documentation is in the code.
100+
The following files demonstrate the performance gains offered by prioritisation
101+
and the improvements to task cancellation and timeouts. They also show the use
102+
of these features. Documentation is in the code.
86103

104+
Tests and benchmarks to run against the official and `fast_io` versions:
87105
* `benchmarks/latency.py` Shows the effect on latency with and without low
88106
priority usage.
89107
* `benchmarks/rate.py` Shows the frequency with which uasyncio schedules
90108
minimal coroutines (coros).
91109
* `benchmarks/rate_esp.py` As above for ESP32 and ESP8266.
110+
* `fast_io/ms_timer.py` An I/O device driver providing a timer with higher
111+
precision timing than `wait_ms()` when run under the `fast_io` version.
112+
* `fast_io/ms_timer_test.py` Test/demo program for above.
113+
* `fast_io/pin_cb.py` An I/O device driver which causes a pin state change to
114+
trigger a callback. This is a driver, not an executable test program.
115+
* `fast_io/pin_cb_test.py` Demo of above driver: illustrates performance gain
116+
under `fast_io`.
117+
118+
Tests requiring the current version of the `fast_io` fork:
92119
* `benchmarks/rate_fastio.py` Measures the rate at which coros can be scheduled
93120
if the fast I/O mechanism is used but no I/O is pending.
94-
* `benchmarks/call_lp.py` Demos low priority callbacks.
121+
* `benchmarks/call_lp.py` Demo of low priority callbacks.
95122
* `benchmarks/overdue.py` Demo of maximum overdue feature.
96123
* `benchmarks/priority_test.py` Cancellation of low priority coros.
97-
* `fast_io/ms_timer.py` Provides higher precision timing than `wait_ms()`.
98-
* `fast_io/ms_timer_test.py` Test/demo program for above.
99-
* `fast_io/pin_cb.py` Demo of an I/O device driver which causes a pin state
100-
change to trigger a callback.
101-
* `fast_io/pin_cb_test.py` Demo of above.
102-
103-
With the exceptions of `call_lp`, `priority` and `rate_fastio`, benchmarks can
104-
be run against the official and priority versions of usayncio.
124+
* `fast_io/fast_can_test.py` Demo of cancellation of paused tasks.
125+
* `fast_io/iorw_can.py` Cancellation of task waiting on I/O.
126+
* `fast_io/iorw_to.py` Timeouts applies to tasks waiting on I/O.
105127

106128
# 2. Rationale
107129

@@ -325,8 +347,8 @@ See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks)
325347
## 3.3 Other Features
326348

327349
Variable:
328-
* `version` Contains 'fast_io'. Enables the presence of this version to be
329-
determined at runtime.
350+
* `version` Returns a 2-tuple. Current contents ('fast_io', '0.24'). Enables
351+
the presence and realease state of this version to be determined at runtime.
330352

331353
Function:
332354
* `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has
@@ -348,6 +370,8 @@ bar = Bar() # Constructor calls get_event_loop()
348370
# and renders these args inoperative
349371
loop = asyncio.get_event_loop(runq_len=40, waitq_len=40)
350372
```
373+
This is mainly for retro-fitting to existing classes and functions. The
374+
preferred approach is to pass the event loop to classes as a constructor arg.
351375

352376
###### [Contents](./FASTPOLL.md#contents)
353377

@@ -415,8 +439,8 @@ priority task to become overdue by more than 1s.
415439
### 3.4.1 Task Cancellation and Timeouts
416440

417441
Tasks which yield in a low priority manner may be subject to timeouts or be
418-
cancelled in the same way as normal tasks. See [Task cancellation](./TUTORIAL.md#36-task-cancellation)
419-
and [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts).
442+
cancelled in the same way as normal tasks. See [Task cancellation](./TUTORIAL.md#521-task-cancellation)
443+
and [Coroutines with timeouts](./TUTORIAL.md#522-coroutines-with-timeouts).
420444

421445
###### [Contents](./FASTPOLL.md#contents)
422446

@@ -468,22 +492,27 @@ Support was finally [added here](https://github.com/micropython/micropython/pull
468492

469493
# 6. Performance
470494

471-
This version is designed to enable existing applications to run without change
472-
to code and to minimise the effect on raw scheduler performance in the case
473-
where the added functionality is unused.
495+
The `fast_io` version is designed to enable existing applications to run
496+
unchanged and to minimise the effect on raw scheduler performance in cases
497+
where the priority functionality is unused.
498+
499+
The benchmark `rate.py` measures the rate at which tasks can be scheduled;
500+
`rate_fastio` is identical except it instantiates an I/O queue and a low
501+
priority queue. The benchmarks were run on a Pyboard V1.1 under official
502+
`uasyncio` V2 and under the current `fast_io` version V0.24. Results were as
503+
follows:
474504

475-
The benchmark `rate.py` measures the rate at which tasks can be scheduled. It
476-
was run (on a Pyboard V1.1) under official `uasyncio` V2, then under this
477-
version. The benchmark `rate_fastio` is identical except it instantiates an I/O
478-
queue and a low priority queue. Results were as follows.
505+
| Script | Uasyncio version | Period (100 coros) | Overhead | PBD |
506+
|:------:|:----------------:|:------------------:|:--------:|:---:|
507+
| rate | Official V2 | 156μs | 0% | 123μs |
508+
| rate | fast_io | 162μs | 3.4% | 129μs |
509+
| rate_fastio | fast_io | 206μs | 32% | 181μs |
479510

480-
| Script | Uasyncio version | Period (100 coros) | Overhead |
481-
|:------:|:----------------:|:------------------:|:--------:|
482-
| rate | Official V2 | 156μs | 0% |
483-
| rate | fast_io | 162μs | 3.4% |
484-
| rate_fastio | fast_io | 206μs | 32% |
511+
The last column shows times from a Pyboard D SF2W.
485512

486513
If an I/O queue is instantiated I/O is polled on every scheduler iteration
487514
(that is its purpose). Consequently there is a significant overhead. In
488515
practice the overhead will increase with the number of I/O devices being
489516
polled and will be determined by the efficiency of their `ioctl` methods.
517+
518+
Timings for current `fast_io` V0.24 and the original version were identical.

PRIMITIVES.md

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ obvious workround is to produce a version with unused primitives removed.
3838
4.3 [Class NamedTask](./PRIMITIVES.md#43-class-namedtask) Associate tasks with names for cancellation.
3939
4.3.1 [Latency and Barrier objects](./PRIMITIVES.md#431-latency-and-barrier-objects)
4040
4.3.2 [Custom cleanup](./PRIMITIVES.md#432-custom-cleanup)
41-
4.3.3 [Changes](./PRIMITIVES.md#433-changes) June 2018 asyn API changes affecting cancellation.
4241

4342
## 1.1 Synchronisation Primitives
4443

@@ -456,18 +455,46 @@ loop.run_until_complete(main(loop))
456455

457456
# 4. Task Cancellation
458457

459-
This has been under active development. Existing users please see
460-
[Changes](./PRIMITIVES.md#433-changes) for recent API changes.
458+
All current `uasyncio` versions have a `cancel(coro)` function. This works by
459+
throwing an exception to the coro in a special way: cancellation is deferred
460+
until the coro is next scheduled. This mechanism works with nested coros.
461461

462-
`uasyncio` now provides a `cancel(coro)` function. This works by throwing an
463-
exception to the coro in a special way: cancellation is deferred until the coro
464-
is next scheduled. This mechanism works with nested coros. However there is a
465-
limitation. If a coro issues `await uasyncio.sleep(secs)` or
466-
`await uasyncio.sleep_ms(ms)` scheduling will not occur until the time has
467-
elapsed. This introduces latency into cancellation which matters in some
468-
use-cases. Other potential sources of latency take the form of slow code.
469-
`uasyncio` has no mechanism for verifying when cancellation has actually
470-
occurred. The `asyn.py` library provides solutions in the form of two classes.
462+
There is a limitation with official `uasyncio` V2.0. In this version a coro
463+
which is waiting on a `sleep()` or `sleep_ms()` or pending I/O will not get the
464+
exception until it is next scheduled. This means that cancellation can take a
465+
long time: there is often a need to be able to verify when this has occurred.
466+
467+
This problem can now be circumvented in two ways both involving running
468+
unofficial code. The solutions fix the problem by ensuring that the cancelled
469+
coro is scheduled promptly. Assuming `my_coro` is coded normally the following
470+
will ensure that cancellation is complete, even if `my_coro` is paused at the
471+
time of cancellation:
472+
```python
473+
my_coro_instance = my_coro()
474+
loop.add_task(my_coro_instance)
475+
# Do something
476+
asyncio.cancel(my_coro_instance)
477+
await asyncio.sleep(0)
478+
# The task is now cancelled
479+
```
480+
The unofficial solutions are:
481+
* To run the `fast_io` version of `uasyncio` presented her, with official
482+
MicroPython firmware.
483+
* To run [Paul Sokolovsky's Pycopy firmware fork](https://github.com/pfalcon/pycopy)
484+
plus `uasyncio` V2.4 from
485+
[Paul Sokolovsky's library fork](https://github.com/pfalcon/micropython-lib)
486+
487+
The following describes workrounds for those wishing to run official code (for
488+
example the current realease build which includes `uasyncio` V2.0). There is
489+
usually a need to establish when cancellation has occured: the classes and
490+
decorators described below facilitate this.
491+
492+
If a coro issues `await uasyncio.sleep(secs)` or `await uasyncio.sleep_ms(ms)`
493+
scheduling will not occur until the time has elapsed. This introduces latency
494+
into cancellation which matters in some use-cases. Other potential sources of
495+
latency take the form of slow code. `uasyncio` V2.0 has no mechanism for
496+
verifying when cancellation has actually occurred. The `asyn.py` library
497+
provides solutions in the form of two classes.
471498

472499
These are `Cancellable` and `NamedTask`. The `Cancellable` class allows the
473500
creation of named groups of tasks which may be cancelled as a group; this
@@ -759,18 +786,4 @@ async def foo():
759786
return True # Normal exit
760787
```
761788

762-
### 4.3.3 Changes
763-
764-
The `NamedTask` class has been rewritten as a subclass of `Cancellable`. This
765-
is to simplify the code and to ensure accuracy of the `is_running` method. The
766-
latest API changes are:
767-
* `Cancellable.stopped()` is no longer a public method.
768-
* `NamedTask.cancel()` is now asynchronous.
769-
* `NamedTask` and `Cancellable` coros no longer receive a `TaskId` instance as
770-
their 1st arg.
771-
* `@asyn.namedtask` still works but is now an alias for `@asyn.cancellable`.
772-
773-
The drive to simplify code comes from the fact that `uasyncio` is itself under
774-
development. Tracking changes is an inevitable headache.
775-
776789
###### [Contents](./PRIMITIVES.md#contents)

README.md

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,25 @@ This repository comprises the following parts.
3535

3636
# 2. Version and installation of uasyncio
3737

38-
As of 24th Dec 2018 Paul Sokolovsky has released uasyncio V2.2. This version
38+
Paul Sokolovsky (`uasyncio` author) has released `uasyncio` V2.4. This version
3939
is on PyPi and requires his [Pycopy](https://github.com/pfalcon/micropython)
40-
fork of MicroPython.
40+
fork of MicroPython firmware. His `uasyncio` code may also be found in
41+
[his fork of micropython-lib](https://github.com/pfalcon/micropython-lib).
4142

4243
I support only the official build of MicroPython. The library code guaranteed
4344
to work with this build is in [micropython-lib](https://github.com/micropython/micropython-lib).
44-
Most of the resources in here should work with Paul's forks (the great majority
45-
work with CPython). I am unlikely to fix issues which are only evident in an
46-
unofficial fork.
45+
Most of the resources in here should work with Paul's forks (most work with
46+
CPython).
4747

48-
The documentation and code in this repository assume `uasyncio` version
49-
2.0.x, the version in [micropython-lib](https://github.com/micropython/micropython-lib).
50-
This requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O
51-
mechanism requires firmware after 17th June 2018.
48+
Most documentation and code in this repository assumes the current official
49+
version of `uasyncio`. This is V2.0 from
50+
[micropython-lib](https://github.com/micropython/micropython-lib).
51+
If release build of MicroPython V1.10 or later is used, V2.0 is incorporated
52+
and no installation is required. Some examples illustrate the features of the
53+
`fast_io` fork and therefore require this version.
5254

5355
See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for
54-
installation instructions.
56+
installation instructions where a realease build is not used.
5557

5658
# 3. uasyncio development state
5759

@@ -114,27 +116,22 @@ providing greater control over scheduling behaviour.
114116

115117
To take advantage of the reduced latency device drivers should be written to
116118
employ stream I/O. To operate at low latency they are simply run under the
117-
`fast_io` version. The [tutorial](./TUTORIAL.md#54-writing-streaming-device-drivers)
119+
`fast_io` version. The [tutorial](./TUTORIAL.md#64-writing-streaming-device-drivers)
118120
has details of how to write streaming drivers.
119121

122+
The current `fast_io` version 0.24 fixes an issue with task cancellation and
123+
timeouts. In version 2.0, where a coroutine is waiting on a `sleep()` or on
124+
I/O, a timeout or cancellation are deferred until the coroutine is next
125+
scheduled. This introduces uncertainty into when the coroutine is stopped. This
126+
issue is also addressed in Paul Sokolovsky's fork.
127+
120128
## 4.1 A Pyboard-only low power module
121129

122130
This is documented [here](./lowpower/README.md). In essence a Python file is
123131
placed on the device which configures the `fast_io` version of `uasyncio` to
124132
reduce power consumption at times when it is not busy. This provides a means of
125133
using `uasyncio` in battery powered projects.
126134

127-
## 4.2 Historical note
128-
129-
This repo formerly included `asyncio_priority.py` which is obsolete. Its main
130-
purpose was to provide a means of servicing fast hardware devices by means of
131-
coroutines running at a high priority. This was essentially a workround.
132-
133-
The official firmware now includes
134-
[this major improvement](https://github.com/micropython/micropython/pull/3836)
135-
which offers a much more efficient way of achieving the same end using stream
136-
I/O and efficient polling using `select.poll`.
137-
138135
# 5. The asyn.py library
139136

140137
This library ([docs](./PRIMITIVES.md)) provides 'micro' implementations of the

0 commit comments

Comments
 (0)