Improvements to the Activity API

Summary:
Revised the Activity implementation
- added flow annotations
- fixed some lint warnings
- added event `options`, specifically a `telemetric` option which indicates that events tagged in this manner are relevant for telemetry. The duration of these events is now highlighted in the Activity log

Reviewed By: bestander, kentaromiura

Differential Revision: D3770753

fbshipit-source-id: 6976535fd3bf5269c6263d825d657ab0805ecaad
This commit is contained in:
Ovidiu Viorel Iepure 2016-08-25 09:51:31 -07:00 committed by Facebook Github Bot 7
parent 053720b66f
commit ad7aa8434a
8 changed files with 178 additions and 74 deletions

25
react-packager/src/Activity/Types.js vendored Normal file
View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*
*/
'use strict';
export type EventOptions = {
telemetric: boolean,
};
export type Event = {
id: number,
startTimeStamp: number,
endTimeStamp?: number,
name: string,
data?: any,
options: EventOptions,
};

View File

@ -13,14 +13,17 @@ jest.disableAutomock();
var Activity = require('../'); var Activity = require('../');
describe('Activity', () => { describe('Activity', () => {
// eslint-disable-next-line no-console-disallow
const origConsoleLog = console.log; const origConsoleLog = console.log;
beforeEach(() => { beforeEach(() => {
// eslint-disable-next-line no-console-disallow
console.log = jest.fn(); console.log = jest.fn();
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
}); });
afterEach(() => { afterEach(() => {
// eslint-disable-next-line no-console-disallow
console.log = origConsoleLog; console.log = origConsoleLog;
}); });
@ -32,7 +35,9 @@ describe('Activity', () => {
Activity.startEvent(EVENT_NAME, DATA); Activity.startEvent(EVENT_NAME, DATA);
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
// eslint-disable-next-line no-console-disallow
expect(console.log.mock.calls.length).toBe(1); expect(console.log.mock.calls.length).toBe(1);
// eslint-disable-next-line no-console-disallow
const consoleMsg = console.log.mock.calls[0][0]; const consoleMsg = console.log.mock.calls[0][0];
expect(consoleMsg).toContain('START'); expect(consoleMsg).toContain('START');
expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(EVENT_NAME);
@ -49,7 +54,9 @@ describe('Activity', () => {
Activity.endEvent(eventID); Activity.endEvent(eventID);
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
// eslint-disable-next-line no-console-disallow
expect(console.log.mock.calls.length).toBe(2); expect(console.log.mock.calls.length).toBe(2);
// eslint-disable-next-line no-console-disallow
const consoleMsg = console.log.mock.calls[1][0]; const consoleMsg = console.log.mock.calls[1][0];
expect(consoleMsg).toContain('END'); expect(consoleMsg).toContain('END');
expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(EVENT_NAME);

View File

@ -5,98 +5,122 @@
* This source code is licensed under the BSD-style license found in the * This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant * LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*
*/ */
'use strict'; 'use strict';
import type {EventOptions} from './Types';
import type {Event} from './Types';
const chalk = require('chalk'); const chalk = require('chalk');
const events = require('events'); const events = require('events');
const _eventStarts = Object.create(null); let ENABLED = true;
const _eventEmitter = new events.EventEmitter(); let UUID = 1;
let _uuid = 1; const EVENT_INDEX: {[key: number]: Event} = Object.create(null);
let _enabled = true; const EVENT_EMITTER = new events.EventEmitter();
function endEvent(eventId) { function startEvent(
const eventEndTime = Date.now(); name: string,
if (!_eventStarts[eventId]) { data: any = null,
throw new Error('event(' + eventId + ') either ended or never started'); options?: EventOptions = {telemetric: false},
): number {
if (name == null) {
throw new Error('No event name specified!');
} }
_writeAction({ const id = UUID++;
action: 'endEvent', EVENT_INDEX[id] = {
eventId: eventId, id,
tstamp: eventEndTime startTimeStamp: Date.now(),
}); name,
} data,
options,
function startEvent(eventName, data) {
const eventStartTime = Date.now();
if (eventName == null) {
throw new Error('No event name specified');
}
if (data == null) {
data = null;
}
const eventId = _uuid++;
const action = {
action: 'startEvent',
data: data,
eventId: eventId,
eventName: eventName,
tstamp: eventStartTime,
}; };
_eventStarts[eventId] = action; logEvent(id, 'startEvent');
_writeAction(action); return id;
return eventId;
} }
function disable() { function endEvent(id: number): void {
_enabled = false; getEvent(id).endTimeStamp = Date.now();
logEvent(id, 'endEvent');
} }
function _writeAction(action) { function getEvent(id: number): Event {
_eventEmitter.emit(action.action, action); if (!EVENT_INDEX[id]) {
throw new Error(`Event(${id}) either ended or never started`);
}
if (!_enabled) { return EVENT_INDEX[id];
}
function forgetEvent(id: number): void {
delete EVENT_INDEX[id];
}
function logEvent(id: number, phase: 'startEvent' | 'endEvent'): void {
const event = EVENT_INDEX[id];
EVENT_EMITTER.emit(phase, id);
if (!ENABLED) {
return; return;
} }
const data = action.data ? ': ' + JSON.stringify(action.data) : ''; const {
const fmtTime = new Date(action.tstamp).toLocaleTimeString(); startTimeStamp,
endTimeStamp,
name,
data,
options,
} = event;
switch (action.action) { const duration = +endTimeStamp - startTimeStamp;
const dataString = data ? ': ' + JSON.stringify(data) : '';
const {telemetric} = options;
switch (phase) {
case 'startEvent': case 'startEvent':
console.log(chalk.dim( // eslint-disable-next-line no-console-disallow
'[' + fmtTime + '] ' + console.log(
'<START> ' + action.eventName + chalk.dim(
data '[' + new Date(startTimeStamp).toLocaleString() + '] ' +
)); '<START> ' + name + dataString
)
);
break; break;
case 'endEvent': case 'endEvent':
const startAction = _eventStarts[action.eventId]; // eslint-disable-next-line no-console-disallow
const startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : ''; console.log(
console.log(chalk.dim( chalk.dim('[' + new Date(endTimeStamp).toLocaleString() + '] ' + '<END> ' + name) +
'[' + fmtTime + '] ' + chalk.dim(dataString) +
'<END> ' + startAction.eventName + (telemetric ? chalk.reset.cyan(' (' + (duration) + 'ms)') : chalk.dim(' (' + (duration) + 'ms)'))
' (' + (action.tstamp - startAction.tstamp) + 'ms)' + );
startData forgetEvent(id);
));
delete _eventStarts[action.eventId];
break; break;
default: default:
throw new Error('Unexpected scheduled action type: ' + action.action); throw new Error('Unexpected scheduled event type: ' + name);
} }
} }
function enable(): void {
ENABLED = true;
}
exports.endEvent = endEvent; function disable(): void {
exports.startEvent = startEvent; ENABLED = false;
exports.disable = disable; }
exports.eventEmitter = _eventEmitter;
module.exports = {
startEvent,
endEvent,
getEvent,
forgetEvent,
enable,
disable,
eventEmitter: EVENT_EMITTER,
};

View File

@ -356,7 +356,13 @@ class Bundler {
onModuleTransformed = noop, onModuleTransformed = noop,
finalizeBundle = noop, finalizeBundle = noop,
}) { }) {
const findEventId = Activity.startEvent('find dependencies'); const findEventId = Activity.startEvent(
'Finding dependencies',
null,
{
telemetric: true,
},
);
const modulesByName = Object.create(null); const modulesByName = Object.create(null);
if (!resolutionResponse) { if (!resolutionResponse) {

View File

@ -485,7 +485,7 @@ class Server {
_processAssetsRequest(req, res) { _processAssetsRequest(req, res) {
const urlObj = url.parse(req.url, true); const urlObj = url.parse(req.url, true);
const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/);
const assetEvent = Activity.startEvent(`processing asset request ${assetPath[1]}`); const assetEvent = Activity.startEvent('Processing asset request', {asset: assetPath[1]});
this._assetServer.get(assetPath[1], urlObj.query.platform) this._assetServer.get(assetPath[1], urlObj.query.platform)
.then( .then(
data => res.end(this._rangeRequestMiddleware(req, res, data, assetPath)), data => res.end(this._rangeRequestMiddleware(req, res, data, assetPath)),
@ -610,7 +610,15 @@ class Server {
return; return;
} }
const startReqEventId = Activity.startEvent('request:' + req.url); const startReqEventId = Activity.startEvent(
'Requesting bundle',
{
url: req.url,
},
{
telemetric: true,
},
);
const options = this._getOptionsFromUrl(req.url); const options = this._getOptionsFromUrl(req.url);
debug('Getting bundle for request'); debug('Getting bundle for request');
const building = this._useCachedOrUpdateOrCreateBundle(options); const building = this._useCachedOrUpdateOrCreateBundle(options);
@ -661,7 +669,13 @@ class Server {
} }
_symbolicate(req, res) { _symbolicate(req, res) {
const startReqEventId = Activity.startEvent('symbolicate'); const startReqEventId = Activity.startEvent(
'Symbolicating',
null,
{
telemetric: true,
},
);
new Promise.resolve(req.rawBody).then(body => { new Promise.resolve(req.rawBody).then(body => {
const stack = JSON.parse(body).stack; const stack = JSON.parse(body).stack;

View File

@ -1,4 +1,4 @@
/** /**
* Copyright (c) 2015-present, Facebook, Inc. * Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved. * All rights reserved.
* *
@ -60,6 +60,10 @@ class DeprecatedAssetMap {
if (activity) { if (activity) {
processAsset_DEPRECATEDActivity = activity.startEvent( processAsset_DEPRECATEDActivity = activity.startEvent(
'Building (deprecated) Asset Map', 'Building (deprecated) Asset Map',
null,
{
telemetric: true,
},
); );
} }

View File

@ -46,7 +46,13 @@ class Fastfs extends EventEmitter {
let fastfsActivity; let fastfsActivity;
const activity = this._activity; const activity = this._activity;
if (activity) { if (activity) {
fastfsActivity = activity.startEvent('Building in-memory fs for ' + this._name); fastfsActivity = activity.startEvent(
'Building in-memory fs for ' + this._name,
null,
{
telemetric: true,
},
);
} }
files.forEach(filePath => { files.forEach(filePath => {
const root = this._getRoot(filePath); const root = this._getRoot(filePath);

View File

@ -1,4 +1,4 @@
/** /**
* Copyright (c) 2015-present, Facebook, Inc. * Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved. * All rights reserved.
* *
@ -91,8 +91,20 @@ class DependencyGraph {
} }
const {activity} = this._opts; const {activity} = this._opts;
const depGraphActivity = activity.startEvent('Building Dependency Graph'); const depGraphActivity = activity.startEvent(
const crawlActivity = activity.startEvent('Crawling File System'); 'Building Dependency Graph',
null,
{
telemetric: true,
},
);
const crawlActivity = activity.startEvent(
'Crawling File System',
null,
{
telemetric: true,
},
);
const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED); const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED);
this._crawling = crawl(allRoots, { this._crawling = crawl(allRoots, {
ignore: this._opts.ignoreFilePath, ignore: this._opts.ignoreFilePath,
@ -148,7 +160,13 @@ class DependencyGraph {
this._loading = Promise.all([ this._loading = Promise.all([
this._fastfs.build() this._fastfs.build()
.then(() => { .then(() => {
const hasteActivity = activity.startEvent('Building Haste Map'); const hasteActivity = activity.startEvent(
'Building Haste Map',
null,
{
telemetric: true,
},
);
return this._hasteMap.build().then(map => { return this._hasteMap.build().then(map => {
activity.endEvent(hasteActivity); activity.endEvent(hasteActivity);
return map; return map;