-
Notifications
You must be signed in to change notification settings - Fork 9.5k
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
misc(logger): declaratively mark functions for timings #6515
Conversation
lighthouse-logger/index.js
Outdated
/* eslint-disable no-invalid-this */ | ||
static timeDecorate(originalFn, opts) { | ||
return function timeDecoratedFn(...args) { | ||
const timeStartLogLevel = opts.timeStartLogLevel || 'log'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maintaining the same feature set as before, with how messages are only logged w/ the proper verbosity level.
Log.timeEnd(status, timeEndLogLevel); | ||
return value; | ||
}).catch(err => { | ||
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't work if minified. Is just the viewer minified? is it mangled? I need to check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC our "minification" is just removing whitespace at the beginning of each line, but viewer might be special
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about packages that bundle lighthouse (and likely minify) themselves? AFAICT, minifiers like uglify-js
do not support "do not mangle me" comment markers, which would be the only way to ensure timeDecoratedFn
is always named as such.
Anyway, it's not a big deal. Even if this fails, that just means an extra stack in an already unreadable (minified) stack trace. Unless source maps are applied, then it would show up. Should only address it if anyone ever complains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to timeEnd
in this .catch
block too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ❤️this! so much cleaner to read everything! :D
also encourages splitting off parts of functions into methods so double bonus 😉
lighthouse-logger/index.js
Outdated
static timeDecorate(originalFn, opts) { | ||
return function timeDecoratedFn(...args) { | ||
const timeStartLogLevel = opts.timeStartLogLevel || 'log'; | ||
const timeEndLogLevel = opts.timeStartLogLevel || 'verbose'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like this should be a different opt?
@@ -128,6 +128,72 @@ class Log { | |||
marky.stop(id); | |||
} | |||
|
|||
/* eslint-disable no-invalid-this */ | |||
static timeDecorate(originalFn, opts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typedefs here? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it happen, but had to type the entire file too :)
lighthouse-logger/index.js
Outdated
if (typeof opts.msg === 'string') { | ||
msg = opts.msg; | ||
} else if (typeof opts.msg === 'function') { | ||
msg = opts.msg.bind(this)(...args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
msg = opts.msg.bind(this)(...args); | |
msg = opts.msg.apply(this, args); |
lighthouse-logger/index.js
Outdated
if (typeof opts.id === 'string') { | ||
id = opts.id; | ||
} else if (typeof opts.id === 'function') { | ||
id = opts.id.bind(this)(...args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
lighthouse-logger/index.js
Outdated
|
||
let result; | ||
try { | ||
result = originalFn.bind(this)(...args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
} | ||
/* eslint-enable no-invalid-this */ | ||
|
||
static timeDecorateClass(klass, methods) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typedefs :)
static timeDecorateClass(klass, methods) { | ||
for (const [method, opts] of Object.entries(methods)) { | ||
if (!opts.id) { | ||
const className = (typeof klass === 'function' ? klass : klass.constructor).name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe a quick note on why we need to handle both? seems like if this is always called on classes we shouldn't need the situation where it's not a function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's called on classes and prototypes - the former are functions but the latter are objects. To decorate instance methods, the first arg should be the prototype. For static methods, it should be the class itself.
It's clearer w/ the comments in the .d.ts
file. Should I just move those to the implementation file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha!
lighthouse-logger/test/index-test.js
Outdated
@@ -0,0 +1,152 @@ | |||
/** | |||
* @license Copyright 2016 Google Inc. All Rights Reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2018 🎉
}); | ||
|
||
it('accepts function template for msg and id', () => { | ||
// eslint-disable-next-line no-unused-vars |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe assert the values are what you expect them to be or just get rid of them if they're not important to the test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the "retains parameters" test tests for parameters being passed correctly, so I didn't want to keep testing that in subsequent tests. Could remove the parameter list from the function, but it does better document exactly what "value1/value2" of the msg/id template functions are.
typings/lighthouse-logger/index.d.ts
Outdated
export function time(status: Status, level?: string): void; | ||
export function timeEnd(status: Status, level?: string): void; | ||
|
||
type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh here are the typedefs! very nice :)
I bit the bullet and moved all the typings for the logger to the .js file, instead of typings/lighthouse-logger. I don't know how to generate a .d.ts from the Closure-typed comments, so my workaround was to create ./lib/log.js to reexport lighthouse-logger within the Closure/TS type system, and change all the logger requires to that instead. |
Tests will fail until new version of lighthouse-logger is released. It's tricky to separate developing the changes to lighthouse-logger and usages, so after all comments are resolved in the PR I'll branch and send a PR w/ just the logger changes, followed by a PR for usage changes. |
docs/readme.md
Outdated
@@ -74,7 +74,7 @@ and set an appropriate logging level in your code. You'll also need to pass | |||
the `logLevel` flag when calling `lighthouse`. | |||
|
|||
```javascript | |||
const log = require('lighthouse-logger'); | |||
const log = require('../lib/log'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure we should be changing the public docs on how to use it though ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hahaha nice catch. got too global search-and-replace happy
Log.timeEnd(status, timeEndLogLevel); | ||
return value; | ||
}).catch(err => { | ||
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC our "minification" is just removing whitespace at the beginning of each line, but viewer might be special
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm happy enough with the API to move on to split landing 👍
Just the unresolved question about ditching lighthouse-logger
that might make this easier for you before you jump on it :)
@@ -7,7 +7,7 @@ | |||
|
|||
const lighthouse = require('../lighthouse-core/index.js'); | |||
const RawProtocol = require('../lighthouse-core/gather/connections/raw.js'); | |||
const log = require('lighthouse-logger'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we're doing this it seems like we might as well just directly require in from ../lighthouse-logger
and expose the logger object for setting level and whatnot at require('lighthouse')
, then we wouldn't need to worry about this publish business.
honestly, the only reason it was separate package at all was so that ChromeLauncher could share the logging functions/levels but I'm thinking there are much better ways to go about sharing a log object with chrome launcher than to continue to jump through all these hoops. I realize this is a bit more than you probably wanted to chew on for this change 😆 but if we're rewriting all the logger require statements to make this work, it's worth discussing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the better way to share this code with ChromeLauncher? I would very much like to pull this package back into core.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking ChromeLauncher could just accept a logger
parameter as an option that conforms to the API we have and we can pass lighthouse-logger
into, but we could use regular ol' debug
by default
lighthouse-logger/index.js
Outdated
const timeStartLogLevel = opts.timeStartLogLevel || 'log'; | ||
const timeEndLogLevel = opts.timeEndLogLevel || 'verbose'; | ||
|
||
let msg; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big deal, but it seems like msg
and id
could be turned into functions outside of the timeDecoratedFn
so we could simplify what's going on inside each invocation a bit, i.e. line 212-234 becomes just const status = {msg: computeMsg(this, args), id: computeId(this, args)}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Log.timeEnd(status, timeEndLogLevel); | ||
return value; | ||
}).catch(err => { | ||
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/, ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to timeEnd
in this .catch
block too?
* @param {{[key in Prop]: TimeDecorateOpts<Class, ArgumentTypes<IsFunction<Class[key]>>>}} methods | ||
*/ | ||
static timeDecorateClass(klass, methods) { | ||
for (const [method, opts] of Object.entries(methods)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised this works with class prototypes, last time I dealt with this I feel like they weren't enumerable and I had to use getOwnPropertyNames
for some reason. If you happened to run into some issues that you could share why I struggled last time I'd love to hear your insight :)
Ohhhhhh you're iterating over the methods that are manually specified, not the class members! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah you got it. these are explicitly defined via an object literal when calling the function
static timeDecorateClass(klass, methods) { | ||
for (const [method, opts] of Object.entries(methods)) { | ||
if (!opts.id) { | ||
const className = (typeof klass === 'function' ? klass : klass.constructor).name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah gotcha!
opts.id = `lh:${className}:${method}`; | ||
} | ||
/** @type {IsFunction<Class[typeof method]>} */ | ||
const original = klass[method]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe just do a quick spelling check here? if (!original) throw new Error('Cannot decorate non-existant method ${method}')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean existence check, yeah? :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean existence check, yeah? :P
hey hey let's give ms. anonymous future coder the benefit of the doubt that the method does exist and it was just a typo 😆 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahhh i see i see. misunderstood.
speaking of spelling mistakes; existent* haha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
touché
static log(title, ...args) { | ||
Log.events.issueStatus(title, args); | ||
return Log._logToStdErr(title, args); | ||
Log._logToStdErr(title, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this really change anything or was it just for ts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, changes nothing. The return value was void.
@patrickhulce how about this;
|
Love it @hoten, let's make it happen! |
Fixes #6425.
Whew, this was a journey into TypeScript for me.
I also converted most usages of
time
/timeEnd
. The ones remaining either 1) are around just part of a function, and I did not want to decompose it; 2) in a constructor, which cannot be decorated, nor decomposed without making TS very unhappy; 3) behind a conditional, so converting wouldn't be semantically equivalent.I noticed that VSCode's TS language server can sometimes fail to identify the types. Requires restarting the language server, and also first viewing the type (you can hover over it) - otherwise it will identify as
any
. For example, theauditDefn
parameter inrunner.js
.After, follow up on #6410.