#File io with timeout

1 messages · Page 1 of 1 (latest)

junior delta
#

i'm trying to optimize this reading function (which reads from /dev/hvc0), but ive hit what the limit of what i know about file I/O, does anyone have any good pointers for optimizing this? most time is spent in readFile so thats my current main focus, /dev/hvc0 never hits EOF, if you try to read when theres no data it just waits until theres more data, and each chunk of data is prefixed and suffixed by a null byte, i cant think of a way to do this more perfomantly unless theres a way to like do a timeout read where it times out after like 10ms of no data, then i can just read the size of my whole buffer, and let it time out, since a single read that times out after a short while is probably faster than a bunch of readByte, im fine linking against libc if this is something not provided by the zig stdlib, ive looked into the stdlib and didnt see anything for file reading timeouts
each readData call takes like 450ms

#
    ///Reads data from the bus, caller owns returned memory
    fn readData(self: Bus, allocator: std.mem.Allocator) ![]const u8 {
        var section = beginProfileSection(@src());
        defer section.endProfileSection();

        var reader = self.file.reader();

        if (try reader.readByte() == 0) {
            var len: usize = 0;
            var capacity: usize = 2048;
            var result = try allocator.alloc(u8, capacity);

            var b: u8 = try reader.readByte();
            while (b != 0) {
                result[len] = b;
                len += 1;

                if (len >= capacity) {
                    var section1 = beginProfileSectionManual("makeReadBigger");
                    defer section1.endProfileSection();

                    capacity += 2048;
                    //Allocate a new array
                    var new = try allocator.alloc(u8, capacity);
                    //Copy the old data into the new array
                    std.mem.copy(u8, new, result);
                    //Free the old data
                    allocator.free(result);
                    result = new;
                }

                b = try reader.readByte();
            }

            return result[0..len];
        }

        //if no data was read, return 0
        return &.{};
    }
fossil tusk
#

It seems odd to continuously allocate and free a buffer. Could just use an ArrayList(u8) and use .resize(count), .items, and return list.toOwnedSlice().

junior delta
#

makeReadBigger is never hit, the practical max limit ive ever seen from /dev/hvc0 is like 1500 chars and the make bigger is only there as a last resort

fossil tusk
#

Gotcha.

#

FWIW, readByte is inefficient on unbuffered streams.

#

That might be it.

#

I'm not sure how this reader works in practice, but you could just read a blob of data, and then AFTERWARDS scan it for the nulls.

#

Maybe using an arraylist to buffer it up until you've found one.

junior delta
#

its reading from this file

junior delta
fossil tusk
junior delta
#

the only thing this app does is read a bunch of data as fast as possible, putting it on another thread i dont think would help when 95% of runtime is spent reading said data, theres almost nothing else to do inbetween reading data and the next time i have to read data, same with non-blocking, unless itd let me do a timeout thing

#

since a single readByte is slow on unbuffered streams, ideally id have it read as much data as possible from the buffer using normal read, then timeout after ~10ms of no data

fossil tusk
junior delta
#

well if it blocks then i cant read the data because execution doesnt continue

fossil tusk
#

Right - but there's no data to process, otherwise it would have returned it, right?

#

Or am I missing something?

junior delta
#

say /dev/hvc0 has 1050 bytes for me to read
i call read with a 2048 byte buffer, it will not unblock until its read 2048 bytes

#

no more bytes will come until i send a command to /dev/hvc0

#

so its reached a deadlock

fossil tusk
junior delta
#

yeah i tested it

fossil tusk
#

That description is what readAll is meant to do, because it keeps looping until it's filled it.

#

What type of device is this that you're reading from?

junior delta
#

hypervisor console

#

i can double check again

fossil tusk
#

Is there something special that you are meant to do in order to discover how many bytes are available to read?

junior delta
#

you read until you hit a null byte, then you know its done

fossil tusk
#

Because it seems very strange to me that it would require that you give it EXACTLY the buffer of the right size.

fossil tusk
junior delta
#

yes

junior delta
#

yup it hangs

fossil tusk
#

That's very strange.

#

reader.read() on a std.fs.File ultimately just invokes the read syscall.

#

At least, AFAICT.

#

FWIW, rather than doing this whole buffer thing yourself, you should be able to use file.reader().readUntilDelimiterOrEofAlloc (or file.reader().readUntilDelimiterOrEof) instead, which just does the same thing, using readByte.

#

I would normally suggest that you could use std.io.bufferedReader(file.reader()) in order to buffer it, to speed up those readBytes, but the apparently doesn't work for your usecase.

#

The only real option left would probably be to use some sort of asynchronous I/O.

#

Like io_uring, I/O Completion Ports, or epoll/kqueue.

junior delta
#

is there an async file io lib that exists already? ideally one with builtin timeouts because managing that myself sounds like a nightmare

fossil tusk
#

Zig has an async feature, and the stdlib can opt into it by putting this in your entry point file:

pub const io_mode = .evented;

Plus, Zig async doesn't work on the latest Zig; you'd need to revert back to the previous release to use it.

#

However, I'm not sure I would recommend trying to use this feature for anything yet, even if you do revert.

#

Beyond that, AFAIK, there's not really any such library as of yet.
There's stuff like https://github.com/zigzap/zap, but I'm not familiar enough with it to be able to advise on it.

#

Actually - after giving it more of a glance, that might be a web-tech type thing, rather than a general async runtime type-thing. 🤔

junior delta
#

yeah its seems to be a webserver rather than a file io lib

fossil tusk
#

Indeed.

#

I find it hard to believe that this type of file cannot be configured to read a block of data, and just get a short-read if that's all there is.
Makes me wonder if there's some sort of ioctl thing you can do to affect that.

#

If you haven't tried it yet, might be worth trying to make the file handle nonblocking, and seeing if it will still block in this situation then.
The unfortunate thing about that is that I'm not sure you can set a timeout on file I/O. 🤔

junior delta
#

maybe? im not well versed in linux file io let alone hypervisor consoles

#

i can try making it non-blocking

fossil tusk
#

Hmmm. A bit of digging seems to suggest that file I/O cannot in fact be nonblocking on Linux. But it can't hurt to try can it 🤣
Especially when it seems like this works differently from most file handles!

#

Seems like you can do it via std.os.system.fcntl and the_file.handle.

#

Something like this, IIUC: std.os.system.fcntl(file.handle, std.os.F.SETFL, std.os.O.NONBLOCK);

#

(Ideally, you'd do an fcntl with GETFL and then pass flags & std.os.O.NONBLOCK to this one.)

junior delta
#

yeah ill add the GETFL

#
    _ = std.os.linux.fcntl(
        file.handle,
        @intCast(i32, std.os.F.SETFL),
        @intCast(
            usize,
            std.os.linux.fcntl(file.handle, @intCast(i32, std.os.F.GETFL), @intCast(usize, 0)) | @intCast(usize, std.os.O.NONBLOCK),
        ),
    );
``` i think this should work, theres a lot of intCast but it seems to compile
fossil tusk
#

That seems like progress(?) 🤣

junior delta
#

maybe?

#

the first few readData calls are only a couple dozen bytes

fossil tusk
#

WouldBlock is the error that read returns when it otherwise would have blocked. (There's no data to be read.)

junior delta
#

oh interesting

fossil tusk
#

This is the... slightly-strange magic to nonblocking 😛

junior delta
#

its odd that its hitting that, it should only be reading what it needs, i removed the extra read

#

unless its reading before the hypervisor has more data to give

fossil tusk
#

That is possible, yeah.

#

Obviously, it doesn't wait for data to be available anymore, at any point.

junior delta
#

in that case for the first byte read (to check for the null byte header begin) should i loop to try to force that one specific read to be sync?

#

since once that one byte is there the rest should be(?)

#

oh wait is it possible to tell how many bytes are available to read

fossil tusk
#

I'm not sure. IIRC, there's not really a good way to do that, because it's kinda TOCTOU issues.
You can use the poll/epoll syscall to block until there's data ready to be read, but that's different, of course.

My thinking is that you should treat it just like a stream where you'll get whatever's ready when it's ready.
So you just keep reading until you've got everything, updating your current 'state' as you go.
Essentially, just assume you'll get a partial blob each time you read. So buffer it up, and scan that buffer for the nulls, basically.

#

And when you find a complete 'message', for the want of a better word, then pop out just that part from the buffer, and process it.

#

Then repeat.

#

The epoll thing may be useful to you at some point in order to prevent you from busy-waiting, for the record.
But that's probably for after you get it working at all. 😄

junior delta
#

that makes sense

#

i'll try to implement that

fossil tusk
#

Outstanding 🔥

junior delta
#

ArrayList.items's length is the actual capacity not the items themselves right?

#

or do i use result.allocatedSlice

#

theres ArrayList variants of reader.read* but not for plain read

junior delta
#

this is a bunch of brain hurting logic but this seems fine, time to test

#

im gonna print what i read

#

since its json data

#

this looks fine

#

unless theres hidden null bytes?

#

oh i know whats happening

#

im including the null byte at the end in my returned string

#

oops

fossil tusk
#

I would suggest just using list.resize though, because then you don't have to mess about with allocatedSlice().

junior delta
#

holy shit its going to fast one sec

#

either its broken or its really working

#

i removed all the spam debug stuff to see how fast its actually going

#

its going speed

#

from a glance its going at 6-7kbps now

#

up from 2.5

#

let me do a proper benchmark

#

just a couple memory leaks :^)

#

lemme crunch the numbers for the speed

#

okay its about exactly 5kb/s

#

cool so this is fast but it leaking memory is concerning

#

now i just gotta free 3mb of storage on the VM to slap a debug build

fossil tusk
#

Not half bad at all!

#

5kbps seems kinda slow, but maybe it's just not writing that much data to the handle anyway 🤷‍♂️

junior delta
#

so its about 1.5-2k bytes per 512 bytes of actual file

#

and this is a 100MHz RISCV vm so im also limited by parse speed

fossil tusk
junior delta
fossil tusk
fossil tusk
junior delta
#

if its sending multiple packets from 1 request then something has gone horribly wrong, and ive verified on the hypervisor side it cant send multiple

junior delta
fossil tusk
junior delta
#

ive reworked the code heavily since that screenshot

fossil tusk
#

Okay 😄

junior delta
#
    ///Reads data from the bus, caller owns returned memory
    fn readData(self: Bus, allocator: std.mem.Allocator) ![]const u8 {
        var section = beginProfileSection(@src());
        defer section.endProfileSection();

        var reader = self.file.reader();

        var searching_for_header = true;
        while (searching_for_header) {
            var b = reader.readByte() catch |err| {
                if (err != error.WouldBlock) {
                    return err;
                }

                continue;
            };

            if (b == 0) {
                searching_for_header = false;
            }
        }

        //read the header null byte
        var capacity: usize = 2048;

        //Init a new list
        var result = std.ArrayList(u8).init(allocator);
        //ensure it can store 2048 bytes
        try result.ensureTotalCapacity(capacity);

        var end_found: bool = false;
        while (!end_found) {
            //ensure there is at least 2048 bytes available in the buffer
            try result.ensureUnusedCapacity(capacity);

            //read as many bytes as possible into the buffer
            var read: usize = reader.read(result.allocatedSlice()[result.items.len..]) catch |err| {
                if (!std.mem.eql(u8, @errorName(err), "WouldBlock"))
                    return err;

                //If we would block
                continue;
            };
            //UNSAFE: increasing the size here *should* be valid
            result.items.len += read;

            //If the last byte in the array is a null byte, then we have reached the end
            if (result.items[result.items.len - 1] == 0) {
                end_found = true;
                //Remove the null byte from the end of the list
                result.items.len -= 1;
            }
        }

        return result.toOwnedSlice();
    }
#

each fragment i get is guarenteed to never end in a null byte unless its the final fragment

fossil tusk
junior delta
#

yes that can never happen here

fossil tusk
#

Okay, cool.

#

I'm not sure how you know that, but if you do then 👍 😄

junior delta
fossil tusk
# junior delta

So your program is a program that is running on virtual machine inside Minecraft, which is talking to the virtual hypervisor on the virtual computer that is running this virtual virtual machine? 🤣

junior delta
#

so its a VM on Java (which is a VM in it of itself)

#

its a bit of a mess, but its fun to mess with

fossil tusk
junior delta
#

yeah thats why i only break from the loop if it ends with a null byte, if it doesnt end with a null byte, then i try to read again until the message does end with a null byte

fossil tusk
#

Or rather - that it may contain multiple messages, more precisely.

#

Like - each message sent will result in only one message - but multiple messages being sent will be buffered into the same buffer that you're asking the OS to copy from.

#

Or at least - that is the concern I have.

junior delta
#

i never send a packet without reading one, theres always a matching read for each write

fossil tusk
junior delta
#

yes

fossil tusk
#

Okay, gotcha.

#

In that case I think you're probably fine.

junior delta
#

worst case i just cross that bridge when i get there

fossil tusk
#

My thinking is that it could be hard to debug etc if it ever did by mistake, essentially.
Can you tell that I program fairly defensively? 🤣

junior delta
#

makes sense, i think i have a good enough mental model of the protocol to be able to not spend too long backtracing my mistakes

fossil tusk
#

Power to ya 😁

junior delta
#

right now im just tryina fix all the leaks, which is made a lot easier by a faster transfer program, given the size of even a gzip compressed debug build

#

i want to build a test harness so that i can emulate the hypervisor console on my normal desktop linux box

#

but thats for another time

#

ive already been writing code for 10 hours today

#

dont wanna start on something that hard :^)

#

fixed all the memory leaks

#

time to compile a ReleaseFast build (ive been doing ReleaseSmall and Debug so far)

#

so readData has gone from ~400ms to 6-12ms

#

and each fileImportRead (aka each 512 bytes of the file)

#

is taking 18-38ms to recieve

fossil tusk
junior delta
#

yes

fossil tusk
junior delta
#

and the data is being transferred as JSON arrays of bytes over a hypervisor console

fossil tusk
#

Not half bad

junior delta
#

fast enough to transfer 400-500k in reasonable time

fossil tusk
#

Fascinating.

junior delta
#

all of this to transfer png files to the VM to display on a projector in minecraft

fossil tusk
#

The file handle that your Zig program reads is a fake handle that is actually not a real fd at all?

#

So how that works is governed by the RISCV VM impl of syscalls or something like that?

junior delta
#

it is a real file handle to the VM, as its just a normal linux device

#

im not sure about the deeper implementations of the RISCV emulator, i havent dug that deep into that part of the code, just enough to understand the JSON hypervisor console API

fossil tusk
#

Hmm. Is your userspace HV creating a named pipe or something? Or are hypervisors a Linux thing that both Java and your HV are using to talk to each other?

#

(I'm curious how you're getting to create "/dev/hv0".)

junior delta
#

i dont think its a named pipe, i think the hypervisor on Java is exposing a "hypervisor console" device to the VM through the device tree, which then gets talked to through the driver in the kernel somehow

echo ether
#

i know the VM uses a lot of virtio drivers, so I feel there's definitely some of that going on, its possible its using some shared memory?

fossil tusk
#

Hmm. Curious.

#

Does the userspace HV program also open /dev/hv0 in order to talk to it? Or is that just for the program running inside the VM?

junior delta
#

its just the program in the VM

fossil tusk
#

Gotcha.

#

So it's more that the RISCV VM is exposing a fake pipe of some sort to the VM via /dev/hv0 in the VM's program.
And then from the userspace HV outside on your machine, you have a separate way to [open a handle that maps through to that pipe] from outside, so that the userspace program has a bidirectional pipe to the program inside the VM?

junior delta
#

from the perspective of the API level im working at, its just a few java methods with attributes, but i think lower level in the actual HV code the syscalls to talk to it are being intercepted and parsed, which then calls the java methods through reflection magic

#

all of this, to display anime girls in a movie theater in minecraft
(using another program i wrote, which decodes PNG files, then converts them to RGB565, then blits it to /dev/fb0 which is read and sent over the network to clients as H264 video to then be projected ingame)

echo ether
#

she has been doing this for weeks

junior delta
#

not sure how the hypervisor gets access to the linux framebuffer data

#

but it does

#

and it works

#

(sometimes)

echo ether
junior delta
#

ah

echo ether
#

i see some related stuff on startup through the fb

junior delta
fossil tusk
#

"I see you're one of those 'Don't Do Things By Half' types of people!" 🤣

#

That's some bloody good work there.