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

core(driver): create eval code using interface #10816

Merged
merged 30 commits into from
Dec 2, 2020

Conversation

connorjclark
Copy link
Collaborator

Idea 1 from #10781

Where simple, I also converted from getElementsInDocumentString to getElementsInDocument. Skipped for tap-targets, was too much.

Copied HTMLElementTagNameMap from dom.js for getElementsInDocument. I renamed it because TS wasn't picking it up, I'm surprised it even worked in dom.js because it's overriding the same identifier.

@connorjclark connorjclark requested a review from a team as a code owner May 20, 2020 01:39
@connorjclark connorjclark requested review from paulirish and removed request for a team May 20, 2020 01:39
@connorjclark connorjclark changed the title core(driver): create page code using structured interface core(driver): create eval code using structured interface May 20, 2020
@connorjclark connorjclark changed the title core(driver): create eval code using structured interface core(driver): create eval code using interface May 20, 2020
Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

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

This is looking great.

It is still going to be weird/misleading to have files that are half running in node and half running in the browser so I think it's probably worth opening up to the eng sync next week, but I'm feeling net-good about the tradeoff :)

Copied HTMLElementTagNameMap from dom.js for getElementsInDocument. I renamed it because TS wasn't picking it up, I'm surprised it even worked in dom.js because it's overriding the same identifier.

Isn't it called HTMLElementByTagName though? I don't think it should be overriding anything..

* @template T, R
* @param {string | ((...args: T[]) => R)} expression
* @param {{useIsolation?: boolean, mode?: 'iffe'|'function', args?: T[], deps?: (Function|string)[]}=} options
* @return {Promise<R>}
Copy link
Member

Choose a reason for hiding this comment

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

Should we just split these into two eval functions?. Keep evaluateAsync and make a new evaluateFunction or something:

  • js-style "overloading" tends to grow in implementation complications over time and is harder to understand as a caller (e.g. options could be included with a string expression, but would it do anything in that case? Now we have to document that, etc)
  • There seems to be a bug in jsdoc generics where R defaults to any here if expression is a string (so there's no inferred R). In the typescript equivalent R will default to unknown (as of Typescript 3.5). If that gets fixed for JS, all the evaluateAsync(string) calls will have to be changed too. I can't think of a great way to set the return type the right way since you still can't do generic defaults or conditional types in jsdoc (AFAIK).

Copy link
Member

Choose a reason for hiding this comment

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

("fun" fact: evaluateAsync is called evaluateAsync and not evaluate because way back, "evaluate" sounded synchronous and we didn't have types to give IDE clues or type checking to ensure that calling code would handle a promised return value. That does make variations on evaluateAsync a little awkward, though, since the "Async" part seems really unnecessary now :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the great feedback!

js-style "overloading" tends to grow in implementation complications over time and is harder to understand as a caller (e.g. options could be included with a string expression, but would it do anything in that case? Now we have to document that, etc)

I took a second pass to incorporate this feedback, and added some docs. Running to a meeting, will respond to the rest later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would you prefer splitting this out to evaluate(fn, ...) and evaluateString(code, ...) (alias for evalauteAsync)?

Copy link
Member

Choose a reason for hiding this comment

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

rather than add another evaluate*() hop with _evaluate, what do you think about keeping it called evaluateAsync, so

evaluate(fn, ...) calls evaluateAsync(string, opts) calls _evaluateInContext(string, contextId)

then in the next breaking change we either incorporate the guts of evaluateAsync into evaluate so it's back to only two functions, or we rename evaluateAsync to evaluateString or whatever if we do want to keep it around for folks to use?

I see the reason for doing it the _evaluate way, but the number of evaluate layers doesn't seem worth it when external to driver we still only have evaluate() and evaluateAsync either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

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 a little confused about the purpose of _evaluate. Is it for a possible patch where evaluateAsync is removed but we still need a function for executing short expressions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I just forgot to delete _evaluate.

@connorjclark
Copy link
Collaborator Author

Isn't it called HTMLElementByTagName though? I don't think it should be overriding anything..

Yeah, I messed up the copy/paste and got confused. Put the names back. Should one of these files do a type import for the other?

* @return {Promise<*>}
*/
async evaluateAsync(expression, options = {}) {
return this.evaluate(expression, options);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could drop this. if we like this, I'd follow up with a PR that changes all existing usages to this.

* See the documentation for `createEvalCode` in `page-functions.js`.
* @template T, R
* @param {string | ((...args: T[]) => R)} expressionOrMainFn
* @param {{useIsolation?: boolean, args?: T[], deps?: (Function|string)[]}=} options
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FYI I would like to drop the string part eventually.

Copy link
Member

Choose a reason for hiding this comment

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

FYI I would like to drop the string part eventually.

could we drop it now and say "if you still want to use a string use the legacy evaluateAsync()"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@brendankenny
Copy link
Member

brendankenny commented May 20, 2020

Should one of these files do a type import for the other?

I had the same thought but the files are in such different contexts I don't know which one should import from the other :)

edit: maybe it doesn't matter and it could be in both. dom.js is kind of the root for DOM stuff in the renderer so it makes sense for other renderer files to import from there, same with page-functions.js being kind of the root for driver.evaluate DOM stuff.

@connorjclark
Copy link
Collaborator Author

@brendankenny any more feedback?

lighthouse-core/gather/driver.js Outdated Show resolved Hide resolved
* @template T, R
* @param {string | ((...args: T[]) => R)} expression
* @param {{useIsolation?: boolean, mode?: 'iffe'|'function', args?: T[], deps?: (Function|string)[]}=} options
* @return {Promise<R>}
Copy link
Member

Choose a reason for hiding this comment

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

rather than add another evaluate*() hop with _evaluate, what do you think about keeping it called evaluateAsync, so

evaluate(fn, ...) calls evaluateAsync(string, opts) calls _evaluateInContext(string, contextId)

then in the next breaking change we either incorporate the guts of evaluateAsync into evaluate so it's back to only two functions, or we rename evaluateAsync to evaluateString or whatever if we do want to keep it around for folks to use?

I see the reason for doing it the _evaluate way, but the number of evaluate layers doesn't seem worth it when external to driver we still only have evaluate() and evaluateAsync either way.

* declaration statement. Args should match the args of `mainFn`, and can be any serializable
* value. `deps` are functions that must be defined for `mainFn` to work.
*/
function createEvalCode(mainFn, {mode, args, deps} = {}) {
Copy link
Member

Choose a reason for hiding this comment

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

could this be directly incorporated into evaluate() (and evaluateFunctionOnObject) instead of splitting it into this file? It's mostly not shared code since the two callers take different branches in the conditional (and it's not really a "page function" in the same way as the other functions in this file are).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I prefer it here bc/ it's easier to test (driver has some overhead with beforeEach etc.) and
keeps the driver file smaller (it's so big). Maybe lib/eval.js?

And then export two function: createFunctionEvalCode and createIffeEvalCode?

Copy link
Member

Choose a reason for hiding this comment

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

I prefer it here bc/ it's easier to test (driver has some overhead with beforeEach etc.) and
keeps the driver file smaller (it's so big). Maybe lib/eval.js?

And then export two function: createFunctionEvalCode and createIffeEvalCode?

well it would definitely be good to split the two, but it'll also have a new home after #11633, and it also looks trivial to add to .evaluateAsync in driver-test?

const {expression} = connectionStub.sendCommand.findInvocation('Runtime.evaluate');
expect(expression).toEqual(`(() => {
function mainFn() {
  return true;
}
return mainFn();
})()`);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@patrickhulce would you prefer this going into driver.js or a new file lib/eval.js?

Copy link
Collaborator

Choose a reason for hiding this comment

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

new lib/eval.js file SGTM, though I'll also +1 @brendankenny's test suggest to make sure .evaluate uses it :)

Copy link
Member

Choose a reason for hiding this comment

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

new lib/eval.js file

Isn't the (currently named) RuntimeController going to be this already in #11633, though? I guess I don't understand what the problem is just inlining the five lines or so :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't the (currently named) RuntimeController going to be this already in #11633, though?

Ya, that's why I don't really care much where it lives in this PR :)

I guess I don't understand what the problem is just inlining the five lines or so

I mean I don't feel terribly strongly about it, the rebase gets slightly more annoying with inline but not by much.

@connorjclark
Copy link
Collaborator Author

saving for separate pr

return `function () {
  ${depsSerialized}
  ${mainFn}
  return ${mainFn.name}.call(this, ${argsSerialized});
}`;

----

it('function', () => {
  function mainFn(a, b) {
    return adder(a, b);
  }
  function adder(a, b) {
    return a + b;
  }
  const code = createEvalCode(mainFn, {
    args: [1, 2],
    mode: 'function',
    deps: [adder],
  });
  expect(code).toEqual(`function () {
    function adder(a, b) {
    return a + b;
  }
    function mainFn(a, b) {
    return adder(a, b);
  }
    return mainFn.call(this, 1,2);
  }`);
  expect(eval(`(${code})()`)).toEqual(3);
});

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

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

LGTM

});
}())`.trim();
expect(expression).toBe(expected);
expect(await eval(expression)).toBe(1);
Copy link
Member

Choose a reason for hiding this comment

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

with this eval idea you're introducing we could probably add some tests that exercise the error catching some time

cc @patrickhulce :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

my hope is that everything in driver becomes less scary and more obvious to test well when it's not all together :)

lighthouse-core/test/gather/driver-test.js Outdated Show resolved Hide resolved

/**
* @param {{a: number, b: number}} _
* @param {any} passThru
Copy link
Member

Choose a reason for hiding this comment

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

no need to change, but I'm curious if there's a reason to make this any?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nothing happens to it, so the most applicable type is any.

lighthouse-core/test/gather/driver-test.js Outdated Show resolved Hide resolved
@connorjclark
Copy link
Collaborator Author

connorjclark commented Nov 13, 2020

oof, globalThis requires node 12. #11656

well, this has been open for 544 days (I really thought I opened this much more recently...woah), so what's a few more :)

@adamraine adamraine removed their assignment Nov 16, 2020
@patrickhulce
Copy link
Collaborator

well, this has been open for 544 days (I really thought I opened this much more recently...woah), so what's a few more :)

This and #11633 will have quite the merge conundrum to sort out :) Are you planning on landing this imminently?

@brendankenny
Copy link
Member

This and #11633 will have quite the merge conundrum to sort out :) Are you planning on landing this imminently?

shouldn't it be not too hard? It's just the one function now and the tests are a separate block (.evaluate() instead of .evaluateAsync())

@patrickhulce
Copy link
Collaborator

shouldn't it be not too hard? It's just the one function now and the tests are a separate block (.evaluate() instead of .evaluateAsync())

I mean sure, no PRs of reasonable size are ever really "too hard", but on the spectrum from normal Lighthouse PR with no merge conflicts to literally every line has a conflict, we're closer to the latter if we assume .evaluate logic moves into the controller as well.

@connorjclark
Copy link
Collaborator Author

Blocked on #11656 for usage of globalThis in our tests. Guess I'll get the honors of resolving the merge conflict.

@patrickhulce
Copy link
Collaborator

Guess I'll get the honors of resolving the merge conflict.

Sorry bud 😞

Don't worry though, I hear it's not too hard 😉

@brendankenny
Copy link
Member

Don't worry though, I hear it's not too hard 😉

it's not automatic, but it's not hard :P

  • copy evaluate()
  • git merge -X theirs master
  • paste evaluate() back

everything is good!

  • move evaluate() to executionContext

@connorjclark
Copy link
Collaborator Author

not sure what to do with the jsdocs. 100% duplicate in both places?

@brendankenny

This comment has been minimized.

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

Successfully merging this pull request may close these issues.

7 participants