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
#File io with timeout
1 messages · Page 1 of 1 (latest)
///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 &.{};
}
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().
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
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.
its reading from this file
nope, if i read too much it hangs until theres more data
You could either make a thread to run this in, or use nonblocking IO. (Setting the fd to be nonblocking, and handling error.WouldBlock)
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
Right - so then what's the problem with it blocking?
Like - that won't stop you reading a blob of data and then scanning that blob for the nulls, right.
well if it blocks then i cant read the data because execution doesnt continue
Right - but there's no data to process, otherwise it would have returned it, right?
Or am I missing something?
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
Are you sure that's how that's working? That's not normally how it works.
That's why it returns the number of bytes that were read, after all.
yeah i tested it
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?
Is there something special that you are meant to do in order to discover how many bytes are available to read?
you read until you hit a null byte, then you know its done
Because it seems very strange to me that it would require that you give it EXACTLY the buffer of the right size.
Also, just to confirm, you've opened that as a std.fs.File, and are doing file.reader() ?
yes
currently uploading a build which just calls read with a 1 byte buffer after ive read the last null byte to see if it hangs or just returns 0 bytes read
yup it hangs
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.
is there an async file io lib that exists already? ideally one with builtin timeouts because managing that myself sounds like a nightmare
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. 🤔
yeah its seems to be a webserver rather than a file io lib
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. 🤔
maybe? im not well versed in linux file io let alone hypervisor consoles
i can try making it non-blocking
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.)
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
🤔
WouldBlock is the error that read returns when it otherwise would have blocked. (There's no data to be read.)
oh interesting
This is the... slightly-strange magic to nonblocking 😛
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
That is possible, yeah.
Obviously, it doesn't wait for data to be available anymore, at any point.
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
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. 😄
Outstanding 🔥
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
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
what
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
.items is the slice of the currently inserted elements; allocatedSlice() returns you a slice of the entire allocated capacity.
I would suggest just using list.resize though, because then you don't have to mess about with allocatedSlice().
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
Not half bad at all!
5kbps seems kinda slow, but maybe it's just not writing that much data to the handle anyway 🤷♂️
the java hypervisor only sends 512 bytes of actual file to the hypervisor console, but that 512 bytes is stored in json as an array of numbers
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
I might also suggest that it may not actually be that the last byte in the buffer is the null. It could be anywhere in theory.
Which is why I said to scan for it.
std.mem.lastIndexOfScalar might be useful there. (It returns null if it didn't find it.)
(Or indexOfScalar, for course.)
in practice it cant be anywhere, it only sends a "packet" over the hypervisor console when i ask it to
Right - but I'd want to know for a fact that packets cannot be coalesced at the read end.
I have so many questions. 🤣
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
this is an import export utility that goes over the hypervisor console, to send files in and out of a java RISCV virtual machine running in minecraft
No, I mean that my thinking is you may get fragments of a packet each time you read.
And a fragment may or may not span across multiple packets - in which case you could potentially get the last 10 bytes of one AND the first 10 bytes of the second one.
ive reworked the code heavily since that screenshot
Okay 😄
///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
If you know for a fact that you can never get something like this happening:
// this is the fragment you just read:
[ 'w', 'o', 'r', 'l', 'd', 0, 'f', 'o', 'o' ]
^ ^
// this is the end ---| |
// of the packet |
|
// and this is the first byte of the next packet
yes that can never happen here
the abstraction on the java hypervisor side is built in a way where each request i send, is guarenteed to send me one and exactly one "response" json object
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? 🤣
the hypervisor is running on my actual machine, the RISCV VM is a Java implementation that runs as a mod on the server
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
Right - though, just to be entirely clear here - when you read, especially if you're nonblocking, I'd imagine - all your doing is saying "I want to copy the data from the buffer into my buffer."
AFAIK, there's no thing that says that the contents of the OS buffer has to contain the complete message that was written to it.
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
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.
i never send a packet without reading one, theres always a matching read for each write
You mean that you interlock them such that the read end must have entirely read and processed an entire message, before a second one will ever start being sent?
yes
worst case i just cross that bridge when i get there
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? 🤣
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
Power to ya 😁
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
The hypervisor is a normal userspace program running on your actual computer; the RISCV VM is written in Java, and is running as a Minecraft mod; and the Zig program is a RISCV executable that is being interpreted by the aforementioned RISCV VM? 🤣
yes
Native syscalls are something on the order of 100us, for comparison, IIRC.
and the data is being transferred as JSON arrays of bytes over a hypervisor console
Not half bad
fast enough to transfer 400-500k in reasonable time
Fascinating.
all of this to transfer png files to the VM to display on a projector in minecraft
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?
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
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".)
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
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?
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?
its just the program in the VM
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?
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)
she has been doing this for weeks
not sure how the hypervisor gets access to the linux framebuffer data
but it does
and it works
(sometimes)
pretty sure the guest allocates a bit of memory and tells the host where that memory is through whatever channel of communication
ah
i see some related stuff on startup through the fb
https://github.com/Beyley/Coverett/blob/f4ead0226606dd268a5373cf0353d18cafbcf232/src/zigguratt.zig#L159
heres the final working reading code if you want to take a look
Damnnnnn.
"I see you're one of those 'Don't Do Things By Half' types of people!" 🤣
That's some bloody good work there.