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

misc(logger): declaratively mark functions for timings #6515

Closed
wants to merge 12 commits into from

Conversation

connorjclark
Copy link
Collaborator

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, the auditDefn parameter in runner.js.

After, follow up on #6410.

/* eslint-disable no-invalid-this */
static timeDecorate(originalFn, opts) {
return function timeDecoratedFn(...args) {
const timeStartLogLevel = opts.timeStartLogLevel || 'log';
Copy link
Collaborator Author

@connorjclark connorjclark Nov 10, 2018

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/, '');
Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yup!

Copy link
Collaborator

@patrickhulce patrickhulce left a 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 😉

static timeDecorate(originalFn, opts) {
return function timeDecoratedFn(...args) {
const timeStartLogLevel = opts.timeStartLogLevel || 'log';
const timeEndLogLevel = opts.timeStartLogLevel || 'verbose';
Copy link
Collaborator

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

typedefs here? :)

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 made it happen, but had to type the entire file too :)

if (typeof opts.msg === 'string') {
msg = opts.msg;
} else if (typeof opts.msg === 'function') {
msg = opts.msg.bind(this)(...args);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
msg = opts.msg.bind(this)(...args);
msg = opts.msg.apply(this, args);

if (typeof opts.id === 'string') {
id = opts.id;
} else if (typeof opts.id === 'function') {
id = opts.id.bind(this)(...args);
Copy link
Collaborator

Choose a reason for hiding this comment

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

same


let result;
try {
result = originalFn.bind(this)(...args);
Copy link
Collaborator

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) {
Copy link
Collaborator

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;
Copy link
Collaborator

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

Copy link
Collaborator Author

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?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah gotcha!

@@ -0,0 +1,152 @@
/**
* @license Copyright 2016 Google Inc. All Rights Reserved.
Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

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;
Copy link
Collaborator

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 :)

@connorjclark
Copy link
Collaborator Author

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.

@connorjclark
Copy link
Collaborator Author

connorjclark commented Nov 12, 2018

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');
Copy link
Collaborator

@patrickhulce patrickhulce Nov 29, 2018

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 ;)

Copy link
Collaborator Author

@connorjclark connorjclark Nov 29, 2018

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/, '');
Copy link
Collaborator

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

Copy link
Collaborator

@patrickhulce patrickhulce 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 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');
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

const timeStartLogLevel = opts.timeStartLogLevel || 'log';
const timeEndLogLevel = opts.timeEndLogLevel || 'verbose';

let msg;
Copy link
Collaborator

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)}

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

Log.timeEnd(status, timeEndLogLevel);
return value;
}).catch(err => {
err.stack = err.stack.replace(/.* at timeDecoratedFn .*\n/, '');
Copy link
Collaborator

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)) {
Copy link
Collaborator

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! 👍

Copy link
Collaborator Author

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;
Copy link
Collaborator

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];
Copy link
Collaborator

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}')

Copy link
Collaborator Author

@connorjclark connorjclark Nov 29, 2018

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

Copy link
Collaborator

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 😆 😛

Copy link
Collaborator Author

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

Copy link
Collaborator

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);
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

@connorjclark
Copy link
Collaborator Author

@patrickhulce how about this;

  1. split logger changes in a separate PR, publish new version
  2. update usages of it in another PR
  3. open issue to discuss pulling the logger package into core

@patrickhulce
Copy link
Collaborator

Love it @hoten, let's make it happen!

@brendankenny brendankenny deleted the issue-6425-auto-mark branch December 10, 2018 20:04
@brendankenny brendankenny restored the issue-6425-auto-mark branch December 10, 2018 20:04
@brendankenny brendankenny deleted the issue-6425-auto-mark branch December 12, 2018 01:41
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.

2 participants