Skip to content
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

meta: planning for promisified fs and crypto #15413

Closed
jasnell opened this issue Sep 14, 2017 · 48 comments
Closed

meta: planning for promisified fs and crypto #15413

jasnell opened this issue Sep 14, 2017 · 48 comments
Labels
crypto Issues and PRs related to the crypto subsystem. fs Issues and PRs related to the fs subsystem / file system. promises Issues and PRs related to ECMAScript promises.

Comments

@jasnell
Copy link
Member

jasnell commented Sep 14, 2017

This is largely a heads up.

Yesterday I posted a poll to Twitter asking how users felt about core providing a promisified API for fs... with two hours remaining in the poll, this is the result:

image

It's clear that there is strong demand. I have heard from many who like the fact that util.promisify() is now a thing but feel that being forced to use it in order to get promisified core APIs is not a very ergonomic experience, and while many are using Promise-wrapper libraries to achieve this, there is obviously a strong sentiment towards having these provided by core itself.

Therefore, I will be actively working towards the goal of providing Promise-enabled versions of all of the fs and crypto APIs in core. This will not be done all at once as there are a number of changes that may be required. For the crypto APIs, I will likely wait until async-iterators have landed with V8 6.2.

I understand that not everyone is in love with Promises and not everyone wants to use them. The changes I have in mind will not touch the existing APIs, so anyone who wants to ignore the Promisified versions will be able to.

@vsemozhetbyt
Copy link
Contributor

wait until async-iterators have landed with V8 6.2

Is there any reassuring info about it? They are not mentioned in the official blog for 6.2 and still under the flag in the 6.3 dev.

@jasnell
Copy link
Member Author

jasnell commented Sep 14, 2017

It may come later. If it does those pieces will wait or I'll take a more iterative approach

@evanlucas
Copy link
Contributor

Sounds awesome! Thanks James. One question I do have... for async crypto, will we be adding actual async apis or just wrapping them in a promise (since most of them are really just sync streams with the exception of those with *Sync counterparts)?

@jasnell
Copy link
Member Author

jasnell commented Sep 14, 2017

I'm planning to work on actual async crypto. This will mean making fairly large updates to the native layer, so it will take some time.

@mscdex mscdex added crypto Issues and PRs related to the crypto subsystem. fs Issues and PRs related to the fs subsystem / file system. promises Issues and PRs related to ECMAScript promises. labels Sep 14, 2017
@bnoordhuis
Copy link
Member

@indutny experimented with async crypto at one time but IIRC performance-wise the results were rather disappointing. See #678 for some discussion; there was also another issue that I can't find anymore.

@indutny
Copy link
Member

indutny commented Sep 14, 2017

@jasnell when you say async crypto - what do you actually mean?

Most of the crypto is too fast for being threaded, and things that are slow (like DH) are already offloaded to the worker threads.

I'm all in for promisified crypto, but it might be as well done just using nextTick.

@jasnell
Copy link
Member Author

jasnell commented Sep 14, 2017

Yeah, I played around with this quite some time ago also and it's going to take some doing and I do not really anticipate performance being the critical success factor on it. The idea more is to support the ergonomics of the Promises use case.

@indutny ... I'm going to be experimenting with different options here and benchmarking the heck out of everything as I go. For most things, we ought to be able to just wrap the existing sync APIs into a promise and use async iterators. For the most part, that will Just Work(tm) for the majority of use cases.

For the most part, however, I'm still working out exactly what it'll mean and I should warn you to expect many pings from me as I go through it :-)

@jasnell
Copy link
Member Author

jasnell commented Sep 14, 2017

Here's an example of what I mean about wrapping things into a Promise and using async iterators...

'use strict';

const crypto = require('crypto');

async function hash(alg, data, enc) {
  const hash = crypto.createHash(alg);
  for await (const chunk of data) {
    hash.update(chunk);
  }
  return hash.digest(enc);
}

var n = 0;
async function* dataIterator() {
  while (n++ < 10)
    yield `testing${n}`;
}

const p = hash('sha256', dataIterator(), 'hex');
p.then(console.log).catch(console.error);

The crypto operation itself is still sync...

@indutny
Copy link
Member

indutny commented Sep 14, 2017

That looks good. My point was that moving it to threads might be slower than doing it in main thread.

@jasnell
Copy link
Member Author

jasnell commented Sep 14, 2017

yep, I think for the overwhelming majority of situations that's definitely going to be the case

@seishun
Copy link
Contributor

seishun commented Sep 14, 2017

@jasnell

Therefore, I will be actively working towards the goal of providing Promise-enabled versions of all of the fs and crypto APIs in core.

Leaving crypto aside, how do you imagine promisified fs APIs? It seems you were against switching the return type when the callback is not provided (quoted here but I can't view the actual comment, thanks GitHub), and I think most people who want promisified APIs in core wouldn't be too happy about having to use a separate module like fs/promises.

My suggestion would be to take advantage of the fact that Modules are still experimental and are expected to break some things. I made an informal proposal in nodejs/CTC#12 (comment) but it seems it didn't get much attention, so I'll take the liberty of quoting it here.

In short:

require('fs') returns callback-based API as before.
import "fs" returns promise-based API.

Personally, I'm not a huge fan of promises, but if they are (or are going to become) the de-facto standard in the JS ecosystem, it will be quite sad if many years from now people using Node.js will still have to explicitly opt-in to use them. It seems ES6 modules is a chance to make them the "default" without breaking backward compatibility.

We might also want to introduce some mechanism to get the callback-based API with import (or vice versa), e.g. import "fs/callback". But that's beside the point - the main idea is that people can use promise-based API without any special magic like util.awaitable or fs/promise or fs.readFile.promise.

@zenflow
Copy link
Contributor

zenflow commented Sep 14, 2017

@seishun I think what you propose would be confusing in practice. I don't like the idea of having import "x" give me something different than require("x"). There is certainly less "magic" in requiring/importing fs/promise, no?

I'd be fine with importing/requiring fs/promise, but returning a promise when no callback is provided would be pretty rad.

@rmhrisk
Copy link

rmhrisk commented Sep 15, 2017

If changing the 'crypto' API to be promise based it should really move to be aligned with Web Crypto. It is not a perfect API but being able to share code across browser and server is very powerful.

Two projects I've created in this spirit are:
https://github.com/PeculiarVentures/node-webcrypto-p11
https://github.com/PeculiarVentures/node-webcrypto-ossl

@bnoordhuis
Copy link
Member

That's moving the goalposts too much, IMO. One thing at a time.

@seishun
Copy link
Contributor

seishun commented Sep 15, 2017

but returning a promise when no callback is provided would be pretty rad.

I'd personally be fine with that, I just suggested an alternative that doesn't require that.

@jasnell perhaps you could conduct another Twitter poll to find out how people feel about require('fs/promise')?

@jasnell
Copy link
Member Author

jasnell commented Sep 16, 2017

I've started working on the implementation for fs. The first step has been to compare the performance differences between using the traditional async methods (e.g. fs.access()), using promisify() (e.g. util.promisify(fs.access()) and using a hybrid of the two that eliminates much of the additional mechanism promisify() introduces. Here's a series of benchmark results using fs.access()

james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 129,179.49410162005
fs/promises-access.js method="promisify" n=200000: 62,033.72907780782
fs/promises-access.js method="promise" n=200000: 99,851.57522659813
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 131,823.8670939539
fs/promises-access.js method="promisify" n=200000: 92,211.0193389906
fs/promises-access.js method="promise" n=200000: 130,812.55867094509
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 103,898.76694294077
fs/promises-access.js method="promisify" n=200000: 59,435.60157205645
fs/promises-access.js method="promise" n=200000: 75,441.8021781834
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 144,433.5795648312
fs/promises-access.js method="promisify" n=200000: 60,828.50132958056
fs/promises-access.js method="promise" n=200000: 102,694.83071372041
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 139,102.81191556106
fs/promises-access.js method="promisify" n=200000: 52,655.62402722651
 fs/promises-access.js method="promise" n=200000: 84,820.60525113577
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 119,764.03330853581
fs/promises-access.js method="promisify" n=200000: 69,578.83127013533
fs/promises-access.js method="promise" n=200000: 78,187.92857696606
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 124,934.62920461498
fs/promises-access.js method="promisify" n=200000: 67,273.76496749399
fs/promises-access.js method="promise" n=200000: 84,629.54787474376

Note that the traditional async method is consistently much faster and that using util.promisify() has a definite and significant cost. The third option, promise, resolves the promise in js-land, which ends up coming at an increased cost making it a bit slower on average than traditional async. My next step is to try the promise handling entirely at the native layer to avoid the extra boundary crossing.

@addaleax
Copy link
Member

@jasnell Hey, this reminded me of something :)

One thing that I had in mind when I wrote the code for util.promisify() is that it would be cool if the callback passed to C++ could be eliminated completely, and instead the Promise could just be resolved from there. Thanks to the custom override mechanism for promisify(), that should work fine – just add a Promise instead of a callback as the oncomplete property or something like that, then instead of calling the callback resolve the promise in the libuv After handler.

The reason I haven’t implemented that right away is that it was blocked #5691 (the microtask queue would not be run after the resolution), plus it would have broken async_hooks tracking. However, since we just fixed that by adding (Internal)CallbackScopes, I think there’s nothing standing in the way of that anymore.

@jasnell
Copy link
Member Author

jasnell commented Sep 16, 2017

Great minds and something ;-) ... yeah that's right along the lines of what I was thinking but I'm going to be playing with a few ideas along the way. (specifically, I'm going to see about creating the Promise in native-land without having to set the oncomplete property at all... everything else would be essentially how you describe it)

@TimothyGu
Copy link
Member

TimothyGu commented Sep 17, 2017

@jasnell / @addaleax

@targos has a branch that allowed the use of more efficient promise operations in JS through V8 Extras. Since Promise creation and resolution are not actually implemented in C++ but rather into TurboFan's CodeStubAssembler, resolving it through the C++ API may be counterproductive.

Some preliminary results from that branch against util.promisify are in nodejs/promises#31 (comment) (thanks to @benjamingr), but this advice should apply to promises in general as well.

/cc @nodejs/v8

More context

@littledan said:

An alternative, which could avoid the jump to and from C++ here and in the constructor, would be to do the same using V8 extras. extras exposes extrasUtils.resolvePromise which does basically the same thing here. (#12442 (comment))

V8 extras may be a faster way to optimize promisify than using the V8 API since it avoids a JS/C++ transition; ... (nodejs/CTC#12 (comment))

@gsathya said:

util.promisify is implemented using the C++ API which is probably causing most of this overhead. The best way to address this would be for V8 to implement util.promisify using our assembler and expose it using v8-extras. (nodejs/promises#31 (comment))

The ResolvePromise builtin (which is exposed by V8 Extras in JS) is implemented in TF:

// ES #sec-fulfillpromise
TF_BUILTIN(ResolvePromise, PromiseBuiltinsAssembler) {
Node* const promise = Parameter(Descriptor::kPromise);
Node* const result = Parameter(Descriptor::kValue);
Node* const context = Parameter(Descriptor::kContext);
InternalResolvePromise(context, promise, result);
Return(UndefinedConstant());
}

{ // Internal: ResolvePromise
// Also exposed as extrasUtils.resolvePromise.
Handle<JSFunction> function = SimpleCreateFunction(
isolate, factory->empty_string(), Builtins::kResolvePromise, 2, true);
function->shared()->set_native(false);
native_context()->set_promise_resolve(*function);
}

InstallFunction(extras_utils, isolate()->promise_resolve(),
factory()->NewStringFromAsciiChecked("resolvePromise"));

v8::Promise::Resolver::Resolve() calls into JS:

node/deps/v8/src/api.cc

Lines 7566 to 7570 in 631c59b

has_pending_exception =
i::Execution::Call(isolate, isolate->promise_resolve(),
isolate->factory()->undefined_value(), arraysize(argv),
argv)
.is_null();

Similarly for promise creation:

TF_BUILTIN(PromiseInternalConstructor, PromiseBuiltinsAssembler) {
Node* const parent = Parameter(Descriptor::kParent);
Node* const context = Parameter(Descriptor::kContext);
Return(AllocateAndInitJSPromise(context, parent));
}

{ // Internal: PromiseInternalConstructor
// Also exposed as extrasUtils.createPromise.
Handle<JSFunction> function =
SimpleCreateFunction(isolate, factory->empty_string(),
Builtins::kPromiseInternalConstructor, 1, true);
function->shared()->set_native(false);
native_context()->set_promise_internal_constructor(*function);
}

InstallFunction(extras_utils, isolate()->promise_internal_constructor(),
factory()->NewStringFromAsciiChecked("createPromise"));

node/deps/v8/src/api.cc

Lines 7535 to 7544 in 631c59b

MaybeLocal<Promise::Resolver> Promise::Resolver::New(Local<Context> context) {
PREPARE_FOR_EXECUTION(context, Promise_Resolver, New, Resolver);
i::Handle<i::Object> result;
has_pending_exception =
!i::Execution::Call(isolate, isolate->promise_internal_constructor(),
isolate->factory()->undefined_value(), 0, NULL)
.ToHandle(&result);
RETURN_ON_FAILED_EXECUTION(Promise::Resolver);
RETURN_ESCAPED(Local<Promise::Resolver>::Cast(Utils::ToLocal(result)));
}

@addaleax
Copy link
Member

@TimothyGu I know that makes sense when promisifying userland code, but does it matter as much even when we’re calling from C++ code anyway whether we do fn->Call() + resolve a Promise in that function vs just resolving it from C++?

@jasnell
Copy link
Member Author

jasnell commented Sep 17, 2017

I'll have some benchmark results on the resolve from C++ either today or Monday.

@benjamingr
Copy link
Member

I'm in conferences and trips until Oct 12th but afternoon that I want to schedule the promise performance meeting so we can move ahead with performance

@jasnell
Copy link
Member Author

jasnell commented Sep 18, 2017

Updated the code to resolve from C/C++, still not the most efficient iteration but definitely an improvement

james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 174,955.7272782565
fs/promises-access.js method="promisify" n=200000: 82,492.1608380116
fs/promises-access.js method="promise" n=200000: 140,742.31188365704
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 124,527.00999190034
fs/promises-access.js method="promisify" n=200000: 49,064.526922610916
fs/promises-access.js method="promise" n=200000: 101,194.76202958806
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 194,674.9124755951
fs/promises-access.js method="promisify" n=200000: 57,349.73743965421
fs/promises-access.js method="promise" n=200000: 140,588.98293681847
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 91,471.07024680685
fs/promises-access.js method="promisify" n=200000: 49,875.564170654856
fs/promises-access.js method="promise" n=200000: 128,471.69206014258
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 203,195.65522498416
fs/promises-access.js method="promisify" n=200000: 60,863.65764523673
fs/promises-access.js method="promise" n=200000: 68,404.0122920642
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 194,106.02188932057
fs/promises-access.js method="promisify" n=200000: 68,617.0461168789
fs/promises-access.js method="promise" n=200000: 153,583.25723302134
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 132,254.5262851897
fs/promises-access.js method="promisify" n=200000: 72,590.50219045834
fs/promises-access.js method="promise" n=200000: 70,892.8012604672
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 133,725.66616201162
fs/promises-access.js method="promisify" n=200000: 51,536.910010469386
fs/promises-access.js method="promise" n=200000: 83,467.71799387963

@addaleax
Copy link
Member

@jasnell just curious, would you be willing to share the code/benchmarks used for that?

@jasnell
Copy link
Member Author

jasnell commented Sep 18, 2017

Yeah, will likely push the branch in the morning. I'm still actively iterating on it. Not really in Write-Code-That-I'd-Actually-Push mode yet, just playing with variations.

Just made another tweak and the results are much better...

james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 237,621.13329837585
fs/promises-access.js method="promisify" n=200000: 136,155.75128701224
fs/promises-access.js method="promise" n=200000: 320,660.86511210236
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 404,077.7383744739
fs/promises-access.js method="promisify" n=200000: 141,172.00928700628
fs/promises-access.js method="promise" n=200000: 301,973.1266837012
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 159,149.29519815533
fs/promises-access.js method="promisify" n=200000: 151,828.16921041757
fs/promises-access.js method="promise" n=200000: 349,717.977281299
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 127,574.00154649415
fs/promises-access.js method="promisify" n=200000: 146,099.61584764245
fs/promises-access.js method="promise" n=200000: 361,786.14914426696
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 145,359.19310106023
fs/promises-access.js method="promisify" n=200000: 85,080.19945682972
fs/promises-access.js method="promise" n=200000: 158,890.83557438728
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 306,723.02800457616
fs/promises-access.js method="promisify" n=200000: 95,618.27065922068
fs/promises-access.js method="promise" n=200000: 204,664.28074301226
james@ubuntu:~/node/node$ ./node benchmark/fs/promises-access.js
fs/promises-access.js method="legacy" n=200000: 282,905.99230353965
fs/promises-access.js method="promisify" n=200000: 86,871.98370346567
fs/promises-access.js method="promise" n=200000: 348,408.7117584573

@TimothyGu
Copy link
Member

TimothyGu commented Sep 18, 2017

@addaleax

but does it matter as much even when we’re calling from C++ code anyway whether we do fn->Call() + resolve a Promise in that function vs just resolving it from C++?

In that case, probably not.

@jasnell
Copy link
Member Author

jasnell commented Sep 18, 2017

Playing around with it, @addaleax is right, it doesn't make much difference using the v8-extras. For now, I'm going with the method she suggested and I'm working on a number of other improvements along the way. The PR is going to end up being semver-major because it's going to include some needed improvements to error handling in the existing fs code, in addition to switching to internal/errors and adding the promise versions. I'm expecting a fairly large PR.

@refack
Copy link
Contributor

refack commented Sep 29, 2017

Should have posted #15204 (comment) here:

WHATWG landed AbortController (whatwg/fetch#523):

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
}).catch(err => {
  if (err.name === 'AbortError') {
    console.log('Fetch aborted');
  } else {
    console.error('Uh oh, an error!', err);
  }
});

We could implement that (even before promisifying the API) as a means to cancel ongoing async tasks.

P.S. AbortError could be excluded from --abort-on-unhandled-rejection

@benjamingr
Copy link
Member

@refack AbortController was a compromise for getting the API the capability. We probably shouldn't settle on that API, not to mention we never really had cancellation with callbacks so I don't see why we'd introduce it now - and even if we did - I think it requires more careful consideration.

@coreyfarrell
Copy link
Member

If there will be a way to request the promisified version of fs, how would this deal with situations where a module becomes promisified in the future? What I mean, if I request the promisified version of fs, createWriteStream would not be promise capable - but I posted #16989 intended to make promisify support that function.

So would require('fs/promise') omit createWriteStream if it's not supported by promisify? This is the one concern I have with require('fs/promise'), how to deal with adding promise functionality to an existing function. I think it'd be important to avoid having fs/promise publish any method that could return promise but currently doesn't, otherwise how do you make it start returning promise without being a breaking change?

@MikeKovarik
Copy link

@coreyfarrell Why would you even bother bringing this up. This makes no sense. Promisifying fs concerns that the methods doing (for a lack of a better term) file operations return promises. But fs.createReadStream is not a file operation. It is just a factory function that returns instance of something that does the operation. So why promisify fs.createReadStream? Do you want to promisify the whole fs namespace? Why not put fs.ReadStream behind a promise while you're at it? fs.createReadStream should keep returning stream and only fs.readFile, fs.writeFile, fs.readdir, etc... should return promise

@addaleax
Copy link
Member

@MikeKovarik Please try to stay respectful of each other on this issue tracker.

fs.createReadStream is different from factory functions in that it does perform fs.open under the hood and isn’t really usable until that operation completes; it doesn’t just create an instance of ReadStream.

Also, we don’t quite have a good story for mixing streams with promises yet anyway.

@jasnell
Copy link
Member Author

jasnell commented Nov 13, 2017

+1 to what @addaleax said. There are many considerations here and much we have yet to work out. I will be updating the initial draft of the fs promises impl in the coming couple of weeks.

@coreyfarrell
Copy link
Member

@MikeKovarik: I only commented here so everyone can consider how require('fs/promise') or similar will deal with functions that later get support for promisify, I didn't intend to hijack the conversation here to talk about my bug report.

@MikeKovarik
Copy link

MikeKovarik commented Nov 13, 2017

@addaleax Yes, I crossed the line there, I'm sorry. @coreyfarrell

What drove me over the edge on this topic is the ammount of discussion on this topic (or if it should be done at all which we're thanfully over now given that this issue exists). I as an every day Node user see this as "Why is there even a discussion when there's obvious solution at hand". This reminds me of the same thing I've seen with Angular vs Aurelia framework. Back when Angular 2 was being developed, the devs kept arguing and making decisions to prioritize performance as much as possible which striped it of all the things that made Angular 1 so popular - two way binding. That's why Rob Eisenberg went his own way to create Aurelia. He and his team somewhere stated that "they're not a framework builders, they are app builders first" and that's what makes Aurelia's API so much more pleasing to use than Angular.

What I (and I believe a lot of node users) would like to see from promisified fs is that if you call a method (that expects a callback) without a callback, it would just return promise. That's the code I see in modules, async fs wrappers and apps. And that's the code I would like to write. I don't understand why is there so much fear about breaking something when major semver is here for making breaking changes. Plus if fs promisified in this way could work in both callback and promisify mode depending on wheter the callback argument is defined, then there's not much to break in userland.

I do care about Node, because I love using it. I would like this to go through as an exciting bullet in changelog, rather than something that just brings confusion.

@benjamingr
Copy link
Member

benjamingr commented Nov 13, 2017

Thanks for owning up @MikeKovarik - I'll try explaining why things are happening so slowly.

"Why is there even a discussion when there's obvious solution at hand"

It took me very long to not have this mindset, it is very tempting to think that solving this is easy but this isn't easy from an API perspective and it isn't easy from a "what works" perspective. There have been previous attempts before. There is a lot of information there about omitting the last argument vs. other approaches (mostly, some methods don't allow this, some are not well behaved - that sort of thing).

As a platform, Node has to be very careful about what we break, even in semver-major, we've made this mistake before with breaking tools which cost a lot of people a lot of time debugging. This is why Node.js has a "canary in the goldmine" process - you can see this talk for more information about it. Whenever Node.js breaks something - the community trusts Node.js a little less.

I do care about Node, because I love using it. I would like this to go through as an exciting bullet in changelog, rather than something that just brings confusion.

We all do, and we're making progress - it's just a lot of work and the project is community driven mostly, adding util.promisify was a nice step for interop in my opinion.

I encourage you to try participating more in the project - we're always looking for volunteers to help maintain the project and promote new features. If you'd like to get more involved feel free to reach out to me (my email is in the home page) and I'll try to work with you - there are several issues that are suitable for new contributors relating to promises :)


Sorry for the off topic, I thought this deserves a public reply for future readers.

@MikeKovarik
Copy link

MikeKovarik commented Nov 13, 2017

@benjamingr Thanks for the explainer. I understand breaking changes are difficult. But I'm worried that if we stop breaking things from time to time, we will eventually stop innovating.

I encourage you to try participating more in the project

I would love to. As a matter of fact I was considering contributing recently. Over the past couple of weeks I was going over the fs source code over and over, while working on uwp-fs wrapper for Windows store apps (Please note it is nowhere near done and the code quality is still very much experimental. But this gave me reason to go public quicker. Also note that it does promises :D ) and found a few ideas for improvements, but ended up not doing it, assuming it'd get shot down because of inexperience. But I will be contacting you :)

Also sorry for going OP. I thought I'd drop in a plug for uwp-fs :) since I've already written it with promises and async/await.

@jasnell
Copy link
Member Author

jasnell commented Nov 13, 2017

@MikeKovarik ... again at the risk of going off topic, my advice would be to start small, focusing on focused incremental improvements then working up from there. I do I a work in progress PR and a larger effort underway to add promises to the fs module that will be seeing significant iteration over the coming couple of months.

@jorangreef
Copy link
Contributor

jorangreef commented Mar 5, 2018

Just going back a little bit in this thread to the merits of doing async crypto in the threadpool:

It all depends on buffer size. Buffer size is critical when talking about async crypto performance.

Crypto throughput and latency on the main thread is better for buffers less than 1024 bytes, but anything larger is probably better off in the threadpool for a miniscule latency loss but much higher throughput overall.

Here are some actual numbers for 1 MB buffers, see crypto-async:

   AES-256-CTR: 64 x 1048576 Bytes
        crypto: Latency: 1.105ms Throughput: 945.20 MB/s
  crypto-async: Latency: 1.362ms Throughput: 3050.40 MB/s
  
   HASH-SHA256: 64 x 1048576 Bytes
        crypto: Latency: 3.023ms Throughput: 347.71 MB/s
  crypto-async: Latency: 3.162ms Throughput: 1290.56 MB/s

   HMAC-SHA256: 64 x 1048576 Bytes
        crypto: Latency: 3.134ms Throughput: 335.54 MB/s
  crypto-async: Latency: 3.974ms Throughput: 1048.58 MB/s

If Node can only ever do single core crypto, then Node will never be able to do high-throughput crypto or compete with systems that can.

@gireeshpunathil
Copy link
Member

@jasnell - what would be the next action on this? Has the discussion run its course to be able to take any decision?

@jasnell
Copy link
Member Author

jasnell commented Apr 28, 2018

Step one was getting fs/promises delivered and ensuring it was stable. Next is identifying the other modules that make sense to give similar treatment to and setting up a tracking issue to monitor progress.

Promisified crypto is going to be a challenge because it's going to be difficult to do in any performant way.

We can use this as the tracking issue or close it and open a separate one. Whichever works best

@gireeshpunathil
Copy link
Member

@jasnell - given lot of valuable insights above, I don't feel like proposing a close. However, as a prospect of a PR is not around, the issue just lingers, so I leave it upto you for a call!

@benjamingr
Copy link
Member

A net or http promisified and using asnyc iterators which is now stable could be pretty great :)

@coreyfarrell
Copy link
Member

Is it possible to consider supporting require('fs/promises')? The current method of require('fs').promises is fine for require, but when using ES module import's this will require a second step and pollute the current module's global namespace. Example:

// Direct import
import {readFile} from 'fs/promises';

// Current method
import fs from 'fs';
const {readFile} = fs.promises;

I'm not necessarily against the existence of require('fs').promises but I feel that a documented directly accessible module would be make this more usable. I didn't see anything in this thread about the decision to expose promises as a property of fs instead of making it a new module, does anyone know of another thread where this was discussed?

@benjamingr
Copy link
Member

Hey @coreyfarrell thanks for chiming in.

It was fs/promises for a while (10.0 and 10.1 I think?) and was reverted because we want to have a better long-term strategy for scoping modules. See nodejs/TSC#389

@coreyfarrell
Copy link
Member

@benjamingr Thanks for the response and link. This addresses my concern as it seems a long-term plan is being developed to provide direct import of the fs promises members.

@benjamingr
Copy link
Member

@coreyfarrell there is, yeah.

@jasnell
Copy link
Member Author

jasnell commented Aug 11, 2018

We can close this tracking issue now that the work is progressing

@jasnell jasnell closed this as completed Aug 11, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crypto Issues and PRs related to the crypto subsystem. fs Issues and PRs related to the fs subsystem / file system. promises Issues and PRs related to ECMAScript promises.
Projects
None yet
Development

No branches or pull requests