Skip to content

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 uasyncio to 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
  • uasyncio basics 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

  1. Install Thonny from: https://thonny.org
  2. Connect the Pico 2 W via USB.
  3. In Thonny, select:
  4. Tools > Options > Interpreter
  5. Choose MicroPython (Raspberry Pi Pico)
  6. Select the correct port
  7. 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.py on the device to auto-run after boot.
  • Use the Thonny shell to stop a running script with Ctrl+C.

Session Outline

  1. What responsiveness means in embedded async systems
  2. Measuring time with time.ticks_ms() and time.ticks_us()
  3. Identifying blocking code and its impact
  4. Tuning uasyncio tasks for fairness and latency
  5. Hands-on lab: responsive button monitor with performance logging
  6. 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 milliseconds
  • time.ticks_us() for microseconds
  • time.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 while loops without await
  • 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
  • uasyncio works well when tasks cooperate
  • Use time.ticks_ms() and time.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

  1. Why is await asyncio.sleep(...) better than time.sleep(...) in async code?
  2. What does time.ticks_diff() protect you from?
  3. Why should time-critical tasks avoid excessive printing?
  4. What is the effect of await asyncio.sleep(0)?
  5. 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

Back to Chapter | Back to Master Plan | Previous Session