Session 4: Measuring Responsiveness and Tuning Performance
Synopsis
Introduces practical ways to observe scheduling delays, task timing, CPU usage patterns, and bottlenecks affecting overall system responsiveness.
Session Content
Session 4: Measuring Responsiveness and Tuning Performance
Duration: ~45 minutes
Audience: Python developers with basic programming knowledge
Platform: Raspberry Pi Pico 2 W
Language: MicroPython
IDE: Thonny
Session Goals
By the end of this session, learners will be able to:
- Measure task latency and responsiveness in MicroPython on the Pico 2 W
- Identify common causes of poor responsiveness in asynchronous applications
- Use
uasyncioto keep the system responsive under load - Apply simple performance tuning strategies for timing-sensitive Pico projects
- Build a small async demo that measures loop timing while handling I/O
Prerequisites
- Raspberry Pi Pico 2 W running MicroPython
- Thonny installed on a PC/Mac/Linux machine
- USB cable for power and programming
- Basic familiarity with:
- Python functions and loops
- GPIO input/output
uasynciobasics from earlier sessions
Required Hardware
- Raspberry Pi Pico 2 W
- Onboard LED
- 1 push button
- 1 resistor (optional if using internal pull-up; not required)
- Breadboard and jumper wires
Development Environment Setup
Thonny Setup
- Install Thonny from: https://thonny.org
- Connect the Pico 2 W via USB.
- In Thonny, select:
- Tools > Options > Interpreter
- Choose MicroPython (Raspberry Pi Pico)
- Select the correct port
- Confirm the REPL works by entering:
print("Hello, Pico 2 W")
Expected output:
Hello, Pico 2 W
MicroPython Runtime Notes
- Use the latest stable Pico MicroPython firmware when possible.
- Save scripts as
main.pyon the device to auto-run after boot. - Use the Thonny shell to stop a running script with Ctrl+C.
Session Outline
- What responsiveness means in embedded async systems
- Measuring time with
time.ticks_ms()andtime.ticks_us() - Identifying blocking code and its impact
- Tuning
uasynciotasks for fairness and latency - Hands-on lab: responsive button monitor with performance logging
- Review and discussion
1) Theory: What Responsiveness Means
In embedded systems, responsiveness is the time between an event and the system reacting to it.
Examples: - A button press should be detected quickly - A blinking LED should not freeze when another task runs - A network operation should not stop sensor reads - A UI should remain usable while background work continues
In uasyncio, responsiveness depends on:
- How often tasks yield control with
await - Whether tasks avoid long blocking operations
- The size and frequency of delays like
await asyncio.sleep(...) - Whether I/O is done in a non-blocking way
Common Symptoms of Poor Responsiveness
- LED blinking pauses unexpectedly
- Button presses are missed
- Serial output appears in bursts instead of smoothly
- The board feels “frozen” during network calls
- Timers drift or become inaccurate
2) Measuring Time in MicroPython
MicroPython provides functions for measuring elapsed time safely:
time.ticks_ms()for millisecondstime.ticks_us()for microsecondstime.ticks_diff(a, b)for safe subtraction across wraparound
Example: Measuring a Delay
import time
start = time.ticks_ms()
time.sleep_ms(100)
end = time.ticks_ms()
elapsed = time.ticks_diff(end, start)
print("Elapsed ms:", elapsed)
Expected output:
Elapsed ms: 100
Why ticks_diff() Matters
Do not subtract ticks directly. Use:
time.ticks_diff(end, start)
This is safe even when counters wrap around.
3) Blocking vs Non-Blocking Behavior
Blocking Example
import time
from machine import Pin
led = Pin("LED", Pin.OUT)
while True:
led.toggle()
time.sleep(1)
This is fine for simple blinking, but in a larger program it blocks everything else for 1 second at a time.
Async-Friendly Version
import uasyncio as asyncio
from machine import Pin
led = Pin("LED", Pin.OUT)
async def blink():
while True:
led.toggle()
await asyncio.sleep(1)
asyncio.run(blink())
This task yields control back to the scheduler every second, allowing other tasks to run.
4) Tuning Performance in Async Applications
Best Practices
- Keep each task short and cooperative
- Use
await asyncio.sleep(0)to yield if doing repeated work - Avoid long
whileloops withoutawait - Minimize heavy string formatting inside tight loops
- Reduce unnecessary prints in time-critical loops
- Use
ticks_ms()for periodic work instead of busy-waiting
Example: Cooperative Work Chunking
import uasyncio as asyncio
async def count_task():
total = 0
for i in range(100000):
total += i
if i % 1000 == 0:
await asyncio.sleep(0) # Yield to other tasks
print("Done:", total)
asyncio.run(count_task())
5) Hands-On Lab: Responsive Button Monitor with Performance Logging
Objective
Build an async application that:
- Blinks the onboard LED
- Monitors a button press
- Measures task loop intervals
- Reports responsiveness over serial output
Wiring
Use the Pico 2 W onboard LED and one push button:
- One side of the button to GP14
- Other side of the button to GND
- Use internal pull-up in software
If you prefer a different pin, update the code accordingly.
Code: Responsive Monitor
Save as main.py:
import time
import uasyncio as asyncio
from machine import Pin
# ----------------------------
# Hardware setup
# ----------------------------
led = Pin("LED", Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)
# ----------------------------
# Shared state
# ----------------------------
button_pressed = False
# ----------------------------
# Task 1: Blink LED
# ----------------------------
async def blink_led():
while True:
led.toggle()
await asyncio.sleep_ms(500)
# ----------------------------
# Task 2: Monitor button
# ----------------------------
async def monitor_button():
global button_pressed
last_state = button.value()
while True:
current_state = button.value()
# Detect a falling edge: released (1) -> pressed (0)
if last_state == 1 and current_state == 0:
button_pressed = True
last_state = current_state
await asyncio.sleep_ms(20) # Simple debounce and yield
# ----------------------------
# Task 3: Performance logger
# ----------------------------
async def log_timing():
"""
Measures how long it takes between iterations of this task.
This gives a rough view of scheduling responsiveness.
"""
last_time = time.ticks_ms()
while True:
await asyncio.sleep_ms(1000)
now = time.ticks_ms()
elapsed = time.ticks_diff(now, last_time)
last_time = now
print("Logger interval ms:", elapsed)
# ----------------------------
# Task 4: React to button presses
# ----------------------------
async def handle_button_events():
global button_pressed
while True:
if button_pressed:
button_pressed = False
print("Button pressed! LED is currently:", led.value())
await asyncio.sleep_ms(10)
# ----------------------------
# Main entry point
# ----------------------------
async def main():
print("Starting responsive async demo...")
print("Press the button on GP14 to trigger an event.")
await asyncio.gather(
blink_led(),
monitor_button(),
log_timing(),
handle_button_events()
)
asyncio.run(main())
Expected Output
When the script starts, you may see:
Starting responsive async demo...
Press the button on GP14 to trigger an event.
Logger interval ms: 1000
Logger interval ms: 1000
Button pressed! LED is currently: 1
Logger interval ms: 1000
What to Observe
- The LED continues blinking while the button is monitored
- The logger prints once per second
- Button presses are detected without freezing the board
- Responsiveness remains steady because each task yields frequently
6) Exercise: Measure the Effect of a Blocking Task
Task
Modify the application to add a fake “heavy” task that blocks for 2 seconds using time.sleep(2).
Example Blocking Task
import time
import uasyncio as asyncio
async def bad_task():
while True:
print("Starting blocking work...")
time.sleep(2) # Bad: blocks the event loop
print("Blocking work finished")
await asyncio.sleep(1)
Observation Questions
- What happens to the LED blink timing?
- Are button presses delayed?
- Does the logger still print every second?
- How does the system feel compared with the cooperative version?
Improvement Task
Replace the blocking sleep with:
await asyncio.sleep(2)
Then compare behavior.
7) Exercise: Tune a Busy Loop
Task
Create a task that does some computation in chunks and yields periodically.
import uasyncio as asyncio
async def chunked_work():
total = 0
for i in range(50000):
total += i
if i % 500 == 0:
await asyncio.sleep(0)
print("Total:", total)
asyncio.run(chunked_work())
Questions
- What happens if you remove
await asyncio.sleep(0)? - How does yielding affect responsiveness?
- Can the device still handle other tasks smoothly?
8) Practical Tuning Tips for Pico 2 W Projects
Timing Tips
- Use
await asyncio.sleep_ms(...)for periodic tasks - Keep polling intervals realistic; 10–50 ms is enough for many buttons/sensors
- Avoid printing inside every loop iteration
- Separate fast tasks from slow tasks
- Use simple state flags for communication between tasks
Network and IoT Considerations
For Wi-Fi-connected projects: - Connection setup may take time; do it once at startup - Avoid frequent reconnect attempts in tight loops - Separate sensor sampling from network publishing - Buffer readings if the network is temporarily unavailable
9) Optional Extension: Latency Measurement Task
This example measures the delay between a scheduled wake-up and actual execution.
import time
import uasyncio as asyncio
async def latency_monitor():
while True:
scheduled = time.ticks_ms()
await asyncio.sleep_ms(100)
actual = time.ticks_ms()
latency = time.ticks_diff(actual, scheduled) - 100
print("Approx scheduling delay ms:", latency)
asyncio.run(latency_monitor())
Example Output
Approx scheduling delay ms: 0
Approx scheduling delay ms: 1
Approx scheduling delay ms: 0
10) Mini Review
Key Takeaways
- Responsiveness is critical in embedded async systems
uasyncioworks well when tasks cooperate- Use
time.ticks_ms()andtime.ticks_diff()to measure timing - Blocking code harms latency and should be avoided
- Small design choices greatly affect real-time behavior on Pico 2 W
11) Check for Understanding
- Why is
await asyncio.sleep(...)better thantime.sleep(...)in async code? - What does
time.ticks_diff()protect you from? - Why should time-critical tasks avoid excessive printing?
- What is the effect of
await asyncio.sleep(0)? - How can you test whether your Pico application is responsive?
12) Suggested Homework
Homework 1: Responsive Sensor Logger
Create an async program that: - Reads a button or sensor periodically - Blinks the onboard LED - Prints timestamps for each reading - Remains responsive while doing all three
Homework 2: Compare Two Versions
Write two versions of the same program:
- One using blocking calls
- One using uasyncio
Then compare:
- LED smoothness
- Button response time
- Console output regularity
13) Reference Resources
- MicroPython Wiki: https://github.com/micropython/micropython/wiki
- MicroPython Quick Reference: https://docs.micropython.org/en/latest/rp2/quickref.html
- Raspberry Pi MicroPython Guide: https://www.raspberrypi.com/documentation/microcontrollers/micropython.html
- Raspberry Pi Pico 2 W Documentation: https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html#pico2