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

performance: fix timerify bug #40625

Closed
wants to merge 4 commits into from

Conversation

iam-frankqiu
Copy link
Contributor

Fix #40623

@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Oct 27, 2021
@@ -73,11 +69,11 @@ function timerify(fn, options = {}) {

if (fn[kTimerified]) return fn[kTimerified];

const constructor = isConstructor(fn);
const isClass = (fn) => /^\s*class/.test(fn.toString());
Copy link
Member

Choose a reason for hiding this comment

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

This would fail if someone is using "old" classes which can be common when transpiling cross-platform code that targets old targets (for example: using tooling that uses es5 at any point)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would fail if someone is using "old" classes which can be common when transpiling cross-platform code that targets old targets (for example: using tooling that uses es5 at any point)

Maybe you can add a test on test-performance-function.js or just show me an example function(I will add to the former file). I will solve the problem to ensure all tests have been passed.

Copy link
Member

Choose a reason for hiding this comment

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

Sure

function MyClass {
  this.foo = 15;
}
MyClass.prototype.sayFoo {
  console.log(this.foo);
}
const instance = new MyClass(); ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this.foo = 15;
Hasn't it a return value ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

function MyClass {
  this.foo = 15;
}
MyClass.prototype.sayFoo {
  console.log(this.foo);
}
const instance = new MyClass(); ?

If a function is like this. Actually, we can't judge which way is to call. Because using new to call and invoke directly is different. So I think we just invoke it directly if the first augment is a function. Using new to call if it is a class. wdyt?

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 that will not work for a large chunk of our ecosystem unfortunately and there are people who still use "old style" constructors or have existing projects that do it. So I think for me personally this not breaking for "old style" classes is important.


function timerified(...args) {
const start = now();
const result = constructor ?
const result = isClass(fn) ?
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should change the check from "is it a constructor?" to "is it called as a constructor?". i.e. new.target !== undefined. wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

That sounds like a very reasonable approach to me

Copy link
Member

Choose a reason for hiding this comment

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

Note new.target can be set but the return value can still be overridden by returning something different - so a constructor check is would still need to check the return value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we should change the check from "is it a constructor?" to "is it called as a constructor?". i.e. new.target !== undefined. wdyt?

Actually. I have considered this problem. It seems like new.target only can be used within a function. I didn't find any way to use it outside of a function.

Copy link
Member

Choose a reason for hiding this comment

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

It seems like new.target only can be used within a function. I didn't find any way to use it outside of a function.

This should be fine. timerified is a function.

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 seems like new.target only can be used within a function. I didn't find any way to use it outside of a function.

This should be fine. timerified is a function.

I think we don't need to judge if it is called a constructor. we only judge whether the argument is a function or not. if is a class we use new to call, else we invoked directly.

Copy link
Member

Choose a reason for hiding this comment

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

I don't agree. In my opinion, timerified should be a proxy to the function and behave as much as possible the same as if the function was called directly.

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 don't agree. In my opinion, timerified should be a proxy to the function and behave as much as possible the same as if the function was called directly.

There must have been some misunderstanding in there. What I do is just like you said. Are there any conflicts?

Copy link
Contributor

Choose a reason for hiding this comment

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

The conflict is that, in JavaScript, a function can be a class. What Michaël is saying is that, no matter what we can infer of the argument being a "class" or not, if timerified is invoke using new keyboard (i.e. if new.target !== undefined), we should pass that along.

Comment on lines +68 to +69
const result = isCalledAsConstructor(fn) ?
ReflectConstruct(fn, args, fn) : ReflectApply(fn, this, args);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const result = isCalledAsConstructor(fn) ?
ReflectConstruct(fn, args, fn) : ReflectApply(fn, this, args);
const result = new.target !== undefined ?
ReflectConstruct(fn, args, fn) :
ReflectApply(fn, this, args);

Copy link
Contributor

Choose a reason for hiding this comment

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

You marked this conversation as resolved but didn't take my suggestion. Is it because you disagree with it, or because you forgot to push?

Copy link
Contributor

Choose a reason for hiding this comment

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

@aduh95 wouldn't new.target not be undefined only if timerified() is called with new instead of fn()?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but that's the point. We want the timerified function to behave the same as the original function in both cases.

Copy link
Contributor

@RaisinTen RaisinTen Oct 31, 2021

Choose a reason for hiding this comment

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

Ah, we are returning timerified from timerify. Yea, this 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.

Leaving a review so this doesn't land by accident before we have consensus on the semantics - I feel strongly that this should work for "old style" classes before we merge and that we should address the fact that classes can have return value (and a test) - but I am ready to be convinced otherwise.

@iam-frankqiu
Copy link
Contributor Author

Leaving a review so this doesn't land by accident before we have consensus on the semantics - I feel strongly that this should work for "old style" classes before we merge and that we should address the fact that classes can have return value (and a test) - but I am ready to be convinced otherwise.

Ok. If a function like below:

function MyClass () {
  this.foo = 15;
  return 20
}
MyClass.prototype.sayFoo = () => {
  console.log(this.foo);
}
new MyClass()
> MyClass {foo: 15}
MyClass()
> 20

If this function is a parameter to timerify. I really don't know we should use Reflect.constructor or
Reflect.apply to execute it. I think that is the question. Who can tell me how to do? @benjamingr @targos That's what I want to say. Thank you.

@iam-frankqiu
Copy link
Contributor Author

I think maybe we can add a comment to the parameter to tell users. This is the only way I can think of to solve the question. This is a bug and we need to solve it. How about this way? @benjamingr @targos

@aduh95
Copy link
Contributor

aduh95 commented Oct 30, 2021

I still think #40625 (comment) would be a better solution, and would align with what Benjamin and Michaël are asking. Is there a reason you don't want to implement it?

@hax
Copy link
Contributor

hax commented Apr 12, 2022

I think we should usenew.target trick. Is there any other reason why this PR not landed?

@cola119
Copy link
Member

cola119 commented Jun 5, 2022

@iam-frankqiu Any updates? If not, I'd like to create another PR that fixes this issue with new.target way.

@aduh95
Copy link
Contributor

aduh95 commented Jun 12, 2022

Superseded by #43330.

@aduh95 aduh95 closed this Jun 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

performance.timerify doesn't return wrapped function's return value
9 participants