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

fs: Optimizations 3: The Revenge #12105

Closed
wants to merge 15 commits into from

Conversation

mscdex
Copy link
Contributor

@mscdex mscdex commented Mar 28, 2017

As part of the continuing fs optimization saga, I offer the latest from our courageous adventurer, node.js core, and their continuing crusade against slow code!

These commits bring improvements to just about every method in fs, some are affected more greatly than others. I'm tentatively marking this as semver-major mostly for two reasons: the switch to using binding methods directly instead of using exported fs methods and secondly due to the stricter (or what I consider to be more correct) check when using getOptions() in sync methods, namely that an options parameter should not be checked for a function type.

Here are results for some of the relevant, existing benchmarks:

 fs/read-stream-create.js n=2000000                                               32.23 %        *** 4.048752e-21
 fs/bench-stat.js kind="lstat" n=200000                                            7.58 %        *** 3.879035e-23
 fs/bench-stat.js kind="stat" n=200000                                             6.56 %        *** 1.431107e-24
 fs/bench-statSync.js kind="lstatSync" n=1000000                                  27.74 %        *** 1.382031e-27
 fs/bench-statSync.js kind="statSync" n=1000000                                   27.05 %        *** 1.528482e-26
 fs/read-stream-throughput-dur.js size=1024 dur=5 type="asc"                      17.25 %        *** 2.236051e-25
 fs/read-stream-throughput-dur.js size=1024 dur=5 type="buf"                      13.94 %        *** 4.733833e-12
 fs/read-stream-throughput-dur.js size=1024 dur=5 type="utf"                      18.47 %        *** 1.789672e-25
 fs/read-stream-throughput-dur.js size=1048576 dur=5 type="asc"                    3.86 %        *** 1.036992e-04
 fs/read-stream-throughput-dur.js size=1048576 dur=5 type="buf"                    2.06 %            6.049078e-02
 fs/read-stream-throughput-dur.js size=1048576 dur=5 type="utf"                    0.18 %            2.948700e-01
 fs/read-stream-throughput-dur.js size=4096 dur=5 type="asc"                      16.92 %        *** 3.641115e-22
 fs/read-stream-throughput-dur.js size=4096 dur=5 type="buf"                      19.54 %        *** 1.931802e-21
 fs/read-stream-throughput-dur.js size=4096 dur=5 type="utf"                      18.03 %        *** 6.067689e-32
 fs/read-stream-throughput-dur.js size=65535 dur=5 type="asc"                      5.02 %        *** 6.193170e-22
 fs/read-stream-throughput-dur.js size=65535 dur=5 type="buf"                      8.67 %        *** 1.318807e-13
 fs/read-stream-throughput-dur.js size=65535 dur=5 type="utf"                      5.34 %        *** 1.434471e-23
 fs/readfile.js concurrent=1 len=1024 dur=5                                       11.02 %        *** 2.441570e-27
 fs/readfile.js concurrent=1 len=16777216 dur=5                                    0.56 %            1.088713e-01
 fs/readfile.js concurrent=10 len=1024 dur=5                                      12.88 %        *** 4.513205e-34
 fs/readfile.js concurrent=10 len=16777216 dur=5                                   0.93 %          * 2.394970e-02
 fs/readFileSync.js n=600000                                                      28.63 %        *** 3.117574e-44
 fs/write-stream-throughput.js size=1024 type="asc" file="/dev/null" dur=5         8.16 %        *** 6.103539e-10
 fs/write-stream-throughput.js size=1024 type="buf" file="/dev/null" dur=5         8.91 %        *** 4.220987e-11
 fs/write-stream-throughput.js size=1024 type="utf" file="/dev/null" dur=5         4.03 %        *** 1.321584e-05
 fs/write-stream-throughput.js size=1048576 type="asc" file="/dev/null" dur=5      4.31 %        *** 7.286723e-04
 fs/write-stream-throughput.js size=1048576 type="buf" file="/dev/null" dur=5     12.79 %        *** 1.203880e-18
 fs/write-stream-throughput.js size=1048576 type="utf" file="/dev/null" dur=5     -0.80 %         ** 6.914996e-03
 fs/write-stream-throughput.js size=2 type="asc" file="/dev/null" dur=5            0.43 %            7.643183e-01
 fs/write-stream-throughput.js size=2 type="buf" file="/dev/null" dur=5           -0.15 %            8.397873e-01
 fs/write-stream-throughput.js size=2 type="utf" file="/dev/null" dur=5           -0.57 %            0.7450881
 fs/write-stream-throughput.js size=65535 type="asc" file="/dev/null" dur=5        1.47 %            2.210894e-01
 fs/write-stream-throughput.js size=65535 type="buf" file="/dev/null" dur=5       12.47 %        *** 9.412604e-15
 fs/write-stream-throughput.js size=65535 type="utf" file="/dev/null" dur=5        0.46 %            6.000189e-01
 fs/writeFile.js size=1024 type="asc" file="/dev/null" dur=5                       7.39 %        *** 1.113940e-15
 fs/writeFile.js size=1024 type="buf" file="/dev/null" dur=5                      10.26 %        *** 4.005523e-26
 fs/writeFile.js size=1024 type="utf" file="/dev/null" dur=5                       7.51 %        *** 1.369845e-15
 fs/writeFile.js size=1048576 type="asc" file="/dev/null" dur=5                   14.15 %        *** 4.819530e-36
 fs/writeFile.js size=1048576 type="buf" file="/dev/null" dur=5                   10.60 %        *** 9.444402e-29
 fs/writeFile.js size=1048576 type="utf" file="/dev/null" dur=5                    0.97 %         ** 1.147428e-03
 fs/writeFile.js size=2 type="asc" file="/dev/null" dur=5                          6.66 %        *** 4.917892e-13
 fs/writeFile.js size=2 type="buf" file="/dev/null" dur=5                         10.62 %        *** 4.281740e-26
 fs/writeFile.js size=2 type="utf" file="/dev/null" dur=5                          7.37 %        *** 2.635052e-15
 fs/writeFile.js size=65536 type="asc" file="/dev/null" dur=5                      9.75 %        *** 2.124569e-16
 fs/writeFile.js size=65536 type="buf" file="/dev/null" dur=5                     10.07 %        *** 2.777137e-24
 fs/writeFile.js size=65536 type="utf" file="/dev/null" dur=5                      4.72 %        *** 8.172503e-11
 fs/writeFileSync.js size=1024 type="asc" file="/dev/null" n=100000               36.18 %        *** 3.433125e-51
 fs/writeFileSync.js size=1024 type="buf" file="/dev/null" n=100000               61.55 %        *** 3.531040e-53
 fs/writeFileSync.js size=1024 type="utf" file="/dev/null" n=100000               28.34 %        *** 5.821697e-39
 fs/writeFileSync.js size=1048576 type="asc" file="/dev/null" n=100000            36.15 %        *** 8.386398e-26
 fs/writeFileSync.js size=1048576 type="buf" file="/dev/null" n=100000            61.16 %        *** 6.169762e-52
 fs/writeFileSync.js size=1048576 type="utf" file="/dev/null" n=4000               4.50 %        *** 1.8929e-36
 fs/writeFileSync.js size=2 type="asc" file="/dev/null" n=100000                  34.44 %        *** 1.904218e-38
 fs/writeFileSync.js size=2 type="buf" file="/dev/null" n=100000                  60.58 %        *** 3.458357e-47
 fs/writeFileSync.js size=2 type="utf" file="/dev/null" n=100000                  37.85 %        *** 6.717352e-34
 fs/writeFileSync.js size=65536 type="asc" file="/dev/null" n=100000              17.47 %        *** 6.744990e-14
 fs/writeFileSync.js size=65536 type="buf" file="/dev/null" n=100000              62.03 %        *** 2.363003e-57
 fs/writeFileSync.js size=65536 type="utf" file="/dev/null" n=100000               5.52 %        *** 4.678978e-12

These benchmarks compare binaries that both include V8 5.7 and have cfc8422 reverted (see my comments on that commit's PR for more discussion on that -- although as of this PR it would only affect fs.realpath*(), which was not really touched in this PR).

I did add a few new benchmark files, but adding new benchmarks for every single fs method and running each one would take forever and would most likely either extend past the semver-major landing deadline for node v8 (which I am hoping this will make it into yet) or would be too close to the deadline, not giving enough time for review.

Will our brave and mighty hero, node.js core, survive the onslaught of V8 optimization changes? Will they finally accrue enough frequent flier miles to get that vacation they've always wanted? Tune in next time....!

CI: https://ci.nodejs.org/job/node-test-pull-request/7080/
CITGM: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/676/

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)
  • fs

@mscdex mscdex added fs Issues and PRs related to the fs subsystem / file system. performance Issues and PRs related to the performance of Node.js. semver-major PRs that contain breaking changes and should be released in the next major version. labels Mar 28, 2017
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. util Issues and PRs related to the built-in util module. labels Mar 28, 2017
@mscdex
Copy link
Contributor Author

mscdex commented Mar 29, 2017

Looking at the CITGM results, I'm not sure why graceful-fs is failing. I just tested it locally and all tests are passing.

@mscdex mscdex added this to the 8.0.0 milestone Mar 29, 2017
@mscdex
Copy link
Contributor Author

mscdex commented Mar 29, 2017

/cc @nodejs/collaborators

@ronkorving
Copy link
Contributor

Amazing work, @mscdex. I'm thrilled to see this progress. I skimmed through it and nothing stuck out to me as worth discussing. I'll give it another run later, and hope some other @nodejs/collaborators will give it a pass too.

@lpinca
Copy link
Member

lpinca commented Mar 29, 2017

@mscdex WRT Object.create(null) see also emberjs/ember.js#15001 (comment).
Object creation time is the same when enabling the TurboFan compiler.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 29, 2017

@lpinca Yep, but who knows when it will get switched over to TurboFan since it seems it currently doesn't? Perhaps it is switched over in a later version of V8...

There still might be some parts where we won't expect there to be many properties, so using a StorageObject constructor may still be useful. One example is (well was, before this PR) the copying of fs method options which is typically only about 3 or 4 options, which IIRC is not enough to trigger dictionary mode. However for other parts of the API, like querystring and such where there is no such expectation, it may be better to start off in dictionary mode. But until TurboFan is used for Object.create(null), I'm still in favor of reverting those changes.

@lpinca
Copy link
Member

lpinca commented Mar 29, 2017

Yes it makes sense.

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work, lots of changes but they're very regular (in that you applied the same optimization to a lot of places).

Left some comments but those are mainly questions. LGTM

@@ -43,6 +43,18 @@ const internalUtil = require('internal/util');
const assertEncoding = internalFS.assertEncoding;
const stringToFlags = internalFS.stringToFlags;
const getPathFromURL = internalURL.getPathFromURL;
const O_RDONLY = constants.O_RDONLY | 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the | 0 here adn not in constants?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied these verbatim from lib/internal/fs.js. It implicitly converts the value to a 32-bit integer, so there may be a reason for that, performance or otherwise?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex it gives a hint to the compiler that it's a SMI.

I'm just wondering why it isn't done on the constant itself. I guess that since this is copied over it shouldn't be a big deal here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I wasn't sure of the upper ceiling for SMIs offhand, but yes that's probably what it would be.

if (typeof options === 'string') {
defaultOptions = util._extend({}, defaultOptions);
defaultOptions = shallowClone(defaultOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any benchmarks comparing util._extend Object.assign and shallowClone?

Would it be faster to do defaultOptions = Object.create(defaultOptions) and use the fact it's on the prototype?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested Object.assign() with V8 5.7 but I would guess it's still slow. shallowClone is really fast because it literally copies everything as-is (including frozen/sealed state, property attributes, etc.). From my own separate benchmarks, shallowClone() was something like at least twice as fast as for (... in ...) and util._extend() uses Object.keys() to iterate instead which the last time I tested is even slower than for (... in ...) in many/most cases.

lib/fs.js Outdated
@@ -167,7 +167,7 @@ function nullCheck(path, callback) {
}

function isFd(path) {
return (path >>> 0) === path;
return (typeof path === 'number' && ~~path === path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change faster? I'm not a fan of the old code but doing ~~path === path should at least be motivated IMO.

Copy link
Contributor Author

@mscdex mscdex Mar 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the conversion by itself seems to be slightly faster in V8 5.7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave a comment on that? I'm not sure how many people understand what ~~ does here.

@@ -255,11 +255,15 @@ Object.defineProperties(fs, {
});

function handleError(val, callback) {
if (typeof val === 'string')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind explaining this change?

It looks like it means wrapping a lot of other calls with if(typeof x !== 'string')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just optimizing for the case where URL instances are passed to fs methods. handleError() is passed the result of a function which converts URL instances to string paths. By checking for string path results first, we can avoid the instanceof, which is probably a less common case.

};

const kReadFileBufferLength = 8 * 1024;
function ReadFileContext(callback, encoding, isUserFd) {
this.req = new FSReqWrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, moving things here looks a lot cleaner. I think any slots saved here could be a big win

@@ -470,7 +480,7 @@ function readFileAfterRead(err, bytesRead) {

function readFileAfterClose(err) {
var context = this.context;
var buffer = null;
var buffer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason a default null isn't assigned anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unnecessary because it's re-assigned some kind of value below every time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex I'm guessing that it was put there as an optimization (so there is always a value here) - I don't see why it would make things faster to assign to null in this particular case though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without looking, my guess is that a default case/branch was missing at some point in the past, but that is no longer the case in this function.

fs.write(fd, buffer, offset, length, position, function(writeErr, written) {
if (writeErr) {
function writeAll(fd, isUserFd, buffer, offset, length, position, callback) {
var req = new FSReqWrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice.

@@ -0,0 +1,50 @@
// test the throughput of the fs.ReadStream class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: capital T

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just copied from the other benchmark. TBH I don't think the comment even needs to exist.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 29, 2017

@benjamingr I've changed the isFd() implementation now to something that performs similarly but should be more readily understandable FWIW.

@TimothyGu
Copy link
Member

  1. In light of Ignition + TurboFan: Node.js benchmarks #11851, have you checked if the performance improvements are reproducible with the Ignition/TurboFan pipeline? Particularly, right now the overhead of calling into C++ methods seems to be greater with I/TF than in FCG/CS in my experience.
  2. In general, I'm okay with reverting lib: use Object.create(null) directly #11930 if it can be established for that specific case for which performance of creation of the object outweighs accessing the properties. I believe for fs module it is indeed justified. But caution needs to be taken for other modules since it might not be the case for certain more complex (and arguably more common) use cases.

@@ -319,39 +329,46 @@ fs.existsSync = function(path) {
}
};

const defaultReadFileOpts = Object.create(null, {
flag: { value: O_RDONLY, writable: true }
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain how much performance impact this may have, but because of the Object.create(null, ...):

const { O_RDONLY } = fs.constants;
const defaultReadFileOpts = Object.create(null, {
  flag: { value: O_RDONLY, writable: true }
});
console.log(%HasFastProperties(defaultReadFileOpts));
  // Prints false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's expected because of the changes made in V8 to Object.create(null), but at least it's only created once during startup, so the performance cost of creating the object is amortized.

Copy link
Member

@TimothyGu TimothyGu Mar 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually thinking about property access, not object creation. Shallow-cloning an object created with Object.create(StorageObject.prototype, ...) (so that it has fast properties) is much faster (~40%), since slow properties are only faster for map-like usage:

benchmark
const { shallowClone } = process.binding('util');
const { constants: { O_RDONLY } } = require('fs');

const n = 1e7;

function StorageObject() {}
StorageObject.prototype = Object.create(null);

const direct = Object.create(null, {
  flag: { value: O_RDONLY, writable: true }
});
const proxied = Object.create(StorageObject.prototype, {
  flag: { value: O_RDONLY, writable: true }
});

console.log(%HasFastProperties(direct)); // false
console.log(%HasFastProperties(proxied)); // true

function cloneDirect() { return shallowClone(direct); }
function cloneProxied() { return shallowClone(proxied); }

// warmup
for (var i = 0; i < n; i++)
  cloneDirect();
for (var i = 0; i < n; i++)
  cloneProxied();

console.time('direct');
for (var i = 0; i < n; i++)
  cloneDirect();
console.timeEnd('direct');

console.time('proxied');
for (var i = 0; i < n; i++)
  cloneProxied();
console.timeEnd('proxied');
false
true
direct: 624.346ms
proxied: 369.772ms

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping this issue.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 30, 2017

@TimothyGu

In light of #11851, have you checked if the performance improvements are reproducible with the Ignition/TurboFan pipeline? Particularly, right now the overhead of calling into C++ methods seems to be greater with I/TF than in FCG/CS in my experience.

No, I haven't bothered checking because in that issue it's pretty clear that there is quite a negative impact across the board with it enabled. I'm guessing (or my hope is) that the V8 team is still working on optimizing the new pipeline.

In general, I'm okay with reverting #11930 if it can be established for that specific case for which performance of creation of the object outweighs accessing the properties. I believe for fs module it is indeed justified. But caution needs to be taken for other modules since it might not be the case for certain more complex (and arguably more common) use cases.

As mentioned, the only place where this really matters as this PR currently stands is fs.realpath*(), where resolved paths and such are cached. I have not benchmarked those two functions before and after the manual inlining of Object.create(null), especially because this PR doesn't really touch those functions.

@jasnell
Copy link
Member

jasnell commented Apr 4, 2017

@MylesBorins ... any thoughts on this one re:graceful-fs and the CITGM results?

Copy link
Member

@mhdawson mhdawson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on a quick review LGTM given the data showing the perf benefits and assuming a green CI.

@mscdex
Copy link
Contributor Author

mscdex commented Apr 13, 2017

The main focus of this commit is to:

* Avoid options object copying where possible
* Use V8's object cloning only for default options objects where we
are in control of the object's configuration since the object clone
will also copy properties' configuration, frozen/sealed state, etc.
* Refactor writeFile*()/appendFile*() to reuse shared logic without
the redundant argument checking and to reuse FSReqWrap instances more
@mscdex mscdex removed the ctc-review label May 2, 2017
@mscdex mscdex added the wip Issues and PRs that are still a work in progress. label May 11, 2017
@mscdex
Copy link
Contributor Author

mscdex commented May 19, 2017

@nodejs/v8 Would it be possible for us to float our own V8 patch to add an IsCloneable() to be able to get this PR landed in time for v8.0.0?

@jasnell
Copy link
Member

jasnell commented May 28, 2017

@mscdex ... it is too late to get this in to 8.0.0 at this point.

@jasnell jasnell removed this from the 8.0.0 milestone May 28, 2017
@tniessen tniessen added this to the 9.0.0 milestone Aug 16, 2017
@BridgeAR
Copy link
Member

This needs a rebase. Does this need anything else to be ready to land?

@mscdex
Copy link
Contributor Author

mscdex commented Sep 12, 2017

@BridgeAR my last comment still stands, we need either an Object::IsCloneable() method in V8 or a modified Object::Clone() API that returns a Maybe or whatever so that we can fall back to a js-land "object clone" for a few edge cases.

Also, I'd need to re-benchmark these changes anyway.

@BridgeAR BridgeAR mentioned this pull request Sep 19, 2017
4 tasks
@targos targos modified the milestones: 9.0.0, 10.0.0 Oct 23, 2017
@targos
Copy link
Member

targos commented Oct 23, 2017

I suppose it's too late to get this into 9.0.0. Changed the milestone to 10.0.0.

@joyeecheung
Copy link
Member

@mscdex Are you still interested in getting this in? This would need a rebase but we now have a CI job to run the benchmarks.

@mscdex
Copy link
Contributor Author

mscdex commented Oct 23, 2017

@joyeecheung Yes, but it's blocked on changes needed in V8.

@BridgeAR
Copy link
Member

I am confident that the performance gain is not a lot anymore with newer V8 versions.

@mscdex would you be so kind and rebase this and rerun the benchmarks?

var flag;
var encoding;
if (options != null) {
options = getOptions(options, defaultReadFileOpts);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of duplicates, you have O_RDONLY in defaultReadFileOpts and in 552 and 555 line. Better to remove defaultReadFileOpts and add defaltReadFileFlag. stringToFlags should be in getOptions.

const defaltReadFileFlag = O_RDONLY;
if (options != null) {
    options = getOptions(options);
    flag = options.flag || defaultReadFileOpts;
    encoding = options.encoding;
} else {
    flag = defaultReadFileOpts;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And getOptions should be like:

function getOptions(options) {
  const type = typeof options;
  if (type === 'string') {
    return { flag: stringToFlags(options) };
  } else if (type !== 'object') {
    throw new TypeError('"options" must be a string or an object, got ' +
                        typeof options + ' instead.');
  }
  if (options.flag)
    options.flag = stringToFlags(options);
  if (options.encoding !== 'buffer')
    assertEncoding(options.encoding);
  return options;
}

@BridgeAR
Copy link
Member

Ping @mscdex

@BridgeAR
Copy link
Member

Closing due to long inactivity.

@mscdex please feel free to reopen if you want to continue working on it!

@BridgeAR BridgeAR closed this Jan 19, 2018
@tniessen tniessen removed this from the 10.0.0 milestone Mar 14, 2018
@tniessen
Copy link
Member

I removed this from the node 10 milestone. Feel free to reopen and assign a milestone when appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. performance Issues and PRs related to the performance of Node.js. semver-major PRs that contain breaking changes and should be released in the next major version. util Issues and PRs related to the built-in util module. wip Issues and PRs that are still a work in progress.
Projects
None yet
Development

Successfully merging this pull request may close these issues.