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

New: Emit preview metric messages #648

Merged

Conversation

JustinHoldstock
Copy link
Contributor

@JustinHoldstock JustinHoldstock commented Feb 9, 2018

The intention of this PR is to

  1. Provide setup for emitting preview_metric events
  2. Emit a preview_metric event with load related metrics

How to use

Timer Mechanism
To begin timing the load events, we call Timer.start(tag), the tag being a unique tag for that event. I've been using the name of the event, paired with the file id of the file we're tracking. ie) Timer.start('file_info_time_1234545')

To stop a timer, to get an accurate reading of how long it took, call Timer.stop(tag) with the same tag. ie) Timer.stop('file_info_time_1234545')

To get timer values, call Timer.get(tag), and you'll receive an object with start, end, and elapsed values. If the tag has been started and not stopped, no end or elapsed will exist. If you've never started the tag, then you will receive either an object with undefined values OR undefined.

I've been using Timer.start() either right before a GET is made on the URL, or right before the function call that does so. I've been using Timer.stop() when the asset is received, parsed, and usable. IE) For full_document_load_time and DocBaseViewer, I start when we get the PDF loading task (in initViewer()) and I stop inside of finishLoading(), (in Preview.), when we emit 'load'.

Finally, on Preview.destroy() or Preview.finishLoading() (guarantees we log either when the document loads or errored out along the way), we emit the message with the relevant props.

Relies of PR #619

@boxcla
Copy link

boxcla commented Feb 9, 2018

Verified that @JustinHoldstock has signed the CLA. Thanks for the pull request!

@JustinHoldstock
Copy link
Contributor Author

Note that this is unfinished. I want to get some eyes on it before I continue

* @param {Error} error - The error that occurred.
* @return {void}
*/
logPreviewError(error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe different names to differentiate from logPreviewEvent() since this isn't actually logging anything while the other function is actually making a log call?

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'll rename both to emitPreviewError() and emitLoadMetric()

@@ -867,6 +888,8 @@ class Preview extends EventEmitter {
const { apiHost, queryParams } = this.options;
const fileVersionId = this.getFileOption(this.file.id, FILE_OPTION_FILE_VERSION_ID) || '';

Timer.start(`${LOAD_METRIC.fileInfoTime}_${this.file.id}`);
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 use file_${this.file.id}_XXX since we're using file_${this.file.id} for the internal file object key. We probably don't need to worry about file versions for now but this will disambiguate if we choose to do so in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DONE!


const event = {
event_name: 'preview_load',
value: total, // Sum of all available load times.
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you tested whether this value matches the legacy total_load_time?

Copy link
Contributor Author

@JustinHoldstock JustinHoldstock Feb 9, 2018

Choose a reason for hiding this comment

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

From the handful of tests I've run, this total is closer to Chrome's Network tab

@@ -80,6 +83,8 @@ class RepStatus extends EventEmitter {
return Promise.resolve();
}

// Using content.url_template to guarantee uniqueness
Timer.start(`${LOAD_METRIC.convertTime}_${this.representation.content.url_template}`);
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 going to get started multiple times since when we hit pending as a status, we call this function again.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, we don't really need to key on the url template right? We just care about 'How long did this file spend converting'. Doesn't matter which representation (or does it?)

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've made it so start only starts the timer once. I'm now extracting the file id from the template URL, for a similar tag name to the rest

src/lib/Timer.js Outdated
Object.keys(this.times).forEach((key) => {
delete this.times[key];
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

JSDoc when you get a chance :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


// Do nothing if there is nothing worth logging.
const infoTime = Timer.get(`${LOAD_METRIC.fileInfoTime}_${this.file.id}`) || {};
if (!infoTime.elapsed) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could elapsed be 0 here? e.g. if something is already cached we don't spend any time fetching file info?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If file info is in the cache already, will the representation asset that it belongs to be loaded into browser cache? If that's the case, we don't want to log how fast the browser can load things from cache

@@ -163,6 +166,13 @@ class Preview extends EventEmitter {
this.navigateLeft = this.navigateLeft.bind(this);
this.navigateRight = this.navigateRight.bind(this);
this.keydownHandler = this.keydownHandler.bind(this);

this.on(PREVIEW_ERROR, (msg) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Arrow function is unnecessary here

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 section is for debugging purposes only

@@ -163,6 +166,13 @@ class Preview extends EventEmitter {
this.navigateLeft = this.navigateLeft.bind(this);
this.navigateRight = this.navigateRight.bind(this);
this.keydownHandler = this.keydownHandler.bind(this);

this.on(PREVIEW_ERROR, (msg) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean we need to remove the console.error from everywhere else in the code base?

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 section is for debugging purposes only

console.error(msg);
});
this.on(PREVIEW_METRIC, (msg) => {
console.log(msg);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we always want to log these?

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 section is for debugging purposes only, but yes, we will always want to log PREVIEW_METRICs

@@ -260,6 +262,16 @@ class BaseViewer extends EventEmitter {
}, this.loadTimeout);
}

/**
* Start the laod timer for fullDocumentLoad event.
Copy link
Contributor

Choose a reason for hiding this comment

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

load

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

src/lib/Timer.js Outdated
}

reset() {
Object.keys(this.times).forEach((key) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you just set it back to {}?

Copy link
Contributor

Choose a reason for hiding this comment

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

Would there ever be a case that you would want to reset only a single timer?

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've decided to just reset the values in the struct to undefined. Also added case to target a single timer.

expect(log.timestamp).to.exist;
expect(log.file_id).to.equal(id);
expect(log.file_version_id).to.exist;
expect(log.content_type).to.exsit;
Copy link
Contributor

Choose a reason for hiding this comment

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

typeo?

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 want to know why these tests passed, locally, if this was here :O

* @private
* @return {Object} Log details for viewer session and current file.
*/
createLog() {
Copy link
Contributor

Choose a reason for hiding this comment

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

to follow Tony's comment below, maybe this becomes createEvent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

createLogEvent()? Since it contains relevant logging values?

* @private
* @return {void}
*/
logLoadMetrics() {
Copy link
Contributor

Choose a reason for hiding this comment

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

this and the above function both actually emit as opposed to log. How do you feel about emitLoadMetrics and emitPreviewError instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above response to Tony's comment. It's exactly that :)

error: err,
...this.createLog()
};

Copy link
Contributor

Choose a reason for hiding this comment

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

We could move all of our console.errors here whenever we call logPreview error (or maybe in triggerError), to reduce console.error clutter.

@@ -129,6 +129,7 @@ class MediaBaseViewer extends BaseViewer {
return this.getRepStatus()
.getPromise()
.then(() => {
this.startLoadTimer();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we move the startLoadTimer to a part of the promise chain in RepStatus.js? We could then it over there once and avoid doing it everywhere else. Basically if the repstatus is success then we want to start timing the content load.

Copy link
Contributor Author

@JustinHoldstock JustinHoldstock Feb 9, 2018

Choose a reason for hiding this comment

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

This is not meant to track time for RepStatus. This begins the timer for when we setup and send the GET request for the representation. Inside of RepStatus, you'll see a timer related to representation conversion time

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but since we want to track from once rep status comes back to success until the file is done loading, why not append to the end of rep status so we only have to add it in one place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I see, I misunderstood your Q. We don't want to do that because we don't want to include the time to process the JS between the rep coming back and then making the request for the rep. We ONLY want to be tracking network related times, for that metric.

Copy link
Contributor

@pramodsum pramodsum left a comment

Choose a reason for hiding this comment

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

Some tiny nit-picky things but this is AWESOME 🎉

@@ -882,6 +899,9 @@ class Preview extends EventEmitter {
*/
handleFileInfoResponse(response) {
let file = response;
// Stop timer for file info time event.
Copy link
Contributor

Choose a reason for hiding this comment

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

newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -1096,7 +1128,6 @@ class Preview extends EventEmitter {
} else {
// Bump up preview count
this.count.success += 1;

Copy link
Contributor

Choose a reason for hiding this comment

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

leave this newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -1195,12 +1226,15 @@ class Preview extends EventEmitter {

// Check if hit the retry limit
if (this.retryCount > RETRY_COUNT) {
let errorCode = ERROR_CODE.retriesExceeded;
let errorMessage = __('error_refresh');
if (err.response && err.response.status === 429) {
Copy link
Contributor

Choose a reason for hiding this comment

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

new line

*/
emitPreviewError(error) {
const err = error;
// If we haven't supplied a code, then it was thrown by the browser
Copy link
Contributor

Choose a reason for hiding this comment

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

newline before comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

src/lib/Timer.js Outdated
*/
stop(tag) {
const time = this.get(tag);
// The timer has already been stopped, or hasn't started. Don't stop it again.
Copy link
Contributor

Choose a reason for hiding this comment

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

newline

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

src/lib/Timer.js Outdated
reset(tag) {
if (tag) {
const time = this.get(tag);
// We don't need to clean up nothin'
Copy link
Contributor

Choose a reason for hiding this comment

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

newline and maybe cleanup the comment :P

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

src/lib/Timer.js Outdated
time.end = undefined;
time.elapsed = undefined;
} else {
Object.keys(this.times).forEach((timeTag) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Object.keys(this.times).forEach(this.reset);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

boom, done

/* global BoxAnnotations */
const boxAnnotations = this.options.boxAnnotations || new BoxAnnotations(viewerOptions);
if (!global.BoxAnnotations) {
const error = createPreviewError(ERROR_CODE.annotationsLoadFail);
Copy link
Contributor

Choose a reason for hiding this comment

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

😍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🎉

@JustinHoldstock JustinHoldstock merged commit d5606d1 into box:master Feb 13, 2018
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.

6 participants