Skip to content

Issues with Windows timeout behavior #55

@mlsvrts

Description

@mlsvrts

So I am having some issues with performance when reading interrupted byte streams from the serial port. Consider this example:

Cargo.toml

[package]
name = "async-serial-test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", features = ["io-util", "macros", "rt-multi-thread"]}
tokio-serial = "5.4.1"

main.rs

use std::time;
use tokio::{task, io::{self, AsyncReadExt, AsyncWriteExt}};
use tokio_serial::SerialPortBuilderExt;

#[tokio::main]
async fn main() {
    let mut args = std::env::args();
    let path = args.nth(1).expect("specify the serial port to connect to!");
    let bytes = args.next()
        .map_or(Ok(1024), |s| usize::from_str_radix(s.as_str(), 10))
        .expect("expected a number of bytes as the second argument");

    let port = tokio_serial::new(&path, 115200)
        .open_native_async()
        .expect("failed to open the system serial port");

    let (mut reader, mut writer) = io::split(port);
    
    let message = vec![13u8; bytes];
    let expected = message.len();

    let read = task::spawn(async move {
        let mut buf = [0u8; 1];

        let mut total = 0;
        let mut timestamp;
        let now = time::Instant::now();
        loop {
            match reader.read(&mut buf).await {
                Ok(n) => {
                    total += n;
                    timestamp = now.elapsed().as_secs_f32();
                    // println!("[{}] read {} bytes: {:?}", timestamp, n, &buf[..n])
                    if total >= expected { break; }
                },
                Err(e) => {
                    println!("serial read encountered an error: {:?}", e);
                    panic!("failed to read any data from the io device");
                }
            }
        }

        println!("read {} bytes in {}s --> {}b/s", total, timestamp, total as f32 / timestamp);
    });

    writer.write(&message).await
        .expect("failed to write to the serial port");

    println!("waiting for read to complete...");
    
    tokio::try_join!(read)
        .expect("failed to join read task");
}

If we run this program we can see the following:

C:\>.\build\async-serial-test.exe COM1 8192
waiting for read to complete...
read 8192 bytes in 0.7650555s --> 10707.72b/s

C:\>.\build\async-serial-test.exe COM1 8191
waiting for read to complete...
read 8191 bytes in 0.78255415s --> 10467.007b/s

It takes longer (by ~17ms (!)) to write/read one byte less of data! If we examine the syscalls, I think we can find the culprit:

comm_timeouts

The serial library is issuing read requests for 4096 bytes, which of course time out when only 4095 bytes are available -- the problem is that this timeout should* be 1ms, but in practice appears to be quite long.

*based on: https://github.com/berkowski/mio-serial/blob/8cb54a5667e66632016db5ee5b9977c67713ceec/src/lib.rs#L810

So this leaves me with two questions:

  1. Do you know how/why COMMTIMEOUTS seems to be basically ignored? I have done some searching, and it seems like the answer to this question is that no-one actually knows what this windows code really does.
  2. How is the read-request size set? Can I use an different API to poll for smaller chunks of bytes? PuTTy polls for 1-byte reads, for instance; this way the pending read is returned as soon as there is more data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions