Conversation
A frame names columns a program is already holding as a table of one connection, and statements read those buffers where they lie. Nothing is copied at registration and nothing is copied at read. Describing a million rows over three columns and registering them costs 0.51 ns a row, against 630 ns a row to put the same rows through a loader, and the scan afterwards is 1.06 ns a row against 1.14 for the same numbers in a database, so the query is not paying for it later. Every buffer has to be direct and one that is not is refused rather than copied. Everywhere else in this client a heap buffer costs a memcpy and nothing else, because the call reads it and is finished. A frame keeps the pointer for as long as it is registered, so a copy here would mean the engine reading a copy for the rest of the frame's life, which is a frame that is not a frame. The direct buffer a caller hands over is looked after for it. A frame holds a reference to everything it was given and lets go at the moment the engine says it has finished, which is after the last statement reading the frame ends and is neither the unregister that preceded it nor the close. Without that a buffer nothing else refers to is freed by a cleaner while the engine is still pointing at it, which is what the float benchmark found before this held on to anything. That moment is an upcall, which is the first thing in this binding that hands the engine a pointer to Java code rather than the other way round. The stub has to outlive the frame, because the callback is the last thing to happen, and it cannot free itself either, since closing the arena a stub lives in from inside a call through that same stub is closing the ground you are standing on. So a spent arena goes on a queue and the next stub to be made closes it. Nothing may be thrown out of an upcall, so the callback logs and swallows. Twenty three tests over every column shape the ABI carries, including a collector run against a frame nothing else refers to, and a benchmark that puts a frame beside the database it is standing in for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A frame names columns a program is already holding as a table of one connection, and statements read those buffers where they lie. This is the other direction from a loader: a loader takes your columns and writes a database out of them, a frame writes nothing at all.
A million rows over three columns on an M-series laptop, JDK 25:
sum(p.id)over a framesum(p.id)over the same numbers in a databaseHalf a millisecond to make a million rows queryable against six hundred to write them down, and the query is not paying for it afterwards.
Every buffer has to be direct and one that is not is refused rather than copied. Everywhere else in this client a heap buffer costs a memcpy and nothing else, because the call reads it and is finished. A frame keeps the pointer for as long as it is registered, so a copy here would mean the engine reading a copy of the caller's data for the rest of the frame's life, which is a frame that is not a frame, and quietly making one is worse than saying so.
The direct buffer a caller hands over is looked after for it. A frame holds a reference to everything it was given and lets go at the moment the engine says it has finished, which is after the last statement reading the frame ends and is neither the unregister that preceded it nor the close. Without that, a buffer nothing else refers to has its memory freed by a cleaner while the engine is still pointing at it, which is what the float benchmark found before the frame held on to anything.
That moment is an upcall, and it is the first thing in this binding that hands the engine a pointer to Java code rather than the other way round. The stub has to outlive the frame, because the callback is the last thing to happen and may arrive after the frame was freed, so it cannot hang off the frame. It cannot free itself either, since closing the arena a stub lives in from inside a call through that same stub is closing the ground you are standing on. So a spent arena goes on a queue and the next stub to be made closes it, which costs nothing, needs no thread of ours, and bounds the outstanding stubs at the number of frames whose callbacks have not run yet. Nothing may be thrown out of an upcall, because an exception crossing one takes the whole JVM down, so the callback logs and swallows.
What is covered, per column shape: 64, 32 and 16 bit signed integers, an 8 bit unsigned one, a 32 bit unsigned one, doubles and single-precision floats, a bitmap of booleans, Arrow's Utf8 and LargeUtf8 and Utf8View, dates as Date32, and timestamps at Arrow's microseconds scaled to the nanoseconds this engine counts time in. Then the rules: one frame on two connections, the names a connection is holding, the release callback firing and not firing early, a frame no connection ever saw still letting go, a heap buffer refused, a column of the wrong length refused at that column, a name a stored table already holds refused, a frame replacing a frame of the same name, registering inside a transaction refused, a statement that would write to a frame refused, a slice being the slice and not the whole buffer, a closed frame saying so, and two hundred thousand rows read whole.
Twenty three new tests, 161 in all, green locally on JDK 25 with the release library.