-
Notifications
You must be signed in to change notification settings - Fork 30.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] fs promises #17739
[WIP] fs promises #17739
Conversation
statValues[8], statValues[9] < 0 ? undefined : statValues[9], | ||
statValues[10], statValues[11], statValues[12], | ||
statValues[13]); | ||
function statsFromValues(values = statValues) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is necessary because using an async function for stat operations means we have a race condition on the use of statValues
when there's just a single TypedArray. So, for the normal callback case, we keep the single shared typed array, but for the promises version, we allocate one TypedArray per Promise. It's a bit more expensive for the promises case, but keeps things status quo for the callback case.
lib/fs.js
Outdated
yield buffer.slice(0, length); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mcollina ... take note of this function. It is an async generator used in an async iterator within fs.promises.readFile()
. Significantly less complicated than the current callback based implementation.
lib/fs.js
Outdated
let position = 0; | ||
const buffer = Buffer.allocUnsafe(kReadFileBufferLength); | ||
while (!eof) { | ||
if (loops > 5) process.exit(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is no place where loop
is incremented
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that's a left over... should have removed that.
lib/fs.js
Outdated
data : Buffer.from('' + data, options.encoding || 'utf8'); | ||
const position = /a/.test(flag) ? null : 0; | ||
|
||
writeEverything(fd, buffer, 0, buffer.length, position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be a return
?
lib/fs.js
Outdated
async function readFd(fd) { | ||
let ret = Buffer.alloc(0); | ||
for await(const chunk of readEverything(fd)) | ||
ret = Buffer.concat([ret, chunk]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid it's bad for performance to concat for each chunk. Is it possible to push to an array and concat once we are out of the loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, whatever works best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for await is very slow at the moment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep... part of my goal is to make this available then see what @bmeurer and team can do in terms of making it faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell this would make the promise API prohibitebly expensive to use though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the API is still experimental, performance is not the priority. If we're not able to get the performance optimized adequately, then we can move to a different approach.
lib/fs.js
Outdated
const buffer = Buffer.allocUnsafe(kReadFileBufferLength); | ||
while (!eof) { | ||
const { length } = | ||
await fs.promises.read(fd, buffer, 0, buffer.length, position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d drop the , position
if it isn’t necessary… without it, this should work on un-seekable files (e.g. TTYs, pipes) as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, had that on my list to work through
lib/fs.js
Outdated
} | ||
|
||
fs.promises = { | ||
async access(path, mode = fs.F_OK) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you set fs.access[util.promisify.custom] = fs.promises.access;
etc.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I will be
src/node_file.cc
Outdated
.ToLocalChecked(), | ||
AsyncWrap::PROVIDER_FD), | ||
fd_(fd) { | ||
CHECK_GE(fd, 3); // Do not allow wrapping of stdio fd |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep myself from doing silly things until it's a bit more baked.
src/node_file.cc
Outdated
FillStatsArray(env->fs_stats_field_array(), | ||
static_cast<const uv_stat_t*>(req->ptr)); | ||
req_wrap->Resolve(Undefined(req_wrap->env()->isolate())); | ||
req_wrap->FillStatsArray(static_cast<const uv_stat_t*>(req->ptr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sidenote, using &req->statbuf
will let you get rid of the cast + remove one layer of indirection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call
src/node_file.h
Outdated
using v8::Undefined; | ||
using v8::Value; | ||
|
||
namespace fs { | ||
|
||
class FSReqWrap: public ReqWrap<uv_fs_t> { | ||
enum Ownership { COPY, MOVE }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this enum class
? I think that would be a bit more readable since COPY
and MOVE
are pretty short identifiers and it’s not obvious what kind of thing they refer to…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also good call
async fchmod(fd, mode) { | ||
mode = modeNum(mode); | ||
if (fd == null || typeof fd !== 'object' || !Number.isInteger(fd.fd)) | ||
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'FD'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using ERR_INVALID_FD
might be more suitable for the errors like this.
78ed377
to
21f59ec
Compare
eof = true; | ||
yield buffer.slice(0, length); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to be very slow. Do we know if there is some improvements in the area from the V8 team?
Can you use the same class approach I used for Async Iterators? It ended up being 2x the async generator function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this can use the class approach, but I'm purposefully leaving this in so that folks my @bmeurer can take a look and figure out how to make it faster. I will be switching it to the class approach a bit later.
length -= written; | ||
if (position !== null) | ||
position += written; | ||
return writeEverything(fd, buffer, offset, length, position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the buffer is big enough, it could lead to a stack overflow. Can this be done as a loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that it returns a Promise, does it overflow the stack? :-)
That said, I'm already planning to rework this function. This was a temporary impl
@bmeurer / @fhinkel ... question for you. With this implementation currently... within an async function, I create an instance of It works rather nicely as is, however... it is a bit inefficient because we already know that we are running within an async function, which we know will create it's own Promise. What I would like to be able to do is avoid creating the This would save two additional allocations (one for the Thing is, I don't know if this would be possible within V8 (I know it's not possible now). For instance.... something like:
|
@jasnell I think that would basically be equivalent to having native async functions, right? I can’t tell you whether V8 wants it or not, but it should certainly be doable…? |
Yes, that's exactly it. I want the native code to be able to know that it's running within an |
Or, perhaps a different approach... having the ability to explicitly create Async Functions at the native level with an void AsyncThing(const AsyncFunctionCallbackInfo<Value>& args) {
Local<Promise> promise = args.Promise();
// resolve sync or async here... setting the return value like below would resolve sync
args.GetReturnValue().Set(1);
} such that the function itself returns a However it works, I just want to avoid having to allocate the |
Oh, exciting to see all this progress! Nice work @jasnell 😄 Async API functions can be supported but I don't see the need here. Looking at methods in A couple of them do use
The double call out to C++ can be avoided by having the binding create and return the promise. This would improve performance greatly. For example: Instead of doing this:
we could do:
With this change, there's only one call out to C++. |
Yeah, I've been going back and forth on this a bit. One of the key reasons they are async functions now is to simplify the error handling. The
Yeah, as I was digging in to it more I realized that also. It would still be good to have the ability to create an async function at the native layer so that I can do something like: void AsyncThing(const AsyncFunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(123);
} At the C++ level and... const binding = process.binding('foo');
await binding.asyncThing(); At the JavaScript level without having to worry about the explicit Promise creation stuff within C++.
Yep, and I that's ultimately where I'm going to go. The reason I've gone with the |
How would you model the “resume” strategy in C++? |
TBD, I think. I'm not yet 100% sure. |
@caitp C# models them (effectively) with a class (rather than a function) with a state variable and an entry hook.
However I simply don't see any benefit to this over using a regular function and doing this yourself. boost coroutines (if were used) could provide a more elegant solution - but let's don't :) |
Closing in favor of #18297 |
This is a work in progress continuation of the prototyping work started originally in #15485.
Ping @MylesBorins @addaleax @mcollina
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
fs