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

[fix]: Removed synchronous operation with file system. #879

Merged
merged 13 commits into from Oct 11, 2017

Conversation

CheerlessCloud
Copy link
Contributor

I'm completely discouraged by this code. @aslushnikov How could you write such a thing?
I would somehow understand this solution if the code were written on callbacks. But you use the async
function
! And inside the asynchronous function you are happy to block the EventLoop. What for?

If someone tells me where to put this "promisify", I'll move it without problems. But still I recommended somehow to solve this problem in the near future.

The remaining cases use synchronous writing and reading are not as critical, but it is advisable to fix them too: 1, 2.

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks.


  • If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your email is set on your git commits.
  • If your company signed a CLA, they designated a Point of Contact who decides which employees are authorized to participate. You may need to contact the Point of Contact for your company and ask to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the project maintainer to go/cla#troubleshoot.
  • In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again.

@CheerlessCloud
Copy link
Contributor Author

I signed it!

@googlebot
Copy link

CLAs look good, thanks!

Copy link
Contributor

@Garbee Garbee left a comment

Choose a reason for hiding this comment

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

LGTM

lib/Page.js Outdated
@@ -26,6 +26,12 @@ const {Keyboard, Mouse, Touchscreen} = require('./Input');
const Tracing = require('./Tracing');
const {helper, debugError} = require('./helper');

function writeFileAsync(...options) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@aslushnikov , should this be added in helper? I could use it in #881.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As it seems to me, in a certain file/folder "utils" or "helpers" it is necessary to make the function promisify (as here) and use it to create wrappers of the fs methods or some other in local module scope (as here). Or we can add some external dependency, such as Bluebird or pify.
IMHO, I, as a person external to the project, can not make such decisions. And I do not want to rewrite the code, so let's work out solution and I will implement it in this PR. This is almost my first PR to open source project, so I may not know some rules, sorry =)

Copy link
Contributor

Choose a reason for hiding this comment

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

@CheerlessCloud let's add both promisify and writeFileAsync into helper.

@aslushnikov
Copy link
Contributor

aslushnikov commented Sep 27, 2017

Hey @CheerlessCloud, thank you for the pull request!

I'm completely discouraged by this code. @aslushnikov How could you write such a thing?
I would somehow understand this solution if the code were written on callbacks. But you use the async
function! And inside the asynchronous function you are happy to block the EventLoop. What for?

writeFileSync is bad. Thanks to your passion, the software is getting better!

The remaining cases use synchronous writing and reading are not as critical, but it is advisable to fix them too: 1, 2.

I'd be happy to review PRs.

@CheerlessCloud CheerlessCloud changed the title [fix](page): Replaced writeFileSync by asynchronous version in Page.js [fix]: Removed synchronous operation with file system. Sep 27, 2017
@CheerlessCloud
Copy link
Contributor Author

Do not you think that the file "helper" for 300+ lines is antipattern? What about separate his to files, single file for one method? Yes, this does not completely solve the problem, but it will make it a little easier.

@aslushnikov
Copy link
Contributor

Do not you think that the file "helper" for 300+ lines is antipattern?

I think its fine.

lib/Launcher.js Outdated
@@ -122,7 +127,7 @@ class Launcher {
process.kill(-chromeProcess.pid, 'SIGKILL');
// Attempt to remove temporary profile directory to avoid littering.
try {
removeSync(temporaryUserDataDir);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should stay sync: handlers for process exit are expected to complete synchronously.

lib/Tracing.js Outdated
const {helper} = require('./helper');
const fs = require('fs');

fs.openAsync = helper.promisify(fs.open);
Copy link
Contributor

Choose a reason for hiding this comment

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

let's not spoil the globals

const openAsync = helper.promisify(fs.open);
const writeAsync = helper.promisify(fs.write);
const closeAsync = helper.promisify(fs.close);

lib/Launcher.js Outdated
@@ -15,16 +15,18 @@
*/
const os = require('os');
const path = require('path');
const removeSync = require('rimraf').sync;
const {helper} = require('./helper');
const rimrafAsync = helper.promisify(require('rimraf'));
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: let's name this removeAsync: the rimraf doesn't say much of what the function is doing.

lib/Launcher.js Outdated
const ChromiumRevision = require('../package.json').puppeteer.chromium_revision;

fs.mkdtempAsync = helper.promisify(fs.mkdtemp);
Copy link
Contributor

Choose a reason for hiding this comment

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

let's not spoil the global:

const mkdtempAsync = helper.promisify(fs.mkdtemp);

lib/helper.js Outdated
* const fs = Helper.promisifyAll(require('fs'));
* fs.writeFileAsync(...); // -> Promise
*/
static promisifyAll(object) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't need this method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I can remove it, but it may be needed in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, we'll add it if/when the need arises.

lib/helper.js Outdated
@@ -225,6 +226,86 @@ class Helper {
static isNumber(obj) {
return typeof obj === 'number' || obj instanceof Number;
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

let's not fallback to util - the helper.promisify implementation is simple enough for our use case. It's easier to read and maintain a single code path.

/**
 * @param {function(?)} nodeFunction
 * @return {function(?):!Promise<?>}
 */
static promisify(nodeFunction) {
  /**
   * @param {!Array<?>} options
   * @return {!Promise<?>}
   */
  return function(...options) {
    return new Promise(function(fulfill, reject) {
      options.push(callback);
      nodeFunction.call(null, ...options);
      function callback(err, result) {
        if (err)
          reject(err);
        else
          fulfill(result);
      }
    });
  };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By "not fallback to util" you mean using util.promisify.custom symbol?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, and also the util.promisify method.

I'd just stick with the simple shim we already have somewhere.

Copy link

Choose a reason for hiding this comment

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

you can use https://npmjs.com/util.promisify if you want a compatible shim.

lib/helper.js Outdated
@@ -225,6 +226,86 @@ class Helper {
static isNumber(obj) {
return typeof obj === 'number' || obj instanceof Number;
}

/**
* @static
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't use @static, @throws and @example declarations. They are harmless, but I'd prefer them to be removed for consistency with the rest of the code.

lib/Launcher.js Outdated
@@ -90,8 +92,11 @@ class Launcher {
chromeProcess.once('close', () => {
// Cleanup as processes exit.
if (temporaryUserDataDir)
removeSync(temporaryUserDataDir);
fulfill();
rimrafAsync(temporaryUserDataDir)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: braces are required around multi-line branches:

if (temporaryUserDataDir) {
  rimrafAsync(..)
     ..
     ....
} else {
  fulfill();
}

lib/Launcher.js Outdated
fulfill();
rimrafAsync(temporaryUserDataDir)
.then(() => fulfill())
.catch(err => console.error(err));
Copy link
Contributor

Choose a reason for hiding this comment

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

let's reject the waitForChromeToClose promise

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a few cases we don't catch errors from this promise, so we can get an unhandledRejection.

Rename rimrafAsync to removeFolderAsync.
Place mkdtempAsync locally.
Use removeFolder.sync in killChrome function.
@petrgrishin
Copy link

👍

@@ -86,12 +89,16 @@ class Launcher {
chromeProcess.stderr.pipe(process.stderr);
}

const waitForChromeToClose = new Promise(fulfill => {
const waitForChromeToClose = new Promise((fulfill, reject) => {
Copy link

@graingert graingert Sep 28, 2017

Choose a reason for hiding this comment

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

Don't mix new Promise, x.then and async/await. Convert callback apis into promises ASAP, then switch back to the highest abstraction.

const waitForCloseEvent = () => new Promise(resolve => chromeProcess.once('close', resolve);
const waitForChromeToClose = (async () => {
  await waitForCloseEvent();
  if (temporaryUserDateDir) {
    try {
      await removeFolderAsync(temporaryUserDataDir);
    } catch(e) {
       console.error(e); // are you sure, what's wrong with letting it throw?
    }
  }
})();

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert I don't see much benefits in the approach you suggested, nor it is easier for me to read. I'd say the current code is just good enough.

Choose a reason for hiding this comment

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

@aslushnikov I disagree, new Promise should not be mixed with x.then.

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert I might be misunderstanding something. Lets split this discussion out of this thread: would you mind sending a separate PR that does changes that you find important and explains why are they necessary?

@@ -585,7 +587,7 @@ class Page extends EventEmitter {

const buffer = new Buffer(result.data, 'base64');
if (options.path)
fs.writeFileSync(options.path, buffer);
await writeFileAsync(options.path, buffer);

Choose a reason for hiding this comment

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

Why is there even an option to write the file here? Either puppeteer should return the actual stream or users should just write the buffer themselves.

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert two reasons:

  • it comes handy from user standpoint: the majority of page.screenshot usages have path option in it.
  • it's cheap from the project standpoint.

lib/helper.js Outdated
* @param {function(?)} nodeFunction
* @return {function(?):!Promise<?>}
*/
static promisify(nodeFunction) {

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert no need to pull in a dependency for 30 lines of code.

Copy link

Choose a reason for hiding this comment

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

when it's reliable and faster in modern node, it's always better to have more deps :-)

if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir)
.then(() => fulfill())
.catch(err => console.error(err));

Choose a reason for hiding this comment

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

why not just let this throw, like removeSync would have done.

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert to avoid unhandledPromiseRejection.

Copy link

@graingert graingert Sep 29, 2017

Choose a reason for hiding this comment

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

presumably people are going to await launch(); and thus catch any errors

Copy link
Contributor

Choose a reason for hiding this comment

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

How's Page.pdf related to this code?

Choose a reason for hiding this comment

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

@aslushnikov ah you're right. Edited comment

Copy link
Contributor

@aslushnikov aslushnikov left a comment

Choose a reason for hiding this comment

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

Thank you @CheerlessCloud for working on this. I left a few minor comments before we can merge this.

@@ -86,12 +89,16 @@ class Launcher {
chromeProcess.stderr.pipe(process.stderr);
}

const waitForChromeToClose = new Promise(fulfill => {
const waitForChromeToClose = new Promise((fulfill, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: reject is not used

@@ -86,12 +89,16 @@ class Launcher {
chromeProcess.stderr.pipe(process.stderr);
}

const waitForChromeToClose = new Promise(fulfill => {
const waitForChromeToClose = new Promise((fulfill, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert I don't see much benefits in the approach you suggested, nor it is easier for me to read. I'd say the current code is just good enough.

@@ -585,7 +587,7 @@ class Page extends EventEmitter {

const buffer = new Buffer(result.data, 'base64');
if (options.path)
fs.writeFileSync(options.path, buffer);
await writeFileAsync(options.path, buffer);
Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert two reasons:

  • it comes handy from user standpoint: the majority of page.screenshot usages have path option in it.
  • it's cheap from the project standpoint.

lib/helper.js Outdated
* @param {function(?)} nodeFunction
* @return {function(?):!Promise<?>}
*/
static promisify(nodeFunction) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert no need to pull in a dependency for 30 lines of code.

lib/helper.js Outdated
if (typeof nodeFunction !== 'function')
throw new TypeError('A function is expected');

// Because native util.promisify differently work with arguments
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: let's drop the comment

lib/helper.js Outdated
});
}

promisified.__isPromisified__ = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is never used - let's drop it.

lib/helper.js Outdated
if (err)
return reject(err);

// If callback invoked with single argument, we return in in Promise directly
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: let's drop the comment, the code is self explanatory

lib/helper.js Outdated
* @return {function(?):!Promise<?>}
*/
static promisify(nodeFunction) {
if (typeof nodeFunction !== 'function')
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's drop the check - the API is for internal use only.

if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir)
.then(() => fulfill())
.catch(err => console.error(err));
Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert to avoid unhandledPromiseRejection.

removeSync(temporaryUserDataDir);
fulfill();
if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir)
Copy link
Contributor

Choose a reason for hiding this comment

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

@CheerlessCloud I've seen your comment that the promise is not not catched in a few places, thanks for the clarification.

let's fulfill regardless of the temprorary data directory cleanup, and use debugError instead of console.error:

if (temporaryUserDataDir) {
  removeFolderAsync(temporaryUserDataDir)
    .catch(debugError)
    .then(fulfill);
} else {
  fulfill();
}

Choose a reason for hiding this comment

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

I want to be able to try/catch the error that removing the temporaryUserDataDir throws. You should await it and throw.

Copy link
Contributor

Choose a reason for hiding this comment

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

@graingert what are you going to do with the exception? The temporaryUserDataDir is internal to launcher's implementation.

removeSync(temporaryUserDataDir);
fulfill();
if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir)

Choose a reason for hiding this comment

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

I want to be able to try/catch the error that removing the temporaryUserDataDir throws. You should await it and throw.

@aslushnikov
Copy link
Contributor

@CheerlessCloud any updates on this? I'm very interested in this going in.

@CheerlessCloud
Copy link
Contributor Author

@aslushnikov Sorry, I've been busy the last few days. I'll try to finish it in the near future)

@aslushnikov
Copy link
Contributor

Hey @CheerlessCloud!

I see you're pressed on time. I'd like to add a few commits to this PR to address all the comments and merge this in. Let me know if this would work for you.

@aslushnikov aslushnikov merged commit ff08e45 into puppeteer:master Oct 11, 2017
ithinkihaveacat pushed a commit to ithinkihaveacat/puppeteer that referenced this pull request Oct 31, 2017
This patch:
- introduces `helper.promisify` - a simple polyfill for the `util.promisify`. The 
  `util.promisify` could not be used due to Node6 compatibility issues.
- migrates all sync filesystem operations to the async replicas

Fixes puppeteer#884.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants