Skip to content

Commit f83c95e

Browse files
authored
Merge pull request #4242 from NativeScript/kddimitrov/track-perf-decorator
Kddimitrov/track perf decorator
2 parents f4768e0 + e97247e commit f83c95e

32 files changed

+413
-45
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ $injector.require("nativescript-cli", "./nativescript-cli");
88
$injector.requirePublicClass("constants", "./constants-provider");
99
$injector.require("projectData", "./project-data");
1010
$injector.requirePublic("projectDataService", "./services/project-data-service");
11+
$injector.require("performanceService", "./services/performance-service");
1112
$injector.requirePublic("projectService", "./services/project-service");
1213
$injector.require("androidProjectService", "./services/android-project-service");
1314
$injector.require("androidPluginBuildService", "./services/android-plugin-build-service");

lib/commands/debug.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { cache } from "../common/decorators";
44
import { DebugCommandErrors } from "../constants";
55
import { ValidatePlatformCommandBase } from "./command-base";
66
import { LiveSyncCommandHelper } from "../helpers/livesync-command-helper";
7+
import { performanceLog } from "../common/decorators";
78

89
export class DebugPlatformCommand extends ValidatePlatformCommandBase implements ICommand {
910
public allowedParameters: ICommandParameter[] = [];
@@ -55,6 +56,7 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements
5556
});
5657
}
5758

59+
@performanceLog()
5860
public async getDeviceForDebug(): Promise<Mobile.IDevice> {
5961
if (this.$options.forDevice && this.$options.emulator) {
6062
this.$errors.fail(DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR);

lib/common/decorators.ts

+47
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { AnalyticsEventLabelDelimiter } from "../constants";
2+
13
/**
24
* Caches the result of the first execution of the method and returns it whenever it is called instead of executing it again.
35
* Works with methods and getters.
@@ -83,3 +85,48 @@ export function exported(moduleName: string): any {
8385
return descriptor;
8486
};
8587
}
88+
89+
export function performanceLog(injector?: IInjector): any {
90+
injector = injector || $injector;
91+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
92+
const originalMethod = descriptor.value;
93+
const className = target.constructor.name;
94+
const trackName = `${className}${AnalyticsEventLabelDelimiter}${propertyKey}`;
95+
const performanceService: IPerformanceService = injector.resolve("performanceService");
96+
97+
//needed for the returned function to have the same name as the original - used in hooks decorator
98+
const functionWrapper = {
99+
[originalMethod.name]: function (...args: Array<any>) {
100+
const start = performanceService.now();
101+
const result = originalMethod.apply(this, args);
102+
const resolvedPromise = Promise.resolve(result);
103+
let end;
104+
105+
if (resolvedPromise !== result) {
106+
end = performanceService.now();
107+
performanceService.processExecutionData(trackName, start, end, args);
108+
} else {
109+
resolvedPromise
110+
.then(() => {
111+
end = performanceService.now();
112+
performanceService.processExecutionData(trackName, start, end, args);
113+
})
114+
.catch((err) => {
115+
end = performanceService.now();
116+
performanceService.processExecutionData(trackName, start, end, args);
117+
});
118+
}
119+
120+
return result;
121+
}
122+
};
123+
descriptor.value = functionWrapper[originalMethod.name];
124+
125+
// used to get parameter names in hooks decorator
126+
descriptor.value.toString = () => {
127+
return originalMethod.toString();
128+
};
129+
130+
return descriptor;
131+
};
132+
}

lib/common/definitions/google-analytics.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ interface IEventActionData {
4848
* Project directory, in case the action is executed inside project.
4949
*/
5050
projectDir?: string;
51+
52+
/**
53+
* Value that should be tracked
54+
*/
55+
value?: number;
5156
}
5257

5358
/**

lib/common/helpers.ts

+28
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,34 @@ export function stringify(value: any, replacer?: (key: string, value: any) => an
568568
return JSON.stringify(value, replacer, space || 2);
569569
}
570570

571+
//2019-01-07 18:29:50.745
572+
export function getFixedLengthDateString(): string {
573+
const currentDate = new Date();
574+
const year = currentDate.getFullYear();
575+
const month = getFormattedDateComponent((currentDate.getMonth() + 1));
576+
const day = getFormattedDateComponent(currentDate.getDate());
577+
const hour = getFormattedDateComponent(currentDate.getHours());
578+
const minutes = getFormattedDateComponent(currentDate.getMinutes());
579+
const seconds = getFormattedDateComponent(currentDate.getSeconds());
580+
const milliseconds = getFormattedMilliseconds(currentDate);
581+
582+
return `${[year, month, day].join('-')} ${[hour, minutes, seconds].join(":")}.${milliseconds}`;
583+
}
584+
585+
export function getFormattedDateComponent(component: number): string {
586+
const stringComponent = component.toString();
587+
return stringComponent.length === 1 ? `0${stringComponent}` : stringComponent;
588+
}
589+
590+
export function getFormattedMilliseconds(date: Date): string {
591+
let milliseconds = date.getMilliseconds().toString();
592+
while (milliseconds.length < 3) {
593+
milliseconds = `0${milliseconds}`;
594+
}
595+
596+
return milliseconds;
597+
}
598+
571599
//--- begin part copied from AngularJS
572600

573601
//The MIT License

lib/common/services/hooks-service.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from "path";
22
import * as util from "util";
33
import { annotate, getValueFromNestedObject } from "../helpers";
4+
import { AnalyticsEventLabelDelimiter } from "../../constants";
45

56
class Hook implements IHook {
67
constructor(public name: string,
@@ -22,7 +23,8 @@ export class HooksService implements IHooksService {
2223
private $staticConfig: Config.IStaticConfig,
2324
private $injector: IInjector,
2425
private $projectHelper: IProjectHelper,
25-
private $options: IOptions) { }
26+
private $options: IOptions,
27+
private $performanceService: IPerformanceService) { }
2628

2729
public get hookArgsName(): string {
2830
return "hookArgs";
@@ -93,9 +95,11 @@ export class HooksService implements IHooksService {
9395
hookArguments = hookArguments || {};
9496
const results: any[] = [];
9597
const hooks = this.getHooksByName(directoryPath, hookName);
98+
9699
for (let i = 0; i < hooks.length; ++i) {
97100
const hook = hooks[i];
98-
this.$logger.info("Executing %s hook from %s", hookName, hook.fullPath);
101+
const relativePath = path.relative(directoryPath, hook.fullPath);
102+
const trackId = relativePath.replace(new RegExp('\\' + path.sep, 'g'), AnalyticsEventLabelDelimiter);
99103
let command = this.getSheBangInterpreter(hook);
100104
let inProc = false;
101105
if (!command) {
@@ -106,6 +110,7 @@ export class HooksService implements IHooksService {
106110
}
107111
}
108112

113+
const startTime = this.$performanceService.now();
109114
if (inProc) {
110115
this.$logger.trace("Executing %s hook at location %s in-process", hookName, hook.fullPath);
111116
const hookEntryPoint = require(hook.fullPath);
@@ -155,7 +160,11 @@ export class HooksService implements IHooksService {
155160
if (output.exitCode !== 0) {
156161
throw new Error(output.stdout + output.stderr);
157162
}
163+
164+
this.$logger.trace("Finished executing %s hook at location %s with environment ", hookName, hook.fullPath, environment);
158165
}
166+
const endTime = this.$performanceService.now();
167+
this.$performanceService.processExecutionData(trackId, startTime, endTime, [hookArguments]);
159168
}
160169

161170
return results;

lib/common/test/unit-tests/decorators.ts

+139-11
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ import { assert } from "chai";
44
import { CacheDecoratorsTest } from "./mocks/decorators-cache";
55
import { InvokeBeforeDecoratorsTest } from "./mocks/decorators-invoke-before";
66
import { isPromise } from "../../helpers";
7+
import * as stubs from "../../../../test/stubs";
8+
import * as sinon from "sinon";
9+
import { PerformanceService } from "../../../services/performance-service";
710

811
describe("decorators", () => {
9-
const moduleName = "moduleName", // This is the name of the injected dependency that will be resolved, for example fs, devicesService, etc.
10-
propertyName = "propertyName"; // This is the name of the method/property from the resolved module
12+
const moduleName = "moduleName"; // This is the name of the injected dependency that will be resolved, for example fs, devicesService, etc.
13+
const propertyName = "propertyName"; // This is the name of the method/property from the resolved module
14+
const expectedResults: any[] = [
15+
"string result",
16+
1,
17+
{ a: 1, b: "2" },
18+
["string 1", "string2"],
19+
true,
20+
undefined,
21+
null
22+
];
1123

1224
beforeEach(() => {
1325
$injector = new Yok();
26+
$injector.register("performanceService", stubs.PerformanceService);
1427
});
1528

1629
after(() => {
@@ -19,15 +32,6 @@ describe("decorators", () => {
1932
});
2033

2134
describe("exported", () => {
22-
const expectedResults: any[] = [
23-
"string result",
24-
1,
25-
{ a: 1, b: "2" },
26-
["string 1", "string2"],
27-
true,
28-
undefined,
29-
null
30-
];
3135

3236
const generatePublicApiFromExportedDecorator = () => {
3337
assert.deepEqual($injector.publicApi.__modules__[moduleName], undefined);
@@ -358,4 +362,128 @@ describe("decorators", () => {
358362
});
359363
});
360364
});
365+
366+
describe("performanceLog", () => {
367+
const testErrorMessage = "testError";
368+
let testInjector: IInjector;
369+
let sandbox: sinon.SinonSandbox;
370+
interface ITestInterface {
371+
testMethod(arg: any): any;
372+
throwMethod?(): void;
373+
testAsyncMehtod(arg: any): Promise<any>;
374+
rejectMethod?(): Promise<any>;
375+
}
376+
let testInstance: ITestInterface;
377+
let undecoratedTestInstance: ITestInterface;
378+
379+
function createTestInjector(): IInjector {
380+
testInjector = new Yok();
381+
testInjector.register("performanceService", PerformanceService);
382+
testInjector.register("options", {});
383+
testInjector.register("fs", stubs.FileSystemStub);
384+
testInjector.register("logger", stubs.LoggerStub);
385+
testInjector.register("analyticsService", {
386+
trackEventActionInGoogleAnalytics: () => { return Promise.resolve(); }
387+
});
388+
389+
return testInjector;
390+
}
391+
392+
beforeEach(() => {
393+
sandbox = sinon.sandbox.create();
394+
testInjector = createTestInjector();
395+
396+
class TestClass implements ITestInterface {
397+
@decoratorsLib.performanceLog(testInjector)
398+
testMethod(arg: any) {
399+
return arg;
400+
}
401+
402+
@decoratorsLib.performanceLog(testInjector)
403+
throwMethod() {
404+
throw new Error("testErrorMessage");
405+
}
406+
407+
@decoratorsLib.performanceLog(testInjector)
408+
async testAsyncMehtod(arg: any) {
409+
return Promise.resolve(arg);
410+
}
411+
412+
rejectMethod() {
413+
return Promise.reject(testErrorMessage);
414+
}
415+
}
416+
417+
class UndecoratedTestClass implements ITestInterface {
418+
testMethod(arg: any) {
419+
return arg;
420+
}
421+
422+
async testAsyncMehtod(arg: any) {
423+
return Promise.resolve(arg);
424+
}
425+
}
426+
427+
undecoratedTestInstance = new UndecoratedTestClass();
428+
testInstance = new TestClass();
429+
});
430+
431+
afterEach(() => {
432+
sandbox.restore();
433+
});
434+
435+
_.each(expectedResults, (expectedResult: any) => {
436+
it("returns proper result", () => {
437+
const actualResult = testInstance.testMethod(expectedResult);
438+
assert.deepEqual(actualResult, expectedResult);
439+
});
440+
441+
it("returns proper result when async", () => {
442+
const promise = testInstance.testAsyncMehtod(expectedResult);
443+
444+
assert.notDeepEqual(promise.then, undefined);
445+
446+
return promise.then((actualResult: any) => {
447+
assert.deepEqual(actualResult, expectedResult);
448+
});
449+
});
450+
});
451+
452+
it("method has same toString", () => {
453+
assert.equal(testInstance.testMethod.toString(), undecoratedTestInstance.testMethod.toString());
454+
});
455+
456+
it("method has same name", () => {
457+
assert.equal(testInstance.testMethod.name, undecoratedTestInstance.testMethod.name);
458+
});
459+
460+
it("does not eat errors", () => {
461+
assert.throws(testInstance.throwMethod, testErrorMessage);
462+
assert.isRejected(testInstance.rejectMethod(), testErrorMessage);
463+
});
464+
465+
it("calls performance service on method call", async () => {
466+
const performanceService = testInjector.resolve("performanceService");
467+
const processExecutionDataStub: sinon.SinonStub = sinon.stub(performanceService, "processExecutionData");
468+
469+
const checkSubCall = (call: sinon.SinonSpyCall, methodData: string) => {
470+
const callArgs = call.args;
471+
const methodInfo = callArgs[0];
472+
const startTime = callArgs[1];
473+
const endTime = callArgs[2];
474+
475+
assert(methodInfo === methodData);
476+
assert.isNumber(startTime);
477+
assert.isNumber(endTime);
478+
assert.isTrue(endTime > startTime);
479+
assert.isDefined(callArgs[3][0] === "test");
480+
};
481+
482+
testInstance.testMethod("test");
483+
await testInstance.testAsyncMehtod("test");
484+
485+
checkSubCall(processExecutionDataStub.firstCall, "TestClass__testMethod");
486+
checkSubCall(processExecutionDataStub.secondCall, "TestClass__testAsyncMehtod");
487+
});
488+
});
361489
});

lib/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ export const enum TrackActionNames {
152152
CheckLocalBuildSetup = "Check Local Build Setup",
153153
CheckEnvironmentRequirements = "Check Environment Requirements",
154154
Options = "Options",
155-
AcceptTracking = "Accept Tracking"
155+
AcceptTracking = "Accept Tracking",
156+
Performance = "Performance"
156157
}
157158

158159
export const AnalyticsEventLabelDelimiter = "__";

lib/declarations.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ interface INodePackageManager {
7575
getCachePath(): Promise<string>;
7676
}
7777

78+
interface IPerformanceService {
79+
// Will process the data based on the command opitons (--performance flag and user-reporting setting)
80+
processExecutionData(methodInfo: string, startTime: number, endTime: number, args: any[]): void;
81+
82+
// Will return a reference time in milliseconds
83+
now(): number;
84+
}
85+
7886
interface IPackageInstallationManager {
7987
install(packageName: string, packageDir: string, options?: INpmInstallOptions): Promise<any>;
8088
getLatestVersion(packageName: string): Promise<string>;
@@ -563,6 +571,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
563571
hmr: boolean;
564572
link: boolean;
565573
analyticsLogFile: string;
574+
performance: Object;
566575
}
567576

568577
interface IEnvOptions {

lib/options.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export class Options {
119119
analyticsLogFile: { type: OptionType.String, hasSensitiveValue: true },
120120
hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
121121
link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
122-
aab: { type: OptionType.Boolean, hasSensitiveValue: false }
122+
aab: { type: OptionType.Boolean, hasSensitiveValue: false },
123+
performance: { type: OptionType.Object, hasSensitiveValue: true }
123124
};
124125
}
125126

0 commit comments

Comments
 (0)