This repository was archived by the owner on Sep 6, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Expand file tree
/
Copy pathPerfUtils.js
More file actions
433 lines (380 loc) · 14.8 KB
/
PerfUtils.js
File metadata and controls
433 lines (380 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
/*
* Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets */
/**
* This is a collection of utility functions for gathering performance data.
*/
define(function (require, exports, module) {
"use strict";
var _ = require("thirdparty/lodash"),
StringUtils = require("utils/StringUtils");
// make sure the global brackets variable is loaded
require("utils/Global");
/**
* Flag to enable/disable performance data gathering. Default is true (enabled)
* @type {boolean} enabled
*/
var enabled = brackets && !!brackets.app.getElapsedMilliseconds;
/**
* Peformance data is stored in this hash object. The key is the name of the
* test (passed to markStart/addMeasurement), and the value is the time, in
* milliseconds, that it took to run the test. If multiple runs of the same test
* are made, the value is an Array with each run stored as an entry in the Array.
*/
var perfData = {};
/**
* Active tests. This is a hash of all tests that have had markStart() called,
* but have not yet had addMeasurement() called.
*/
var activeTests = {};
/**
* Updatable tests. This is a hash of all tests that have had markStart() called,
* and have had updateMeasurement() called. Caller must explicitly remove tests
* from this list using finalizeMeasurement()
*/
var updatableTests = {};
/**
* @private
* Keeps the track of measurements sequence number for re-entrant sequences with
* the same name currently running. Entries are created and deleted as needed.
*/
var _reentTests = {};
/**
* @private
* A unique key to log performance data
*
* @param {(string|undefined)} id Unique ID for this measurement name
* @param {!string} name A short name for this measurement
* @param {?number} reent Sequence identifier for parallel tests of the same name
*/
function PerfMeasurement(id, name, reent) {
this.name = name;
this.reent = reent;
if (id) {
this.id = id;
} else {
this.id = (reent) ? "[reent " + this.reent + "] " + name : name;
}
}
/**
* Override toString() to allow using PerfMeasurement as an array key without
* explicit conversion.
*/
PerfMeasurement.prototype.toString = function () {
return this.name;
};
/**
* Create a new PerfMeasurement key. Adds itself to the module export.
* Can be accessed on the module, e.g. PerfUtils.MY_PERF_KEY.
*
* @param {!string} id Unique ID for this measurement name
* @param {!name} name A short name for this measurement
*/
function createPerfMeasurement(id, name) {
var pm = new PerfMeasurement(id, name);
exports[id] = pm;
return pm;
}
/**
* @private
* Generates PerfMeasurements based on the name or array of names.
*/
function _generatePerfMeasurements(name) {
// always convert it to array so that the rest of the routines could rely on it
var id = (!Array.isArray(name)) ? [name] : name;
// generate unique identifiers for each name
var i;
for (i = 0; i < id.length; i++) {
if (!(id[i] instanceof PerfMeasurement)) {
if (_reentTests[id[i]] === undefined) {
_reentTests[id[i]] = 0;
} else {
_reentTests[id[i]]++;
}
id[i] = new PerfMeasurement(undefined, id[i], _reentTests[id[i]]);
}
}
return id;
}
/**
* @private
* Helper function for markStart()
*
* @param {Object} id Timer id.
* @param {number} time Timer start time.
*/
function _markStart(id, time) {
if (activeTests[id.id]) {
console.error("Recursive tests with the same id are not supported. Timer id: " + id.id);
}
activeTests[id.id] = { startTime: time };
}
/**
* Start a new named timer. The name should be as descriptive as possible, since
* this name will appear as an entry in the performance report.
* For example: "Open file: /Users/brackets/src/ProjectManager.js"
*
* Multiple timers can be opened simultaneously.
*
* Returns an opaque set of timer ids which can be stored and used for calling
* addMeasurement(). Since name is often creating via concatenating strings this
* return value allows clients to construct the name once.
*
* @param {(string|Array.<string>)} name Single name or an Array of names.
* @return {(Object|Array.<Object>)} Opaque timer id or array of timer ids.
*/
function markStart(name) {
if (!enabled) {
return;
}
var time = brackets.app.getElapsedMilliseconds();
var id = _generatePerfMeasurements(name);
var i;
for (i = 0; i < id.length; i++) {
_markStart(id[i], time);
}
return id.length > 1 ? id : id[0];
}
/**
* Stop a timer and add its measurements to the performance data.
*
* Multiple measurements can be stored for any given name. If there are
* multiple values for a name, they are stored in an Array.
*
* If markStart() was not called for the specified timer, the
* measured time is relative to app startup.
*
* @param {Object} id Timer id.
*/
function addMeasurement(id) {
if (!enabled) {
return;
}
if (!(id instanceof PerfMeasurement)) {
id = new PerfMeasurement(id, id);
}
var elapsedTime = brackets.app.getElapsedMilliseconds();
if (activeTests[id.id]) {
elapsedTime -= activeTests[id.id].startTime;
delete activeTests[id.id];
}
if (perfData[id]) {
// We have existing data, add to it
if (Array.isArray(perfData[id])) {
perfData[id].push(elapsedTime);
} else {
// Current data is a number, convert to Array
perfData[id] = [perfData[id], elapsedTime];
}
} else {
perfData[id] = elapsedTime;
}
if (id.reent !== undefined) {
if (_reentTests[id] === 0) {
delete _reentTests[id];
} else {
_reentTests[id]--;
}
}
}
/**
* This function is similar to addMeasurement(), but it allows timing the
* *last* event, when you don't know which event will be the last one.
*
* Tests that are in the activeTests list, have not yet been added, so add
* measurements to the performance data, and move test to updatableTests list.
* A test is moved to the updatable list so that it no longer passes isActive().
*
* Tests that are already in the updatableTests list are updated.
*
* Caller must explicitly remove test from the updatableTests list using
* finalizeMeasurement().
*
* If markStart() was not called for the specified timer, there is no way to
* determine if this is the first or subsequent call, so the measurement is
* not updatable, and it is handled in addMeasurement().
*
* @param {Object} id Timer id.
*/
function updateMeasurement(id) {
var elapsedTime = brackets.app.getElapsedMilliseconds();
if (updatableTests[id.id]) {
// update existing measurement
elapsedTime -= updatableTests[id].startTime;
// update
if (perfData[id] && Array.isArray(perfData[id])) {
// We have existing data and it's an array, so update the last entry
perfData[id][perfData[id].length - 1] = elapsedTime;
} else {
// No current data or a single entry, so set/update it
perfData[id] = elapsedTime;
}
} else {
// not yet in updatable list
if (activeTests[id.id]) {
// save startTime in updatable list before addMeasurement() deletes it
updatableTests[id.id] = { startTime: activeTests[id.id].startTime };
}
// let addMeasurement() handle the initial case
addMeasurement(id);
}
}
/**
* Remove timer from lists so next action starts a new measurement
*
* updateMeasurement may not have been called, so timer may be
* in either or neither list, but should never be in both.
*
* @param {Object} id Timer id.
*/
function finalizeMeasurement(id) {
if (activeTests[id.id]) {
delete activeTests[id.id];
}
if (updatableTests[id.id]) {
delete updatableTests[id.id];
}
}
/**
* Returns whether a timer is active or not, where "active" means that
* timer has been started with addMark(), but has not been added to perfdata
* with addMeasurement().
*
* @param {Object} id Timer id.
* @return {boolean} Whether a timer is active or not.
*/
function isActive(id) {
return (activeTests[id.id]) ? true : false;
}
/**
* return single value, or comma separated values for an array or return aggregated values with
* <min value, average, max value, standard deviation>
* @param {Array} entry An array or a single value
* @param {Boolean} aggregateStats If set, the returned value will be aggregated in the form -
* <min(avg)max[standard deviation]>
* @returns {String} a single value, or comma separated values in an array or
* <min(avg)max[standard deviation]> if aggregateStats is set
*/
function getValueAsString(entry, aggregateStats) {
if (!Array.isArray(entry)) {
return entry;
}
if (aggregateStats) {
var sum = 0,
avg,
min = _.min(entry),
max = _.max(entry),
sd,
variationSum = 0;
entry.forEach(function (value) {
sum += value;
});
avg = Math.round(sum / entry.length);
entry.forEach(function (value) {
variationSum += Math.pow(value - avg, 2);
});
sd = Math.round(Math.sqrt(variationSum / entry.length));
return min + "(" + avg + ")" + max + "[" + sd + "]";
} else {
return entry.join(", ");
}
}
/**
* Returns the performance data as a tab delimited string
* @return {string}
*/
function getDelimitedPerfData() {
var result = "";
_.forEach(perfData, function (entry, testName) {
result += getValueAsString(entry) + "\t" + testName + "\n";
});
return result;
}
/**
* Returns the measured value for the given measurement name.
* @param {Object} id The measurement to retreive.
*/
function getData(id) {
if (!id) {
return perfData;
}
return perfData[id];
}
/**
* Returns the Performance metrics to be logged for health report
* @returns {Object} An object with the health data logs to be sent
*/
function getHealthReport() {
var healthReport = {
projectLoadTimes : "",
fileOpenTimes : ""
};
_.forEach(perfData, function (entry, testName) {
if (StringUtils.startsWith(testName, "Application Startup")) {
healthReport.AppStartupTime = getValueAsString(entry);
} else if (StringUtils.startsWith(testName, "brackets module dependencies resolved")) {
healthReport.ModuleDepsResolved = getValueAsString(entry);
} else if (StringUtils.startsWith(testName, "Load Project")) {
healthReport.projectLoadTimes += ":" + getValueAsString(entry, true);
} else if (StringUtils.startsWith(testName, "Open File")) {
healthReport.fileOpenTimes += ":" + getValueAsString(entry, true);
}
});
return healthReport;
}
function searchData(regExp) {
var keys = Object.keys(perfData).filter(function (key) {
return regExp.test(key);
});
var datas = [];
keys.forEach(function (key) {
datas.push(perfData[key]);
});
return datas;
}
/**
* Clear all logs including metric data and active tests.
*/
function clear() {
perfData = {};
activeTests = {};
updatableTests = {};
_reentTests = {};
}
// create performance measurement constants
createPerfMeasurement("INLINE_WIDGET_OPEN", "Open inline editor or docs");
createPerfMeasurement("INLINE_WIDGET_CLOSE", "Close inline editor or docs");
// extensions may create additional measurement constants during their lifecycle
exports.addMeasurement = addMeasurement;
exports.finalizeMeasurement = finalizeMeasurement;
exports.isActive = isActive;
exports.markStart = markStart;
exports.getData = getData;
exports.searchData = searchData;
exports.updateMeasurement = updateMeasurement;
exports.getDelimitedPerfData = getDelimitedPerfData;
exports.createPerfMeasurement = createPerfMeasurement;
exports.clear = clear;
exports.getHealthReport = getHealthReport;
});