Skip to content

http: add writeFile to OutgoingMessage #50576

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

Closed
wants to merge 2 commits into from
Closed

Conversation

ronag
Copy link
Member

@ronag ronag commented Nov 6, 2023

Provide an optimized path for sending files over HTTP. Removes the entire memory and processing overhead of creating and operating an intermediate Readable.

@ronag ronag requested a review from mcollina November 6, 2023 12:05
@nodejs-github-bot
Copy link
Collaborator

nodejs-github-bot commented Nov 6, 2023

Review requested:

  • @nodejs/crypto
  • @nodejs/http
  • @nodejs/net
  • @nodejs/startup
  • @nodejs/streams

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Nov 6, 2023
@ronag ronag force-pushed the send-file branch 2 times, most recently from 389eda2 to 277b2b8 Compare November 6, 2023 12:07
@ronag
Copy link
Member Author

ronag commented Nov 6, 2023

@nodejs/performance Could I maybe get some help with creating a relevant benchmark?

ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 6, 2023
One of the most common usages of http servers are to response with files.
This provides a faster alternative to:

stream.pipeline(fs.createReadStream(path, { start, end }), res, callback)

PR-URL: nodejs#50576
@ronag ronag requested review from rluvaton and targos November 10, 2023 08:44
ronag added a commit to nxtedition/node that referenced this pull request Nov 10, 2023
One of the most common usages of http servers are to
response with files. This provides a faster alternative
to:

stream.pipeline(
  fs.createReadStream(path, { start, end }), res, callback
)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 10, 2023
One of the most common usages of http servers are to
response with files. This provides a faster alternative
to:

stream.pipeline(
  fs.createReadStream(path, { start, end }), res, callback
)

PR-URL: nodejs#50576
@rluvaton
Copy link
Member

rluvaton commented Nov 10, 2023

Also I'm not on Linux.

You can use docker and I'm pretty sure you can can also find the fd in Mac

Also, I'm pretty sure that we save file descriptors references in the fs file

@ronag
Copy link
Member Author

ronag commented Nov 10, 2023

Sorry, that kind of test is beyond me. If we have some existing test I can use as a reference I'm happy to add it. Otherwise, a follow-up PR is welcome. There are many other places that are similar that could also use such a test, e.g. file streams.

ronag added a commit to nxtedition/node that referenced this pull request Nov 10, 2023
One of the most common usages of http servers are to
response with files. This provides a faster alternative
to:

stream.pipeline(
  fs.createReadStream(path, { start, end }), res, callback
)

PR-URL: nodejs#50576
ronag added a commit to nxtedition/node that referenced this pull request Nov 10, 2023
One of the most common usages of http servers are to
response with files. This provides a faster alternative
to:

stream.pipeline(
  fs.createReadStream(path, { start, end }), res, callback
)

PR-URL: nodejs#50576
One of the most common usages of http servers are to
response with files. This provides a faster alternative
to:

stream.pipeline(
  fs.createReadStream(path, { start, end }), res, callback
)

PR-URL: nodejs#50576
callback(err, bytesWritten);
});
} else if (typeof file === 'string' || isURL(file)) {
fs.open(getValidatedPath(file), (err, fd) => {
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a test to the permission model? Just in case someone changes this implementation for a function that doesn't handle permissions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you have any good tests I can look at for reference?

@RafaelGSS RafaelGSS added the semver-minor PRs that contain new features and should be released in the next minor version. label Nov 10, 2023
Copy link
Member

@anonrig anonrig left a comment

Choose a reason for hiding this comment

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

I think this implementation should be done in C++. Performance wise this is not looking good.

doc/api/http.md Outdated
@@ -3187,6 +3187,55 @@ Removes a header that is queued for implicit sending.
outgoingMessage.removeHeader('Content-Encoding');
```

### `outgoingMessage.sendFile(pahtOrFd, optsOrCallback, callbackOrNil)`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
### `outgoingMessage.sendFile(pahtOrFd, optsOrCallback, callbackOrNil)`
### `outgoingMessage.sendFile(pathOrFd, optsOrCallback, callbackOrNil)`

callback(err, bytesWritten);
});
} else if (typeof file === 'string' || isURL(file)) {
fs.open(getValidatedPath(file), (err, fd) => {
Copy link
Member

Choose a reason for hiding this comment

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

This line has missing toNamespacedPath

Copy link
Member Author

Choose a reason for hiding this comment

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

What's that? Also fs streams don't have any reference to toNamespacedPath? Is it missing there as well?

}
};

function _writeFile(fd, res, start, end, callback) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this function should be implemented in C++. There are too many JS -> C++ boundary calls.

Copy link
Member Author

Choose a reason for hiding this comment

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

Follow up PR welcome 🙏

Copy link
Member

Choose a reason for hiding this comment

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

There may be a challenge implementing this in c++ given how much of the implementation of OutgoingMessage is in JavaScript.

doc/api/http.md Outdated
added: REPLACEME
-->

* `file` {string|number|URL} file path, URL or descriptor.
Copy link
Member

Choose a reason for hiding this comment

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

The order of types and the descriptions are not same.

@ronag ronag force-pushed the send-file branch 4 times, most recently from 20f3904 to 5f91a90 Compare November 11, 2023 12:56

> Stability: 1.1 - Active development.

* `file` {string|URL|number} file path, URL or descriptor.
Copy link
Member

Choose a reason for hiding this comment

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

Probably worthwhile for this to support FileHandle also.

Copy link
Member Author

Choose a reason for hiding this comment

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

There are some issues with filehandle I'd like to sort out before that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would recommend against using that api in general due to issue with how the reference counting behaves.

req.resume();
fs.open(fname, (err, fd) => {
assert(!err);
res.writeFile(fd, (err) => {
Copy link
Member

Choose a reason for hiding this comment

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

This should also include tests verifying the function of start and end as well as input argument validation

@ronag
Copy link
Member Author

ronag commented Nov 11, 2023

I might have been a bit naive in how much effort this would take. Tests is half the job.

At this stage with no native implementation this is possible to just add in user space. Which is good enough for our use case.

@ronag ronag closed this Nov 11, 2023
@ronag
Copy link
Member Author

ronag commented Nov 11, 2023

Here is the user space implementation for those that want it:

function writeFile(fd, res, start, end, callback) {
  const hwm = res.writableHighWaterMark;

  let reading = false;
  let pos = start;
  let resume = null;
  let bytesWritten = 0;

  const finish = (err) => {
    if (resume != null) {
      res
        .off('drain', resume)
        .off('close', resume);
    }
    callback(err, bytesWritten);
  }

  const next = (err, bytesRead, buf) => {
    reading = false;
    if (pos !== undefined) {
      pos += bytesRead;
    }
    bytesWritten += bytesRead;

    buf = bytesRead === 0 ? null : bytesRead < buf.byteLength ? buf.subarray(0, bytesRead) : buf;

    const wState = res.socket && res.socket._writableState;

    if (res.destroyed) {
      finish(null);
    } else if (err && err.code === 'EAGAIN') {
      setImmediate(read);
    } else if (err || buf === null) {
      finish(err);
    } else if (res.write(buf)) {
      read();
    } else if (wState && wState.length - wState.writelen < hwm) {
      // In order to ensure we don't stall we need to continue
      // reading the file while the socket is writing.
      read();
    } else if (resume == null) {
      resume = read;
      res
        .on('drain', resume)
        .on('close', resume);
    }
  };

  const read = () => {
    if (reading) {
      return;
    }
    reading = true;

    const n = pos !== undefined ?
      Math.min(end - pos + 1, hwm) :
      Math.min(end - bytesWritten + 1, hwm);

    assert(n >= 0);

    if (res.destroyed) {
      finish(null);
    } else if (n === 0) {
      next(null, 0, null);
    } else {
      fs.read(fd, Buffer.allocUnsafe(n), 0, n, pos, next);
    }
  };

  read();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fs Issues and PRs related to the fs subsystem / file system. http Issues or PRs related to the http subsystem. needs-ci PRs that need a full CI run. review wanted PRs that need reviews. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.