Skip to content

Commit 83c63f5

Browse files
committed
feat: add initial preformance tracking
1 parent f4768e0 commit 83c63f5

19 files changed

+194
-31
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
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/common/decorators.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { performance } = require('perf_hooks');
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,49 @@ 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}.${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 tempObject = {
99+
[originalMethod.name]: function (...args: Array<any>) {
100+
const start = performance.now();
101+
const result = originalMethod.apply(this, args);
102+
const resolvedPromise = Promise.resolve(result);
103+
let end;
104+
105+
if (resolvedPromise !== result) {
106+
end = performance.now();
107+
performanceService.processExecutionData(trackName, start, end, args);
108+
} else {
109+
resolvedPromise
110+
.then(() => {
111+
end = performance.now();
112+
performanceService.processExecutionData(trackName, start, end, args);
113+
})
114+
.catch((err) => {
115+
end = performance.now();
116+
performanceService.processExecutionData(trackName, start, end, args);
117+
});
118+
119+
}
120+
121+
return result;
122+
}
123+
}
124+
descriptor.value = tempObject[originalMethod.name]
125+
126+
// used to get parameter names in hooks decorator
127+
descriptor.value.toString = () => {
128+
return originalMethod.toString();
129+
};
130+
131+
return descriptor;
132+
};
133+
}

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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,33 @@ export function stringify(value: any, replacer?: (key: string, value: any) => an
568568
return JSON.stringify(value, replacer, space || 2);
569569
}
570570

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

573600
//The MIT License

lib/constants.ts

Lines changed: 2 additions & 1 deletion
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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ interface INodePackageManager {
7575
getCachePath(): Promise<string>;
7676
}
7777

78+
interface IPerformanceService {
79+
processExecutionData(methodInfo: string, startTime: number, endTime: number, args: any[]): void;
80+
}
81+
7882
interface IPackageInstallationManager {
7983
install(packageName: string, packageDir: string, options?: INpmInstallOptions): Promise<any>;
8084
getLatestVersion(packageName: string): Promise<string>;
@@ -563,6 +567,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
563567
hmr: boolean;
564568
link: boolean;
565569
analyticsLogFile: string;
570+
performance: Object;
566571
}
567572

568573
interface IEnvOptions {

lib/options.ts

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EOL } from "os";
2+
import { getFormattedDate } from "../../common/helpers";
23

34
export class AnalyticsLoggingService implements IAnalyticsLoggingService {
45
constructor(private $fs: IFileSystem,
@@ -7,35 +8,8 @@ export class AnalyticsLoggingService implements IAnalyticsLoggingService {
78
public logData(analyticsLoggingMessage: IAnalyticsLoggingMessage): void {
89
if (this.logFile && analyticsLoggingMessage && analyticsLoggingMessage.message) {
910
analyticsLoggingMessage.type = analyticsLoggingMessage.type || AnalyticsLoggingMessageType.Info;
10-
const formattedDate = this.getFormattedDate();
11+
const formattedDate = getFormattedDate();
1112
this.$fs.appendFile(this.logFile, `[${formattedDate}] [${analyticsLoggingMessage.type}] ${analyticsLoggingMessage.message}${EOL}`);
1213
}
1314
}
14-
15-
private getFormattedDate(): string {
16-
const currentDate = new Date();
17-
const year = currentDate.getFullYear();
18-
const month = this.getFormattedDateComponent((currentDate.getMonth() + 1));
19-
const day = this.getFormattedDateComponent(currentDate.getDate());
20-
const hour = this.getFormattedDateComponent(currentDate.getHours());
21-
const minutes = this.getFormattedDateComponent(currentDate.getMinutes());
22-
const seconds = this.getFormattedDateComponent(currentDate.getSeconds());
23-
const milliseconds = this.getFormattedMilliseconds(currentDate);
24-
25-
return `${[year, month, day].join('-')} ${[hour, minutes, seconds].join(":")}.${milliseconds}`;
26-
}
27-
28-
private getFormattedDateComponent(component: number): string {
29-
const stringComponent = component.toString();
30-
return stringComponent.length === 1 ? `0${stringComponent}` : stringComponent;
31-
}
32-
33-
private getFormattedMilliseconds(date: Date): string {
34-
let milliseconds = date.getMilliseconds().toString();
35-
while (milliseconds.length < 3) {
36-
milliseconds = `0${milliseconds}`;
37-
}
38-
39-
return milliseconds;
40-
}
4115
}

lib/services/analytics/analytics-service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ export class AnalyticsService implements IAnalyticsService, IDisposable {
122122
googleAnalyticsDataType: GoogleAnalyticsDataType.Event,
123123
action: data.action,
124124
label,
125-
customDimensions
125+
customDimensions,
126+
value: data.value
126127
};
127128

128129
await this.trackInGoogleAnalytics(googleAnalyticsEventData);

lib/services/android-project-service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-androi
77
import { attachAwaitDetach, isRecommendedAarFile } from "../common/helpers";
88
import { Configurations, LiveSyncPaths } from "../common/constants";
99
import { SpawnOptions } from "child_process";
10+
import { performanceLog } from ".././common/decorators";
1011

1112
export class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService {
1213
private static VALUES_DIRNAME = "values";
@@ -325,6 +326,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
325326
return true;
326327
}
327328

329+
@performanceLog()
328330
public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
329331
let task;
330332
const gradleArgs = this.getGradleBuildOptions(buildConfig, projectData);

lib/services/livesync/android-device-livesync-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AndroidDeviceLiveSyncServiceBase } from "./android-device-livesync-service-base";
2+
import { performanceLog } from "../../common/decorators";
23
import * as helpers from "../../common/helpers";
34
import { LiveSyncPaths } from "../../common/constants";
45
import * as path from "path";
@@ -75,6 +76,7 @@ export class AndroidDeviceLiveSyncService extends AndroidDeviceLiveSyncServiceBa
7576
await this.device.applicationManager.restartApplication({ appId: deviceAppData.appIdentifier, projectName });
7677
}
7778

79+
@performanceLog()
7880
public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise<void> {
7981
const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, {
8082
appIdentifier: deviceAppData.appIdentifier,
@@ -111,6 +113,7 @@ export class AndroidDeviceLiveSyncService extends AndroidDeviceLiveSyncServiceBa
111113
return true;
112114
}
113115

116+
@performanceLog()
114117
public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise<void> {
115118
const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, {
116119
appIdentifier: deviceAppData.appIdentifier,

lib/services/livesync/android-livesync-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AndroidDeviceLiveSyncService } from "./android-device-livesync-service";
22
import { AndroidDeviceSocketsLiveSyncService } from "./android-device-livesync-sockets-service";
33
import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base";
4+
import { performanceLog } from "../../common/decorators";
45
import * as semver from "semver";
56

67
export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService {
@@ -23,6 +24,7 @@ export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implemen
2324
return this.$injector.resolve<INativeScriptDeviceLiveSyncService>(AndroidDeviceLiveSyncService, { device, data });
2425
}
2526

27+
@performanceLog()
2628
public async liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise<IAndroidLiveSyncResultInfo> {
2729
let result = await this.liveSyncWatchActionCore(device, liveSyncInfo);
2830

@@ -45,6 +47,7 @@ export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implemen
4547
return result;
4648
}
4749

50+
@performanceLog()
4851
public async fullSync(syncInfo: IFullSyncInfo): Promise<IAndroidLiveSyncResultInfo> {
4952
const liveSyncResult = await super.fullSync(syncInfo);
5053
const result = await this.finalizeSync(syncInfo.device, syncInfo.projectData, liveSyncResult);

lib/services/livesync/device-livesync-service-base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { cache } from "../../common/decorators";
22
import * as path from "path";
3+
import { performanceLog } from "../../common/decorators";
34

45
export abstract class DeviceLiveSyncServiceBase {
56
private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"];
@@ -27,6 +28,7 @@ export abstract class DeviceLiveSyncServiceBase {
2728
return fastSyncFileExtensions;
2829
}
2930

31+
@performanceLog()
3032
public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise<Mobile.ILocalToDevicePathData[]> {
3133
let transferredFiles: Mobile.ILocalToDevicePathData[] = [];
3234

lib/services/livesync/ios-device-livesync-service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as constants from "../../constants";
22
import * as minimatch from "minimatch";
33
import * as net from "net";
44
import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base";
5+
import { performanceLog } from "../../common/decorators";
56

67
let currentPageReloadId = 0;
78

@@ -33,6 +34,7 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen
3334
return true;
3435
}
3536

37+
@performanceLog()
3638
public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise<void> {
3739
await Promise.all(_.map(localToDevicePaths, localToDevicePathData => this.device.fileSystem.deleteFile(localToDevicePathData.getDevicePath(), deviceAppData.appIdentifier)));
3840
}

lib/services/livesync/ios-livesync-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as temp from "temp";
44
import { IOSDeviceLiveSyncService } from "./ios-device-livesync-service";
55
import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base";
66
import { APP_FOLDER_NAME, TNS_MODULES_FOLDER_NAME } from "../../constants";
7+
import { performanceLog } from "../../common/decorators";
78

89
export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService {
910
constructor(protected $fs: IFileSystem,
@@ -17,6 +18,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I
1718
super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider);
1819
}
1920

21+
@performanceLog()
2022
public async fullSync(syncInfo: IFullSyncInfo): Promise<ILiveSyncResultInfo> {
2123
const device = syncInfo.device;
2224

@@ -57,6 +59,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I
5759
};
5860
}
5961

62+
@performanceLog()
6063
public liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise<ILiveSyncResultInfo> {
6164
if (liveSyncInfo.isReinstalled) {
6265
// In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir.

lib/services/performance-service.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { TrackActionNames } from "../constants";
2+
const EOL = require("os").EOL;
3+
import { getFormattedDate } from "../common/helpers";
4+
5+
export class PerformanceService implements IPerformanceService {
6+
public static LOG_MESSAGE_TEMPLATE = `Execution of method "%s" took %s ms.`;
7+
public static FAIL_LOG_MESSAGE_TEMPLATE = `Failed to log pefromance data for method %s.`;
8+
9+
constructor(
10+
private $options: IOptions,
11+
private $fs: IFileSystem,
12+
private $logger: ILogger,
13+
private $analyticsService: IAnalyticsService
14+
) { }
15+
16+
public processExecutionData(methodInfo: string, startTime: number, endTime: number, args: any[]): void {
17+
const executionTime = Math.floor(endTime - startTime);
18+
19+
this.trackAnalyticsData(methodInfo, executionTime);
20+
21+
if (typeof this.$options.performance === "string") {
22+
this.logDataToFile(this.$options.performance, methodInfo, executionTime, args);
23+
} else if (this.$options.performance) {
24+
this.$logger.info(PerformanceService.LOG_MESSAGE_TEMPLATE, methodInfo, executionTime);
25+
}
26+
}
27+
28+
private trackAnalyticsData(methodInfo: string, executionTime: number): void {
29+
this.$analyticsService.trackEventActionInGoogleAnalytics({
30+
action: TrackActionNames.Performance,
31+
additionalData: methodInfo,
32+
value: executionTime
33+
})
34+
.catch((err) => {
35+
throw err;
36+
});
37+
}
38+
39+
private logDataToFile(filePath: string, methodInfo: string, executionTime: number, args: any[]) {
40+
let methodArgs;
41+
const getCircularReplacer = () => {
42+
const seen = new WeakSet();
43+
seen.add(this.$options);
44+
return (key: any, value: any) => {
45+
if (typeof value === "object" && value !== null) {
46+
if (seen.has(value) || _.startsWith(key, "$")) {
47+
return;
48+
}
49+
seen.add(value);
50+
}
51+
return value;
52+
};
53+
};
54+
55+
try {
56+
methodArgs = JSON.stringify(args, getCircularReplacer());
57+
} catch (e) {
58+
methodArgs = "cyclic args";
59+
}
60+
61+
const info = {
62+
methodInfo,
63+
executionTime,
64+
timestamp: getFormattedDate(),
65+
methodArgs: JSON.parse(methodArgs)
66+
}
67+
68+
try {
69+
this.$fs.appendFile(filePath, `${JSON.stringify(info)}${EOL}`);
70+
} catch (e) {
71+
this.$logger.trace(PerformanceService.FAIL_LOG_MESSAGE_TEMPLATE, methodInfo);
72+
}
73+
}
74+
}
75+
76+
$injector.register('performanceService', PerformanceService);

0 commit comments

Comments
 (0)